From ca89fa4b1875c849ac8d6fda37d86b83ede316bc Mon Sep 17 00:00:00 2001
From: Janne Koschinski <janne@kuschku.de>
Date: Wed, 21 Feb 2018 12:40:23 +0100
Subject: [PATCH] Implement markerline display

---
 app/build.gradle.kts                          |  2 +
 .../ui/chat/BufferViewConfigFragment.kt       |  2 +-
 .../ui/chat/FormattedMessage.kt               |  3 +-
 .../quasseldroid_ng/ui/chat/MessageAdapter.kt | 38 +++++++---
 .../ui/chat/MessageListFragment.kt            | 21 +++++-
 .../ui/chat/MessageRenderer.kt                |  3 +-
 .../ui/chat/QuasselMessageRenderer.kt         | 45 ++++++++----
 .../ui/chat/QuasselMessageViewHolder.kt       |  3 +
 .../ui/chat/ToolbarFragment.kt                |  2 +-
 .../util/helper/LiveDataHelper.kt             | 12 +--
 .../res/layout/widget_chatmessage_action.xml  | 64 +++++++++-------
 .../res/layout/widget_chatmessage_error.xml   | 31 +++++---
 .../res/layout/widget_chatmessage_info.xml    | 31 +++++---
 .../res/layout/widget_chatmessage_notice.xml  | 31 +++++---
 .../layout/widget_chatmessage_placeholder.xml | 10 ++-
 .../res/layout/widget_chatmessage_plain.xml   | 30 +++++---
 .../res/layout/widget_chatmessage_server.xml  | 31 +++++---
 app/src/main/res/values/attrs.xml             |  3 +-
 app/src/main/res/values/themes_quassel.xml    |  2 +
 lib/build.gradle.kts                          |  2 +
 .../quassel/syncables/BufferSyncer.kt         | 73 +++++++++++++------
 21 files changed, 302 insertions(+), 137 deletions(-)

diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index e4bad8b79..2831cb257 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -137,6 +137,7 @@ dependencies {
   androidTestImplementation("com.android.support.test.espresso", "espresso-core", "3.0.1")
 }
 
