diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 9500fe193331b416a8e88152cad684e03d99ab08..fed896206e1d5f321a79dfea03aa236ff3daa668 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 c9f670522e200b15c25cdb7a9327831f020f9807..d72031943389e9531229a36a5101ccc475803f39 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 0000000000000000000000000000000000000000..3dca55290c23630e9f863ed511bc4e28f0d72f59
--- /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 0000000000000000000000000000000000000000..89a6de1d7d74ddb2968b6eee30a24dc127f9c520
--- /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 0000000000000000000000000000000000000000..5965a4375b0d15778e7ef7364ff0bc4835443437
--- /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 0000000000000000000000000000000000000000..150a45f50b82868101c895af5597d8a740abd3cc
--- /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 ef595a103c909fa2f7fcb0d15dea9d598479ac3c..96cf4cec85e89210531d8ba89bfbc69f4eb2318d 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 a52a88fb688d4f23b726be9c2c42cd5c553addb6..428e82c52cc87b0d04700e81c0361d0e89aac04f 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 6f2edb9d05f631263254dbbed2981a155e6e7ce7..fc63885b58f8d18f0036b76597bc85807b40f00a 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 993619d8cf654e533fe933a8d400491877881dfe..d3cf4eb3e76a7c861a9653438f2faa56b0b5de71 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 d8a5cd6a2ab1c66f1f7c59479a6c4c92ede5ed2a..fa3b1a6369a9de858f2dcd809bcff3da0229674c 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 c1424a357c50c082cc6f43a15ace87dba7d4f2f6..efe03af838ba32f88249adccfbbf3a8d27f8aaa5 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 0000000000000000000000000000000000000000..953f9e9f3d11c9fc2411a945a5dac6ec63f9c994
--- /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 0000000000000000000000000000000000000000..ae018dfd7fd07b832a3714e6be574137bc8a5d0e
--- /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 0000000000000000000000000000000000000000..0645ae772865aeb5a18ac62a9b4a2147dc14c9f2
--- /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 fba9e2fdbf112b744f1a1c527983d4366745c8ca..ac3c389fcc6379ee141522c1cd5447f2674757a2 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 e6ac4bd99df36f9a93c6c768bae8afb2c704893f..757b3118b93cedc5c8e2004e9326a8d79a0e5d56 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 09ee1010faed3e51ef9e5b80037ba306c2764672..0b9d1123c47e1cb739b763f025f868e1cbbf5573 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 789c050790c866e07f4230e71779dcaf485ce233..e40af49613b49a8bd0d53bc4a552303f4743f5d2 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 ff30e9f8fc0ef879e35d45aceaf9d66672af8354..0000000000000000000000000000000000000000
--- 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 0000000000000000000000000000000000000000..1deb227bc5f68597bdeb072e50773a151d4583bd
--- /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 0000000000000000000000000000000000000000..2ca165b979a1ef0ded6b91de69626325b917db31
--- /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 79fc84a549bcc15d64a1f2746f7081b1dd26ddba..e26136bb1c371cb5b6a3555807ed74ad953f901e 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 4258909f6bfd691639f75c909be300225557c341..6594147bcbd9fac76ccc084748be18c04d14569e 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"