From 35b5612950c8fab68779dba076b57897c73d823a Mon Sep 17 00:00:00 2001
From: Janne Mareike Koschinski <janne@kuschku.de>
Date: Wed, 2 Mar 2022 00:41:20 +0100
Subject: [PATCH] feat: implement basic message loading

---
 app/src/main/AndroidManifest.xml              |  4 +-
 .../quasseldroid/messages/MessageStore.kt     | 32 ++++---
 .../sample/SampleBooleanProvider.kt           |  7 ++
 .../sample/SampleLocalDateProvider.kt         | 10 ++
 .../sample/SampleMessageProvider.kt           | 87 +++++++++++++++++
 .../sample/SampleMessagesProvider.kt          | 10 ++
 .../sample/SampleSecurityLevelProvider.kt     |  2 +-
 .../service/ClientSessionWrapper.kt           |  4 +-
 .../quasseldroid/service/QuasselBackend.kt    |  1 -
 .../quasseldroid/service/QuasselBinder.kt     |  1 -
 .../quasseldroid/ui/components/LoginView.kt   | 13 ++-
 .../{MessageView.kt => MessageBaseView.kt}    | 21 +----
 .../ui/components/MessageDayChangeView.kt     | 92 ++++++++++++++++++
 .../quasseldroid/ui/components/MessageList.kt | 65 +++++++++++++
 .../ui/components/NewMessageView.kt           | 44 +++++++++
 .../ui/components/PasswordTextField.kt        |  2 -
 .../quasseldroid/ui/icons/AvatarIcon.kt       |  2 +-
 .../quasseldroid/ui/routes/HomeRoute.kt       | 93 +++++++++++++++----
 .../quasseldroid/ui/routes/LoginRoute.kt      | 42 ++++++++-
 .../ui/routes/SampleMessageProvider.kt        | 86 -----------------
 .../extensions/LazyListStateExtensions.kt     | 50 ++++++++++
 .../util/extensions/ListExtensions.kt         | 11 +++
 .../util/irc/SenderColorUtilTest.kt           |  2 +-
 gradle/libs.versions.toml                     |  2 +-
 24 files changed, 522 insertions(+), 161 deletions(-)
 create mode 100644 app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleBooleanProvider.kt
 create mode 100644 app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleLocalDateProvider.kt
 create mode 100644 app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleMessageProvider.kt
 create mode 100644 app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleMessagesProvider.kt
 rename app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/{MessageView.kt => MessageBaseView.kt} (84%)
 create mode 100644 app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/MessageDayChangeView.kt
 create mode 100644 app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/MessageList.kt
 create mode 100644 app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/NewMessageView.kt
 delete mode 100644 app/src/main/kotlin/de/justjanne/quasseldroid/ui/routes/SampleMessageProvider.kt
 create mode 100644 app/src/main/kotlin/de/justjanne/quasseldroid/util/extensions/LazyListStateExtensions.kt
 create mode 100644 app/src/main/kotlin/de/justjanne/quasseldroid/util/extensions/ListExtensions.kt

diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 9500fe193..fed896206 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -14,8 +14,8 @@
       android:name=".MainActivity"
       android:exported="true"
       android:launchMode="singleTask"
-      android:windowSoftInputMode="adjustResize"
-      android:theme="@style/Theme.AppCompat.DayNight.NoActionBar">
+      android:theme="@style/Theme.AppCompat.DayNight.NoActionBar"
+      android:windowSoftInputMode="adjustResize">
       <intent-filter>
         <action android:name="android.intent.action.MAIN" />
         <action android:name="android.intent.action.VIEW" />
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 c9f670522..d72031943 100644
--- a/app/src/main/kotlin/de/justjanne/quasseldroid/messages/MessageStore.kt
+++ b/app/src/main/kotlin/de/justjanne/quasseldroid/messages/MessageStore.kt
@@ -1,7 +1,11 @@
 package de.justjanne.quasseldroid.messages
 
+import androidx.collection.LruCache
+import de.justjanne.bitflags.none
 import de.justjanne.libquassel.client.syncables.ClientBacklogManager
 import de.justjanne.libquassel.protocol.models.Message
+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.util.StateHolder
@@ -41,51 +45,49 @@ class MessageStore(
     }
   }.launchIn(scope)
 