+/*
 tasks.withType(KotlinCompile::class.java) {
   kotlinOptions {
     freeCompilerArgs = listOf(
@@ -145,6 +146,7 @@ tasks.withType(KotlinCompile::class.java) {
     )
   }
 }
+*/
 
 fun cmd(vararg command: String) = try {
   val stdOut = ByteArrayOutputStream()
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/BufferViewConfigFragment.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/BufferViewConfigFragment.kt
index 23d8508c7..f0de1e7bd 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/BufferViewConfigFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/BufferViewConfigFragment.kt
@@ -92,7 +92,7 @@ class BufferViewConfigFragment : ServiceBoundFragment() {
         config.live_buffers.switchMap { ids ->
           val bufferSyncer = manager.bufferSyncer
           if (bufferSyncer != null) {
-            bufferSyncer.live_bufferInfos.switchMap {
+            bufferSyncer.liveBufferInfos().switchMap {
               Observable.combineLatest(
                 ids.mapNotNull { id ->
                   bufferSyncer.bufferInfo(id)
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/FormattedMessage.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/FormattedMessage.kt
index d245270de..68f1c9748 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/FormattedMessage.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/FormattedMessage.kt
@@ -3,5 +3,6 @@ package de.kuschku.quasseldroid_ng.ui.chat
 class FormattedMessage(
   val id: Int,
   val time: CharSequence,
-  val content: CharSequence
+  val content: CharSequence,
+  val markerline: Boolean
 )
\ No newline at end of file
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/MessageAdapter.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/MessageAdapter.kt
index 547b5601f..02e301411 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/MessageAdapter.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/MessageAdapter.kt
@@ -1,23 +1,35 @@
 package de.kuschku.quasseldroid_ng.ui.chat
 
+import android.arch.lifecycle.LiveData
 import android.arch.paging.PagedListAdapter
 import android.content.Context
+import android.support.v7.recyclerview.extensions.DiffCallback
 import android.util.LruCache
 import android.view.LayoutInflater
 import android.view.ViewGroup
-import de.kuschku.libquassel.protocol.Message_Flag
-import de.kuschku.libquassel.protocol.Message_Flags
-import de.kuschku.libquassel.protocol.Message_Type
-import de.kuschku.libquassel.protocol.Message_Types
+import de.kuschku.libquassel.protocol.*
 import de.kuschku.libquassel.util.hasFlag
 import de.kuschku.quasseldroid_ng.persistence.QuasselDatabase
+import de.kuschku.quasseldroid_ng.persistence.QuasselDatabase.DatabaseMessage
 import de.kuschku.quasseldroid_ng.ui.settings.data.RenderingSettings
 import de.kuschku.quasseldroid_ng.util.helper.getOrPut
 
-class MessageAdapter(context: Context) :
-  PagedListAdapter<QuasselDatabase.DatabaseMessage, QuasselMessageViewHolder>(
-    QuasselDatabase.DatabaseMessage.MessageDiffCallback
-  ) {
+class MessageAdapter(
+  context: Context,
+  private val markerLine: LiveData<Pair<MsgId, MsgId>?>
+) : PagedListAdapter<QuasselDatabase.DatabaseMessage, QuasselMessageViewHolder>(
+  object : DiffCallback<QuasselDatabase.DatabaseMessage>() {
+    override fun areItemsTheSame(oldItem: QuasselDatabase.DatabaseMessage,
+                                 newItem: QuasselDatabase.DatabaseMessage)
+      = DatabaseMessage.MessageDiffCallback.areItemsTheSame(oldItem, newItem)
+
+    override fun areContentsTheSame(oldItem: QuasselDatabase.DatabaseMessage,
+                                    newItem: QuasselDatabase.DatabaseMessage)
+      = DatabaseMessage.MessageDiffCallback.areContentsTheSame(oldItem, newItem) &&
+        oldItem.messageId != markerLine.value?.first &&
+        oldItem.messageId != markerLine.value?.second
+  }
+) {
   private val messageRenderer: MessageRenderer = QuasselMessageRenderer(
     context,
     RenderingSettings(
@@ -34,8 +46,14 @@ class MessageAdapter(context: Context) :
     getItem(position)?.let {
       messageRenderer.bind(
         holder,
-        messageCache.getOrPut(it.messageId) {
-          messageRenderer.render(it)
+        if (it.messageId == markerLine.value?.second || it.messageId == markerLine.value?.first) {
+          val value = messageRenderer.render(it, markerLine.value?.second ?: -1)
+          messageCache.put(it.messageId, value)
+          value
+        } else {
+          messageCache.getOrPut(it.messageId) {
+            messageRenderer.render(it, markerLine.value?.second ?: -1)
+          }
         }
       )
     }
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/MessageListFragment.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/MessageListFragment.kt
index 757575c1f..6ed8b5ce5 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/MessageListFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/MessageListFragment.kt
@@ -15,6 +15,7 @@ import android.view.ViewGroup
 import butterknife.BindView
 import butterknife.ButterKnife
 import de.kuschku.libquassel.protocol.BufferId
+import de.kuschku.libquassel.protocol.MsgId
 import de.kuschku.libquassel.session.Backend
 import de.kuschku.libquassel.session.SessionManager
 import de.kuschku.quasseldroid_ng.R
@@ -22,6 +23,8 @@ import de.kuschku.quasseldroid_ng.persistence.QuasselDatabase
 import de.kuschku.quasseldroid_ng.util.AndroidHandlerThread
 import de.kuschku.quasseldroid_ng.util.helper.*
 import de.kuschku.quasseldroid_ng.util.service.ServiceBoundFragment
+import io.reactivex.Observable
+import io.reactivex.functions.BiFunction
 
 class MessageListFragment : ServiceBoundFragment() {
   val currentBuffer: MutableLiveData<LiveData<BufferId?>?> = MutableLiveData()
@@ -29,6 +32,19 @@ class MessageListFragment : ServiceBoundFragment() {
 
   private val sessionManager: LiveData<SessionManager?> = backend.map(Backend::sessionManager)
 
+  val markerLine = buffer.switchMap { buffer ->
+    sessionManager.switchMapRx { manager ->
+      manager.session.switchMap { session ->
+        val raw = session.bufferSyncer?.liveMarkerLine(buffer)
+        Observable.zip(
+          Observable.just(-1).concatWith(raw),
+          raw,
+          BiFunction<MsgId, MsgId, Pair<MsgId, MsgId>> { a, b -> a to b }
+        )
+      }
+    }
+  }
+
   private val handler = AndroidHandlerThread("Chat")
 
   private lateinit var database: QuasselDatabase
@@ -60,12 +76,13 @@ class MessageListFragment : ServiceBoundFragment() {
     val view = inflater.inflate(R.layout.fragment_messages, container, false)
     ButterKnife.bind(this, view)
 
-    val adapter = MessageAdapter(context!!)
+    val adapter = MessageAdapter(context!!, markerLine)
 
     messageList.adapter = adapter
     val linearLayoutManager = LinearLayoutManager(context)
     linearLayoutManager.reverseLayout = true
     messageList.layoutManager = linearLayoutManager
+    messageList.itemAnimator = null
     messageList.setItemViewCacheSize(20)
 
     messageList.addOnScrollListener(
@@ -117,6 +134,8 @@ class MessageListFragment : ServiceBoundFragment() {
       )
     }
 
+    markerLine.observe(this, Observer { adapter.notifyDataSetChanged() })
+
     data.observe(
       this, Observer { list ->
       val findFirstVisibleItemPosition = linearLayoutManager.findFirstVisibleItemPosition()
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/MessageRenderer.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/MessageRenderer.kt
index 99403809e..206b100ae 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/MessageRenderer.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/MessageRenderer.kt
@@ -2,6 +2,7 @@ package de.kuschku.quasseldroid_ng.ui.chat
 
 import android.support.annotation.LayoutRes
 import de.kuschku.libquassel.protocol.Message_Type
+import de.kuschku.libquassel.protocol.MsgId
 import de.kuschku.quasseldroid_ng.persistence.QuasselDatabase
 
 interface MessageRenderer {
@@ -9,7 +10,7 @@ interface MessageRenderer {
   fun layout(type: Message_Type?, hasHighlight: Boolean): Int
 
   fun bind(holder: QuasselMessageViewHolder, message: FormattedMessage)
-  fun render(message: QuasselDatabase.DatabaseMessage): FormattedMessage
+  fun render(message: QuasselDatabase.DatabaseMessage, markerLine: MsgId): FormattedMessage
   fun init(viewHolder: QuasselMessageViewHolder,
            messageType: Message_Type?,
            hasHighlight: Boolean) {
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/QuasselMessageRenderer.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/QuasselMessageRenderer.kt
index 4829aef05..48d1f87ec 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/QuasselMessageRenderer.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/QuasselMessageRenderer.kt
@@ -11,6 +11,7 @@ import android.text.style.URLSpan
 import de.kuschku.libquassel.protocol.Message.MessageType.*
 import de.kuschku.libquassel.protocol.Message_Flag
 import de.kuschku.libquassel.protocol.Message_Type
+import de.kuschku.libquassel.protocol.MsgId
 import de.kuschku.libquassel.util.hasFlag
 import de.kuschku.quasseldroid_ng.R
 import de.kuschku.quasseldroid_ng.persistence.QuasselDatabase
@@ -18,6 +19,7 @@ import de.kuschku.quasseldroid_ng.ui.settings.data.RenderingSettings
 import de.kuschku.quasseldroid_ng.ui.settings.data.RenderingSettings.ColorizeNicknamesMode
 import de.kuschku.quasseldroid_ng.ui.settings.data.RenderingSettings.ShowPrefixMode
 import de.kuschku.quasseldroid_ng.util.helper.styledAttributes
+import de.kuschku.quasseldroid_ng.util.helper.visibleIf
 import de.kuschku.quasseldroid_ng.util.irc.format.IrcFormatDeserializer
 import de.kuschku.quasseldroid_ng.util.quassel.IrcUserUtils
 import de.kuschku.quasseldroid_ng.util.ui.SpanFormatter
@@ -81,9 +83,11 @@ class QuasselMessageRenderer(
   override fun bind(holder: QuasselMessageViewHolder, message: FormattedMessage) {
     holder.time.text = message.time
     holder.content.text = message.content
+    holder.markerline.visibleIf(message.markerline)
   }
 
-  override fun render(message: QuasselDatabase.DatabaseMessage): FormattedMessage {
+  override fun render(message: QuasselDatabase.DatabaseMessage,
+                      markerLine: MsgId): FormattedMessage {
     return when (Message_Type.of(message.type).enabledValues().firstOrNull()) {
       Message_Type.Plain  -> FormattedMessage(
         message.messageId,
@@ -93,7 +97,8 @@ class QuasselMessageRenderer(
           formatPrefix(message.senderPrefixes),
           formatNick(message.sender, Message_Flag.of(message.flag).hasFlag(Message_Flag.Self)),
           formatContent(message.content)
-        )
+        ),
+        message.messageId == markerLine
       )
       Message_Type.Action -> FormattedMessage(
         message.messageId,
@@ -103,7 +108,8 @@ class QuasselMessageRenderer(
           formatPrefix(message.senderPrefixes),
           formatNick(message.sender, Message_Flag.of(message.flag).hasFlag(Message_Flag.Self)),
           formatContent(message.content)
-        )
+        ),
+        message.messageId == markerLine
       )
       Message_Type.Notice -> FormattedMessage(
         message.messageId,
@@ -113,7 +119,8 @@ class QuasselMessageRenderer(
           formatPrefix(message.senderPrefixes),
           formatNick(message.sender, Message_Flag.of(message.flag).hasFlag(Message_Flag.Self)),
           formatContent(message.content)
-        )
+        ),
+        message.messageId == markerLine
       )
       Message_Type.Nick   -> FormattedMessage(
         message.messageId,
@@ -124,7 +131,8 @@ class QuasselMessageRenderer(
           formatNick(message.sender, Message_Flag.of(message.flag).hasFlag(Message_Flag.Self)),
           formatPrefix(message.senderPrefixes),
           formatNick(message.content, Message_Flag.of(message.flag).hasFlag(Message_Flag.Self))
-        )
+        ),
+        message.messageId == markerLine
       )
       Message_Type.Mode   -> FormattedMessage(
         message.messageId,
@@ -134,7 +142,8 @@ class QuasselMessageRenderer(
           message.content,
           formatPrefix(message.senderPrefixes),
           formatNick(message.sender, Message_Flag.of(message.flag).hasFlag(Message_Flag.Self))
-        )
+        ),
+        message.messageId == markerLine
       )
       Message_Type.Join   -> FormattedMessage(
         message.messageId,
@@ -143,7 +152,8 @@ class QuasselMessageRenderer(
           context.getString(R.string.message_format_join),
           formatPrefix(message.senderPrefixes),
           formatNick(message.sender, Message_Flag.of(message.flag).hasFlag(Message_Flag.Self))
-        )
+        ),
+        message.messageId == markerLine
       )
       Message_Type.Part   -> FormattedMessage(
         message.messageId,
@@ -161,7 +171,8 @@ class QuasselMessageRenderer(
             formatNick(message.sender, Message_Flag.of(message.flag).hasFlag(Message_Flag.Self)),
             message.content
           )
-        }
+        },
+        message.messageId == markerLine
       )
       Message_Type.Quit -> FormattedMessage(
         message.messageId,
@@ -179,7 +190,8 @@ class QuasselMessageRenderer(
             formatNick(message.sender, Message_Flag.of(message.flag).hasFlag(Message_Flag.Self)),
             message.content
           )
-        }
+        },
+        message.messageId == markerLine
       )
       Message_Type.NetsplitJoin -> {
         val split = message.content.split("#:#")
@@ -190,7 +202,8 @@ class QuasselMessageRenderer(
           timeFormatter.format(message.time.atZone(zoneId)),
           context.resources.getQuantityString(
             R.plurals.message_netsplit_join, usersAffected, server1, server2, usersAffected
-          )
+          ),
+          message.messageId == markerLine
         )
       }
       Message_Type.NetsplitQuit -> {
@@ -202,7 +215,8 @@ class QuasselMessageRenderer(
           timeFormatter.format(message.time.atZone(zoneId)),
           context.resources.getQuantityString(
             R.plurals.message_netsplit_quit, usersAffected, server1, server2, usersAffected
-          )
+          ),
+          message.messageId == markerLine
         )
       }
       Message_Type.Server,
@@ -210,12 +224,14 @@ class QuasselMessageRenderer(
       Message_Type.Error -> FormattedMessage(
         message.messageId,
         timeFormatter.format(message.time.atZone(zoneId)),
-        formatContent(message.content)
+        formatContent(message.content),
+        message.messageId == markerLine
       )
       Message_Type.Topic -> FormattedMessage(
         message.messageId,
         timeFormatter.format(message.time.atZone(zoneId)),
-        formatContent(message.content)
+        formatContent(message.content),
+        message.messageId == markerLine
       )
       else -> FormattedMessage(
         message.messageId,
@@ -226,7 +242,8 @@ class QuasselMessageRenderer(
           formatPrefix(message.senderPrefixes),
           formatNick(message.sender, Message_Flag.of(message.flag).hasFlag(Message_Flag.Self)),
           message.content
-        )
+        ),
+        message.messageId == markerLine
       )
     }
   }
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/QuasselMessageViewHolder.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/QuasselMessageViewHolder.kt
index cf54d42c9..cfddcb3e5 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/QuasselMessageViewHolder.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/QuasselMessageViewHolder.kt
@@ -15,6 +15,9 @@ class QuasselMessageViewHolder(itemView: View) : RecyclerView.ViewHolder(itemVie
   @BindView(R.id.content)
   lateinit var content: TextView
 
+  @BindView(R.id.markerline)
+  lateinit var markerline: View
+
   init {
     ButterKnife.bind(this, itemView)
     content.movementMethod = LinkMovementMethod.getInstance()
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/ToolbarFragment.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/ToolbarFragment.kt
index 4ef64d13b..0e3176638 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/ToolbarFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/ToolbarFragment.kt
@@ -59,7 +59,7 @@ class ToolbarFragment : ServiceBoundFragment() {
       manager.session.switchMap {
         val bufferSyncer = it.bufferSyncer
         if (bufferSyncer != null) {
-          bufferSyncer.live_bufferInfos.switchMap {
+          bufferSyncer.liveBufferInfos().switchMap {
             val info = bufferSyncer.bufferInfo(id)
             val network = manager.networks[info?.networkId]
             if (info == null) {
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/LiveDataHelper.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/LiveDataHelper.kt
index 17c04403f..a72e74e0e 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/LiveDataHelper.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/LiveDataHelper.kt
@@ -7,7 +7,7 @@ import io.reactivex.Observable
 
 @MainThread
 inline fun <X, Y> LiveData<X?>.switchMap(
-  noinline func: (X) -> LiveData<Y>?
+  crossinline func: (X) -> LiveData<Y>?
 ): LiveData<Y> {
   val result = MediatorLiveData<Y>()
   result.addSource(
@@ -35,7 +35,7 @@ inline fun <X, Y> LiveData<X?>.switchMap(
 @MainThread
 inline fun <X, Y> LiveData<X?>.switchMapRx(
   strategy: BackpressureStrategy,
-  noinline func: (X) -> Observable<Y>?
+  crossinline func: (X) -> Observable<Y>?
 ): LiveData<Y?> {
   val result = MediatorLiveData<Y>()
   result.addSource(
@@ -61,13 +61,13 @@ inline fun <X, Y> LiveData<X?>.switchMapRx(
 }
 
 @MainThread
-inline fun <X, Y> LiveData<X?>.switchMapRx(
-  noinline func: (X) -> Observable<Y>?
+inline fun <X, Y> LiveData<out X?>.switchMapRx(
+  crossinline func: (X) -> Observable<Y>?
 ): LiveData<Y?> = switchMapRx(BackpressureStrategy.LATEST, func)
 
 @MainThread
 inline fun <X, Y> LiveData<X?>.map(
-  noinline func: (X) -> Y?
+  crossinline func: (X) -> Y?
 ): LiveData<Y?> {
   val result = MediatorLiveData<Y?>()
   result.addSource(this) { x ->
@@ -78,7 +78,7 @@ inline fun <X, Y> LiveData<X?>.map(
 
 @MainThread
 inline fun <X> LiveData<X>.orElse(
-  noinline func: () -> X
+  crossinline func: () -> X
 ): LiveData<X> {
   val result = object : MediatorLiveData<X>() {
     override fun getValue() = super.getValue() ?: func()
diff --git a/app/src/main/res/layout/widget_chatmessage_action.xml b/app/src/main/res/layout/widget_chatmessage_action.xml
index dee8b8853..b23606b69 100644
--- a/app/src/main/res/layout/widget_chatmessage_action.xml
+++ b/app/src/main/res/layout/widget_chatmessage_action.xml
@@ -25,35 +25,47 @@
   android:layout_height="wrap_content"
   android:clickable="true"
   android:focusable="true"
-  android:gravity="top"
-  android:orientation="horizontal"
-  android:paddingBottom="@dimen/message_vertical"
-  android:paddingEnd="@dimen/message_horizontal"
-  android:paddingLeft="@dimen/message_horizontal"
-  android:paddingRight="@dimen/message_horizontal"
-  android:paddingStart="@dimen/message_horizontal"
-  android:paddingTop="@dimen/message_vertical"
+  android:orientation="vertical"
   android:textAppearance="?android:attr/textAppearanceListItemSmall"
   tools:background="@android:color/background_light"
   tools:theme="@style/Theme.ChatTheme.Quassel_Light">
 
-  <TextView
-    android:id="@+id/time"
-    android:layout_width="wrap_content"
+  <LinearLayout
+    android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    android:layout_marginEnd="@dimen/message_horizontal"
-    android:layout_marginRight="@dimen/message_horizontal"
-    android:textColor="?attr/colorForegroundSecondary"
-    android:typeface="monospace"
-    tools:text="[15:55]" />
+    android:orientation="horizontal"
+    android:paddingBottom="@dimen/message_vertical"
+    android:paddingEnd="@dimen/message_horizontal"
+    android:paddingLeft="@dimen/message_horizontal"
+    android:paddingRight="@dimen/message_horizontal"
+    android:paddingStart="@dimen/message_horizontal"
+    android:paddingTop="@dimen/message_vertical">
 
-  <TextView
-    android:id="@+id/content"
-    android:layout_width="0dip"
-    android:layout_height="wrap_content"
-    android:layout_weight="1"
-    android:textColor="?attr/colorForegroundAction"
-    android:textIsSelectable="true"
-    android:textStyle="italic"
-    tools:text="-*- justJanne loves the new version" />
-</LinearLayout>
+    <TextView
+      android:id="@+id/time"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:layout_marginEnd="@dimen/message_horizontal"
+      android:layout_marginRight="@dimen/message_horizontal"
+      android:textColor="?attr/colorForegroundSecondary"
+      android:typeface="monospace"
+      tools:text="[15:55]" />
+
+    <TextView
+      android:id="@+id/content"
+      android:layout_width="0dip"
+      android:layout_height="wrap_content"
+      android:layout_weight="1"
+      android:textColor="?attr/colorForegroundAction"
+      android:textIsSelectable="true"
+      android:textStyle="italic"
+      tools:text="-*- justJanne loves the new version" />
+  </LinearLayout>
+
+  <View
+    android:id="@+id/markerline"
+    android:layout_width="match_parent"
+    android:layout_height="1dp"
+    android:background="?colorMarkerLine"
+    android:visibility="gone" />
+</LinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/widget_chatmessage_error.xml b/app/src/main/res/layout/widget_chatmessage_error.xml
index 55547dc3a..1485a969a 100644
--- a/app/src/main/res/layout/widget_chatmessage_error.xml
+++ b/app/src/main/res/layout/widget_chatmessage_error.xml
@@ -3,21 +3,24 @@
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
-  android:background="?attr/colorBackgroundSecondary"
   android:clickable="true"
   android:focusable="true"
-  android:gravity="top"
-  android:orientation="horizontal"
-  android:paddingBottom="@dimen/message_vertical"
-  android:paddingEnd="@dimen/message_horizontal"
-  android:paddingLeft="@dimen/message_horizontal"
-  android:paddingRight="@dimen/message_horizontal"
-  android:paddingStart="@dimen/message_horizontal"
-  android:paddingTop="@dimen/message_vertical"
+  android:orientation="vertical"
   android:textAppearance="?android:attr/textAppearanceListItemSmall"
   tools:background="@android:color/background_light"
   tools:theme="@style/Theme.ChatTheme.Quassel_Light">
 
+  <LinearLayout
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="horizontal"
+    android:paddingBottom="@dimen/message_vertical"
+    android:paddingEnd="@dimen/message_horizontal"
+    android:paddingLeft="@dimen/message_horizontal"
+    android:paddingRight="@dimen/message_horizontal"
+    android:paddingStart="@dimen/message_horizontal"
+    android:paddingTop="@dimen/message_vertical">
+
   <TextView
     android:id="@+id/time"
     android:layout_width="wrap_content"
@@ -36,4 +39,12 @@
     android:textColor="?attr/colorForegroundError"
     android:textIsSelectable="true"
     tools:text="everyone: deserves a chance to fly. No such channel" />
-</LinearLayout>
+  </LinearLayout>
+
+  <View
+    android:id="@+id/markerline"
+    android:layout_width="match_parent"
+    android:layout_height="1dp"
+    android:background="?colorMarkerLine"
+    android:visibility="gone" />
+</LinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/widget_chatmessage_info.xml b/app/src/main/res/layout/widget_chatmessage_info.xml
index 0530deda0..b6d1f048c 100644
--- a/app/src/main/res/layout/widget_chatmessage_info.xml
+++ b/app/src/main/res/layout/widget_chatmessage_info.xml
@@ -3,21 +3,24 @@
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
-  android:background="?attr/colorBackgroundSecondary"
   android:clickable="true"
   android:focusable="true"
-  android:gravity="top"
-  android:orientation="horizontal"
-  android:paddingBottom="@dimen/message_vertical"
-  android:paddingEnd="@dimen/message_horizontal"
-  android:paddingLeft="@dimen/message_horizontal"
-  android:paddingRight="@dimen/message_horizontal"
-  android:paddingStart="@dimen/message_horizontal"
-  android:paddingTop="@dimen/message_vertical"
+  android:orientation="vertical"
   android:textAppearance="?android:attr/textAppearanceListItemSmall"
   tools:background="@android:color/background_light"
   tools:theme="@style/Theme.ChatTheme.Quassel_Light">
 
+  <LinearLayout
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="horizontal"
+    android:paddingBottom="@dimen/message_vertical"
+    android:paddingEnd="@dimen/message_horizontal"
+    android:paddingLeft="@dimen/message_horizontal"
+    android:paddingRight="@dimen/message_horizontal"
+    android:paddingStart="@dimen/message_horizontal"
+    android:paddingTop="@dimen/message_vertical">
+
   <TextView
     android:id="@+id/time"
     android:layout_width="wrap_content"
@@ -37,4 +40,12 @@
     android:textIsSelectable="true"
     android:textStyle="italic"
     tools:text="Connecting to irc.freenode.net:6667..." />
-</LinearLayout>
+  </LinearLayout>
+
+  <View
+    android:id="@+id/markerline"
+    android:layout_width="match_parent"
+    android:layout_height="1dp"
+    android:background="?colorMarkerLine"
+    android:visibility="gone" />
+</LinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/widget_chatmessage_notice.xml b/app/src/main/res/layout/widget_chatmessage_notice.xml
index 0d785dd05..755a7d100 100644
--- a/app/src/main/res/layout/widget_chatmessage_notice.xml
+++ b/app/src/main/res/layout/widget_chatmessage_notice.xml
@@ -3,21 +3,24 @@
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
-  android:background="?attr/colorBackgroundSecondary"
   android:clickable="true"
   android:focusable="true"
-  android:gravity="top"
-  android:orientation="horizontal"
-  android:paddingBottom="@dimen/message_vertical"
-  android:paddingEnd="@dimen/message_horizontal"
-  android:paddingLeft="@dimen/message_horizontal"
-  android:paddingRight="@dimen/message_horizontal"
-  android:paddingStart="@dimen/message_horizontal"
-  android:paddingTop="@dimen/message_vertical"
+  android:orientation="vertical"
   android:textAppearance="?android:attr/textAppearanceListItemSmall"
   tools:background="@android:color/background_light"
   tools:theme="@style/Theme.ChatTheme.Quassel_Light">
 
+  <LinearLayout
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="horizontal"
+    android:paddingBottom="@dimen/message_vertical"
+    android:paddingEnd="@dimen/message_horizontal"
+    android:paddingLeft="@dimen/message_horizontal"
+    android:paddingRight="@dimen/message_horizontal"
+    android:paddingStart="@dimen/message_horizontal"
+    android:paddingTop="@dimen/message_vertical">
+
   <TextView
     android:id="@+id/time"
     android:layout_width="wrap_content"
@@ -36,4 +39,12 @@
     android:textColor="?attr/colorForegroundNotice"
     android:textIsSelectable="true"
     tools:text="Connecting to irc.freenode.net:6667..." />
-</LinearLayout>
+  </LinearLayout>
+
+  <View
+    android:id="@+id/markerline"
+    android:layout_width="match_parent"
+    android:layout_height="1dp"
+    android:background="?colorMarkerLine"
+    android:visibility="gone" />
+</LinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/widget_chatmessage_placeholder.xml b/app/src/main/res/layout/widget_chatmessage_placeholder.xml
index be8f116d7..5021308b2 100644
--- a/app/src/main/res/layout/widget_chatmessage_placeholder.xml
+++ b/app/src/main/res/layout/widget_chatmessage_placeholder.xml
@@ -3,8 +3,6 @@
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="48dp"
-  android:gravity="top"
-  android:orientation="horizontal"
   tools:background="@android:color/background_light"
   tools:theme="@style/Theme.ChatTheme.Quassel_Light">
 
@@ -19,4 +17,10 @@
     android:layout_width="0dip"
     android:layout_height="0dip"
     android:visibility="gone" />
-</LinearLayout>
+
+  <View
+    android:id="@+id/markerline"
+    android:layout_width="0dip"
+    android:layout_height="0dip"
+    android:visibility="gone" />
+</LinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/widget_chatmessage_plain.xml b/app/src/main/res/layout/widget_chatmessage_plain.xml
index 1f52573c3..54b7db732 100644
--- a/app/src/main/res/layout/widget_chatmessage_plain.xml
+++ b/app/src/main/res/layout/widget_chatmessage_plain.xml
@@ -5,18 +5,22 @@
   android:layout_height="wrap_content"
   android:clickable="true"
   android:focusable="true"
-  android:gravity="top"
-  android:orientation="horizontal"
-  android:paddingBottom="@dimen/message_vertical"
-  android:paddingEnd="@dimen/message_horizontal"
-  android:paddingLeft="@dimen/message_horizontal"
-  android:paddingRight="@dimen/message_horizontal"
-  android:paddingStart="@dimen/message_horizontal"
-  android:paddingTop="@dimen/message_vertical"
+  android:orientation="vertical"
   android:textAppearance="?android:attr/textAppearanceListItemSmall"
   tools:background="@android:color/background_light"
   tools:theme="@style/Theme.ChatTheme.Quassel_Light">
 
+  <LinearLayout
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="horizontal"
+    android:paddingBottom="@dimen/message_vertical"
+    android:paddingEnd="@dimen/message_horizontal"
+    android:paddingLeft="@dimen/message_horizontal"
+    android:paddingRight="@dimen/message_horizontal"
+    android:paddingStart="@dimen/message_horizontal"
+    android:paddingTop="@dimen/message_vertical">
+
   <TextView
     android:id="@+id/time"
     android:layout_width="wrap_content"
@@ -35,4 +39,12 @@
     android:textColor="?attr/colorForeground"
     android:textIsSelectable="true"
     tools:text="justJanne: hiii" />
-</LinearLayout>
+  </LinearLayout>
+
+  <View
+    android:id="@+id/markerline"
+    android:layout_width="match_parent"
+    android:layout_height="1dp"
+    android:background="?colorMarkerLine"
+    android:visibility="gone" />
+</LinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/widget_chatmessage_server.xml b/app/src/main/res/layout/widget_chatmessage_server.xml
index 59248a6ab..b9ba45aec 100644
--- a/app/src/main/res/layout/widget_chatmessage_server.xml
+++ b/app/src/main/res/layout/widget_chatmessage_server.xml
@@ -3,21 +3,24 @@
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
-  android:background="?attr/colorBackgroundSecondary"
   android:clickable="true"
   android:focusable="true"
-  android:gravity="top"
-  android:orientation="horizontal"
-  android:paddingBottom="@dimen/message_vertical"
-  android:paddingEnd="@dimen/message_horizontal"
-  android:paddingLeft="@dimen/message_horizontal"
-  android:paddingRight="@dimen/message_horizontal"
-  android:paddingStart="@dimen/message_horizontal"
-  android:paddingTop="@dimen/message_vertical"
+  android:orientation="vertical"
   android:textAppearance="?android:attr/textAppearanceListItemSmall"
   tools:background="@android:color/background_light"
   tools:theme="@style/Theme.ChatTheme.Quassel_Light">
 
+  <LinearLayout
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="horizontal"
+    android:paddingBottom="@dimen/message_vertical"
+    android:paddingEnd="@dimen/message_horizontal"
+    android:paddingLeft="@dimen/message_horizontal"
+    android:paddingRight="@dimen/message_horizontal"
+    android:paddingStart="@dimen/message_horizontal"
+    android:paddingTop="@dimen/message_vertical">
+
   <TextView
     android:id="@+id/time"
     android:layout_width="wrap_content"
@@ -37,4 +40,12 @@
     android:textIsSelectable="true"
     android:typeface="monospace"
     tools:text="Connecting to irc.freenode.net:6667..." />
-</LinearLayout>
+  </LinearLayout>
+
+  <View
+    android:id="@+id/markerline"
+    android:layout_width="match_parent"
+    android:layout_height="1dp"
+    android:background="?colorMarkerLine"
+    android:visibility="gone" />
+</LinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
index cc5cf114c..eeb7e7e31 100644
--- a/app/src/main/res/values/attrs.xml
+++ b/app/src/main/res/values/attrs.xml
@@ -55,11 +55,12 @@
   <attr name="colorBackgroundCard" format="color" />
   <attr name="colorBackgroundDialog" format="color" />
 
+  <attr name="colorMarkerLine" format="color" />
+
   <!-- Tint colors for drawer -->
   <attr name="colorTintActivity" format="color" />
   <attr name="colorTintMessage" format="color" />
   <attr name="colorTintHighlight" format="color" />
-  <attr name="chatlineExpandedSize" />
 
   <!-- Icons -->
   <attr name="colorOffline" format="color" />
diff --git a/app/src/main/res/values/themes_quassel.xml b/app/src/main/res/values/themes_quassel.xml
index f83e1fffb..3d9c05174 100644
--- a/app/src/main/res/values/themes_quassel.xml
+++ b/app/src/main/res/values/themes_quassel.xml
@@ -37,6 +37,8 @@
     <item name="colorBackgroundCard">#FFFFFF</item>
     <item name="colorBackgroundDialog">#FAFAFA</item>
 
+    <item name="colorMarkerLine">#ff0000</item>
+
     <item name="colorTintActivity">#88cc33</item>
     <item name="colorTintMessage">#2277dd</item>
     <item name="colorTintHighlight">#ff8811</item>
diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts
index 1c33f64cc..a7cc8e972 100644
--- a/lib/build.gradle.kts
+++ b/lib/build.gradle.kts
@@ -23,6 +23,7 @@ dependencies {
   testImplementation("junit:junit:4.12")
 }
 
+/*
 tasks.withType(KotlinCompile::class.java) {
   kotlinOptions {
     freeCompilerArgs = listOf(
@@ -31,6 +32,7 @@ tasks.withType(KotlinCompile::class.java) {
     )
   }
 }
+*/
 
 /**
  * Builds the dependency notation for the named AppCompat [module] at the given [version].
diff --git a/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/BufferSyncer.kt b/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/BufferSyncer.kt
index 0e0bbb12c..20279400a 100644
--- a/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/BufferSyncer.kt
+++ b/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/BufferSyncer.kt
@@ -12,15 +12,35 @@ class BufferSyncer constructor(
   proxy: SignalProxy
 ) : SyncableObject(proxy, "BufferSyncer"), IBufferSyncer {
   fun lastSeenMsg(buffer: BufferId): MsgId = _lastSeenMsg[buffer] ?: 0
+  fun liveLastSeenMsg(buffer: BufferId): Observable<MsgId>
+    = live_lastSeenMsg.map { markerLine(buffer) }.distinctUntilChanged()
+
+  fun liveLastSeenMsgs(): Observable<Map<BufferId, MsgId>> = live_lastSeenMsg
+
   fun markerLine(buffer: BufferId): MsgId = _markerLines[buffer] ?: 0
+  fun liveMarkerLine(buffer: BufferId): Observable<MsgId>
+    = live_markerLines.map { markerLine(buffer) }.distinctUntilChanged()
+
+  fun liveMarkerLines(): Observable<Map<BufferId, MsgId>> = live_markerLines
+
   fun activity(buffer: BufferId): Message_Types = _bufferActivities[buffer] ?: Message_Types.of()
   fun liveActivity(buffer: BufferId): Observable<Message_Types>
     = live_bufferActivities.map { activity(buffer) }.distinctUntilChanged()
 
+  fun liveActivities(): Observable<Map<BufferId, Message_Types>> = live_bufferActivities
+
   fun highlightCount(buffer: BufferId): Int = _highlightCounts[buffer] ?: 0
   fun liveHighlightCount(buffer: BufferId): Observable<Int>
     = live_highlightCounts.map { highlightCount(buffer) }.distinctUntilChanged()
 
+  fun liveHighlightCounts(): Observable<Map<BufferId, Int>> = live_highlightCounts
+
+  fun bufferInfo(bufferId: BufferId) = _bufferInfos[bufferId]
+  fun liveBufferInfo(bufferId: BufferId)
+    = live_bufferInfos.map { bufferInfo(bufferId) }.distinctUntilChanged()
+
+  fun liveBufferInfos(): Observable<Map<BufferId, BufferInfo>> = live_bufferInfos
+
   override fun toVariantMap(): QVariantMap = mapOf(
     "Activities" to QVariant_(initActivities(), Type.QVariantList),
     "HighlightCounts" to QVariant_(initHighlightCounts(), Type.QVariantList),
@@ -77,6 +97,7 @@ class BufferSyncer constructor(
     }.forEach { (buffer, activity) ->
       setBufferActivity(buffer, activity)
     }
+    live_bufferActivities.onNext(_bufferActivities)
   }
 
   override fun initSetHighlightCounts(data: QVariantList) {
@@ -85,6 +106,7 @@ class BufferSyncer constructor(
     }.forEach { (buffer, count) ->
       setHighlightCount(buffer, count)
     }
+    live_highlightCounts.onNext(_highlightCounts)
   }
 
   override fun initSetLastSeenMsg(data: QVariantList) {
@@ -93,6 +115,7 @@ class BufferSyncer constructor(
     }.forEach { (buffer, msgId) ->
       setLastSeenMsg(buffer, msgId)
     }
+    live_lastSeenMsg.onNext(_lastSeenMsg)
   }
 
   override fun initSetMarkerLines(data: QVariantList) {
@@ -101,6 +124,7 @@ class BufferSyncer constructor(
     }.forEach { (buffer, msgId) ->
       setMarkerLine(buffer, msgId)
     }
+    live_markerLines.onNext(_markerLines)
   }
 
   fun initSetBufferInfos(infos: QVariantList?) {
@@ -110,20 +134,19 @@ class BufferSyncer constructor(
   }
 
   override fun mergeBuffersPermanently(buffer1: BufferId, buffer2: BufferId) {
-    _lastSeenMsg.remove(buffer2)
-    _markerLines.remove(buffer2)
-    _bufferActivities.remove(buffer2)
-    _highlightCounts.remove(buffer2)
-    live_bufferInfos.onNext(_bufferInfos)
+    _lastSeenMsg.remove(buffer2);live_lastSeenMsg.onNext(_lastSeenMsg)
+    _markerLines.remove(buffer2);live_markerLines.onNext(_markerLines)
+    _bufferActivities.remove(buffer2);live_bufferActivities.onNext(_bufferActivities)
+    _highlightCounts.remove(buffer2);live_highlightCounts.onNext(_highlightCounts)
+    _bufferInfos.remove(buffer2);live_bufferInfos.onNext(_bufferInfos)
   }
 
   override fun removeBuffer(buffer: BufferId) {
-    _lastSeenMsg.remove(buffer)
-    _markerLines.remove(buffer)
-    _bufferActivities.remove(buffer)
-    _highlightCounts.remove(buffer)
-    _bufferInfos.remove(buffer)
-    live_bufferInfos.onNext(_bufferInfos)
+    _lastSeenMsg.remove(buffer);live_lastSeenMsg.onNext(_lastSeenMsg)
+    _markerLines.remove(buffer);live_markerLines.onNext(_markerLines)
+    _bufferActivities.remove(buffer);live_bufferActivities.onNext(_bufferActivities)
+    _highlightCounts.remove(buffer);live_highlightCounts.onNext(_highlightCounts)
+    _bufferInfos.remove(buffer);live_bufferInfos.onNext(_bufferInfos)
   }
 
   override fun renameBuffer(buffer: BufferId, newName: String) {
@@ -134,11 +157,6 @@ class BufferSyncer constructor(
     }
   }
 
-  fun bufferInfo(bufferId: BufferId) = _bufferInfos[bufferId]
-  fun liveBufferInfo(bufferId: BufferId) = live_bufferInfos.distinctUntilChanged().map {
-    bufferInfo(bufferId)
-  }
-
   fun bufferInfoUpdated(info: BufferInfo) {
     if (info != _bufferInfos[info.bufferId]) {
       _bufferInfos[info.bufferId] = info
@@ -153,6 +171,7 @@ class BufferSyncer constructor(
     val oldLastSeenMsg = lastSeenMsg(buffer)
     if (oldLastSeenMsg < msgId) {
       _lastSeenMsg[buffer] = msgId
+      live_lastSeenMsg.onNext(_lastSeenMsg)
       super.setLastSeenMsg(buffer, msgId)
     }
   }
@@ -162,6 +181,7 @@ class BufferSyncer constructor(
       return
 
     _markerLines[buffer] = msgId
+    live_markerLines.onNext(_markerLines)
     super.setMarkerLine(buffer, msgId)
   }
 
@@ -179,15 +199,22 @@ class BufferSyncer constructor(
   }
 
   private val _lastSeenMsg: MutableMap<BufferId, MsgId> = mutableMapOf()
+  private val live_lastSeenMsg
+    = BehaviorSubject.createDefault(mapOf<BufferId, MsgId>())
+
   private val _markerLines: MutableMap<BufferId, MsgId> = mutableMapOf()
+  private val live_markerLines
+    = BehaviorSubject.createDefault(mapOf<BufferId, MsgId>())
+
   private val _bufferActivities: MutableMap<BufferId, Message_Types> = mutableMapOf()
-  private val live_bufferActivities = BehaviorSubject.createDefault(
-    mutableMapOf<BufferId, Message_Types>()
-  )
+  private val live_bufferActivities
+    = BehaviorSubject.createDefault(mapOf<BufferId, Message_Types>())
+
   private val _highlightCounts: MutableMap<BufferId, Int> = mutableMapOf()
-  private val live_highlightCounts = BehaviorSubject.createDefault(
-    mutableMapOf<BufferId, Int>()
-  )
+  private val live_highlightCounts
+    = BehaviorSubject.createDefault(mapOf<BufferId, Int>())
+
   private val _bufferInfos = mutableMapOf<BufferId, BufferInfo>()
-  val live_bufferInfos = BehaviorSubject.createDefault(mutableMapOf<BufferId, BufferInfo>())
+  private val live_bufferInfos
+    = BehaviorSubject.createDefault(mapOf<BufferId, BufferInfo>())
 }
-- 
GitLab