diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/QuasseldroidRouter.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/QuasseldroidRouter.kt
index 8ed54ebf19f9547fc96105e95099c4c78f9bf00c..eff0f83625d731aad420545ac82202d642f8376a 100644
--- a/app/src/main/kotlin/de/justjanne/quasseldroid/QuasseldroidRouter.kt
+++ b/app/src/main/kotlin/de/justjanne/quasseldroid/QuasseldroidRouter.kt
@@ -7,10 +7,12 @@ import androidx.navigation.compose.NavHost
 import androidx.navigation.compose.composable
 import androidx.navigation.compose.rememberNavController
 import androidx.navigation.navArgument
+import de.justjanne.libquassel.protocol.models.ids.BufferId
 import de.justjanne.quasseldroid.service.QuasselBackend
 import de.justjanne.quasseldroid.ui.routes.CoreInfoRoute
 import de.justjanne.quasseldroid.ui.routes.HomeRoute
 import de.justjanne.quasseldroid.ui.routes.LoginRoute
+import de.justjanne.quasseldroid.ui.routes.MessageRoute
 
 @Composable
 fun QuasseldroidRouter(backend: QuasselBackend) {
@@ -28,7 +30,7 @@ fun QuasseldroidRouter(backend: QuasselBackend) {
       "buffer/{bufferId}",
       listOf(navArgument("bufferId") { type = NavType.IntType })
     ) {
-      Text("Buffer ${it.arguments?.getInt("bufferId")}")
+      MessageRoute(backend, navController, BufferId(it.arguments?.getInt("bufferId") ?: -1))
     }
     composable("bufferViewConfigs") {
       Text("List of BufferViewConfigs")
diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/messages/MessageStore.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/messages/MessageStore.kt
index d72031943389e9531229a36a5101ccc475803f39..fe3faa8ba12ba320cd6fc525ba7d02a549e6255a 100644
--- a/app/src/main/kotlin/de/justjanne/quasseldroid/messages/MessageStore.kt
+++ b/app/src/main/kotlin/de/justjanne/quasseldroid/messages/MessageStore.kt
@@ -94,6 +94,14 @@ class MessageStore(
     }
   }
 
+  fun clear(bufferId: BufferId) {
+    scope.launch {
+      state.update { messages ->
+        messages - bufferId
+      }
+    }
+  }
+
   override fun close() {
     runBlocking {
       disposable.cancelAndJoin()
diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleMessageProvider.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleMessageProvider.kt
index 5965a4375b0d15778e7ef7364ff0bc4835443437..bff8b7e404e87f6bb7aea51b25087441f6004470 100644
--- a/app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleMessageProvider.kt
+++ b/app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleMessageProvider.kt
@@ -10,8 +10,13 @@ import de.justjanne.libquassel.protocol.models.flags.MessageType
 import de.justjanne.libquassel.protocol.models.ids.BufferId
 import de.justjanne.libquassel.protocol.models.ids.MsgId
 import de.justjanne.libquassel.protocol.models.ids.NetworkId
+import de.justjanne.libquassel.protocol.util.irc.HostmaskHelper
 import org.threeten.bp.Instant
 
+class SampleNickProvider : PreviewParameterProvider<String> {
+  override val values = SampleMessageProvider().values.map { HostmaskHelper.nick(it.sender) }
+}
+
 class SampleMessageProvider : PreviewParameterProvider<Message> {
   override val values = sequenceOf(
     Message(
diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/MessageBase.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/MessageBase.kt
new file mode 100644
index 0000000000000000000000000000000000000000..762d969a16955ea99549f9ddc86734cc1e10a8b3
--- /dev/null
+++ b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/MessageBase.kt
@@ -0,0 +1,171 @@
+package de.justjanne.quasseldroid.ui.components
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.paddingFrom
+import androidx.compose.foundation.layout.paddingFromBaseline
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.ContentAlpha
+import androidx.compose.material.LocalContentAlpha
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.FirstBaseline
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import de.justjanne.libquassel.protocol.models.Message
+import de.justjanne.libquassel.protocol.util.irc.HostmaskHelper
+import de.justjanne.quasseldroid.sample.SampleMessageProvider
+import de.justjanne.quasseldroid.ui.icons.AvatarIcon
+import de.justjanne.quasseldroid.ui.theme.QuasselTheme
+import de.justjanne.quasseldroid.ui.theme.Typography
+import irc.SenderColorUtil
+import org.threeten.bp.ZoneId
+import org.threeten.bp.format.DateTimeFormatter
+import org.threeten.bp.format.FormatStyle
+
+private val formatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
+
+@Composable
+fun buildNick(nick: String, senderPrefixes: String): AnnotatedString {
+  val senderColor = QuasselTheme.sender.colors[SenderColorUtil.senderColor(nick)]
+
+  return buildAnnotatedString {
+    if (senderPrefixes.isNotEmpty()) {
+      append(senderPrefixes)
+    }
+    pushStyle(SpanStyle(color = senderColor, fontWeight = FontWeight.Bold))
+    append(nick)
+    pop()
+  }
+}
+
+@Preview(name = "Message Base", showBackground = true)
+@Composable
+fun MessageBase(
+  @PreviewParameter(SampleMessageProvider::class)
+  message: Message,
+  followUp: Boolean = false,
+  // avatarSize: Dp = 32.dp
+  content: @Composable () -> Unit = { Text(message.content, style = Typography.body2) }
+) {
+  val avatarSize = 32.dp
+
+  val nick = HostmaskHelper.nick(message.sender)
+
+  Row(
+    modifier = Modifier
+      .padding(2.dp)
+      .fillMaxWidth()
+  ) {
+    if (!followUp) {
+      Spacer(Modifier.width(4.dp))
+      AvatarIcon(
+        nick,
+        size = avatarSize,
+        modifier = Modifier
+          .paddingFromBaseline(top = 28.sp)
+      )
+      Spacer(Modifier.width(4.dp))
+    } else {
+      Spacer(Modifier.width(avatarSize + 8.dp))
+    }
+    Column(modifier = Modifier.align(Alignment.CenterVertically)) {
+      if (!followUp) {
+        Row {
+          Text(
+            buildAnnotatedString {
+              append(buildNick(nick, message.senderPrefixes))
+              append(' ')
+              pushStyle(SpanStyle(color = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.medium)))
+              append(message.realName)
+              pop()
+            },
+            style = Typography.body2
+          )
+        }
+      }
+      Row {
+        Box(modifier = Modifier.weight(1.0f)) {
+          content()
+        }
+        CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
+          Text(
+            message.time
+              .atZone(ZoneId.systemDefault())
+              .format(formatter),
+            style = Typography.body2,
+            fontSize = 12.sp,
+            modifier = Modifier.align(Alignment.Bottom)
+          )
+        }
+      }
+    }
+  }
+}
+
+@Preview(name = "Message Small", showBackground = true)
+@Composable
+fun MessageBaseSmall(
+  @PreviewParameter(SampleMessageProvider::class)
+  message: Message,
+  followUp: Boolean = false,
+  // avatarSize: Dp = 32.dp,
+  content: @Composable () -> Unit = {
+    val nick = HostmaskHelper.nick(message.sender)
+
+    Text(buildAnnotatedString {
+      append("— ")
+      append(buildNick(nick, message.senderPrefixes))
+      append(" ")
+      append(message.content)
+    }, style = Typography.body2)
+  }
+) {
+  val avatarSize = 16.dp
+  val nick = HostmaskHelper.nick(message.sender)
+
+  Row(
+    modifier = Modifier
+      .padding(2.dp)
+      .fillMaxWidth()
+  ) {
+    Spacer(Modifier.width(20.dp))
+    AvatarIcon(
+      nick,
+      modifier = Modifier
+        .align(Alignment.Top)
+        .paddingFromBaseline(top = 14.sp),
+      size = avatarSize
+    )
+    Spacer(Modifier.width(4.dp))
+    Box(modifier = Modifier.weight(1.0f)) {
+      content()
+    }
+    Spacer(Modifier.width(4.dp))
+    CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
+      Text(
+        message.time
+          .atZone(ZoneId.systemDefault())
+          .format(formatter),
+        style = Typography.body2,
+        fontSize = 12.sp,
+        modifier = Modifier.align(Alignment.Bottom)
+      )
+    }
+  }
+}
diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/MessageBaseView.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/MessageBaseView.kt
deleted file mode 100644
index efe03af838ba32f88249adccfbbf3a8d27f8aaa5..0000000000000000000000000000000000000000
--- a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/MessageBaseView.kt
+++ /dev/null
@@ -1,92 +0,0 @@
-package de.justjanne.quasseldroid.ui.components
-
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.width
-import androidx.compose.material.ContentAlpha
-import androidx.compose.material.LocalContentAlpha
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import de.justjanne.libquassel.protocol.models.Message
-import de.justjanne.libquassel.protocol.util.irc.HostmaskHelper
-import de.justjanne.quasseldroid.ui.icons.AvatarIcon
-import de.justjanne.quasseldroid.ui.theme.QuasselTheme
-import de.justjanne.quasseldroid.ui.theme.Typography
-import irc.SenderColorUtil
-import org.threeten.bp.ZoneId
-import org.threeten.bp.format.DateTimeFormatter
-import org.threeten.bp.format.FormatStyle
-
-private val formatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
-
-@Composable
-fun MessageBaseView(
-  message: Message,
-  followUp: Boolean,
-  avatarSize: Dp,
-  content: @Composable () -> Unit
-) {
-  val nick = HostmaskHelper.nick(message.sender)
-  val senderColor = QuasselTheme.sender.colors[SenderColorUtil.senderColor(nick)]
-
-  Row(modifier = Modifier.padding(2.dp)) {
-    if (!followUp) {
-      AvatarIcon(nick, null, modifier = Modifier.padding(vertical = 2.dp))
-      Spacer(Modifier.width(4.dp))
-    } else {
-      Spacer(Modifier.width(avatarSize + 8.dp))
-    }
-    Column {
-      if (!followUp) {
-        Row {
-          Text(
-            message.senderPrefixes,
-            style = Typography.body2,
-            fontWeight = FontWeight.Bold,
-          )
-          Text(
-            nick,
-            style = Typography.body2,
-            fontWeight = FontWeight.Bold,
-            color = senderColor,
-          )
-          Spacer(Modifier.width(4.dp))
-          CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
-            Text(
-              message.realName,
-              modifier = Modifier.weight(1.0f),
-              style = Typography.body2,
-              overflow = TextOverflow.Ellipsis,
-            )
-          }
-        }
-      }
-      Row {
-        Box(modifier = Modifier.weight(1.0f)) {
-          content()
-        }
-        CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
-          Text(
-            message.time
-              .atZone(ZoneId.systemDefault())
-              .format(formatter),
-            style = Typography.body2,
-            fontSize = 12.sp,
-            modifier = Modifier.align(Alignment.Bottom)
-          )
-        }
-      }
-    }
-  }
-}
diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/MessageList.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/MessageList.kt
index ae018dfd7fd07b832a3714e6be574137bc8a5d0e..3faac0d0cf7795b6d4031348bc833a3f79da403c 100644
--- a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/MessageList.kt
+++ b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/MessageList.kt
@@ -6,16 +6,26 @@ import androidx.compose.foundation.lazy.itemsIndexed
 import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.tooling.preview.PreviewParameter
-import androidx.compose.ui.unit.dp
+import de.justjanne.bitflags.of
 import de.justjanne.libquassel.protocol.models.Message
+import de.justjanne.libquassel.protocol.models.flags.MessageType
 import de.justjanne.libquassel.protocol.models.ids.MsgId
+import de.justjanne.libquassel.protocol.util.irc.HostmaskHelper
+import de.justjanne.quasseldroid.R
 import de.justjanne.quasseldroid.sample.SampleMessagesProvider
+import de.justjanne.quasseldroid.ui.theme.QuasselTheme
 import de.justjanne.quasseldroid.ui.theme.Typography
 import de.justjanne.quasseldroid.util.extensions.OnBottomReached
 import de.justjanne.quasseldroid.util.extensions.OnTopReached
 import de.justjanne.quasseldroid.util.extensions.getPrevious
+import de.justjanne.quasseldroid.util.format.IrcFormat
+import de.justjanne.quasseldroid.util.format.IrcFormatDeserializer
+import de.justjanne.quasseldroid.util.format.IrcFormatRenderer
+import de.justjanne.quasseldroid.util.format.TextFormatter
 import org.threeten.bp.ZoneId
 
 @Preview(name = "Messages", showBackground = true)
@@ -41,21 +51,40 @@ fun MessageList(
         message.realName == prev.realName &&
         message.avatarUrl == prev.avatarUrl
 
-      val isNew = prev != null &&
-        prev.messageId <= markerLine &&
+      val isNew = (prev == null || prev.messageId <= markerLine) &&
         message.messageId > markerLine
 
+      val parsed = IrcFormatDeserializer.parse(message.content)
+
       if (prevDate == null || !messageDate.isEqual(prevDate)) {
         MessageDayChangeView(messageDate, isNew)
       } else if (isNew) {
         NewMessageView()
       }
 
-      MessageBaseView(message, followUp, 32.dp) {
-        Text(
-          message.content,
-          style = Typography.body2,
-        )
+      when (message.type) {
+        MessageType.of(MessageType.Plain) -> {
+          MessageBase(message, followUp) {
+            Text(IrcFormatRenderer.render(parsed), style = Typography.body2)
+          }
+        }
+        MessageType.of(MessageType.Action) -> {
+          MessageBaseSmall(message) {
+            val nick = HostmaskHelper.nick(message.sender)
+
+            Text(
+              TextFormatter.format(
+                AnnotatedString(stringResource(R.string.message_format_action)),
+                buildNick(nick, message.senderPrefixes),
+                IrcFormatRenderer.render(
+                  data = parsed.map { it.copy(style = it.style.flipFlag(IrcFormat.Flag.ITALIC)) }
+                )
+              ),
+              style = Typography.body2,
+              color = QuasselTheme.chat.onAction
+            )
+          }
+        }
       }
     }
   }
diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/icons/AvatarIcon.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/icons/AvatarIcon.kt
index 757b3118b93cedc5c8e2004e9326a8d79a0e5d56..06087cf398b23e8bb6ff2cd81e900bc435a880d7 100644
--- a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/icons/AvatarIcon.kt
+++ b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/icons/AvatarIcon.kt
@@ -2,6 +2,7 @@ package de.justjanne.quasseldroid.ui.icons
 
 import android.graphics.Bitmap
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material.ContentAlpha
@@ -15,23 +16,21 @@ import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
+import de.justjanne.quasseldroid.sample.SampleNickProvider
 import de.justjanne.quasseldroid.ui.theme.QuasselTheme
 import irc.SenderColorUtil
 import java.util.*
 
 @Preview
-@Composable
-private fun AvatarIconPreview() {
-  AvatarIcon("justJanne", null)
-}
-
 @Composable
 fun AvatarIcon(
+  @PreviewParameter(SampleNickProvider::class)
   nick: String,
-  avatar: Bitmap?,
   modifier: Modifier = Modifier,
+  avatar: Bitmap? = null,
   size: Dp = 32.dp
 ) {
   val senderColor = QuasselTheme.sender.colors[SenderColorUtil.senderColor(nick)]
diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/routes/HomeRoute.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/routes/HomeRoute.kt
index 0b9d1123c47e1cb739b763f025f868e1cbbf5573..940fd34f9f860ee4d57d754855d454b6bfad5e26 100644
--- a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/routes/HomeRoute.kt
+++ b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/routes/HomeRoute.kt
@@ -1,10 +1,12 @@
 package de.justjanne.quasseldroid.ui.routes
 
-import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
 import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.foundation.rememberScrollState
 import androidx.compose.material.Button
 import androidx.compose.material.Text
 import androidx.compose.material.TextField
@@ -14,114 +16,106 @@ import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.unit.dp
 import androidx.navigation.NavController
-import de.justjanne.libquassel.protocol.models.Message
-import de.justjanne.libquassel.protocol.models.ids.BufferId
-import de.justjanne.libquassel.protocol.models.ids.MsgId
+import de.justjanne.libquassel.protocol.models.BufferInfo
+import de.justjanne.libquassel.protocol.models.ids.NetworkId
+import de.justjanne.libquassel.protocol.models.network.NetworkInfo
+import de.justjanne.libquassel.protocol.syncables.common.Network
 import de.justjanne.libquassel.protocol.util.flatMap
-import de.justjanne.quasseldroid.messages.MessageStore
 import de.justjanne.quasseldroid.service.QuasselBackend
-import de.justjanne.quasseldroid.ui.components.MessageList
 import de.justjanne.quasseldroid.util.mapNullable
 import de.justjanne.quasseldroid.util.rememberFlow
 import de.justjanne.quasseldroid.util.saver.TextFieldValueSaver
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.map
 
-private const val limit = 20
-
 @Composable
 fun HomeRoute(backend: QuasselBackend, navController: NavController) {
-  val session = rememberFlow(null) {
-    backend.flow()
-      .mapNullable { it.session }
+  val side = rememberFlow(null) {
+    backend.flow().mapNullable { it.session.side }
   }
 
   val (buffer, setBuffer) = rememberSaveable(stateSaver = TextFieldValueSaver) {
-    mutableStateOf(TextFieldValue("3747"))
-  }
-  val (position, setPosition) = rememberSaveable(stateSaver = TextFieldValueSaver) {
-    mutableStateOf(TextFieldValue("108113920"))
-  }
-
-  val bufferId = BufferId(buffer.text.toIntOrNull() ?: -1)
-  val positionId = MsgId(position.text.toLongOrNull() ?: -1L)
-
-  val listState = rememberLazyListState()
-
-  val messageStore: MessageStore? = rememberFlow(null) {
-    backend.flow()
-      .mapNullable { it.messages }
+    mutableStateOf(TextFieldValue(""))
   }
 
-  val messages: List<Message> = rememberFlow(emptyList()) {
+  val initStatus = rememberFlow(null) {
     backend.flow()
-      .mapNullable { it.messages }
+      .mapNullable { it.session }
+      .mapNullable { it.baseInitHandler }
       .flatMap()
-      .mapNullable { it[bufferId] }
-      .map { it?.messages.orEmpty() }
   }
 
-  val markerLine: MsgId? = rememberFlow(null) {
-    backend.flow()
+  val buffers: List<Pair<NetworkInfo?, BufferInfo>> = rememberFlow(emptyList()) {
+    val sessions = backend.flow()
       .mapNullable { it.session }
       .flatMap()
+
+    val networks: Flow<Map<NetworkId, Network>> = sessions
+      .mapNullable { it.networks }
+      .map { it.orEmpty() }
+
+    val buffers: Flow<List<BufferInfo>> = sessions
       .mapNullable { it.bufferSyncer }
       .flatMap()
-      .mapNullable { it.markerLines[bufferId] }
+      .mapNullable { it.bufferInfos.values.sortedBy(BufferInfo::bufferName) }
+      .map { it.orEmpty() }
+
+    combine(buffers, networks) { bufferList, networkMap ->
+      bufferList.map {
+        Pair(networkMap[it.networkId]?.networkInfo(), it)
+      }
+    }
   }
 
-  val initStatus = rememberFlow(null) {
-    backend.flow()
-      .mapNullable { it.session }
-      .mapNullable { it.baseInitHandler }
-      .flatMap()
+  val filteredBuffers = buffers.filter { (_, info) ->
+    info.bufferName?.contains(buffer.text) == true
   }
 
   val context = LocalContext.current
-  val buttonScrollState = rememberScrollState()
+
+  val scrollState = rememberLazyListState()
 
   Column {
-    Text("Side: ${session?.side}")
+    Text("Side: $side")
     if (initStatus != null) {
       val done = initStatus.total - initStatus.waiting.size
       Text("Init: ${initStatus.started} $done/ ${initStatus.total}")
     }
-    Row(modifier = Modifier.horizontalScroll(buttonScrollState)) {
-      Button(onClick = { navController.navigate("coreInfo") }) {
-        Text("Core Info")
-      }
-      Button(onClick = {
-        backend.disconnect(context)
-        navController.navigate("login")
-      }) {
-        Text("Disconnect")
-      }
-      Button(onClick = {
-        messageStore?.loadBefore(bufferId, limit)
-      }) {
-        Text("↑")
-      }
-      Button(onClick = {
-        messageStore?.loadAfter(bufferId, limit)
-      }) {
-        Text("↓")
-      }
-      Button(onClick = {
-        messageStore?.loadAround(bufferId, positionId, limit)
-      }) {
-        Text("…")
-      }
+    Button(onClick = { navController.navigate("coreInfo") }) {
+      Text("Core Info")
+    }
+    Button(onClick = {
+      backend.disconnect(context)
+      navController.navigate("login")
+    }) {
+      Text("Disconnect")
     }
     TextField(value = buffer, onValueChange = setBuffer)
-    TextField(value = position, onValueChange = setPosition)
-    MessageList(
-      messages = messages,
-      listState = listState,
-      markerLine = markerLine ?: MsgId(-1),
-      buffer = 5,
-      onLoadAtStart = { messageStore?.loadBefore(bufferId, limit) },
-      onLoadAtEnd = { messageStore?.loadAfter(bufferId, limit) }
-    )
+    LazyColumn(state = scrollState) {
+      items(filteredBuffers, key = { (_, buffer) -> buffer.bufferId }) { (network, buffer) ->
+        Column(modifier = Modifier
+          .padding(4.dp)
+          .fillMaxWidth()
+          .clickable { navController.navigate("buffer/${buffer.bufferId.id}") }
+        ) {
+          Text(
+            network?.networkName ?: "Unknown network",
+            modifier = Modifier.fillMaxWidth()
+          )
+          Text(
+            buffer.type.joinToString(", "),
+            modifier = Modifier.fillMaxWidth()
+          )
+          Text(
+            buffer.bufferName ?: "Unknown buffer",
+            modifier = Modifier.fillMaxWidth()
+          )
+        }
+      }
+    }
   }
 }
 
diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/routes/MessageRoute.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/routes/MessageRoute.kt
new file mode 100644
index 0000000000000000000000000000000000000000..c05908e85dc6ff5195358958085983bfe996044e
--- /dev/null
+++ b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/routes/MessageRoute.kt
@@ -0,0 +1,99 @@
+package de.justjanne.quasseldroid.ui.routes
+
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.material.Button
+import androidx.compose.material.Text
+import androidx.compose.material.TextField
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.navigation.NavController
+import de.justjanne.libquassel.protocol.models.Message
+import de.justjanne.libquassel.protocol.models.ids.BufferId
+import de.justjanne.libquassel.protocol.models.ids.MsgId
+import de.justjanne.libquassel.protocol.util.flatMap
+import de.justjanne.quasseldroid.messages.MessageStore
+import de.justjanne.quasseldroid.service.QuasselBackend
+import de.justjanne.quasseldroid.ui.components.MessageList
+import de.justjanne.quasseldroid.util.mapNullable
+import de.justjanne.quasseldroid.util.rememberFlow
+import de.justjanne.quasseldroid.util.saver.TextFieldValueSaver
+import kotlinx.coroutines.flow.map
+
+private const val limit = 20
+
+@Composable
+fun MessageRoute(
+  backend: QuasselBackend,
+  navController: NavController,
+  buffer: BufferId
+) {
+  val listState = rememberLazyListState()
+
+  val messageStore: MessageStore? = rememberFlow(null) {
+    backend.flow()
+      .mapNullable { it.messages }
+  }
+
+  val messages: List<Message> = rememberFlow(emptyList()) {
+    backend.flow()
+      .mapNullable { it.messages }
+      .flatMap()
+      .mapNullable { it[buffer] }
+      .map { it?.messages.orEmpty() }
+  }
+
+  val markerLine: MsgId? = rememberFlow(null) {
+    backend.flow()
+      .mapNullable { it.session }
+      .flatMap()
+      .mapNullable { it.bufferSyncer }
+      .flatMap()
+      .mapNullable { it.markerLines[buffer] }
+  }
+
+  Column {
+    Row {
+      Button(onClick = { navController.navigate("home") }) {
+        Text("Back")
+      }
+      Button(onClick = {
+        messageStore?.loadBefore(buffer, limit)
+      }) {
+        Text("↑")
+      }
+      Button(onClick = {
+        messageStore?.loadAfter(buffer, limit)
+      }) {
+        Text("↓")
+      }
+      Button(onClick = {
+        messageStore?.loadAround(buffer, markerLine ?: MsgId(-1), limit)
+      }) {
+        Text("N")
+      }
+      Button(onClick = {
+        messageStore?.clear(buffer)
+      }) {
+        Text("Clr")
+      }
+    }
+    MessageList(
+      messages = messages,
+      listState = listState,
+      markerLine = markerLine ?: MsgId(-1),
+      buffer = 5,
+      onLoadAtStart = { messageStore?.loadBefore(buffer, limit) },
+      onLoadAtEnd = { messageStore?.loadAfter(buffer, limit) }
+    )
+  }
+}
+
+
diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/util/extensions/ListExtensions.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/util/extensions/ListExtensions.kt
index 2ca165b979a1ef0ded6b91de69626325b917db31..8a1aa30c1d2be0dff2114c05d18602c8961c879e 100644
--- a/app/src/main/kotlin/de/justjanne/quasseldroid/util/extensions/ListExtensions.kt
+++ b/app/src/main/kotlin/de/justjanne/quasseldroid/util/extensions/ListExtensions.kt
@@ -9,3 +9,18 @@ fun <T> List<T>.getPrevious(index: Int): T? =
 
 fun <T> List<T>.getNext(index: Int): T? =
   getSafe(index + 1)
+
+@Suppress("NOTHING_TO_INLINE")
+inline operator fun <T> List<T>.component6(): T = get(5)
+@Suppress("NOTHING_TO_INLINE")
+inline operator fun <T> List<T>.component7(): T = get(6)
+@Suppress("NOTHING_TO_INLINE")
+inline operator fun <T> List<T>.component8(): T = get(7)
+@Suppress("NOTHING_TO_INLINE")
+inline operator fun <T> List<T>.component9(): T = get(8)
+@Suppress("NOTHING_TO_INLINE")
+inline operator fun <T> List<T>.component10(): T = get(9)
+@Suppress("NOTHING_TO_INLINE")
+inline operator fun <T> List<T>.component11(): T = get(10)
+@Suppress("NOTHING_TO_INLINE")
+inline operator fun <T> List<T>.component12(): T = get(11)
diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/util/format/FormatString.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/util/format/FormatString.kt
index a8c8ea9ff65ef66a67f2c8a9612f5ee9180208c7..850c414a24b8fa41aac0d0bff40c86d4df466418 100644
--- a/app/src/main/kotlin/de/justjanne/quasseldroid/util/format/FormatString.kt
+++ b/app/src/main/kotlin/de/justjanne/quasseldroid/util/format/FormatString.kt
@@ -1,5 +1,7 @@
 package de.justjanne.quasseldroid.util.format
 
+import de.justjanne.quasseldroid.util.extensions.joinString
+
 sealed class FormatString {
   data class FixedValue(
     val content: CharSequence
@@ -10,26 +12,34 @@ sealed class FormatString {
   }
 
   data class FormatSpecifier(
-    val index: Int?,
-    val flags: String?,
-    val width: Int?,
-    val precision: Int?,
-    val time: Boolean,
+    val argumentIndex: Int? = null,
+    val flags: String? = null,
+    val width: Int? = null,
+    val precision: Int? = null,
+    val time: Boolean = false,
     val conversion: Char
   ) : FormatString() {
-    override fun toString(): String = listOfNotNull(
-      index?.let { "index=$index" },
-      flags?.let { "flags='$flags'" },
-      width?.let { "width=$width" },
-      precision?.let { "precision=$precision" },
-      "time=$time",
-      "conversion='$conversion'"
-    ).joinToString(", ", prefix = "FormatSpecifier(", postfix = ")")
+    override fun toString(): String = joinString(", ", "FormatSpecifier(", ")") {
+      if (argumentIndex != null) {
+        append("argumentIndex=$argumentIndex")
+      }
+      if (flags != null) {
+        append("flags=$flags")
+      }
+      if (width != null) {
+        append("width=$width")
+      }
+      if (precision != null) {
+        append("precision=$precision")
+      }
+      append("time=$time")
+      append("conversion=$conversion")
+    }
 
     fun toFormatSpecifier(ignoreFlags: Set<Char> = emptySet()) = buildString {
       append("%")
-      if (index != null) {
-        append(index)
+      if (argumentIndex != null) {
+        append(argumentIndex)
         append("$")
       }
       if (flags != null) {
diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/util/lifecycle/DefaultContextualLifecycleObserver.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/util/lifecycle/DefaultContextualLifecycleObserver.kt
index f6d44679d15803b6dd142fc6ed28ba9be0d30024..cb6a07646e2a07ebdb784db6946a8c645ae201b6 100644
--- a/app/src/main/kotlin/de/justjanne/quasseldroid/util/lifecycle/DefaultContextualLifecycleObserver.kt
+++ b/app/src/main/kotlin/de/justjanne/quasseldroid/util/lifecycle/DefaultContextualLifecycleObserver.kt
@@ -12,42 +12,42 @@ abstract class DefaultContextualLifecycleObserver : ContextualLifecycleObserver
   @CallSuper
   override fun onCreate(owner: Context) {
     require(statusInternal.compareAndSet(LifecycleStatus.DESTROYED, LifecycleStatus.CREATED)) {
-      "Unexpected lifecycle status: onCreate called, but status is not DESTROYED"
+      "Unexpected lifecycle status: onCreate called, but status is not DESTROYED: ${statusInternal.get()}"
     }
   }
 
   @CallSuper
   override fun onStart(owner: Context) {
     require(statusInternal.compareAndSet(LifecycleStatus.CREATED, LifecycleStatus.STARTED)) {
-      "Unexpected lifecycle status: onStart called, but status is not CREATED"
+      "Unexpected lifecycle status: onStart called, but status is not CREATED: ${statusInternal.get()}"
     }
   }
 
   @CallSuper
   override fun onResume(owner: Context) {
     require(statusInternal.compareAndSet(LifecycleStatus.STARTED, LifecycleStatus.RESUMED)) {
-      "Unexpected lifecycle status: onResume called, but status is not STARTED"
+      "Unexpected lifecycle status: onResume called, but status is not STARTED: ${statusInternal.get()}"
     }
   }
 
   @CallSuper
   override fun onPause(owner: Context) {
     require(statusInternal.compareAndSet(LifecycleStatus.RESUMED, LifecycleStatus.STARTED)) {
-      "Unexpected lifecycle status: onPause called, but status is not RESUMED"
+      "Unexpected lifecycle status: onPause called, but status is not RESUMED: ${statusInternal.get()}"
     }
   }
 
   @CallSuper
   override fun onStop(owner: Context) {
     require(statusInternal.compareAndSet(LifecycleStatus.STARTED, LifecycleStatus.CREATED)) {
-      "Unexpected lifecycle status: onStop called, but status is not RESUMED"
+      "Unexpected lifecycle status: onStop called, but status is not RESUMED: ${statusInternal.get()}"
     }
   }
 
   @CallSuper
   override fun onDestroy(owner: Context) {
     require(statusInternal.compareAndSet(LifecycleStatus.CREATED, LifecycleStatus.DESTROYED)) {
-      "Unexpected lifecycle status: onDestroy called, but status is not RESUMED"
+      "Unexpected lifecycle status: onDestroy called, but status is not RESUMED: ${statusInternal.get()}"
     }
   }
 }
diff --git a/app/src/main/res/values/strings_messages.xml b/app/src/main/res/values/strings_messages.xml
index d6c1be59ac4ff04eace35d245b7819a7f454c904..e49db9b8900a7949f50512847bb87d84333793f4 100644
--- a/app/src/main/res/values/strings_messages.xml
+++ b/app/src/main/res/values/strings_messages.xml
@@ -1,17 +1,17 @@
 <?xml version="1.0" encoding="utf-8" ?>
 <resources>
-  <string name="message_format_action">— %1$s%2$s %3$s</string>
-  <string name="message_format_notice">[%1$s%2$s] %3$s</string>
-  <string name="message_format_nick">%1$s%2$s is now known as %3$s%4$s</string>
-  <string name="message_format_nick_self">You are now known as %1$s%2$s</string>
-  <string name="message_format_mode">Mode %1$s by %2$s%3$s</string>
-  <string name="message_format_join">%1$s%2$s joined %3$s</string>
-  <string name="message_format_part_1">%1$s%2$s left</string>
-  <string name="message_format_part_2">%1$s%2$s left (%3$s)</string>
-  <string name="message_format_quit_1">%1$s%2$s quit</string>
-  <string name="message_format_quit_2">%1$s%2$s quit (%3$s)</string>
-  <string name="message_format_kick_1">%1$s was kicked by %2$s%3$s</string>
-  <string name="message_format_kick_2">%1$s was kicked by %2$s%3$s (%4$s)</string>
-  <string name="message_format_kill_1">%1$s was killed by %2$s%3$s</string>
-  <string name="message_format_kill_2">%1$s was killed by %2$s%3$s (%4$s)</string>
+  <string name="message_format_action">— %1$s %2$s</string>
+  <string name="message_format_notice">[%1$s] %2$s</string>
+  <string name="message_format_nick">%1$s is now known as %2$s</string>
+  <string name="message_format_nick_self">You are now known as %1$s</string>
+  <string name="message_format_mode">Mode %1$s by %2$s</string>
+  <string name="message_format_join">%1$s joined %2$s</string>
+  <string name="message_format_part_1">%1$s left</string>
+  <string name="message_format_part_2">%1$s left (%2$s)</string>
+  <string name="message_format_quit_1">%1$s quit</string>
+  <string name="message_format_quit_2">%1$s quit (%2$s)</string>
+  <string name="message_format_kick_1">%1$s was kicked by %2$s</string>
+  <string name="message_format_kick_2">%1$s was kicked by %2$s (%3$s)</string>
+  <string name="message_format_kill_1">%1$s was killed by %2$s</string>
+  <string name="message_format_kill_2">%1$s was killed by %2$s (%3$s)</string>
 </resources>