diff --git a/app/src/main/java/de/justjanne/quasseldroid/ui/routes/HomeRoute.kt b/app/src/main/java/de/justjanne/quasseldroid/ui/routes/HomeRoute.kt
deleted file mode 100644
index 165d193baa37d58b625e4c92afc7b9f694ce1f5a..0000000000000000000000000000000000000000
--- a/app/src/main/java/de/justjanne/quasseldroid/ui/routes/HomeRoute.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-package de.justjanne.quasseldroid.ui.routes
-
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
-import androidx.compose.material.Button
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.platform.LocalContext
-import androidx.navigation.NavController
-import de.justjanne.libquassel.protocol.syncables.state.BufferViewConfigState
-import de.justjanne.libquassel.protocol.util.combineLatest
-import de.justjanne.libquassel.protocol.util.flatMap
-import de.justjanne.quasseldroid.service.QuasselBackend
-import de.justjanne.quasseldroid.util.mapNullable
-import de.justjanne.quasseldroid.util.rememberFlow
-
-@Composable
-fun HomeRoute(backend: QuasselBackend, navController: NavController) {
-  val session = rememberFlow(null) {
-    backend.flow()
-      .mapNullable { it.session }
-  }
-
-  val bufferViewConfigs: List<BufferViewConfigState> = rememberFlow(emptyList()) {
-    backend.flow()
-      .mapNullable { it.session }
-      .flatMap()
-      .mapNullable { it.bufferViewManager }
-      .flatMap()
-      .mapNullable { it.bufferViewConfigs() }
-      .combineLatest()
-  }
-
-  val initStatus = rememberFlow(null) {
-    backend.flow()
-      .mapNullable { it.session }
-      .mapNullable { it.baseInitHandler }
-      .flatMap()
-  }
-
-  val context = LocalContext.current
-  Column {
-    Text("Side: ${session?.side}")
-    if (initStatus != null) {
-      val done = initStatus.total - initStatus.waiting.size
-      Text("Init: ${initStatus.started} $done/ ${initStatus.total}")
-    }
-    Button(onClick = { navController.navigate("coreInfo") }) {
-      Text("Core Info")
-    }
-    Button(onClick = {
-      backend.disconnect(context)
-      navController.navigate("login")
-    }) {
-      Text("Disconnect")
-    }
-    Text("BufferViewConfigs: ${bufferViewConfigs.size}")
-    LazyColumn {
-      items(bufferViewConfigs, key = BufferViewConfigState::bufferViewId) {
-        Row {
-          Text("${it.bufferViewId}: ${it.bufferViewName}")
-        }
-      }
-    }
-  }
-}
diff --git a/app/src/main/java/de/justjanne/quasseldroid/ui/theme/Color.kt b/app/src/main/java/de/justjanne/quasseldroid/ui/theme/Color.kt
deleted file mode 100644
index 269987abf40db3ffbc7e391015ca0c53b2a841bf..0000000000000000000000000000000000000000
--- a/app/src/main/java/de/justjanne/quasseldroid/ui/theme/Color.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package de.justjanne.quasseldroid.ui.theme
-
-import androidx.compose.ui.graphics.Color
-
-val Primary = Color(0xFF0a70c0)
-val PrimaryDark = Color(0xFF105a94)
-val Accent = Color(0xFFffaf3b)
-
-val Secure = Color(0xFF4CAF50)
-val PartiallySecure = Color(0xFFFFC107)
-val Insecure = Color(0xFFD32F2F)
diff --git a/app/src/main/java/de/justjanne/quasseldroid/MainActivity.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/MainActivity.kt
similarity index 100%
rename from app/src/main/java/de/justjanne/quasseldroid/MainActivity.kt
rename to app/src/main/kotlin/de/justjanne/quasseldroid/MainActivity.kt
diff --git a/app/src/main/java/de/justjanne/quasseldroid/QuasseldroidRouter.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/QuasseldroidRouter.kt
similarity index 100%
rename from app/src/main/java/de/justjanne/quasseldroid/QuasseldroidRouter.kt
rename to app/src/main/kotlin/de/justjanne/quasseldroid/QuasseldroidRouter.kt
diff --git a/app/src/main/java/de/justjanne/quasseldroid/messages/MessageBuffer.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/messages/MessageBuffer.kt
similarity index 100%
rename from app/src/main/java/de/justjanne/quasseldroid/messages/MessageBuffer.kt
rename to app/src/main/kotlin/de/justjanne/quasseldroid/messages/MessageBuffer.kt
diff --git a/app/src/main/java/de/justjanne/quasseldroid/messages/MessageStore.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/messages/MessageStore.kt
similarity index 100%
rename from app/src/main/java/de/justjanne/quasseldroid/messages/MessageStore.kt
rename to app/src/main/kotlin/de/justjanne/quasseldroid/messages/MessageStore.kt
diff --git a/app/src/main/java/de/justjanne/quasseldroid/sample/SampleConnectedClientProvider.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleConnectedClientProvider.kt
similarity index 100%
rename from app/src/main/java/de/justjanne/quasseldroid/sample/SampleConnectedClientProvider.kt
rename to app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleConnectedClientProvider.kt
diff --git a/app/src/main/java/de/justjanne/quasseldroid/sample/SampleCoreInfoProvider.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleCoreInfoProvider.kt
similarity index 100%
rename from app/src/main/java/de/justjanne/quasseldroid/sample/SampleCoreInfoProvider.kt
rename to app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleCoreInfoProvider.kt
diff --git a/app/src/main/java/de/justjanne/quasseldroid/service/ClientSessionWrapper.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/service/ClientSessionWrapper.kt
similarity index 100%
rename from app/src/main/java/de/justjanne/quasseldroid/service/ClientSessionWrapper.kt
rename to app/src/main/kotlin/de/justjanne/quasseldroid/service/ClientSessionWrapper.kt
diff --git a/app/src/main/java/de/justjanne/quasseldroid/service/ConnectionData.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/service/ConnectionData.kt
similarity index 100%
rename from app/src/main/java/de/justjanne/quasseldroid/service/ConnectionData.kt
rename to app/src/main/kotlin/de/justjanne/quasseldroid/service/ConnectionData.kt
diff --git a/app/src/main/java/de/justjanne/quasseldroid/service/QuasselBackend.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/service/QuasselBackend.kt
similarity index 100%
rename from app/src/main/java/de/justjanne/quasseldroid/service/QuasselBackend.kt
rename to app/src/main/kotlin/de/justjanne/quasseldroid/service/QuasselBackend.kt
diff --git a/app/src/main/java/de/justjanne/quasseldroid/service/QuasselBinder.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/service/QuasselBinder.kt
similarity index 100%
rename from app/src/main/java/de/justjanne/quasseldroid/service/QuasselBinder.kt
rename to app/src/main/kotlin/de/justjanne/quasseldroid/service/QuasselBinder.kt
diff --git a/app/src/main/java/de/justjanne/quasseldroid/service/QuasselRunner.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/service/QuasselRunner.kt
similarity index 100%
rename from app/src/main/java/de/justjanne/quasseldroid/service/QuasselRunner.kt
rename to app/src/main/kotlin/de/justjanne/quasseldroid/service/QuasselRunner.kt
diff --git a/app/src/main/java/de/justjanne/quasseldroid/service/QuasselService.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/service/QuasselService.kt
similarity index 100%
rename from app/src/main/java/de/justjanne/quasseldroid/service/QuasselService.kt
rename to app/src/main/kotlin/de/justjanne/quasseldroid/service/QuasselService.kt
diff --git a/app/src/main/java/de/justjanne/quasseldroid/ui/components/ConnectedClientCard.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/ConnectedClientCard.kt
similarity index 100%
rename from app/src/main/java/de/justjanne/quasseldroid/ui/components/ConnectedClientCard.kt
rename to app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/ConnectedClientCard.kt
diff --git a/app/src/main/java/de/justjanne/quasseldroid/ui/components/CoreInfoView.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/CoreInfoView.kt
similarity index 100%
rename from app/src/main/java/de/justjanne/quasseldroid/ui/components/CoreInfoView.kt
rename to app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/CoreInfoView.kt
diff --git a/app/src/main/java/de/justjanne/quasseldroid/ui/components/LoginView.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/LoginView.kt
similarity index 98%
rename from app/src/main/java/de/justjanne/quasseldroid/ui/components/LoginView.kt
rename to app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/LoginView.kt
index 2c1e30c759b8997ff616f266758815e2c5c484bd..d8a5cd6a2ab1c66f1f7c59479a6c4c92ede5ed2a 100644
--- a/app/src/main/java/de/justjanne/quasseldroid/ui/components/LoginView.kt
+++ b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/LoginView.kt
@@ -21,7 +21,7 @@ import androidx.compose.ui.text.input.TextFieldValue
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.dp
 import de.justjanne.quasseldroid.service.ConnectionData
