From 69e07d54ee96ea2a1b2db75946479a1be7727abe Mon Sep 17 00:00:00 2001
From: Janne Koschinski <janne@kuschku.de>
Date: Sun, 18 Feb 2018 18:12:54 +0100
Subject: [PATCH] Added topic/realname and lag display, introduced settings
 structs

---
 .../quasseldroid_ng/ui/chat/MessageAdapter.kt |  20 +-
 .../ui/chat/QuasselMessageRenderer.kt         | 187 +++++++++++-------
 .../ui/chat/ToolbarFragment.kt                | 129 ++++++++++--
 .../ui/settings/data/DisplaySettings.kt       |   5 +
 .../ui/settings/data/RenderingSettings.kt     |  37 ++++
 .../util/helper/LiveDataZipHelper.kt          |  41 +++-
 .../res/layout/widget_chatmessage_error.xml   |   1 -
 .../layout/widget_chatmessage_placeholder.xml |  22 +++
 app/src/main/res/values/strings.xml           |  11 ++
 .../de/kuschku/libquassel/session/ISession.kt |   3 +
 .../de/kuschku/libquassel/session/Session.kt  |   9 +
 .../libquassel/session/SessionManager.kt      |   2 +
 12 files changed, 377 insertions(+), 90 deletions(-)
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid_ng/ui/settings/data/DisplaySettings.kt
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid_ng/ui/settings/data/RenderingSettings.kt
 create mode 100644 app/src/main/res/layout/widget_chatmessage_placeholder.xml

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 bd40477de..7831ba4c5 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
@@ -11,13 +11,21 @@ import de.kuschku.libquassel.protocol.Message_Type
 import de.kuschku.libquassel.protocol.Message_Types
 import de.kuschku.libquassel.util.hasFlag
 import de.kuschku.quasseldroid_ng.persistence.QuasselDatabase
+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
   ) {
-  private val messageRenderer: MessageRenderer = QuasselMessageRenderer(context)
+  private val messageRenderer: MessageRenderer = QuasselMessageRenderer(
+    context,
+    RenderingSettings(
+      showPrefix = RenderingSettings.ShowPrefixMode.FIRST,
+      colorizeNicknames = RenderingSettings.ColorizeNicknamesMode.ALL_BUT_MINE,
+      timeFormat = ""
+    )
+  )
 
   private val messageCache = LruCache<Int, FormattedMessage>(512)
 
@@ -42,7 +50,11 @@ class MessageAdapter(context: Context) :
   }
 
   private fun viewType(type: Message_Types, flags: Message_Flags): Int {
-    return (if (flags.hasFlag(Message_Flag.Highlight)) 0x8000 else 0x0000) or (type.value and 0x7FF)
+    if (flags.hasFlag(Message_Flag.Highlight)) {
+      return -type.value
+    } else {
+      return type.value
+    }
   }
 
   override fun getItemId(position: Int): Long {
@@ -50,10 +62,10 @@ class MessageAdapter(context: Context) :
   }
 
   private fun messageType(viewType: Int): Message_Type?
-    = Message_Type.of(viewType and 0x7FF).enabledValues().firstOrNull()
+    = Message_Type.of(Math.abs(viewType)).enabledValues().firstOrNull()
 
   private fun hasHiglight(viewType: Int)