-  fun loadAround(bufferId: BufferId, messageId: MsgId) {
+  fun loadAround(bufferId: BufferId, messageId: MsgId, limit: Int) {
     scope.launch {
       state.update { messages ->
         val (before, after) = listOf(
-          async { backlogManager.backlog(bufferId, messageId) },
-          async { backlogManager.backlogForward(bufferId, messageId) },
-        ).awaitAll()
+          backlogManager.backlog(bufferId, last = messageId, limit = limit)
+            .mapNotNull { it.into<Message>() },
+          backlogManager.backlogForward(bufferId, first = messageId, limit = limit - 1)
+            .mapNotNull { it.into<Message>() },
+        )
 
         val updated = MessageBuffer(
           atEnd = false,
-          messages = (before + after)
-            .mapNotNull { it.into<Message>() }
-            .sortedBy { it.messageId }
+          messages = (before + after).distinct().sortedBy { it.messageId }
         )
         messages + Pair(bufferId, updated)
       }
     }
   }
 
-  fun loadBefore(bufferId: BufferId) {
+  fun loadBefore(bufferId: BufferId, limit: Int) {
     scope.launch {
       state.update { messages ->
         val buffer = messages[bufferId] ?: MessageBuffer(true, emptyList())
         val messageId = buffer.messages.firstOrNull()?.messageId ?: MsgId(-1)
-        val data = backlogManager.backlog(bufferId, messageId)
+        val data = backlogManager.backlog(bufferId, last = messageId, limit = limit)
           .mapNotNull { it.into<Message>() }
         val updated = buffer.copy(
-          messages = (buffer.messages + data)
-            .sortedBy { it.messageId }
+          messages = (buffer.messages + data).distinct().sortedBy { it.messageId }
         )
         messages + Pair(bufferId, updated)
       }
     }
   }
 
-  fun loadAfter(bufferId: BufferId) {
+  fun loadAfter(bufferId: BufferId, limit: Int) {
     scope.launch {
       state.update { messages ->
         val buffer = messages[bufferId] ?: MessageBuffer(true, emptyList())
         val messageId = buffer.messages.lastOrNull()?.messageId ?: MsgId(-1)
-        val data = backlogManager.backlogForward(bufferId, messageId)
+        val data = backlogManager.backlogForward(bufferId, first = messageId, limit = limit)
           .mapNotNull { it.into<Message>() }
         val updated = buffer.copy(
-          messages = (buffer.messages + data)
-            .sortedBy { it.messageId }
+          messages = (buffer.messages + data).distinct().sortedBy { it.messageId }
         )
         messages + Pair(bufferId, updated)
       }
diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleBooleanProvider.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleBooleanProvider.kt
new file mode 100644
index 000000000..3dca55290
--- /dev/null
+++ b/app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleBooleanProvider.kt
@@ -0,0 +1,7 @@
+package de.justjanne.quasseldroid.sample
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+
+class SampleBooleanProvider : PreviewParameterProvider<Boolean> {
+  override val values = sequenceOf(false, true)
+}
diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleLocalDateProvider.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleLocalDateProvider.kt
new file mode 100644
index 000000000..89a6de1d7
--- /dev/null
+++ b/app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleLocalDateProvider.kt
@@ -0,0 +1,10 @@
+package de.justjanne.quasseldroid.sample
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import org.threeten.bp.LocalDate
+
+class SampleLocalDateProvider : PreviewParameterProvider<LocalDate> {
+  override val values = sequenceOf(
+    LocalDate.of(2022, 2, 28)
+  )
+}
diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleMessageProvider.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleMessageProvider.kt
new file mode 100644
index 000000000..5965a4375
--- /dev/null
+++ b/app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleMessageProvider.kt
@@ -0,0 +1,87 @@
+package de.justjanne.quasseldroid.sample
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+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 org.threeten.bp.Instant
+
+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/kotlin/de/justjanne/quasseldroid/sample/SampleMessagesProvider.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleMessagesProvider.kt
new file mode 100644
index 000000000..150a45f50
--- /dev/null
+++ b/app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleMessagesProvider.kt
@@ -0,0 +1,10 @@
+package de.justjanne.quasseldroid.sample
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import de.justjanne.libquassel.protocol.models.Message
+
+class SampleMessagesProvider : PreviewParameterProvider<List<Message>> {
+  override val values = sequenceOf(
+    SampleMessageProvider().values.toList()
+  )
+}
diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleSecurityLevelProvider.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleSecurityLevelProvider.kt
index ef595a103..96cf4cec8 100644
--- a/app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleSecurityLevelProvider.kt
+++ b/app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleSecurityLevelProvider.kt
@@ -3,7 +3,7 @@ package de.justjanne.quasseldroid.sample
 import androidx.compose.ui.tooling.preview.PreviewParameterProvider
 import de.justjanne.quasseldroid.model.SecurityLevel
 
-class SampleSecurityLevelProvider: PreviewParameterProvider<SecurityLevel> {
+class SampleSecurityLevelProvider : PreviewParameterProvider<SecurityLevel> {
   override val values = sequenceOf(
     SecurityLevel.SECURE,
     SecurityLevel.UNVERIFIED,
diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/service/ClientSessionWrapper.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/service/ClientSessionWrapper.kt
index a52a88fb6..428e82c52 100644
--- a/app/src/main/kotlin/de/justjanne/quasseldroid/service/ClientSessionWrapper.kt
+++ b/app/src/main/kotlin/de/justjanne/quasseldroid/service/ClientSessionWrapper.kt
@@ -4,6 +4,6 @@ import de.justjanne.libquassel.client.session.ClientSession
 import de.justjanne.quasseldroid.messages.MessageStore
 
 data class ClientSessionWrapper(
-    val session: ClientSession,
-    val messages: MessageStore
+  val session: ClientSession,
+  val messages: MessageStore
 )
diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/service/QuasselBackend.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/service/QuasselBackend.kt
index 6f2edb9d0..fc63885b5 100644
--- a/app/src/main/kotlin/de/justjanne/quasseldroid/service/QuasselBackend.kt
+++ b/app/src/main/kotlin/de/justjanne/quasseldroid/service/QuasselBackend.kt
@@ -5,7 +5,6 @@ import android.content.Context
 import android.content.ServiceConnection
 import android.os.IBinder
 import android.util.Log
-import de.justjanne.libquassel.client.session.ClientSession
 import de.justjanne.libquassel.protocol.util.StateHolder
 import de.justjanne.libquassel.protocol.util.flatMap
 import de.justjanne.quasseldroid.BuildConfig
diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/service/QuasselBinder.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/service/QuasselBinder.kt
index 993619d8c..d3cf4eb3e 100644
--- a/app/src/main/kotlin/de/justjanne/quasseldroid/service/QuasselBinder.kt
+++ b/app/src/main/kotlin/de/justjanne/quasseldroid/service/QuasselBinder.kt
@@ -1,7 +1,6 @@
 package de.justjanne.quasseldroid.service
 
 import android.os.Binder
-import de.justjanne.libquassel.client.session.ClientSession
 import de.justjanne.libquassel.protocol.util.StateHolder
 import kotlinx.coroutines.flow.StateFlow
 
diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/LoginView.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/LoginView.kt
index d8a5cd6a2..fa3b1a636 100644
--- a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/LoginView.kt
+++ b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/LoginView.kt
@@ -26,18 +26,21 @@ import java.net.InetSocketAddress
 
 @Preview(name = "Login", showBackground = true)
 @Composable
-fun LoginView(onLogin: (ConnectionData) -> Unit = {}) {
+fun LoginView(
+  default: ConnectionData? = null,
+  onLogin: (ConnectionData) -> Unit = {}
+) {
   val (host, setHost) = rememberSaveable(stateSaver = TextFieldValueSaver) {
-    mutableStateOf(TextFieldValue())
+    mutableStateOf(TextFieldValue(default?.address?.hostString ?: ""))
   }
   val (port, setPort) = rememberSaveable(stateSaver = TextFieldValueSaver) {
-    mutableStateOf(TextFieldValue("4242"))
+    mutableStateOf(TextFieldValue(default?.address?.port?.toString() ?: "4242"))
   }
   val (username, setUsername) = rememberSaveable(stateSaver = TextFieldValueSaver) {
-    mutableStateOf(TextFieldValue())
+    mutableStateOf(TextFieldValue(default?.username ?: ""))
   }
   val (password, setPassword) = rememberSaveable(stateSaver = TextFieldValueSaver) {
-    mutableStateOf(TextFieldValue())
+    mutableStateOf(TextFieldValue(default?.password ?: ""))
   }
 
   val focusManager = LocalFocusManager.current
diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/MessageView.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/MessageBaseView.kt
similarity index 84%
rename from app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/MessageView.kt
rename to app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/MessageBaseView.kt
index c1424a357..efe03af83 100644
--- a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/MessageView.kt
+++ b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/MessageBaseView.kt
@@ -15,15 +15,12 @@ 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.tooling.preview.Preview
-import androidx.compose.ui.tooling.preview.PreviewParameter
 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.routes.SampleMessageProvider
 import de.justjanne.quasseldroid.ui.theme.QuasselTheme
 import de.justjanne.quasseldroid.ui.theme.Typography
 import irc.SenderColorUtil
@@ -33,20 +30,6 @@ import org.threeten.bp.format.FormatStyle
 
 private val formatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
 
-@Preview(name = "Message", showBackground = true)
-@Composable
-fun MessageView(
-  @PreviewParameter(SampleMessageProvider::class)
-  message: Message
-) {
-  MessageBaseView(message, false, 32.dp) {
-    Text(
-      message.content,
-      style = Typography.body2,
-    )
-  }
-}
-
 @Composable
 fun MessageBaseView(
   message: Message,
@@ -57,9 +40,9 @@ fun MessageBaseView(
   val nick = HostmaskHelper.nick(message.sender)
   val senderColor = QuasselTheme.sender.colors[SenderColorUtil.senderColor(nick)]
 
-  Row {
+  Row(modifier = Modifier.padding(2.dp)) {
     if (!followUp) {
-      AvatarIcon(nick, null, modifier = Modifier.padding(2.dp))
+      AvatarIcon(nick, null, modifier = Modifier.padding(vertical = 2.dp))
       Spacer(Modifier.width(4.dp))
     } else {
       Spacer(Modifier.width(avatarSize + 8.dp))
diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/MessageDayChangeView.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/MessageDayChangeView.kt
new file mode 100644
index 000000000..953f9e9f3
--- /dev/null
+++ b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/MessageDayChangeView.kt
@@ -0,0 +1,92 @@
+package de.justjanne.quasseldroid.ui.components
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.ContentAlpha
+import androidx.compose.material.Divider
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import de.justjanne.quasseldroid.ui.theme.QuasselTheme
+import de.justjanne.quasseldroid.ui.theme.Typography
+import org.threeten.bp.LocalDate
+import org.threeten.bp.format.DateTimeFormatter
+import org.threeten.bp.format.FormatStyle
+
+private val formatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
+
+@Preview(name = "Day Change", showBackground = true)
+@Composable
+private fun MessageDayChangePreview() {
+  Column {
+    MessageDayChangeView(LocalDate.of(2018, 9, 7), isNew = false)
+    MessageDayChangeView(LocalDate.of(2018, 9, 7), isNew = true)
+  }
+}
+
+@Composable
+fun MessageDayChangeView(
+  date: LocalDate,
+  isNew: Boolean
+) {
+  val foregroundColor =
+    if (isNew) QuasselTheme.security.insecure
+    else MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled)
+
+  Row(modifier = Modifier.padding(vertical = 4.dp)) {
+    Spacer(Modifier.width(8.dp))
+    Divider(
+      color = foregroundColor,
+      modifier = Modifier
+        .weight(1.0f)
+        .height(1.dp)
+        .align(Alignment.CenterVertically)
+    )
+    Spacer(Modifier.width(4.dp))
+    Text(
+      date.format(formatter),
+      modifier = Modifier
+        .align(Alignment.CenterVertically),
+      style = Typography.body2,
+      fontWeight = FontWeight.Medium,
+    )
+    Spacer(Modifier.width(4.dp))
+    Row(
+      modifier = Modifier
+        .weight(1.0f)
+        .align(Alignment.CenterVertically)
+    ) {
+      Divider(
+        color = foregroundColor,
+        modifier = Modifier
+          .weight(1.0f)
+          .height(1.dp)
+          .align(Alignment.CenterVertically)
+      )
+      if (isNew) {
+        Spacer(Modifier.width(4.dp))
+        Text(
+          "New",
+          modifier = Modifier
+            .align(Alignment.CenterVertically),
+          color = QuasselTheme.security.insecure,
+          style = Typography.body2,
+          fontSize = 12.sp,
+          fontWeight = FontWeight.Bold,
+        )
+      }
+    }
+    Spacer(Modifier.width(8.dp))
+  }
+}
+
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
new file mode 100644
index 000000000..ae018dfd7
--- /dev/null
+++ b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/MessageList.kt
@@ -0,0 +1,65 @@
+package de.justjanne.quasseldroid.ui.components
+
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
+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.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.unit.dp
+import de.justjanne.libquassel.protocol.models.Message
+import de.justjanne.libquassel.protocol.models.ids.MsgId
+import de.justjanne.quasseldroid.sample.SampleMessagesProvider
+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 org.threeten.bp.ZoneId
+
+@Preview(name = "Messages", showBackground = true)
+@Composable
+fun MessageList(
+  @PreviewParameter(SampleMessagesProvider::class)
+  messages: List<Message>,
+  listState: LazyListState = rememberLazyListState(),
+  markerLine: MsgId = MsgId(-1),
+  buffer: Int = 0,
+  onLoadAtStart: () -> Unit = { },
+  onLoadAtEnd: () -> Unit = { },
+) {
+  LazyColumn(state = listState) {
+    itemsIndexed(messages, key = { _, item -> item.messageId }) { index, message ->
+      val prev = messages.getPrevious(index)
+      val prevDate = prev?.time?.atZone(ZoneId.systemDefault())?.toLocalDate()
+      val messageDate = message.time.atZone(ZoneId.systemDefault()).toLocalDate()
+
+      val followUp = prev != null &&
+        message.sender == prev.sender &&
+        message.senderPrefixes == prev.senderPrefixes &&
+        message.realName == prev.realName &&
+        message.avatarUrl == prev.avatarUrl
+
+      val isNew = prev != null &&
+        prev.messageId <= markerLine &&
+        message.messageId > markerLine
+
+      if (prevDate == null || !messageDate.isEqual(prevDate)) {
+        MessageDayChangeView(messageDate, isNew)
+      } else if (isNew) {
+        NewMessageView()
+      }
+
+      MessageBaseView(message, followUp, 32.dp) {
+        Text(
+          message.content,
+          style = Typography.body2,
+        )
+      }
+    }
+  }
+
+  listState.OnTopReached(buffer = buffer, onLoadMore = onLoadAtStart)
+  listState.OnBottomReached(buffer = buffer, onLoadMore = onLoadAtEnd)
+}
diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/NewMessageView.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/NewMessageView.kt
new file mode 100644
index 000000000..0645ae772
--- /dev/null
+++ b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/NewMessageView.kt
@@ -0,0 +1,44 @@
+package de.justjanne.quasseldroid.ui.components
+
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.Divider
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import de.justjanne.quasseldroid.ui.theme.QuasselTheme
+import de.justjanne.quasseldroid.ui.theme.Typography
+
+@Preview(name = "New Message", showBackground = true)
+@Composable
+fun NewMessageView() {
+    Row(modifier = Modifier.padding(vertical = 4.dp)) {
+        Spacer(Modifier.width(8.dp))
+        Divider(
+            color = QuasselTheme.security.insecure,
+            modifier = Modifier
+                .height(1.dp)
+                .weight(1.0f)
+                .align(Alignment.CenterVertically),
+        )
+        Spacer(Modifier.width(4.dp))
+        Text(
+            "New",
+            modifier = Modifier
+                .align(Alignment.CenterVertically),
+            color = QuasselTheme.security.insecure,
+            style = Typography.body2,
+            fontSize = 12.sp,
+            fontWeight = FontWeight.Bold,
+        )
+        Spacer(Modifier.width(8.dp))
+    }
+}
diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/PasswordTextField.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/PasswordTextField.kt
index fba9e2fdb..ac3c389fc 100644
--- a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/PasswordTextField.kt
+++ b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/PasswordTextField.kt
@@ -18,14 +18,12 @@ import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.text.input.KeyboardType
 import androidx.compose.ui.text.input.PasswordVisualTransformation
 import androidx.compose.ui.text.input.TextFieldValue
 import androidx.compose.ui.text.input.VisualTransformation
 import androidx.compose.ui.tooling.preview.Preview
-import de.justjanne.quasseldroid.R
 
 @Preview(name = "PasswordTextField", showBackground = true)
 @Composable
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 e6ac4bd99..757b3118b 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
@@ -35,7 +35,7 @@ fun AvatarIcon(
   size: Dp = 32.dp
 ) {
   val senderColor = QuasselTheme.sender.colors[SenderColorUtil.senderColor(nick)]
-  val initial = nick.asSequence().map { it.uppercase(Locale.ENGLISH) }.first()
+  val initial = nick.firstOrNull()?.uppercase(Locale.ENGLISH) ?: "?"
   val fontSize = with(LocalDensity.current) { (size.toPx() * 0.67f).toSp() }
 
   Surface(
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 09ee1010f..0b9d1123c 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,25 +1,34 @@
 package de.justjanne.quasseldroid.ui.routes
 
+import androidx.compose.foundation.horizontalScroll
 import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
+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.MessageView
+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.BufferIdSaver
+import de.justjanne.quasseldroid.util.saver.TextFieldValueSaver
 import kotlinx.coroutines.flow.map
 
+private const val limit = 20
+
 @Composable
 fun HomeRoute(backend: QuasselBackend, navController: NavController) {
   val session = rememberFlow(null) {
@@ -27,18 +36,40 @@ fun HomeRoute(backend: QuasselBackend, navController: NavController) {
       .mapNullable { it.session }
   }
 
-  val (buffer, setBuffer) = rememberSaveable(stateSaver = BufferIdSaver) {
-    mutableStateOf(BufferId(-1))
+  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 }
   }
 
   val messages: List<Message> = rememberFlow(emptyList()) {
     backend.flow()
       .mapNullable { it.messages }
       .flatMap()
-      .mapNullable { it[buffer] }
+      .mapNullable { it[bufferId] }
       .map { it?.messages.orEmpty() }
   }
 
+  val markerLine: MsgId? = rememberFlow(null) {
+    backend.flow()
+      .mapNullable { it.session }
+      .flatMap()
+      .mapNullable { it.bufferSyncer }
+      .flatMap()
+      .mapNullable { it.markerLines[bufferId] }
+  }
+
   val initStatus = rememberFlow(null) {
     backend.flow()
       .mapNullable { it.session }
@@ -47,26 +78,50 @@ fun HomeRoute(backend: QuasselBackend, navController: NavController) {
   }
 
   val context = LocalContext.current
+  val buttonScrollState = rememberScrollState()
+
   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)
+    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("…")
       }
     }
+    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) }
+    )
   }
 }
 
diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/routes/LoginRoute.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/routes/LoginRoute.kt
index 789c05079..e40af4961 100644
--- a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/routes/LoginRoute.kt
+++ b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/routes/LoginRoute.kt
@@ -1,17 +1,49 @@
 package de.justjanne.quasseldroid.ui.routes
 
+import android.content.Context
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.platform.LocalContext
+import androidx.core.content.edit
 import androidx.navigation.NavController
+import de.justjanne.quasseldroid.service.ConnectionData
 import de.justjanne.quasseldroid.service.QuasselBackend
 import de.justjanne.quasseldroid.ui.components.LoginView
+import java.net.InetSocketAddress
 
 @Composable
 fun LoginRoute(backend: QuasselBackend, navController: NavController) {
   val context = LocalContext.current
-    LoginView(onLogin = {
-        if (backend.login(context, it)) {
-            navController.navigate("home")
-        }
-    })
+  LoginView(default = loadConnectionData(context)) {
+    if (backend.login(context, it)) {
+      navController.navigate("home")
+      saveConnectionData(context, it)
+    }
+  }
+}
+
+fun loadConnectionData(context: Context): ConnectionData? {
+  val sharedPreferences = context.getSharedPreferences("login", Context.MODE_PRIVATE)
+  return ConnectionData(
+    address = InetSocketAddress.createUnresolved(
+      sharedPreferences.getString("host", null)
+        ?: return null,
+      sharedPreferences.getInt("port", 0)
+        .takeIf { it > 0 }
+        ?: return null,
+    ),
+    username = sharedPreferences.getString("username", null)
+      ?: return null,
+    password = sharedPreferences.getString("password", null)
+      ?: return null
+  )
+}
+
+fun saveConnectionData(context: Context, connectionData: ConnectionData) {
+  val sharedPreferences = context.getSharedPreferences("login", Context.MODE_PRIVATE)
+  sharedPreferences.edit {
+    putString("host", connectionData.address.hostString)
+    putInt("port", connectionData.address.port)
+    putString("username", connectionData.username)
+    putString("password", connectionData.password)
+  }
 }
diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/routes/SampleMessageProvider.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/routes/SampleMessageProvider.kt
deleted file mode 100644
index ff30e9f8f..000000000
--- a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/routes/SampleMessageProvider.kt
+++ /dev/null
@@ -1,86 +0,0 @@
-package de.justjanne.quasseldroid.ui.routes
-
-import androidx.compose.ui.tooling.preview.PreviewParameterProvider
-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 org.threeten.bp.Instant
-
-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/kotlin/de/justjanne/quasseldroid/util/extensions/LazyListStateExtensions.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/util/extensions/LazyListStateExtensions.kt
new file mode 100644
index 000000000..1deb227bc
--- /dev/null
+++ b/app/src/main/kotlin/de/justjanne/quasseldroid/util/extensions/LazyListStateExtensions.kt
@@ -0,0 +1,50 @@
+package de.justjanne.quasseldroid.util.extensions
+
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.snapshotFlow
+
+@Composable
+fun LazyListState.OnBottomReached(
+  buffer: Int = 0,
+  onLoadMore: () -> Unit
+) {
+  require(buffer >= 0) { "buffer cannot be negative, but was $buffer" }
+
+  val shouldLoadMore = remember {
+    derivedStateOf {
+      val lastVisibleItem = layoutInfo.visibleItemsInfo.lastOrNull()
+        ?: return@derivedStateOf true
+      lastVisibleItem.index >= layoutInfo.totalItemsCount - 1 - buffer
+    }
+  }
+
+  LaunchedEffect(shouldLoadMore) {
+    snapshotFlow { shouldLoadMore.value }
+      .collect { if (it) onLoadMore() }
+  }
+}
+
+@Composable
+fun LazyListState.OnTopReached(
+  buffer: Int = 0,
+  onLoadMore: () -> Unit
+) {
+  require(buffer >= 0) { "buffer cannot be negative, but was $buffer" }
+
+  val shouldLoadMore = remember {
+    derivedStateOf {
+      val lastVisibleItem = layoutInfo.visibleItemsInfo.firstOrNull()
+        ?: return@derivedStateOf true
+      lastVisibleItem.index <= buffer
+    }
+  }
+
+  LaunchedEffect(shouldLoadMore) {
+    snapshotFlow { shouldLoadMore.value }
+      .collect { if (it) onLoadMore() }
+  }
+}
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
new file mode 100644
index 000000000..2ca165b97
--- /dev/null
+++ b/app/src/main/kotlin/de/justjanne/quasseldroid/util/extensions/ListExtensions.kt
@@ -0,0 +1,11 @@
+package de.justjanne.quasseldroid.util.extensions
+
+fun <T> List<T>.getSafe(index: Int): T? =
+  if (index !in 0..size) null
+  else get(index)
+
+fun <T> List<T>.getPrevious(index: Int): T? =
+  getSafe(index - 1)
+
+fun <T> List<T>.getNext(index: Int): T? =
+  getSafe(index + 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
index 79fc84a54..e26136bb1 100644
--- 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
@@ -1,7 +1,7 @@
 package de.kuschku.justjanne.quasseldroid.util.irc
 
 import irc.SenderColorUtil
-import org.junit.jupiter.api.Assertions.*
+import org.junit.jupiter.api.Assertions.assertEquals
 import org.junit.jupiter.api.Test
 
 class SenderColorUtilTest {
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 4258909f6..6594147bc 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,5 +1,5 @@
 [versions]
-libquassel = "0.9.0"
+libquassel = "0.9.2"
 androidx-activity = "1.4.0"
 androidx-appcompat = "1.4.1"
 androidx-compose = "1.1.1"
-- 
GitLab