-import de.justjanne.quasseldroid.util.TextFieldValueSaver
+import de.justjanne.quasseldroid.util.saver.TextFieldValueSaver
 import java.net.InetSocketAddress
 
 @Preview(name = "Login", showBackground = true)
diff --git a/app/src/main/java/de/justjanne/quasseldroid/ui/components/PasswordTextField.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/PasswordTextField.kt
similarity index 100%
rename from app/src/main/java/de/justjanne/quasseldroid/ui/components/PasswordTextField.kt
rename to app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/PasswordTextField.kt
diff --git a/app/src/main/java/de/justjanne/quasseldroid/ui/routes/CoreInfoRoute.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/routes/CoreInfoRoute.kt
similarity index 100%
rename from app/src/main/java/de/justjanne/quasseldroid/ui/routes/CoreInfoRoute.kt
rename to app/src/main/kotlin/de/justjanne/quasseldroid/ui/routes/CoreInfoRoute.kt
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
new file mode 100644
index 0000000000000000000000000000000000000000..436ed14ff573a75b60626c49f7d754a17680d149
--- /dev/null
+++ b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/routes/HomeRoute.kt
@@ -0,0 +1,220 @@
+package de.justjanne.quasseldroid.ui.routes
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.Button
+import androidx.compose.material.Text
+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.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.navigation.NavController
+import de.justjanne.bitflags.of
+import de.justjanne.libquassel.protocol.models.BufferInfo
+import de.justjanne.libquassel.protocol.models.Message
+import de.justjanne.libquassel.protocol.models.flags.BufferType
+import de.justjanne.libquassel.protocol.models.flags.MessageFlag
+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.flatMap
+import de.justjanne.libquassel.protocol.util.irc.HostmaskHelper
+import de.justjanne.quasseldroid.service.QuasselBackend
+import de.justjanne.quasseldroid.ui.theme.SenderColors
+import de.justjanne.quasseldroid.ui.theme.Typography
+import irc.SenderColorUtil
+import de.justjanne.quasseldroid.util.mapNullable
+import de.justjanne.quasseldroid.util.rememberFlow
+import de.justjanne.quasseldroid.util.saver.BufferIdSaver
+import kotlinx.coroutines.flow.map
+import org.threeten.bp.Instant
+import org.threeten.bp.ZoneId
+import org.threeten.bp.format.DateTimeFormatter
+import org.threeten.bp.format.FormatStyle
+
+@Composable
+fun HomeRoute(backend: QuasselBackend, navController: NavController) {
+  val session = rememberFlow(null) {
+    backend.flow()
+      .mapNullable { it.session }
+  }
+
+  val (buffer, setBuffer) = rememberSaveable(stateSaver = BufferIdSaver) {
+    mutableStateOf(BufferId(-1))
+  }
+
+  val messages: List<Message> = rememberFlow(emptyList()) {
+    backend.flow()
+      .mapNullable { it.messages }
+      .flatMap()
+      .mapNullable { it[buffer] }
+      .map { it?.messages.orEmpty() }
+  }
+
+  val initStatus = rememberFlow(null) {
+    backend.flow()
+      .mapNullable { it.session }
+      .mapNullable { it.baseInitHandler }
+      .flatMap()
+  }
+
+  val context = LocalContext.current
+  Column {
+    Text("Side: ${session?.side}")
+    if (initStatus != null) {
+      val done = initStatus.total - initStatus.waiting.size
+      Text("Init: ${initStatus.started} $done/ ${initStatus.total}")
+    }
+    Button(onClick = { navController.navigate("coreInfo") }) {
+      Text("Core Info")
+    }
+    Button(onClick = {
+      backend.disconnect(context)
+      navController.navigate("login")
+    }) {
+      Text("Disconnect")
+    }
+    LazyColumn {
+      items(messages, key = Message::messageId) {
+        MessageView(it)
+      }
+    }
+  }
+}
+
+
+private val formatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
+
+@Preview(name = "Message", showBackground = true)
+@Composable
+fun MessageView(
+  @PreviewParameter(SampleMessageProvider::class)
+  message: Message
+) {
+  val nick = HostmaskHelper.nick(message.sender)
+  val senderColor = SenderColors[SenderColorUtil.senderColor(nick)]
+
+  Column {
+    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))
+      Text(
+        message.realName,
+        modifier = Modifier.weight(1.0f),
+        style = Typography.body2,
+        color = Color(0x8A000000)
+      )
+    }
+    Row {
+      Text(
+        message.content,
+        modifier = Modifier.weight(1.0f),
+        style = Typography.body2
+      )
+      Text(
+        message.time
+          .atZone(ZoneId.systemDefault())
+          .format(formatter),
+        style = Typography.body2,
+        color = Color(0x8A000000),
+        fontSize = 12.sp
+      )
+    }
+  }
+}
+
+class SampleMessageProvider : PreviewParameterProvider<Message> {
+  override val values = sequenceOf(
+    Message(
+      messageId = MsgId(108062924),
+      bufferInfo = BufferInfo(
+        bufferId = BufferId(3746),
+        bufferName = "#quasseldroid",
+        networkId = NetworkId(4),
+        type = BufferType.of(BufferType.Channel)
+      ),
+      time = Instant.parse("2022-02-20T18:24:48.891Z"),
+      type = MessageType.of(MessageType.Quit),
+      sender = "CrazyBonz!~CrazyBonz@user/CrazyBonz",
+      senderPrefixes = "",
+      avatarUrl = "",
+      realName = "CrazyBonz",
+      content = "#quasseldroid",
+      flag = MessageFlag.of()
+    ),
+    Message(
+      messageId = MsgId(108063975),
+      bufferInfo = BufferInfo(
+        bufferId = BufferId(3746),
+        bufferName = "#quasseldroid",
+        networkId = NetworkId(4),
+        type = BufferType.of(BufferType.Channel)
+      ),
+      time = Instant.parse("2022-02-20T19:56:01.588Z"),
+      type = MessageType.of(MessageType.Plain),
+      sender = "winch!~AdminUser@185.14.29.13",
+      senderPrefixes = "",
+      avatarUrl = "",
+      realName = "Wincher,,,",
+      content = "Can i script some actions like in mIRC?",
+      flag = MessageFlag.of()
+    ),
+    Message(
+      messageId = MsgId(108064014),
+      bufferInfo = BufferInfo(
+        bufferId = BufferId(3746),
+        bufferName = "#quasseldroid",
+        networkId = NetworkId(4),
+        type = BufferType.of(BufferType.Channel)
+      ),
+      time = Instant.parse("2022-02-20T20:06:39.159Z"),
+      type = MessageType.of(MessageType.Quit),
+      sender = "mavhq!~quassel@mapp-14-b2-v4wan-161519-cust401.vm15.cable.virginm.net",
+      senderPrefixes = "",
+      avatarUrl = "",
+      realName = "mavhc",
+      content = "Quit: http://quassel-irc.org - Chat comfortably. Anywhere.",
+      flag = MessageFlag.of()
+    ),
+    Message(
+      messageId = MsgId(108064022),
+      bufferInfo = BufferInfo(
+        bufferId = BufferId(3746),
+        bufferName = "#quasseldroid",
+        networkId = NetworkId(4),
+        type = BufferType.of(BufferType.Channel)
+      ),
+      time = Instant.parse("2022-02-20T20:07:13.45Z"),
+      type = MessageType.of(MessageType.Join),
+      sender = "mavhq!~quassel@mapp-14-b2-v4wan-161519-cust401.vm15.cable.virginm.net",
+      senderPrefixes = "",
+      avatarUrl = "",
+      realName = "mavhc",
+      content = "#quasseldroid",
+      flag = MessageFlag.of()
+    )
+  )
+}
diff --git a/app/src/main/java/de/justjanne/quasseldroid/ui/routes/LoginRoute.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/routes/LoginRoute.kt
similarity index 100%
rename from app/src/main/java/de/justjanne/quasseldroid/ui/routes/LoginRoute.kt
rename to app/src/main/kotlin/de/justjanne/quasseldroid/ui/routes/LoginRoute.kt
diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/theme/Color.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/theme/Color.kt
new file mode 100644
index 0000000000000000000000000000000000000000..1460baf88003fe3b174eefe3c464b5439a7e732b
--- /dev/null
+++ b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/theme/Color.kt
@@ -0,0 +1,30 @@
+package de.justjanne.quasseldroid.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val Primary = Color(0xFF0a70c0)
+val PrimaryDark = Color(0xFF105a94)
+val Accent = Color(0xFFffaf3b)
+
+val Secure = Color(0xFF4CAF50)
+val PartiallySecure = Color(0xFFFFC107)
+val Insecure = Color(0xFFD32F2F)
+
+val SenderColors = listOf(
+  Color(0xFF_b80a73),
+  Color(0xFF_814dd5),
+  Color(0xFF_9f0b0b),
+  Color(0xFF_139f2f),
+  Color(0xFF_4e9c9f),
+  Color(0xFF_8b4a9f),
+  Color(0xFF_9f8669),
+  Color(0xFF_2b6b9f),
+  Color(0xFF_e00d8f),
+  Color(0xFF_995bfd),
+  Color(0xFF_c70e0e),
+  Color(0xFF_18c73e),
+  Color(0xFF_61c6c7),
+  Color(0xFF_af5ec7),
+  Color(0xFF_c7a782),
+  Color(0xFF_3684c7),
+)
diff --git a/app/src/main/java/de/justjanne/quasseldroid/ui/theme/Shape.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/theme/Shape.kt
similarity index 100%
rename from app/src/main/java/de/justjanne/quasseldroid/ui/theme/Shape.kt
rename to app/src/main/kotlin/de/justjanne/quasseldroid/ui/theme/Shape.kt
diff --git a/app/src/main/java/de/justjanne/quasseldroid/ui/theme/Theme.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/theme/Theme.kt
similarity index 100%
rename from app/src/main/java/de/justjanne/quasseldroid/ui/theme/Theme.kt
rename to app/src/main/kotlin/de/justjanne/quasseldroid/ui/theme/Theme.kt
diff --git a/app/src/main/java/de/justjanne/quasseldroid/ui/theme/Type.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/theme/Type.kt
similarity index 100%
rename from app/src/main/java/de/justjanne/quasseldroid/ui/theme/Type.kt
rename to app/src/main/kotlin/de/justjanne/quasseldroid/ui/theme/Type.kt
diff --git a/app/src/main/java/de/justjanne/quasseldroid/util/FlowExtensions.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/util/FlowExtensions.kt
similarity index 81%
rename from app/src/main/java/de/justjanne/quasseldroid/util/FlowExtensions.kt
rename to app/src/main/kotlin/de/justjanne/quasseldroid/util/FlowExtensions.kt
index 2dae17ec6cc6c41b68eaf19cb3c6500456dc68b7..9463f4a9dfc267139889ddc86316bba29ddaf000 100644
--- a/app/src/main/java/de/justjanne/quasseldroid/util/FlowExtensions.kt
+++ b/app/src/main/kotlin/de/justjanne/quasseldroid/util/FlowExtensions.kt
@@ -5,6 +5,7 @@ import androidx.compose.runtime.DisallowComposableCalls
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.remember
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.emitAll
 import kotlinx.coroutines.flow.transform
 