-    = viewType and 0x8000 != 0
+    = viewType < 0
 
   override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QuasselMessageViewHolder {
     val messageType = messageType(viewType)
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 0e0f2998c..3cc00924f 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
@@ -7,9 +7,14 @@ import android.text.format.DateFormat
 import android.text.style.ForegroundColorSpan
 import android.text.style.StyleSpan
 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.util.hasFlag
 import de.kuschku.quasseldroid_ng.R
 import de.kuschku.quasseldroid_ng.persistence.QuasselDatabase
+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.quassel.IrcUserUtils
 import de.kuschku.quasseldroid_ng.util.ui.SpanFormatter
@@ -17,9 +22,16 @@ import org.threeten.bp.ZoneId
 import org.threeten.bp.format.DateTimeFormatter
 import java.text.SimpleDateFormat
 
-class QuasselMessageRenderer(context: Context) : MessageRenderer {
+class QuasselMessageRenderer(
+  private val context: Context,
+  private val renderingSettings: RenderingSettings
+) : MessageRenderer {
   private val timeFormatter = DateTimeFormatter.ofPattern(
-    (DateFormat.getTimeFormat(context) as SimpleDateFormat).toLocalizedPattern()
+    if (renderingSettings.timeFormat.isNotBlank()) {
+      renderingSettings.timeFormat
+    } else {
+      (DateFormat.getTimeFormat(context) as SimpleDateFormat).toLocalizedPattern()
+    }
   )
   private lateinit var senderColors: IntArray
 
@@ -38,16 +50,15 @@ class QuasselMessageRenderer(context: Context) : MessageRenderer {
     }
   }
 
-  override fun layout(type: Message_Type?, hasHighlight: Boolean)
-    = when (type) {
-    Nick, Mode, Join, Part, Quit, Kick, Kill, Info, DayChange, Topic, NetsplitJoin,
-    NetsplitQuit, Invite -> R.layout.widget_chatmessage_info
-    Notice               -> R.layout.widget_chatmessage_notice
-    Server               -> R.layout.widget_chatmessage_server
-    Error                -> R.layout.widget_chatmessage_error
-    Action               -> R.layout.widget_chatmessage_action
-    Plain                -> R.layout.widget_chatmessage_plain
-    else                 -> R.layout.widget_chatmessage_plain
+  override fun layout(type: Message_Type?, hasHighlight: Boolean) = when (type) {
+    Notice -> R.layout.widget_chatmessage_notice
+    Server -> R.layout.widget_chatmessage_server
+    Error  -> R.layout.widget_chatmessage_error
+    Action -> R.layout.widget_chatmessage_action
+    Plain  -> R.layout.widget_chatmessage_plain
+    Nick, Mode, Join, Part, Quit, Kick, Kill, Info, DayChange, Topic, NetsplitJoin, NetsplitQuit,
+    Invite -> R.layout.widget_chatmessage_info
+    else   -> R.layout.widget_chatmessage_placeholder
   }
 
   override fun init(viewHolder: QuasselMessageViewHolder,
@@ -67,119 +78,141 @@ class QuasselMessageRenderer(context: Context) : MessageRenderer {
   }
 
   override fun render(message: QuasselDatabase.DatabaseMessage): FormattedMessage {
-    return when (message.type) {
-      Message_Type.Plain.bit  -> FormattedMessage(
+    return when (Message_Type.of(message.type).enabledValues().firstOrNull()) {
+      Message_Type.Plain  -> FormattedMessage(
         message.messageId,
         timeFormatter.format(message.time.atZone(zoneId)),
         SpanFormatter.format(
-          "%s%s: %s",
-          message.senderPrefixes,
-          formatNick(message.sender),
+          context.getString(R.string.message_format_plain),
+          formatPrefix(message.senderPrefixes),
+          formatNick(message.sender, Message_Flag.of(message.flag).hasFlag(Message_Flag.Self)),
           message.content
         )
       )
-      Message_Type.Action.bit -> FormattedMessage(
+      Message_Type.Action -> FormattedMessage(
         message.messageId,
         timeFormatter.format(message.time.atZone(zoneId)),
         SpanFormatter.format(
-          "* %s%s %s",
-          message.senderPrefixes,
-          formatNick(message.sender),
+          context.getString(R.string.message_format_action),
+          formatPrefix(message.senderPrefixes),
+          formatNick(message.sender, Message_Flag.of(message.flag).hasFlag(Message_Flag.Self)),
           message.content
         )
       )
-      Message_Type.Notice.bit -> FormattedMessage(
+      Message_Type.Notice -> FormattedMessage(
         message.messageId,
         timeFormatter.format(message.time.atZone(zoneId)),
         SpanFormatter.format(
-          "[%s%s] %s",
-          message.senderPrefixes,
-          formatNick(message.sender),
+          context.getString(R.string.message_format_notice),
+          formatPrefix(message.senderPrefixes),
+          formatNick(message.sender, Message_Flag.of(message.flag).hasFlag(Message_Flag.Self)),
           message.content
         )
       )
-      Message_Type.Nick.bit   -> FormattedMessage(
+      Message_Type.Nick   -> FormattedMessage(
         message.messageId,
         timeFormatter.format(message.time.atZone(zoneId)),
         SpanFormatter.format(
-          "%s%s is now known as %s%s",
-          message.senderPrefixes,
-          formatNick(message.sender),
-          message.senderPrefixes,
-          formatNick(message.content)
+          context.getString(R.string.message_format_nick),
+          formatPrefix(message.senderPrefixes),
+          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_Type.Join.bit   -> FormattedMessage(
+      Message_Type.Mode   -> FormattedMessage(
         message.messageId,
         timeFormatter.format(message.time.atZone(zoneId)),
         SpanFormatter.format(
-          "%s%s joined",
-          message.senderPrefixes,
-          formatNick(message.sender)
+          context.getString(R.string.message_format_mode),
+          message.content,
+          formatPrefix(message.senderPrefixes),
+          formatNick(message.sender, Message_Flag.of(message.flag).hasFlag(Message_Flag.Self))
         )
       )
-      Message_Type.Part.bit   -> FormattedMessage(
+      Message_Type.Join   -> FormattedMessage(
         message.messageId,
         timeFormatter.format(message.time.atZone(zoneId)),
         SpanFormatter.format(
-          "%s%s left: %s",
-          message.senderPrefixes,
-          formatNick(message.sender),
-          message.content
+          context.getString(R.string.message_format_join),
+          formatPrefix(message.senderPrefixes),
+          formatNick(message.sender, Message_Flag.of(message.flag).hasFlag(Message_Flag.Self))
         )
       )
-      Message_Type.Quit.bit   -> FormattedMessage(
+      Message_Type.Part   -> FormattedMessage(
         message.messageId,
         timeFormatter.format(message.time.atZone(zoneId)),
-        SpanFormatter.format(
-          "%s%s quit: %s",
-          message.senderPrefixes,
-          formatNick(message.sender),
-          message.content
-        )
+        if (message.content.isBlank()) {
+          SpanFormatter.format(
+            context.getString(R.string.message_format_part_1),
+            formatPrefix(message.senderPrefixes),
+            formatNick(message.sender, Message_Flag.of(message.flag).hasFlag(Message_Flag.Self))
+          )
+        } else {
+          SpanFormatter.format(
+            context.getString(R.string.message_format_part_2),
+            formatPrefix(message.senderPrefixes),
+            formatNick(message.sender, Message_Flag.of(message.flag).hasFlag(Message_Flag.Self)),
+            message.content
+          )
+        }
       )
-      Message_Type.Server.bit,
-      Message_Type.Info.bit,
-      Message_Type.Error.bit  -> FormattedMessage(
+      Message_Type.Quit   -> FormattedMessage(
         message.messageId,
         timeFormatter.format(message.time.atZone(zoneId)),
-        SpanFormatter.format(
-          "%s",
-          message.content
-        )
+        if (message.content.isBlank()) {
+          SpanFormatter.format(
+            context.getString(R.string.message_format_quit_1),
+            formatPrefix(message.senderPrefixes),
+            formatNick(message.sender, Message_Flag.of(message.flag).hasFlag(Message_Flag.Self))
+          )
+        } else {
+          SpanFormatter.format(
+            context.getString(R.string.message_format_quit_2),
+            formatPrefix(message.senderPrefixes),
+            formatNick(message.sender, Message_Flag.of(message.flag).hasFlag(Message_Flag.Self)),
+            message.content
+          )
+        }
       )
-      Message_Type.Topic.bit  -> FormattedMessage(
+      Message_Type.Server,
+      Message_Type.Info,
+      Message_Type.Error  -> FormattedMessage(
         message.messageId,
         timeFormatter.format(message.time.atZone(zoneId)),
-        SpanFormatter.format(
-          "%s",
-          message.content
-        )
+        message.content
       )
-      else                    -> FormattedMessage(
+      Message_Type.Topic  -> FormattedMessage(
+        message.messageId,
+        timeFormatter.format(message.time.atZone(zoneId)),
+        message.content
+      )
+      else                -> FormattedMessage(
         message.messageId,
         timeFormatter.format(message.time.atZone(zoneId)),
         SpanFormatter.format(
           "[%d] %s%s: %s",
           message.type,
-          message.senderPrefixes,
-          formatNick(message.sender),
+          formatPrefix(message.senderPrefixes),
+          formatNick(message.sender, Message_Flag.of(message.flag).hasFlag(Message_Flag.Self)),
           message.content
         )
       )
     }
   }
 
-  private fun formatNick(sender: String): CharSequence {
+  private fun formatNickImpl(sender: String, colorize: Boolean): CharSequence {
     val nick = IrcUserUtils.nick(sender)
-    val senderColor = IrcUserUtils.senderColor(nick)
     val spannableString = SpannableString(nick)
-    spannableString.setSpan(
-      ForegroundColorSpan(senderColors[senderColor % senderColors.size]),
-      0,
-      nick.length,
-      SpannableString.SPAN_INCLUSIVE_EXCLUSIVE
-    )
+    if (colorize) {
+      val senderColor = IrcUserUtils.senderColor(nick)
+      spannableString.setSpan(
+        ForegroundColorSpan(senderColors[senderColor % senderColors.size]),
+        0,
+        nick.length,
+        SpannableString.SPAN_INCLUSIVE_EXCLUSIVE
+      )
+    }
     spannableString.setSpan(
       StyleSpan(Typeface.BOLD),
       0,
@@ -188,4 +221,18 @@ class QuasselMessageRenderer(context: Context) : MessageRenderer {
     )
     return spannableString
   }
+
+  private fun formatNick(sender: String, self: Boolean)
+    = when (renderingSettings.colorizeNicknames) {
+    ColorizeNicknamesMode.ALL          -> formatNickImpl(sender, true)
+    ColorizeNicknamesMode.ALL_BUT_MINE -> formatNickImpl(sender, !self)
+    ColorizeNicknamesMode.NONE         -> formatNickImpl(sender, false)
+  }
+
+  private fun formatPrefix(prefix: String)
+    = when (renderingSettings.showPrefix) {
+    ShowPrefixMode.ALL   -> prefix
+    ShowPrefixMode.FIRST -> prefix.substring(0, Math.min(prefix.length, 1))
+    ShowPrefixMode.NONE  -> ""
+  }
 }
\ No newline at end of file
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 ac243f448..61b8f123b 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
@@ -11,12 +11,17 @@ import android.widget.TextView
 import butterknife.BindView
 import butterknife.ButterKnife
 import de.kuschku.libquassel.protocol.BufferId
+import de.kuschku.libquassel.protocol.Buffer_Type
 import de.kuschku.libquassel.quassel.BufferInfo
+import de.kuschku.libquassel.quassel.syncables.interfaces.INetwork
 import de.kuschku.libquassel.session.Backend
 import de.kuschku.libquassel.session.SessionManager
+import de.kuschku.libquassel.util.hasFlag
 import de.kuschku.quasseldroid_ng.R
+import de.kuschku.quasseldroid_ng.ui.settings.data.DisplaySettings
 import de.kuschku.quasseldroid_ng.util.helper.*
 import de.kuschku.quasseldroid_ng.util.service.ServiceBoundFragment
+import io.reactivex.Observable
 
 class ToolbarFragment : ServiceBoundFragment() {
   @BindView(R.id.toolbar_title)
@@ -31,10 +36,83 @@ class ToolbarFragment : ServiceBoundFragment() {
   private val sessionManager: LiveData<SessionManager?>
     = backend.map(Backend::sessionManager)
 
-  private val currentBufferInfo: LiveData<BufferInfo?>
-    = sessionManager.switchMapRx(SessionManager::session).switchMap { session ->
-    buffer.switchMapRx {
-      session.bufferSyncer?.liveBufferInfo(it)
+  private val lag: LiveData<Long?>
+    = sessionManager.switchMapRx { it.session.switchMap { it.lag } }
+
+  private val displaySettings = DisplaySettings(
+    showLag = true
+  )
+
+  private val bufferData: LiveData<BufferData?> = sessionManager.switchMap { manager ->
+    buffer.switchMapRx { id ->
+      manager.session.switchMap {
+        val bufferSyncer = it.bufferSyncer
+        if (bufferSyncer != null) {
+          bufferSyncer.live_bufferInfos.switchMap {
+            val info = bufferSyncer.bufferInfo(id)
+            val network = manager.networks[info?.networkId]
+            if (info == null) {
+              Observable.just(
+                BufferData(
+                  description = "Info was null"
+                )
+              )
+            } else if (network == null) {
+              Observable.just(
+                BufferData(
+                  description = "Network was null"
+                )
+              )
+            } else {
+              when (info.type.toInt()) {
+                BufferInfo.Type.QueryBuffer.toInt()   -> {
+                  network.liveIrcUser(info.bufferName).switchMap { user ->
+                    user.live_realName.map { realName ->
+                      BufferData(
+                        info = info,
+                        network = network.networkInfo(),
+                        description = realName
+                      )
+                    }
+                  }
+                }
+                BufferInfo.Type.ChannelBuffer.toInt() -> {
+                  network.liveIrcChannel(
+                    info.bufferName
+                  ).switchMap { channel ->
+                    channel.live_topic.map { topic ->
+                      BufferData(
+                        info = info,
+                        network = network.networkInfo(),
+                        description = topic
+                      )
+                    }
+                  }
+                }
+                BufferInfo.Type.StatusBuffer.toInt()  -> {
+                  network.liveConnectionState.map {
+                    BufferData(
+                      info = info,
+                      network = network.networkInfo()
+                    )
+                  }
+                }
+                else                                  -> Observable.just(
+                  BufferData(
+                    description = "type is unknown: ${info.type.toInt()}"
+                  )
+                )
+              }
+            }
+          }
+        } else {
+          Observable.just(
+            BufferData(
+              description = "buffersyncer was null"
+            )
+          )
+        }
+      }
     }
   }
 
@@ -46,17 +124,20 @@ class ToolbarFragment : ServiceBoundFragment() {
     }
   }
 
-  var title: CharSequence
+  var title: CharSequence?
     get() = toolbarTitle.text
     set(value) {
-      toolbarTitle.text = value
+      if (value != null)
+        toolbarTitle.text = value
+      else
+        toolbarTitle.setText(R.string.app_name)
     }
 
-  var subtitle: CharSequence
+  var subtitle: CharSequence?
     get() = toolbarTitle.text
     set(value) {
-      toolbarSubtitle.text = value
-      toolbarSubtitle.visibleIf(value.isNotEmpty())
+      toolbarSubtitle.text = value ?: ""
+      toolbarSubtitle.visibleIf(value?.isNotEmpty() == true)
     }
 
   override fun onCreateView(inflater: LayoutInflater,
@@ -65,17 +146,37 @@ class ToolbarFragment : ServiceBoundFragment() {
     val view = inflater.inflate(R.layout.fragment_toolbar, container, false)
     ButterKnife.bind(this, view)
 
-    currentBufferInfo.zip(isSecure).observe(
+    bufferData.zip(isSecure, lag).observe(
       this, Observer {
       if (it != null) {
-        val (info, isSecure) = it
-        this.title = info?.bufferName ?: resources.getString(
-          R.string.app_name
-        )
+        val (data, isSecure, lag) = it
+        if (data?.info?.type?.hasFlag(Buffer_Type.StatusBuffer) == true) {
+          this.title = data.network?.networkName
+        } else {
+          this.title = data?.info?.bufferName
+        }
+
+        if (lag == 0L || !displaySettings.showLag) {
+          this.subtitle = data?.description
+        } else {
+          val description = data?.description
+          if (description.isNullOrBlank()) {
+            this.subtitle = "Lag: ${lag}ms"
+          } else {
+            this.subtitle = "Lag: ${lag}ms ${description}"
+          }
+        }
       }
     }
     )
 
     return view
   }
+
+  data class BufferData(
+    val info: BufferInfo? = null,
+    val network: INetwork.NetworkInfo? = null,
+    val description: String? = null
+  )
+
 }
\ No newline at end of file
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/settings/data/DisplaySettings.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/settings/data/DisplaySettings.kt
new file mode 100644
index 000000000..bfd333313
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/settings/data/DisplaySettings.kt
@@ -0,0 +1,5 @@
+package de.kuschku.quasseldroid_ng.ui.settings.data
+
+data class DisplaySettings(
+  val showLag: Boolean = true
+)
\ No newline at end of file
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/settings/data/RenderingSettings.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/settings/data/RenderingSettings.kt
new file mode 100644
index 000000000..a0a86d5c0
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/settings/data/RenderingSettings.kt
@@ -0,0 +1,37 @@
+package de.kuschku.quasseldroid_ng.ui.settings.data
+
+data class RenderingSettings(
+  val showPrefix: ShowPrefixMode = ShowPrefixMode.FIRST,
+  val colorizeNicknames: ColorizeNicknamesMode = ColorizeNicknamesMode.ALL_BUT_MINE,
+  val timeFormat: String = ""
+) {
+  enum class ColorizeNicknamesMode(val value: Int) {
+    ALL(0),
+    ALL_BUT_MINE(1),
+    NONE(2);
+
+    companion object {
+      fun of(value: Int) = when (value) {
+        0    -> ALL
+        1    -> ALL_BUT_MINE
+        2    -> NONE
+        else -> ALL_BUT_MINE
+      }
+    }
+  }
+
+  enum class ShowPrefixMode(val value: Int) {
+    ALL(0),
+    FIRST(1),
+    NONE(2);
+
+    companion object {
+      fun of(value: Int) = when (value) {
+        0    -> ALL
+        1    -> FIRST
+        2    -> NONE
+        else -> FIRST
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/LiveDataZipHelper.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/LiveDataZipHelper.kt
index c9045e619..47836fb30 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/LiveDataZipHelper.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/LiveDataZipHelper.kt
@@ -57,10 +57,49 @@ fun <A, B> zipLiveData(a: LiveData<A>, b: LiveData<B>): LiveData<Pair<A, B>> {
   }
 }
 
+fun <A, B, C> zipLiveData(a: LiveData<A>, b: LiveData<B>,
+                          c: LiveData<C>): LiveData<Triple<A, B, C>> {
+  return MediatorLiveData<Triple<A, B, C>>().apply {
+    var lastA: A? = null
+    var lastB: B? = null
+    var lastC: C? = null
+
+    fun update() {
+      val localLastA = lastA
+      val localLastB = lastB
+      val localLastC = lastC
+      if (localLastA != null && localLastB != null && localLastC != null)
+        this.value = Triple(localLastA, localLastB, localLastC)
+    }
+
+    addSource(a) {
+      lastA = it
+      update()
+    }
+    addSource(b) {
+      lastB = it
+      update()
+    }
+    addSource(c) {
+      lastC = it
+      update()
+    }
+  }
+}
+
+/**
+ * This is merely an extension function for [zipLiveData].
+ *
+ * @see zipLiveData
+ * @author Mitchell Skaggs
+ */
+fun <A, B> LiveData<A>.zip(b: LiveData<B>): LiveData<Pair<A, B>> = zipLiveData(this, b)
+
 /**
  * This is merely an extension function for [zipLiveData].
  *
  * @see zipLiveData
  * @author Mitchell Skaggs
  */
-fun <A, B> LiveData<A>.zip(b: LiveData<B>): LiveData<Pair<A, B>> = zipLiveData(this, b)
\ No newline at end of file
+fun <A, B, C> LiveData<A>.zip(b: LiveData<B>,
+                              c: LiveData<C>): LiveData<Triple<A, B, C>> = zipLiveData(this, b, c)
\ 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 53233ad25..55547dc3a 100644
--- a/app/src/main/res/layout/widget_chatmessage_error.xml
+++ b/app/src/main/res/layout/widget_chatmessage_error.xml
@@ -35,6 +35,5 @@
     android:layout_weight="1"
     android:textColor="?attr/colorForegroundError"
     android:textIsSelectable="true"
-    android:textStyle="italic"
     tools:text="everyone: deserves a chance to fly. No such channel" />
 </LinearLayout>
diff --git a/app/src/main/res/layout/widget_chatmessage_placeholder.xml b/app/src/main/res/layout/widget_chatmessage_placeholder.xml
new file mode 100644
index 000000000..be8f116d7
--- /dev/null
+++ b/app/src/main/res/layout/widget_chatmessage_placeholder.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+  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">
+
+  <TextView
+    android:id="@+id/time"
+    android:layout_width="0dip"
+    android:layout_height="0dip"
+    android:visibility="gone" />
+
+  <TextView
+    android:id="@+id/content"
+    android:layout_width="0dip"
+    android:layout_height="0dip"
+    android:visibility="gone" />
+</LinearLayout>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 7771d68ff..3527a83de 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -7,4 +7,15 @@
   <string name="crash_text">QD-NG has crashed</string>
   <string name="drawer_open">Open</string>
   <string name="drawer_close">Close</string>
+
+  <string name="message_format_plain">%1$s%2$s: %3$s</string>
+  <string name="message_format_action">* %1$s%2$s %3$s</string>
+  <string name="message_format_notice">[%1$s%2$s] %3$s</string>
+  <string name="message_format_nick">%1$s%2$s is now known as %3$s%4$s</string>
+  <string name="message_format_mode">%1$s by %2$s%3$s</string>
+  <string name="message_format_join">%1$s%2$s joined</string>
+  <string name="message_format_part_1">%1$s%2$s left</string>
+  <string name="message_format_part_2">%1$s%2$s left: %3$s</string>
+  <string name="message_format_quit_1">%1$s%2$s quit</string>
+  <string name="message_format_quit_2">%1$s%2$s quit (%3$s)</string>
 </resources>
diff --git a/lib/src/main/java/de/kuschku/libquassel/session/ISession.kt b/lib/src/main/java/de/kuschku/libquassel/session/ISession.kt
index 4cf9c05f5..862b13a4d 100644
--- a/lib/src/main/java/de/kuschku/libquassel/session/ISession.kt
+++ b/lib/src/main/java/de/kuschku/libquassel/session/ISession.kt
@@ -30,6 +30,8 @@ interface ISession : Closeable {
   val rpcHandler: RpcHandler?
   val initStatus: Observable<Pair<Int, Int>>
 
+  val lag: Observable<Long>
+
   companion object {
     val NULL = object : ISession {
       override val state = BehaviorSubject.createDefault(ConnectionState.DISCONNECTED)
@@ -51,6 +53,7 @@ interface ISession : Closeable {
       override val networks: Map<NetworkId, Network> = emptyMap()
       override val networkConfig: NetworkConfig? = null
       override val initStatus: Observable<Pair<Int, Int>> = Observable.just(0 to 0)
+      override val lag: Observable<Long> = Observable.just(0L)
 
       override fun close() = Unit
     }
diff --git a/lib/src/main/java/de/kuschku/libquassel/session/Session.kt b/lib/src/main/java/de/kuschku/libquassel/session/Session.kt
index 9f729068d..af8384cd5 100644
--- a/lib/src/main/java/de/kuschku/libquassel/session/Session.kt
+++ b/lib/src/main/java/de/kuschku/libquassel/session/Session.kt
@@ -50,6 +50,8 @@ class Session(
 
   override val initStatus = BehaviorSubject.createDefault(0 to 0)
 
+  override val lag = BehaviorSubject.createDefault(0L)
+
   init {
     coreConnection.start()
   }
@@ -62,6 +64,8 @@ class Session(
         password = userData.second
       )
     )
+
+    dispatch(SignalProxyMessage.HeartBeat(Instant.now()))
     return true
   }
 
@@ -101,6 +105,8 @@ class Session(
 
     synchronize(backlogManager)
 
+    dispatch(SignalProxyMessage.HeartBeat(Instant.now()))
+
     return true
   }
 
@@ -111,12 +117,15 @@ class Session(
   override fun onInitDone() {
     coreConnection.setState(ConnectionState.CONNECTED)
     log(INFO, "Session", "Initialization finished")
+
+    dispatch(SignalProxyMessage.HeartBeat(Instant.now()))
   }
 
   override fun handle(f: SignalProxyMessage.HeartBeatReply): Boolean {
     val now = Instant.now()
     val latency = now.toEpochMilli() - f.timestamp.toEpochMilli()
     log(INFO, "Session", "Latency of $latency ms")
+    lag.onNext(latency)
     return true
   }
 
diff --git a/lib/src/main/java/de/kuschku/libquassel/session/SessionManager.kt b/lib/src/main/java/de/kuschku/libquassel/session/SessionManager.kt
index 0f5829de0..2732d93f1 100644
--- a/lib/src/main/java/de/kuschku/libquassel/session/SessionManager.kt
+++ b/lib/src/main/java/de/kuschku/libquassel/session/SessionManager.kt
@@ -48,6 +48,8 @@ class SessionManager(offlineSession: ISession, val backlogStorage: BacklogStorag
     get() = session.or(lastSession).networkConfig
   override val rpcHandler: RpcHandler?
     get() = session.or(lastSession).rpcHandler
+  override val lag: Observable<Long>
+    get() = session.or(lastSession).lag
 
   override fun close() = session.or(lastSession).close()
 
-- 
GitLab