@@ -23,3 +24,8 @@ inline fun <T, R> Flow<T?>.flatMapLatestNullable(crossinline transform: suspend
 inline fun <T> rememberFlow(initial: T, calculation: @DisallowComposableCalls () -> Flow<T>): T {
   return remember(calculation).collectAsState(initial).value
 }
+
+@Composable
+inline fun <T> rememberFlow(calculation: @DisallowComposableCalls () -> StateFlow<T>): T {
+  return remember(calculation).collectAsState().value
+}
diff --git a/app/src/main/java/de/justjanne/quasseldroid/util/lifecycle/ContextualLifecycleObserver.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/util/lifecycle/ContextualLifecycleObserver.kt
similarity index 100%
rename from app/src/main/java/de/justjanne/quasseldroid/util/lifecycle/ContextualLifecycleObserver.kt
rename to app/src/main/kotlin/de/justjanne/quasseldroid/util/lifecycle/ContextualLifecycleObserver.kt
diff --git a/app/src/main/java/de/justjanne/quasseldroid/util/lifecycle/DefaultContextualLifecycleObserver.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/util/lifecycle/DefaultContextualLifecycleObserver.kt
similarity index 100%
rename from app/src/main/java/de/justjanne/quasseldroid/util/lifecycle/DefaultContextualLifecycleObserver.kt
rename to app/src/main/kotlin/de/justjanne/quasseldroid/util/lifecycle/DefaultContextualLifecycleObserver.kt
diff --git a/app/src/main/java/de/justjanne/quasseldroid/util/lifecycle/LifecycleStatus.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/util/lifecycle/LifecycleStatus.kt
similarity index 100%
rename from app/src/main/java/de/justjanne/quasseldroid/util/lifecycle/LifecycleStatus.kt
rename to app/src/main/kotlin/de/justjanne/quasseldroid/util/lifecycle/LifecycleStatus.kt
diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/util/saver/BufferIdSaver.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/util/saver/BufferIdSaver.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b68d97bb7aeb7810692814adddbc155b48bad92b
--- /dev/null
+++ b/app/src/main/kotlin/de/justjanne/quasseldroid/util/saver/BufferIdSaver.kt
@@ -0,0 +1,10 @@
+package de.justjanne.quasseldroid.util.saver
+
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.SaverScope
+import de.justjanne.libquassel.protocol.models.ids.BufferId
+
+object BufferIdSaver : Saver<BufferId, Int> {
+  override fun restore(value: Int) = BufferId(value)
+  override fun SaverScope.save(value: BufferId) = value.id
+}
diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/util/saver/IdentityIdSaver.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/util/saver/IdentityIdSaver.kt
new file mode 100644
index 0000000000000000000000000000000000000000..ac75e837f70378dc3bf1bb155b4e5b9a9f56fe28
--- /dev/null
+++ b/app/src/main/kotlin/de/justjanne/quasseldroid/util/saver/IdentityIdSaver.kt
@@ -0,0 +1,10 @@
+package de.justjanne.quasseldroid.util.saver
+
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.SaverScope
+import de.justjanne.libquassel.protocol.models.ids.IdentityId
+
+object IdentityIdSaver : Saver<IdentityId, Int> {
+  override fun restore(value: Int) = IdentityId(value)
+  override fun SaverScope.save(value: IdentityId) = value.id
+}
diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/util/saver/MsgIdSaver.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/util/saver/MsgIdSaver.kt
new file mode 100644
index 0000000000000000000000000000000000000000..a20623d46b0c22573aa6c15b44bf7d275dd2c4cd
--- /dev/null
+++ b/app/src/main/kotlin/de/justjanne/quasseldroid/util/saver/MsgIdSaver.kt
@@ -0,0 +1,10 @@
+package de.justjanne.quasseldroid.util.saver
+
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.SaverScope
+import de.justjanne.libquassel.protocol.models.ids.MsgId
+
+object MsgIdSaver : Saver<MsgId, Long> {
+  override fun restore(value: Long) = MsgId(value)
+  override fun SaverScope.save(value: MsgId) = value.id
+}
diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/util/saver/NetworkIdSaver.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/util/saver/NetworkIdSaver.kt
new file mode 100644
index 0000000000000000000000000000000000000000..a5eebb4628bab143d8b59ff302702fd2ced0541f
--- /dev/null
+++ b/app/src/main/kotlin/de/justjanne/quasseldroid/util/saver/NetworkIdSaver.kt
@@ -0,0 +1,10 @@
+package de.justjanne.quasseldroid.util.saver
+
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.SaverScope
+import de.justjanne.libquassel.protocol.models.ids.NetworkId
+
+object NetworkIdSaver : Saver<NetworkId, Int> {
+  override fun restore(value: Int) = NetworkId(value)
+  override fun SaverScope.save(value: NetworkId) = value.id
+}
diff --git a/app/src/main/java/de/justjanne/quasseldroid/util/TextFieldValueSaver.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/util/saver/TextFieldValueSaver.kt
similarity index 88%
rename from app/src/main/java/de/justjanne/quasseldroid/util/TextFieldValueSaver.kt
rename to app/src/main/kotlin/de/justjanne/quasseldroid/util/saver/TextFieldValueSaver.kt
index 31df4795c1dc552df254de815e6559e256981e86..e3b061560fa815ae916c9ab2c27a3964b1e646be 100644
--- a/app/src/main/java/de/justjanne/quasseldroid/util/TextFieldValueSaver.kt
+++ b/app/src/main/kotlin/de/justjanne/quasseldroid/util/saver/TextFieldValueSaver.kt
@@ -1,4 +1,4 @@
-package de.justjanne.quasseldroid.util
+package de.justjanne.quasseldroid.util.saver
 
 import androidx.compose.runtime.saveable.Saver
 import androidx.compose.runtime.saveable.SaverScope
diff --git a/app/src/main/kotlin/irc/CRCUtils.kt b/app/src/main/kotlin/irc/CRCUtils.kt
new file mode 100644
index 0000000000000000000000000000000000000000..494d9fa0c8266b13aad95502ea947e6453704d8f
--- /dev/null
+++ b/app/src/main/kotlin/irc/CRCUtils.kt
@@ -0,0 +1,44 @@
+package irc
+
+object CRCUtils {
+  fun qChecksum(data: ByteArray): Int {
+    var crc = 0xffff
+    val crcHighBitMask = 0x8000
+
+    for (b in data) {
+      val c = reflect(b.toInt(), 8)
+      var j = 0x80
+      while (j > 0) {
+        var highBit = crc and crcHighBitMask
+        crc = crc shl 1
+        if (c and j > 0) {
+          highBit = highBit xor crcHighBitMask
+        }
+        if (highBit > 0) {
+          crc = crc xor 0x1021
+        }
+        j = j shr 1
+      }
+    }
+
+    crc = reflect(crc, 16)
+    crc = crc xor 0xffff
+    crc = crc and 0xffff
+
+    return crc
+  }
+
+  private fun reflect(crc: Int, n: Int): Int {
+    var j = 1
+    var crcout = 0
+    var i = 1 shl n - 1
+    while (i > 0) {
+      if (crc and i > 0) {
+        crcout = crcout or j
+      }
+      j = j shl 1
+      i = i shr 1
+    }
+    return crcout
+  }
+}
diff --git a/app/src/main/kotlin/irc/SenderColorUtil.kt b/app/src/main/kotlin/irc/SenderColorUtil.kt
new file mode 100644
index 0000000000000000000000000000000000000000..6b12a9ced4e0686436d5c8a1f4273578aee00db1
--- /dev/null
+++ b/app/src/main/kotlin/irc/SenderColorUtil.kt
@@ -0,0 +1,13 @@
+package irc
+
+import java.util.*
+
+object SenderColorUtil {
+  fun senderColor(nick: String): Int {
+    return 0xf and CRCUtils.qChecksum(
+      nick.trimEnd('_')
+        .lowercase(Locale.ENGLISH)
+        .toByteArray(Charsets.ISO_8859_1)
+    )
+  }
+}
diff --git a/app/src/test/kotlin/de/kuschku/justjanne/quasseldroid/util/irc/SenderColorUtilTest.kt b/app/src/test/kotlin/de/kuschku/justjanne/quasseldroid/util/irc/SenderColorUtilTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..79fc84a549bcc15d64a1f2746f7081b1dd26ddba
--- /dev/null
+++ b/app/src/test/kotlin/de/kuschku/justjanne/quasseldroid/util/irc/SenderColorUtilTest.kt
@@ -0,0 +1,14 @@
+package de.kuschku.justjanne.quasseldroid.util.irc
+
+import irc.SenderColorUtil
+import org.junit.jupiter.api.Assertions.*
+import org.junit.jupiter.api.Test
+
+class SenderColorUtilTest {
+  @Test
+  fun verifyTestData() {
+    assertEquals(0x5, SenderColorUtil.senderColor("mavhq"))
+    assertEquals(0xa, SenderColorUtil.senderColor("winch"))
+    assertEquals(0xc, SenderColorUtil.senderColor("mack"))
+  }
+}
diff --git a/gradle/convention/src/main/kotlin/justjanne.java.gradle.kts b/gradle/convention/src/main/kotlin/justjanne.java.gradle.kts
index ca33bb8d73f0a3cf8acd17992d5893f926de85ed..ac5f5d66c509ff7b09059e92166b072fcf5f9cdf 100644
--- a/gradle/convention/src/main/kotlin/justjanne.java.gradle.kts
+++ b/gradle/convention/src/main/kotlin/justjanne.java.gradle.kts
@@ -1,10 +1,23 @@
+import gradle.kotlin.dsl.accessors._9f9f63157b527b37420ecbe9e569524a.testImplementation
+import gradle.kotlin.dsl.accessors._9f9f63157b527b37420ecbe9e569524a.testRuntimeOnly
+
 plugins {
   java
   id("justjanne.repositories")
 }
 
+dependencies {
+  testImplementation("org.junit.jupiter", "junit-jupiter-api", "5.8.2")
+  testImplementation("org.junit.jupiter", "junit-jupiter-params", "5.8.2")
+  testRuntimeOnly("org.junit.jupiter", "junit-jupiter-engine")
+}
+
 configure<JavaPluginExtension> {
   toolchain {
     languageVersion.set(JavaLanguageVersion.of(8))
   }
 }
+
+tasks.withType<Test> {
+  useJUnitPlatform()
+}
diff --git a/gradle/convention/src/main/kotlin/justjanne.kotlin.android.gradle.kts b/gradle/convention/src/main/kotlin/justjanne.kotlin.android.gradle.kts
index bb75c465bca06b8ede38082676fde791b72fc062..0739cbd609fba21de68b33e47e5761f2f3fc7dd2 100644
--- a/gradle/convention/src/main/kotlin/justjanne.kotlin.android.gradle.kts
+++ b/gradle/convention/src/main/kotlin/justjanne.kotlin.android.gradle.kts
@@ -1,5 +1,6 @@
 import gradle.kotlin.dsl.accessors._9f9f63157b527b37420ecbe9e569524a.implementation
 import gradle.kotlin.dsl.accessors._9f9f63157b527b37420ecbe9e569524a.testImplementation
+import gradle.kotlin.dsl.accessors._9f9f63157b527b37420ecbe9e569524a.testRuntimeOnly
 import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
@@ -11,9 +12,16 @@ plugins {
 
 dependencies {
   implementation("org.jetbrains.kotlin:kotlin-stdlib:1.6.10")
+
   implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0")
   implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0")
   testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0")
+
+  testImplementation("org.junit.jupiter", "junit-jupiter-api", "5.8.2")
+  testImplementation("org.junit.jupiter", "junit-jupiter-params", "5.8.2")
+  testRuntimeOnly("org.junit.jupiter", "junit-jupiter-engine")
+
+  testImplementation("org.jetbrains.kotlin:kotlin-test-junit5:1.6.10")
 }
 
 tasks.withType<KotlinCompile> {
@@ -26,3 +34,13 @@ tasks.withType<KotlinCompile> {
     jvmTarget = "1.8"
   }
 }
+
+tasks.withType<Test> {
+  useJUnitPlatform()
+}
+
+configure<JavaPluginExtension> {
+  toolchain {
+    languageVersion.set(JavaLanguageVersion.of(8))
+  }
+}
diff --git a/gradle/convention/src/main/kotlin/justjanne.kotlin.gradle.kts b/gradle/convention/src/main/kotlin/justjanne.kotlin.gradle.kts
index 04838711e0b75c7dcca6bbf3ccee950be7bfa082..888ec7207555d99f7b4dbfc5fcebcdf64c55d34e 100644
--- a/gradle/convention/src/main/kotlin/justjanne.kotlin.gradle.kts
+++ b/gradle/convention/src/main/kotlin/justjanne.kotlin.gradle.kts
@@ -2,7 +2,6 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
   id("justjanne.java")
-  id("justjanne.repositories")
   id("com.google.devtools.ksp")
   kotlin("jvm")
   kotlin("kapt")
@@ -10,8 +9,11 @@ plugins {
 
 dependencies {
   implementation("org.jetbrains.kotlin:kotlin-stdlib:1.6.10")
+
   implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0")
   testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0")
+
+  testImplementation("org.jetbrains.kotlin:kotlin-test-junit5:1.6.10")
 }
 
 tasks.withType<KotlinCompile> {