From 2ee7ca0beb4da098b0763891690ef36c5fa0500b Mon Sep 17 00:00:00 2001
From: Janne Koschinski <janne@kuschku.de>
Date: Wed, 11 Apr 2018 04:36:51 +0200
Subject: [PATCH] Add support for more avatar formats, realnames in messages,
 64bit time

---
 app/build.gradle.kts                          |   1 +
 .../quasseldroid/QuasseldroidGlideModule.kt   |   9 +-
 .../settings/AutoCompleteSettings.kt          |   5 +-
 .../quasseldroid/settings/MessageSettings.kt  |   5 +-
 .../kuschku/quasseldroid/settings/Settings.kt |  22 +-
 .../ui/chat/info/user/UserInfoFragment.kt     |  21 +-
 .../ui/chat/input/AutoCompleteAdapter.kt      |  17 +-
 .../ui/chat/input/AutoCompleteHelper.kt       |   4 +-
 .../ui/chat/messages/DisplayMessage.kt        |   2 -
 .../ui/chat/messages/MessageAdapter.kt        |  58 +-
 .../ui/chat/messages/MessageListFragment.kt   |  30 +-
 .../chat/messages/QuasselMessageRenderer.kt   |  38 +-
 .../ui/chat/nicks/NickListAdapter.kt          |  14 +-
 .../ui/chat/nicks/NickListFragment.kt         |  25 +-
 .../ui/coresettings/CoreSettingsFragment.kt   |   2 +-
 .../kuschku/quasseldroid/util/AvatarHelper.kt |  71 +++
 .../de/kuschku/quasseldroid/util/Patterns.kt  |  33 +-
 .../quasseldroid/util/backport/codec/Hex.kt   | 497 ++++++++++++++++++
 .../quasseldroid/util/helper/AnyHelper.kt     |   3 +
 .../quasseldroid/util/helper/BooleanHelper.kt |  10 +
 .../util/helper/CharSequenceHelper.kt         |   2 +
 .../quasseldroid/util/helper/GlideHelper.kt   |  33 ++
 .../quasseldroid/util/ui/DoubleClickHelper.kt |  21 +-
 .../main/res/layout/fragment_info_user.xml    |   2 +-
 .../res/values-de/strings_preferences.xml     |  11 +
 .../main/res/values/strings_preferences.xml   |  15 +
 app/src/main/res/xml/preferences.xml          |  30 +-
 .../de/kuschku/libquassel/protocol/Message.kt |   2 +
 .../primitive/serializer/MessageSerializer.kt |  18 +-
 .../libquassel/quassel/ExtendedFeature.kt     |   8 +-
 .../libquassel/quassel/LegacyFeature.kt       |  15 +-
 .../libquassel/quassel/QuasselFeatures.kt     |   4 +-
 .../quassel/syncables/BufferSyncer.kt         |  88 ++--
 .../quassel/syncables/BufferViewConfig.kt     |  75 +--
 .../libquassel/quassel/syncables/Identity.kt  |  40 +-
 .../quassel/syncables/interfaces/IIdentity.kt |  20 +-
 .../de/kuschku/libquassel/session/Session.kt  |   4 +-
 .../libquassel/session/SessionManager.kt      |   8 +-
 .../util/compatibility/HandlerService.kt      |   8 +-
 .../reference/JavaHandlerService.kt           |   3 +
 .../persistence/QuasselBacklogStorage.kt      |   2 +
 .../persistence/QuasselDatabase.kt            |  12 +-
 .../util/helper/ObservableHelper.kt           |  25 +-
 .../quasseldroid/viewmodel/EditorViewModel.kt |   5 +-
 .../viewmodel/QuasselViewModel.kt             |  24 +-
 .../viewmodel/data/AutoCompleteItem.kt        |   4 +-
 .../viewmodel/data/FormattedMessage.kt        |   5 +-
 .../viewmodel/data/IrcUserItem.kt             |   5 +-
 48 files changed, 1075 insertions(+), 281 deletions(-)
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid/util/AvatarHelper.kt
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid/util/backport/codec/Hex.kt
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid/util/helper/AnyHelper.kt
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid/util/helper/BooleanHelper.kt
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid/util/helper/GlideHelper.kt

diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 0a7df83a4..e644d7cb8 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -131,6 +131,7 @@ dependencies {
   implementation("org.threeten", "threetenbp", "1.3.6", classifier = "no-tzdb")
   implementation("org.jetbrains", "annotations", "16.0.1")
   implementation("com.google.code.gson", "gson", "2.8.2")
+  implementation("commons-codec", "commons-codec", "1.11")
   withVersion("8.8.1") {
     implementation("com.jakewharton", "butterknife", version)
     kapt("com.jakewharton", "butterknife-compiler", version)
diff --git a/app/src/main/java/de/kuschku/quasseldroid/QuasseldroidGlideModule.kt b/app/src/main/java/de/kuschku/quasseldroid/QuasseldroidGlideModule.kt
index 1f9c88d88..d7c61ae8a 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/QuasseldroidGlideModule.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/QuasseldroidGlideModule.kt
@@ -1,7 +1,14 @@
 package de.kuschku.quasseldroid
 
+import android.content.Context
+import android.util.Log
+import com.bumptech.glide.GlideBuilder
 import com.bumptech.glide.annotation.GlideModule
 import com.bumptech.glide.module.AppGlideModule
 
 @GlideModule
-class QuasseldroidGlideModule : AppGlideModule()
\ No newline at end of file
+class QuasseldroidGlideModule : AppGlideModule() {
+  override fun applyOptions(context: Context, builder: GlideBuilder) {
+    if (!BuildConfig.DEBUG) builder.setLogLevel(Log.ERROR)
+  }
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/settings/AutoCompleteSettings.kt b/app/src/main/java/de/kuschku/quasseldroid/settings/AutoCompleteSettings.kt
index f66f35a67..169900beb 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/settings/AutoCompleteSettings.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/settings/AutoCompleteSettings.kt
@@ -1,12 +1,13 @@
 package de.kuschku.quasseldroid.settings
 
 data class AutoCompleteSettings(
+  val senderDoubleClick: Boolean = true,
   val button: Boolean = false,
   val doubleTap: Boolean = true,
-  val auto: Boolean = true,
+  val auto: Boolean = false,
   val prefix: Boolean = true
 ) {
   companion object {
     val DEFAULT = AutoCompleteSettings()
   }
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/settings/MessageSettings.kt b/app/src/main/java/de/kuschku/quasseldroid/settings/MessageSettings.kt
index 11503a159..cbd532fa5 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/settings/MessageSettings.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/settings/MessageSettings.kt
@@ -12,7 +12,10 @@ data class MessageSettings(
   val showHostmaskPlain: Boolean = false,
   val nicksOnNewLine: Boolean = false,
   val timeAtEnd: Boolean = false,
-  val showAvatars: Boolean = false,
+  val showRealNames: Boolean = false,
+  val showAvatars: Boolean = true,
+  val showIRCCloudAvatars: Boolean = false,
+  val showGravatarAvatars: Boolean = false,
   val largerEmoji: Boolean = false
 ) {
 
diff --git a/app/src/main/java/de/kuschku/quasseldroid/settings/Settings.kt b/app/src/main/java/de/kuschku/quasseldroid/settings/Settings.kt
index abd7bf374..250923e70 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/settings/Settings.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/settings/Settings.kt
@@ -78,10 +78,22 @@ object Settings {
         context.getString(R.string.preference_time_at_end_key),
         MessageSettings.DEFAULT.timeAtEnd
       ),
+      showRealNames = getBoolean(
+        context.getString(R.string.preference_show_realnames_key),
+        MessageSettings.DEFAULT.showRealNames
+      ),
       showAvatars = getBoolean(
         context.getString(R.string.preference_show_avatars_key),
         MessageSettings.DEFAULT.showAvatars
       ),
+      showIRCCloudAvatars = getBoolean(
+        context.getString(R.string.preference_show_irccloud_avatars_key),
+        MessageSettings.DEFAULT.showIRCCloudAvatars
+      ),
+      showGravatarAvatars = getBoolean(
+        context.getString(R.string.preference_show_gravatar_avatars_key),
+        MessageSettings.DEFAULT.showGravatarAvatars
+      ),
       largerEmoji = getBoolean(
         context.getString(R.string.preference_larger_emoji_key),
         MessageSettings.DEFAULT.largerEmoji
@@ -91,21 +103,25 @@ object Settings {
 
   fun autoComplete(context: Context) = context.sharedPreferences {
     AutoCompleteSettings(
+      senderDoubleClick = getBoolean(
+        context.getString(R.string.preference_autocomplete_sender_doubleclick_key),
+        AutoCompleteSettings.DEFAULT.senderDoubleClick
+      ),
       button = getBoolean(
         context.getString(R.string.preference_autocomplete_button_key),
         AutoCompleteSettings.DEFAULT.button
       ),
       doubleTap = getBoolean(
         context.getString(R.string.preference_autocomplete_doubletap_key),
-        AutoCompleteSettings.DEFAULT.button
+        AutoCompleteSettings.DEFAULT.doubleTap
       ),
       auto = getBoolean(
         context.getString(R.string.preference_autocomplete_auto_key),
-        AutoCompleteSettings.DEFAULT.button
+        AutoCompleteSettings.DEFAULT.auto
       ),
       prefix = getBoolean(
         context.getString(R.string.preference_autocomplete_prefix_key),
-        AutoCompleteSettings.DEFAULT.button
+        AutoCompleteSettings.DEFAULT.prefix
       )
     )
   }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/info/user/UserInfoFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/info/user/UserInfoFragment.kt
index 58f5aec05..72622dfac 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/info/user/UserInfoFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/info/user/UserInfoFragment.kt
@@ -15,8 +15,8 @@ import butterknife.ButterKnife
 import de.kuschku.libquassel.protocol.Buffer_Type
 import de.kuschku.libquassel.quassel.syncables.IrcUser
 import de.kuschku.libquassel.util.IrcUserUtils
-import de.kuschku.quasseldroid.GlideApp
 import de.kuschku.quasseldroid.R
+import de.kuschku.quasseldroid.settings.MessageSettings
 import de.kuschku.quasseldroid.ui.chat.ChatActivity
 import de.kuschku.quasseldroid.ui.chat.input.AutoCompleteHelper.Companion.IGNORED_CHARS
 import de.kuschku.quasseldroid.util.AvatarHelper
@@ -84,6 +84,9 @@ class UserInfoFragment : ServiceBoundFragment() {
   @Inject
   lateinit var contentFormatter: ContentFormatter
 
+  @Inject
+  lateinit var messageSettings: MessageSettings
+
   override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                             savedInstanceState: Bundle?): View? {
     val view = inflater.inflate(R.layout.fragment_info_user, container, false)
@@ -125,17 +128,11 @@ class UserInfoFragment : ServiceBoundFragment() {
         val initial = rawInitial?.toUpperCase().toString()
         val senderColor = senderColors[senderColorIndex]
 
-        val fallbackDrawable = TextDrawable.builder().buildRect(initial, senderColor)
-
-        val avatarUrl = AvatarHelper.avatar(user = user)
-        if (avatarUrl != null) {
-          GlideApp.with(avatar)
-            .load(avatarUrl)
-            .placeholder(fallbackDrawable)
-            .into(avatar)
-        } else {
-          avatar.setImageDrawable(fallbackDrawable)
-        }
+        avatar.loadAvatars(
+          AvatarHelper.avatar(messageSettings, user, maxOf(avatar.width, avatar.height)),
+          TextDrawable.builder().buildRect(initial, senderColor),
+          crop = false
+        )
 
         nick.text = user.nick()
         realName.text = contentFormatter.format(requireContext(), user.realName())
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/AutoCompleteAdapter.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/AutoCompleteAdapter.kt
index 7f295e55e..d7a59d77c 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/AutoCompleteAdapter.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/AutoCompleteAdapter.kt
@@ -11,15 +11,10 @@ import android.widget.ImageView
 import android.widget.TextView
 import butterknife.BindView
 import butterknife.ButterKnife
-import com.bumptech.glide.request.RequestOptions
-import de.kuschku.quasseldroid.GlideApp
 import de.kuschku.quasseldroid.R
 import de.kuschku.quasseldroid.settings.MessageSettings
 import de.kuschku.quasseldroid.ui.chat.nicks.NickListAdapter.Companion.VIEWTYPE_AWAY
-import de.kuschku.quasseldroid.util.helper.getVectorDrawableCompat
-import de.kuschku.quasseldroid.util.helper.styledAttributes
-import de.kuschku.quasseldroid.util.helper.tint
-import de.kuschku.quasseldroid.util.helper.visibleIf
+import de.kuschku.quasseldroid.util.helper.*
 import de.kuschku.quasseldroid.util.ui.SpanFormatter
 import de.kuschku.quasseldroid.viewmodel.data.AutoCompleteItem
 import de.kuschku.quasseldroid.viewmodel.data.BufferStatus
@@ -112,15 +107,7 @@ class AutoCompleteAdapter(
         nick.text = SpanFormatter.format("%s%s", data.modes, data.displayNick ?: data.nick)
         realname.text = data.realname
 
-        if (data.avatarUrl != null) {
-          GlideApp.with(itemView)
-            .load(data.avatarUrl)
-            .apply(RequestOptions.circleCropTransform())
-            .placeholder(data.fallbackDrawable)
-            .into(avatar)
-        } else {
-          avatar.setImageDrawable(data.fallbackDrawable)
-        }
+        avatar.loadAvatars(data.avatarUrls, data.fallbackDrawable)
       }
     }
 
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/AutoCompleteHelper.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/AutoCompleteHelper.kt
index 1894073dc..be99fcca2 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/AutoCompleteHelper.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/AutoCompleteHelper.kt
@@ -44,6 +44,8 @@ class AutoCompleteHelper(
     IntArray(length()) { getColor(it, 0) }
   }
 
+  private val avatarSize = activity.resources.getDimensionPixelSize(R.dimen.avatar_size)
+
   init {
     viewModel.autoCompleteData.toLiveData().observe(activity, Observer {
       val query = it?.first ?: ""
@@ -148,7 +150,7 @@ class AutoCompleteHelper(
                   user.realName(),
                   user.isAway(),
                   network.support("CASEMAPPING"),
-                  AvatarHelper.avatar(user = user)
+                  AvatarHelper.avatar(messageSettings, user, avatarSize)
                 )
               }
 
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/DisplayMessage.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/DisplayMessage.kt
index 0a1872ead..5723bf24b 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/DisplayMessage.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/DisplayMessage.kt
@@ -2,7 +2,6 @@ package de.kuschku.quasseldroid.ui.chat.messages
 
 import de.kuschku.libquassel.protocol.MsgId
 import de.kuschku.quasseldroid.persistence.QuasselDatabase
-import de.kuschku.quasseldroid.util.AvatarHelper
 
 data class DisplayMessage(
   val content: QuasselDatabase.DatabaseMessage,
@@ -32,5 +31,4 @@ data class DisplayMessage(
     isMarkerLine,
     isEmoji
   )
-  val avatarUrl = AvatarHelper.avatar(message = content)
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageAdapter.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageAdapter.kt
index eb54e1f35..6e53f1c92 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageAdapter.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageAdapter.kt
@@ -11,14 +11,14 @@ import android.widget.ImageView
 import android.widget.TextView
 import butterknife.BindView
 import butterknife.ButterKnife
-import com.bumptech.glide.request.RequestOptions
 import de.kuschku.libquassel.protocol.Message_Flag
 import de.kuschku.libquassel.protocol.Message_Type
 import de.kuschku.libquassel.util.flag.hasFlag
-import de.kuschku.quasseldroid.GlideApp
 import de.kuschku.quasseldroid.R
 import de.kuschku.quasseldroid.persistence.QuasselDatabase
 import de.kuschku.quasseldroid.util.helper.getOrPut
+import de.kuschku.quasseldroid.util.helper.loadAvatars
+import de.kuschku.quasseldroid.util.ui.DoubleClickHelper
 import de.kuschku.quasseldroid.viewmodel.data.FormattedMessage
 import me.saket.bettermovementmethod.BetterLinkMovementMethod
 import javax.inject.Inject
@@ -35,23 +35,28 @@ class MessageAdapter @Inject constructor(
   }) {
   private val movementMethod = BetterLinkMovementMethod.newInstance()
   private var clickListener: ((FormattedMessage) -> Unit)? = null
-  private var selectionListener: ((FormattedMessage) -> Unit)? = null
+  private var longClickListener: ((FormattedMessage) -> Unit)? = null
+  private var doubleClickListener: ((QuasselDatabase.DatabaseMessage) -> Unit)? = null
   private var expansionListener: ((QuasselDatabase.DatabaseMessage) -> Unit)? = null
   private var urlLongClickListener: ((TextView, String) -> Boolean)? = null
 
-  fun setOnClickListener(listener: (FormattedMessage) -> Unit) {
+  fun setOnClickListener(listener: ((FormattedMessage) -> Unit)?) {
     this.clickListener = listener
   }
 
-  fun setOnSelectionListener(listener: (FormattedMessage) -> Unit) {
-    this.selectionListener = listener
+  fun setOnLongClickListener(listener: ((FormattedMessage) -> Unit)?) {
+    this.longClickListener = listener
   }
 
-  fun setOnExpansionListener(listener: (QuasselDatabase.DatabaseMessage) -> Unit) {
+  fun setOnDoubleClickListener(listener: ((QuasselDatabase.DatabaseMessage) -> Unit)?) {
+    this.doubleClickListener = listener
+  }
+
+  fun setOnExpansionListener(listener: ((QuasselDatabase.DatabaseMessage) -> Unit)?) {
     this.expansionListener = listener
   }
 
-  fun setOnUrlLongClickListener(listener: (TextView, String) -> Boolean) {
+  fun setOnUrlLongClickListener(listener: ((TextView, String) -> Boolean)?) {
     this.urlLongClickListener = listener
   }
 
@@ -121,7 +126,8 @@ class MessageAdapter @Inject constructor(
         false
       ),
       clickListener,
-      selectionListener,
+      longClickListener,
+      doubleClickListener,
       expansionListener,
       movementMethod
     )
@@ -138,7 +144,8 @@ class MessageAdapter @Inject constructor(
   class QuasselMessageViewHolder(
     itemView: View,
     clickListener: ((FormattedMessage) -> Unit)? = null,
-    selectionListener: ((FormattedMessage) -> Unit)? = null,
+    longClickListener: ((FormattedMessage) -> Unit)? = null,
+    doubleClickListener: ((QuasselDatabase.DatabaseMessage) -> Unit)? = null,
     expansionListener: ((QuasselDatabase.DatabaseMessage) -> Unit)? = null,
     movementMethod: BetterLinkMovementMethod
   ) : RecyclerView.ViewHolder(itemView) {
@@ -167,6 +174,7 @@ class MessageAdapter @Inject constructor(
     var combined: TextView? = null
 
     private var message: FormattedMessage? = null
+    private var original: QuasselDatabase.DatabaseMessage? = null
     private var selectable: Boolean = false
     private var clickable: Boolean = false
 
@@ -181,12 +189,20 @@ class MessageAdapter @Inject constructor(
     private val localLongClickListener = View.OnLongClickListener {
       if (selectable) {
         message?.let {
-          selectionListener?.invoke(it)
+          longClickListener?.invoke(it)
         }
       }
       true
     }
 
+    private val localDoubleClickListener = {
+      if (clickable) {
+        original?.let {
+          doubleClickListener?.invoke(it)
+        }
+      }
+    }
+
     init {
       ButterKnife.bind(this, itemView)
       content?.movementMethod = movementMethod
@@ -194,10 +210,15 @@ class MessageAdapter @Inject constructor(
 
       itemView.setOnClickListener(localClickListener)
       itemView.setOnLongClickListener(localLongClickListener)
+      itemView.setOnTouchListener(DoubleClickHelper(itemView).apply {
+        this.doubleClickListener = localDoubleClickListener
+      })
     }
 
-    fun bind(message: FormattedMessage, selectable: Boolean = true, clickable: Boolean = true) {
+    fun bind(message: FormattedMessage, original: QuasselDatabase.DatabaseMessage,
+             selectable: Boolean = true, clickable: Boolean = true) {
       this.message = message
+      this.original = original
       this.selectable = selectable
       this.clickable = clickable
 
@@ -209,18 +230,7 @@ class MessageAdapter @Inject constructor(
 
       this.itemView.isSelected = message.isSelected
 
-      avatar?.let { avatarView ->
-        if (message.avatarUrl != null) {
-          GlideApp.with(itemView)
-            .load(message.avatarUrl)
-            .apply(RequestOptions.circleCropTransform())
-            .placeholder(message.fallbackDrawable)
-            .into(avatarView)
-        } else {
-          GlideApp.with(itemView).clear(avatarView)
-          avatarView.setImageDrawable(message.fallbackDrawable)
-        }
-      }
+      avatar?.loadAvatars(message.avatarUrls, message.fallbackDrawable)
     }
   }
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageListFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageListFragment.kt
index 68bafbb81..c30714fa1 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageListFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageListFragment.kt
@@ -24,12 +24,16 @@ import de.kuschku.libquassel.protocol.BufferId
 import de.kuschku.libquassel.protocol.MsgId
 import de.kuschku.libquassel.quassel.syncables.BufferSyncer
 import de.kuschku.libquassel.util.helpers.value
+import de.kuschku.libquassel.util.irc.HostmaskHelper
 import de.kuschku.quasseldroid.GlideApp
 import de.kuschku.quasseldroid.R
 import de.kuschku.quasseldroid.persistence.QuasselDatabase
 import de.kuschku.quasseldroid.settings.AppearanceSettings
+import de.kuschku.quasseldroid.settings.AutoCompleteSettings
 import de.kuschku.quasseldroid.settings.BacklogSettings
 import de.kuschku.quasseldroid.settings.MessageSettings
+import de.kuschku.quasseldroid.ui.chat.ChatActivity
+import de.kuschku.quasseldroid.util.AvatarHelper
 import de.kuschku.quasseldroid.util.helper.*
 import de.kuschku.quasseldroid.util.service.ServiceBoundFragment
 import de.kuschku.quasseldroid.util.ui.LinkLongClickMenuHelper
@@ -55,6 +59,9 @@ class MessageListFragment : ServiceBoundFragment() {
   @Inject
   lateinit var appearanceSettings: AppearanceSettings
 
+  @Inject
+  lateinit var autoCompleteSettings: AutoCompleteSettings
+
   @Inject
   lateinit var backlogSettings: BacklogSettings
 
@@ -183,7 +190,7 @@ class MessageListFragment : ServiceBoundFragment() {
         }
       }
     }
-    adapter.setOnSelectionListener { msg ->
+    adapter.setOnLongClickListener { msg ->
       if (actionMode == null) {
         activity?.startActionMode(actionModeCallback)
       }
@@ -191,6 +198,13 @@ class MessageListFragment : ServiceBoundFragment() {
         actionMode?.finish()
       }
     }
+    if (autoCompleteSettings.senderDoubleClick)
+      adapter.setOnDoubleClickListener { msg ->
+        val intent = Intent(requireContext(), ChatActivity::class.java)
+        intent.type = "text/plain"
+        intent.putExtra(Intent.EXTRA_TEXT, "${HostmaskHelper.nick(msg.sender)}: ")
+        startActivity(intent)
+      }
     adapter.setOnUrlLongClickListener(LinkLongClickMenuHelper())
 
     messageList.adapter = adapter
@@ -371,15 +385,15 @@ class MessageListFragment : ServiceBoundFragment() {
       requireContext().resources.displayMetrics
     ).roundToInt()
 
-    val sizeProvider = FixedPreloadSizeProvider<String>(avatarSize, avatarSize)
+    val sizeProvider = FixedPreloadSizeProvider<List<String>>(avatarSize, avatarSize)
 
-    val preloadModelProvider = object : ListPreloader.PreloadModelProvider<String> {
-      override fun getPreloadItems(position: Int) = adapter[position]?.avatarUrl?.let {
-        mutableListOf(it)
-      } ?: mutableListOf()
+    val preloadModelProvider = object : ListPreloader.PreloadModelProvider<List<String>> {
+      override fun getPreloadItems(position: Int) = listOfNotNull(
+        adapter[position]?.content?.let { AvatarHelper.avatar(messageSettings, it, avatarSize) }
+      )
 
-      override fun getPreloadRequestBuilder(item: String) =
-        GlideApp.with(this@MessageListFragment).load(item).override(avatarSize)
+      override fun getPreloadRequestBuilder(item: List<String>) =
+        GlideApp.with(this@MessageListFragment).loadWithFallbacks(item)?.override(avatarSize)
     }
 
     val preloader = RecyclerViewPreloader(Glide.with(this), preloadModelProvider, sizeProvider, 10)
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/QuasselMessageRenderer.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/QuasselMessageRenderer.kt
index 9c0badeca..b6946bae8 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/QuasselMessageRenderer.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/QuasselMessageRenderer.kt
@@ -6,6 +6,7 @@ import android.graphics.drawable.ColorDrawable
 import android.graphics.drawable.LayerDrawable
 import android.os.Build
 import android.text.SpannableString
+import android.text.SpannableStringBuilder
 import android.text.style.ForegroundColorSpan
 import android.text.style.StyleSpan
 import android.util.TypedValue
@@ -23,6 +24,7 @@ import de.kuschku.quasseldroid.persistence.QuasselDatabase
 import de.kuschku.quasseldroid.settings.MessageSettings
 import de.kuschku.quasseldroid.settings.MessageSettings.ColorizeNicknamesMode
 import de.kuschku.quasseldroid.settings.MessageSettings.ShowPrefixMode
+import de.kuschku.quasseldroid.util.AvatarHelper
 import de.kuschku.quasseldroid.util.helper.styledAttributes
 import de.kuschku.quasseldroid.util.helper.visibleIf
 import de.kuschku.quasseldroid.util.irc.format.ContentFormatter
@@ -152,11 +154,10 @@ class QuasselMessageRenderer @Inject constructor(
   override fun bind(holder: MessageAdapter.QuasselMessageViewHolder, message: FormattedMessage,
                     original: QuasselDatabase.DatabaseMessage) =
     Message_Type.of(original.type).hasFlag(DayChange).let { isDayChange ->
-      holder.bind(message, !isDayChange, !isDayChange)
+      holder.bind(message, original, !isDayChange, !isDayChange)
     }
 
-  override fun render(context: Context,
-                      message: DisplayMessage): FormattedMessage {
+  override fun render(context: Context, message: DisplayMessage): FormattedMessage {
     context.theme.styledAttributes(
       R.attr.senderColor0, R.attr.senderColor1, R.attr.senderColor2, R.attr.senderColor3,
       R.attr.senderColor4, R.attr.senderColor5, R.attr.senderColor6, R.attr.senderColor7,
@@ -170,20 +171,30 @@ class QuasselMessageRenderer @Inject constructor(
       selfColor = getColor(16, 0)
     }
 
+    val avatarSize = TypedValue.applyDimension(
+      TypedValue.COMPLEX_UNIT_SP,
+      messageSettings.textSize * 2.5f,
+      context.resources.displayMetrics
+    ).roundToInt()
+
     val self = Message_Flag.of(message.content.flag).hasFlag(Message_Flag.Self)
     val highlight = Message_Flag.of(message.content.flag).hasFlag(Message_Flag.Highlight)
     return when (Message_Type.of(message.content.type).enabledValues().firstOrNull()) {
       Message_Type.Plain        -> {
-        val nick = SpanFormatter.format(
-          "%s%s",
-          formatPrefix(message.content.senderPrefixes, highlight),
-          formatNick(
+        val realName = contentFormatter.format(context, message.content.realName, highlight)
+        val nick = SpannableStringBuilder().apply {
+          append(formatPrefix(message.content.senderPrefixes, highlight))
+          append(formatNick(
             message.content.sender,
             self,
             highlight,
             messageSettings.showHostmaskPlain && messageSettings.nicksOnNewLine
-          )
-        )
+          ))
+          if (messageSettings.showRealNames) {
+            append(" ")
+            append(realName)
+          }
+        }
         val content = contentFormatter.format(context, message.content.content, highlight)
         val nickName = HostmaskHelper.nick(message.content.sender)
         val senderColorIndex = IrcUserUtils.senderColor(nickName)
@@ -200,8 +211,13 @@ class QuasselMessageRenderer @Inject constructor(
           time = timeFormatter.format(message.content.time.atZone(zoneId)),
           name = nick,
           content = content,
-          combined = SpanFormatter.format("%s: %s", nick, content),
-          avatarUrl = message.avatarUrl,
+          combined = SpannableStringBuilder().apply {
+            append(nick)
+            append(": ")
+            append(content)
+          },
+          realName = realName,
+          avatarUrls = AvatarHelper.avatar(messageSettings, message.content, avatarSize),
           fallbackDrawable = TextDrawable.builder().buildRound(initial, senderColor),
           isMarkerLine = message.isMarkerLine,
           isExpanded = message.isExpanded,
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/nicks/NickListAdapter.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/nicks/NickListAdapter.kt
index 94caac564..e724428ab 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/nicks/NickListAdapter.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/nicks/NickListAdapter.kt
@@ -10,10 +10,9 @@ import android.widget.ImageView
 import android.widget.TextView
 import butterknife.BindView
 import butterknife.ButterKnife
-import com.bumptech.glide.request.RequestOptions
-import de.kuschku.quasseldroid.GlideApp
 import de.kuschku.quasseldroid.R
 import de.kuschku.quasseldroid.settings.MessageSettings
+import de.kuschku.quasseldroid.util.helper.loadAvatars
 import de.kuschku.quasseldroid.util.helper.visibleIf
 import de.kuschku.quasseldroid.util.ui.SpanFormatter
 import de.kuschku.quasseldroid.viewmodel.data.IrcUserItem
@@ -86,16 +85,7 @@ class NickListAdapter(
       nick.text = SpanFormatter.format("%s%s", data.modes, data.displayNick ?: data.nick)
       realname.text = data.realname
 
-      if (data.avatarUrl != null) {
-        GlideApp.with(itemView)
-          .load(data.avatarUrl)
-          .apply(RequestOptions.circleCropTransform())
-          .placeholder(data.fallbackDrawable)
-          .into(avatar)
-      } else {
-        GlideApp.with(itemView).clear(avatar)
-        avatar.setImageDrawable(data.fallbackDrawable)
-      }
+      avatar.loadAvatars(data.avatarUrls, data.fallbackDrawable)
     }
   }
 
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/nicks/NickListFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/nicks/NickListFragment.kt
index e86cb6717..5aafe6bda 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/nicks/NickListFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/nicks/NickListFragment.kt
@@ -29,6 +29,8 @@ import de.kuschku.quasseldroid.R
 import de.kuschku.quasseldroid.settings.AppearanceSettings
 import de.kuschku.quasseldroid.settings.MessageSettings
 import de.kuschku.quasseldroid.ui.chat.info.user.UserInfoActivity
+import de.kuschku.quasseldroid.util.AvatarHelper
+import de.kuschku.quasseldroid.util.helper.loadWithFallbacks
 import de.kuschku.quasseldroid.util.helper.styledAttributes
 import de.kuschku.quasseldroid.util.helper.toLiveData
 import de.kuschku.quasseldroid.util.irc.format.IrcFormatDeserializer
@@ -73,6 +75,8 @@ class NickListFragment : ServiceBoundFragment() {
       }
     }
 
+    val avatarSize = resources.getDimensionPixelSize(R.dimen.avatar_size)
+
     viewModel.nickData.map {
       it.map {
         val nickName = it.nick
@@ -111,10 +115,11 @@ class NickListFragment : ServiceBoundFragment() {
           },
           realname = ircFormatDeserializer.formatString(
             requireContext(), it.realname.toString(), messageSettings.colorizeMirc
-          )
+          ),
+          avatarUrls = AvatarHelper.avatar(messageSettings, it, avatarSize)
         )
       }.sortedBy {
-        IrcCaseMappers[it.networkCasemapping].toLowerCase(it.nick)
+        IrcCaseMappers.unicode.toLowerCase(it.nick)
       }.sortedBy {
         it.lowestMode
       }
@@ -124,17 +129,15 @@ class NickListFragment : ServiceBoundFragment() {
       nickList.layoutManager.onRestoreInstanceState(getParcelable(KEY_STATE_LIST))
     }
 
-    val avatar_size = resources.getDimensionPixelSize(R.dimen.avatar_size)
-
-    val sizeProvider = FixedPreloadSizeProvider<String>(avatar_size, avatar_size)
+    val sizeProvider = FixedPreloadSizeProvider<List<String>>(avatarSize, avatarSize)
 
-    val preloadModelProvider = object : ListPreloader.PreloadModelProvider<String> {
-      override fun getPreloadItems(position: Int) = nickListAdapter[position]?.avatarUrl?.let {
-        mutableListOf(it)
-      } ?: mutableListOf()
+    val preloadModelProvider = object : ListPreloader.PreloadModelProvider<List<String>> {
+      override fun getPreloadItems(position: Int) = listOfNotNull(
+        nickListAdapter[position]?.let { AvatarHelper.avatar(messageSettings, it) }
+      )
 
-      override fun getPreloadRequestBuilder(item: String) =
-        GlideApp.with(this@NickListFragment).load(item).override(avatar_size)
+      override fun getPreloadRequestBuilder(item: List<String>) =
+        GlideApp.with(this@NickListFragment).loadWithFallbacks(item)?.override(avatarSize)
     }
 
     val preloader = RecyclerViewPreloader(Glide.with(this), preloadModelProvider, sizeProvider, 10)
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/CoreSettingsFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/CoreSettingsFragment.kt
index 64cbd6ce3..f5e89cdd3 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/CoreSettingsFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/coresettings/CoreSettingsFragment.kt
@@ -95,7 +95,7 @@ class CoreSettingsFragment : ServiceBoundFragment() {
     viewModel.identities.switchMap {
       combineLatest(it.values.map(Identity::liveUpdates)).map {
         it.map {
-          SettingsItem(it.id(), it.identityName())
+          SettingsItem(it.id(), it.identityName() ?: "")
         }.sortedBy(SettingsItem::name)
       }
     }.toLiveData().observe(this, Observer {
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/AvatarHelper.kt b/app/src/main/java/de/kuschku/quasseldroid/util/AvatarHelper.kt
new file mode 100644
index 000000000..c949a544c
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/AvatarHelper.kt
@@ -0,0 +1,71 @@
+package de.kuschku.quasseldroid.util
+
+import de.kuschku.libquassel.quassel.syncables.IrcUser
+import de.kuschku.libquassel.util.irc.HostmaskHelper
+import de.kuschku.libquassel.util.irc.IrcCaseMappers
+import de.kuschku.quasseldroid.persistence.QuasselDatabase
+import de.kuschku.quasseldroid.settings.MessageSettings
+import de.kuschku.quasseldroid.util.backport.codec.Hex
+import de.kuschku.quasseldroid.util.helper.letIf
+import de.kuschku.quasseldroid.util.helper.notBlank
+import de.kuschku.quasseldroid.viewmodel.data.IrcUserItem
+import org.apache.commons.codec.digest.DigestUtils
+
+object AvatarHelper {
+  fun avatar(settings: MessageSettings, message: QuasselDatabase.DatabaseMessage,
+             size: Int? = null) = listOfNotNull(
+    message.avatarUrl.notBlank()?.let { listOf(it) },
+    settings.showIRCCloudAvatars.letIf {
+      ircCloudFallback(HostmaskHelper.user(message.sender), size)
+    },
+    settings.showGravatarAvatars.letIf {
+      gravatarFallback(message.realName, size)
+    }
+  ).flatten()
+
+  fun avatar(settings: MessageSettings, user: IrcUserItem, size: Int? = null) = listOfNotNull(
+    settings.showIRCCloudAvatars.letIf {
+      ircCloudFallback(HostmaskHelper.user(user.hostmask), size)
+    },
+    settings.showGravatarAvatars.letIf {
+      gravatarFallback(user.realname.toString(), size)
+    }
+  ).flatten()
+
+  fun avatar(settings: MessageSettings, user: IrcUser, size: Int? = null) = listOfNotNull(
+    settings.showIRCCloudAvatars.letIf {
+      ircCloudFallback(user.user(), size)
+    },
+    settings.showGravatarAvatars.letIf {
+      gravatarFallback(user.realName(), size)
+    }
+  ).flatten()
+
+  private fun gravatarFallback(realname: String, size: Int?): List<String> {
+    return Regex(Patterns.AUTOLINK_EMAIL_ADDRESS_STR)
+      .findAll(realname)
+      .mapNotNull {
+        it.groups[1]?.value
+      }.map { email ->
+        val hash = Hex.encodeHexString(DigestUtils.md5(IrcCaseMappers.unicode.toLowerCase(email)))
+        if (size == null) {
+          "https://www.gravatar.com/avatar/$hash?d=404"
+        } else {
+          "https://www.gravatar.com/avatar/$hash?d=404&s=${truncateSize(size)}"
+        }
+      }.toList()
+  }
+
+  private fun ircCloudFallback(ident: String, size: Int?): List<String> {
+    val userId = Regex("(?:~?)[us]id(\\d+)").matchEntire(ident)?.groupValues?.lastOrNull()
+                 ?: return emptyList()
+
+    if (size != null) {
+      return listOf("https://static.irccloud-cdn.com/avatar-redirect/w${truncateSize(size)}/$userId")
+    }
+
+    return listOf("https://static.irccloud-cdn.com/avatar-redirect/$userId")
+  }
+
+  private fun truncateSize(originalSize: Int) = if (originalSize > 72) 512 else 72
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/Patterns.kt b/app/src/main/java/de/kuschku/quasseldroid/util/Patterns.kt
index d6f3e59d1..69da950e2 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/util/Patterns.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/Patterns.kt
@@ -5,6 +5,9 @@ import java.util.regex.Pattern
 
 @SuppressWarnings("Access")
 object Patterns {
+  @Language("RegExp")
+  const val WORD_BOUNDARY = "(?:\\b|$|^)"
+
   @Language("RegExp")
   const val IPv4 = "(?:(?:[0-1]?[0-9]?[0-9]|2[0-5][0-5])\\.){3}(?:[0-1]?[0-9]?[0-9]|2[0-5][0-5])"
 
@@ -41,7 +44,7 @@ object Patterns {
   const val TLD = """(?:$PUNYCODE_TLD|[$TLD_CHAR]{2,63})"""
 
   @Language("RegExp")
-  const val HOST_NAME = """(?:$IRI_LABEL\.)+$TLD.?"""
+  const val HOST_NAME = """(?:$IRI_LABEL\.)+$TLD\.?"""
 
   @Language("RegExp")
   const val LOCAL_HOST_NAME = """(?:$IRI_LABEL\.)*$IRI_LABEL"""
@@ -49,4 +52,32 @@ object Patterns {
   @Language("RegExp")
   const val DOMAIN_NAME_STR = """(?:$LOCAL_HOST_NAME|$HOST_NAME|$IP_ADDRESS_STRING)"""
   val DOMAIN_NAME: Pattern = Pattern.compile(DOMAIN_NAME_STR)
+
+  /**
+   * Regular expression for valid email characters. Does not include some of the valid characters
+   * defined in RFC5321: #&~!^`{}/=$*?|
+   */
+  @Language("RegExp")
+  const val EMAIL_CHAR = """$LABEL_CHAR\+-_%'"""
+
+  /**
+   * Regular expression for local part of an email address. RFC5321 section 4.5.3.1.1 limits
+   * the local part to be at most 64 octets.
+   */
+  @Language("RegExp")
+  const val EMAIL_ADDRESS_LOCAL_PART = """[$EMAIL_CHAR](?:[$EMAIL_CHAR.]{0,62}[$EMAIL_CHAR])?"""
+
+  /**
+   * Regular expression for the domain part of an email address. RFC5321 section 4.5.3.1.2 limits
+   * the domain to be at most 255 octets.
+   */
+  @Language("RegExp")
+  const val EMAIL_ADDRESS_DOMAIN = """(?=.{1,255}(?:\s|$|^))$HOST_NAME"""
+
+  /**
+   * Regular expression pattern to match email addresses. It excludes double quoted local parts
+   * and the special characters #&~!^`{}/=$*?| that are included in RFC5321.
+   */
+  const val AUTOLINK_EMAIL_ADDRESS_STR = """($WORD_BOUNDARY(?:$EMAIL_ADDRESS_LOCAL_PART@$EMAIL_ADDRESS_DOMAIN)$WORD_BOUNDARY)"""
+  val AUTOLINK_EMAIL_ADDRESS = Pattern.compile(AUTOLINK_EMAIL_ADDRESS_STR)
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/backport/codec/Hex.kt b/app/src/main/java/de/kuschku/quasseldroid/util/backport/codec/Hex.kt
new file mode 100644
index 000000000..1f927f44a
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/backport/codec/Hex.kt
@@ -0,0 +1,497 @@
+package de.kuschku.quasseldroid.util.backport.codec
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.commons.codec.*
+import java.nio.ByteBuffer
+import java.nio.charset.Charset
+
+/**
+ * Converts hexadecimal Strings. The charset used for certain operation can be set, the default is set in
+ * [.DEFAULT_CHARSET_NAME]
+ *
+ * This class is thread-safe.
+ *
+ * @since 1.1
+ * @version $Id: Hex.java 1811344 2017-10-06 15:19:57Z ggregory $
+ */
+class Hex : BinaryEncoder, BinaryDecoder {
+
+  /**
+   * Gets the charset.
+   *
+   * @return the charset.
+   * @since 1.7
+   */
+  val charset: Charset
+
+  /**
+   * Gets the charset name.
+   *
+   * @return the charset name.
+   * @since 1.4
+   */
+  val charsetName: String
+    get() = this.charset.name()
+
+  /**
+   * Creates a new codec with the default charset name [.DEFAULT_CHARSET]
+   */
+  constructor() {
+    // use default encoding
+    this.charset = DEFAULT_CHARSET
+  }
+
+  /**
+   * Creates a new codec with the given Charset.
+   *
+   * @param charset
+   * the charset.
+   * @since 1.7
+   */
+  constructor(charset: Charset) {
+    this.charset = charset
+  }
+
+  /**
+   * Creates a new codec with the given charset name.
+   *
+   * @param charsetName
+   * the charset name.
+   * @throws java.nio.charset.UnsupportedCharsetException
+   * If the named charset is unavailable
+   * @since 1.4
+   * @since 1.7 throws UnsupportedCharsetException if the named charset is unavailable
+   */
+  constructor(charsetName: String) : this(Charset.forName(charsetName)) {}
+
+  /**
+   * Converts an array of character bytes representing hexadecimal values into an array of bytes of those same values.
+   * The returned array will be half the length of the passed array, as it takes two characters to represent any given
+   * byte. An exception is thrown if the passed char array has an odd number of elements.
+   *
+   * @param array
+   * An array of character bytes containing hexadecimal digits
+   * @return A byte array containing binary data decoded from the supplied byte array (representing characters).
+   * @throws DecoderException
+   * Thrown if an odd number of characters is supplied to this function
+   * @see .decodeHex
+   */
+  @Throws(DecoderException::class)
+  override fun decode(array: ByteArray): ByteArray {
+    return decodeHex(String(array, charset).toCharArray())
+  }
+
+  /**
+   * Converts a buffer of character bytes representing hexadecimal values into an array of bytes of those same values.
+   * The returned array will be half the length of the passed array, as it takes two characters to represent any given
+   * byte. An exception is thrown if the passed char array has an odd number of elements.
+   *
+   * @param buffer
+   * An array of character bytes containing hexadecimal digits
+   * @return A byte array containing binary data decoded from the supplied byte array (representing characters).
+   * @throws DecoderException
+   * Thrown if an odd number of characters is supplied to this function
+   * @see .decodeHex
+   * @since 1.11
+   */
+  @Throws(DecoderException::class)
+  fun decode(buffer: ByteBuffer): ByteArray {
+    return decodeHex(String(buffer.array(), charset).toCharArray())
+  }
+
+  /**
+   * Converts a String or an array of character bytes representing hexadecimal values into an array of bytes of those
+   * same values. The returned array will be half the length of the passed String or array, as it takes two characters
+   * to represent any given byte. An exception is thrown if the passed char array has an odd number of elements.
+   *
+   * @param object
+   * A String, ByteBuffer, byte[], or an array of character bytes containing hexadecimal digits
+   * @return A byte array containing binary data decoded from the supplied byte array (representing characters).
+   * @throws DecoderException
+   * Thrown if an odd number of characters is supplied to this function or the object is not a String or
+   * char[]
+   * @see .decodeHex
+   */
+  @Throws(DecoderException::class)
+  override fun decode(`object`: Any): Any {
+    return if (`object` is String) {
+      decode(`object`.toCharArray())
+    } else if (`object` is ByteArray) {
+      decode(`object`)
+    } else if (`object` is ByteBuffer) {
+      decode(`object`)
+    } else {
+      try {
+        decodeHex(`object` as CharArray)
+      } catch (e: ClassCastException) {
+        throw DecoderException(e.message, e)
+      }
+
+    }
+  }
+
+  /**
+   * Converts an array of bytes into an array of bytes for the characters representing the hexadecimal values of each
+   * byte in order. The returned array will be double the length of the passed array, as it takes two characters to
+   * represent any given byte.
+   *
+   *
+   * The conversion from hexadecimal characters to the returned bytes is performed with the charset named by
+   * [.getCharset].
+   *
+   *
+   * @param array
+   * a byte[] to convert to Hex characters
+   * @return A byte[] containing the bytes of the lower-case hexadecimal characters
+   * @since 1.7 No longer throws IllegalStateException if the charsetName is invalid.
+   * @see .encodeHex
+   */
+  override fun encode(array: ByteArray): ByteArray {
+    return encodeHexString(array).toByteArray(this.charset)
+  }
+
+  /**
+   * Converts byte buffer into an array of bytes for the characters representing the hexadecimal values of each
+   * byte in order. The returned array will be double the length of the passed array, as it takes two characters to
+   * represent any given byte.
+   *
+   *
+   * The conversion from hexadecimal characters to the returned bytes is performed with the charset named by
+   * [.getCharset].
+   *
+   *
+   * @param array
+   * a byte buffer to convert to Hex characters
+   * @return A byte[] containing the bytes of the lower-case hexadecimal characters
+   * @see .encodeHex
+   * @since 1.11
+   */
+  fun encode(array: ByteBuffer): ByteArray {
+    return encodeHexString(array).toByteArray(this.charset)
+  }
+
+  /**
+   * Converts a String or an array of bytes into an array of characters representing the hexadecimal values of each
+   * byte in order. The returned array will be double the length of the passed String or array, as it takes two
+   * characters to represent any given byte.
+   *
+   *
+   * The conversion from hexadecimal characters to bytes to be encoded to performed with the charset named by
+   * [.getCharset].
+   *
+   *
+   * @param object
+   * a String, ByteBuffer, or byte[] to convert to Hex characters
+   * @return A char[] containing lower-case hexadecimal characters
+   * @throws EncoderException
+   * Thrown if the given object is not a String or byte[]
+   * @see .encodeHex
+   */
+  @Throws(EncoderException::class)
+  override fun encode(`object`: Any): Any {
+    val byteArray: ByteArray
+    if (`object` is String) {
+      byteArray = `object`.toByteArray(this.charset)
+    } else if (`object` is ByteBuffer) {
+      byteArray = `object`.array()
+    } else {
+      try {
+        byteArray = `object` as ByteArray
+      } catch (e: ClassCastException) {
+        throw EncoderException(e.message, e)
+      }
+
+    }
+    return encodeHex(byteArray)
+  }
+
+  /**
+   * Returns a string representation of the object, which includes the charset name.
+   *
+   * @return a string representation of the object.
+   */
+  override fun toString(): String {
+    return super.toString() + "[charsetName=" + this.charset + "]"
+  }
+
+  companion object {
+
+    /**
+     * Default charset is [Charsets.UTF_8]
+     *
+     * @since 1.7
+     */
+    val DEFAULT_CHARSET = Charsets.UTF_8
+
+    /**
+     * Default charset name is [CharEncoding.UTF_8]
+     *
+     * @since 1.4
+     */
+    val DEFAULT_CHARSET_NAME = CharEncoding.UTF_8
+
+    /**
+     * Used to build output as Hex
+     */
+    private val DIGITS_LOWER = charArrayOf('0',
+                                           '1',
+                                           '2',
+                                           '3',
+                                           '4',
+                                           '5',
+                                           '6',
+                                           '7',
+                                           '8',
+                                           '9',
+                                           'a',
+                                           'b',
+                                           'c',
+                                           'd',
+                                           'e',
+                                           'f')
+
+    /**
+     * Used to build output as Hex
+     */
+    private val DIGITS_UPPER = charArrayOf('0',
+                                           '1',
+                                           '2',
+                                           '3',
+                                           '4',
+                                           '5',
+                                           '6',
+                                           '7',
+                                           '8',
+                                           '9',
+                                           'A',
+                                           'B',
+                                           'C',
+                                           'D',
+                                           'E',
+                                           'F')
+
+    /**
+     * Converts a String representing hexadecimal values into an array of bytes of those same values. The
+     * returned array will be half the length of the passed String, as it takes two characters to represent any given
+     * byte. An exception is thrown if the passed String has an odd number of elements.
+     *
+     * @param data
+     * A String containing hexadecimal digits
+     * @return A byte array containing binary data decoded from the supplied char array.
+     * @throws DecoderException
+     * Thrown if an odd number or illegal of characters is supplied
+     * @since 1.11
+     */
+    @Throws(DecoderException::class)
+    fun decodeHex(data: String): ByteArray {
+      return decodeHex(data.toCharArray())
+    }
+
+    /**
+     * Converts an array of characters representing hexadecimal values into an array of bytes of those same values. The
+     * returned array will be half the length of the passed array, as it takes two characters to represent any given
+     * byte. An exception is thrown if the passed char array has an odd number of elements.
+     *
+     * @param data
+     * An array of characters containing hexadecimal digits
+     * @return A byte array containing binary data decoded from the supplied char array.
+     * @throws DecoderException
+     * Thrown if an odd number or illegal of characters is supplied
+     */
+    @Throws(DecoderException::class)
+    fun decodeHex(data: CharArray): ByteArray {
+
+      val len = data.size
+
+      if (len and 0x01 != 0) {
+        throw DecoderException("Odd number of characters.")
+      }
+
+      val out = ByteArray(len shr 1)
+
+      // two characters form the hex value.
+      var i = 0
+      var j = 0
+      while (j < len) {
+        var f = toDigit(data[j], j) shl 4
+        j++
+        f = f or toDigit(data[j], j)
+        j++
+        out[i] = (f and 0xFF).toByte()
+        i++
+      }
+
+      return out
+    }
+
+    /**
+     * Converts an array of bytes into an array of characters representing the hexadecimal values of each byte in order.
+     * The returned array will be double the length of the passed array, as it takes two characters to represent any
+     * given byte.
+     *
+     * @param data
+     * a byte[] to convert to Hex characters
+     * @param toLowerCase
+     * `true` converts to lowercase, `false` to uppercase
+     * @return A char[] containing hexadecimal characters in the selected case
+     * @since 1.4
+     */
+    @JvmOverloads
+    fun encodeHex(data: ByteArray, toLowerCase: Boolean = true): CharArray {
+      return encodeHex(data, if (toLowerCase) DIGITS_LOWER else DIGITS_UPPER)
+    }
+
+    /**
+     * Converts a byte buffer into an array of characters representing the hexadecimal values of each byte in order.
+     * The returned array will be double the length of the passed array, as it takes two characters to represent any
+     * given byte.
+     *
+     * @param data
+     * a byte buffer to convert to Hex characters
+     * @param toLowerCase
+     * `true` converts to lowercase, `false` to uppercase
+     * @return A char[] containing hexadecimal characters in the selected case
+     * @since 1.11
+     */
+    @JvmOverloads
+    fun encodeHex(data: ByteBuffer, toLowerCase: Boolean = true): CharArray {
+      return encodeHex(data, if (toLowerCase) DIGITS_LOWER else DIGITS_UPPER)
+    }
+
+    /**
+     * Converts an array of bytes into an array of characters representing the hexadecimal values of each byte in order.
+     * The returned array will be double the length of the passed array, as it takes two characters to represent any
+     * given byte.
+     *
+     * @param data
+     * a byte[] to convert to Hex characters
+     * @param toDigits
+     * the output alphabet (must contain at least 16 chars)
+     * @return A char[] containing the appropriate characters from the alphabet
+     * For best results, this should be either upper- or lower-case hex.
+     * @since 1.4
+     */
+    protected fun encodeHex(data: ByteArray, toDigits: CharArray): CharArray {
+      val l = data.size
+      val out = CharArray(l shl 1)
+      // two characters form the hex value.
+      var i = 0
+      var j = 0
+      while (i < l) {
+        out[j++] = toDigits[(0xF0 and data[i].toInt()).ushr(4)]
+        out[j++] = toDigits[0x0F and data[i].toInt()]
+        i++
+      }
+      return out
+    }
+
+    /**
+     * Converts a byte buffer into an array of characters representing the hexadecimal values of each byte in order.
+     * The returned array will be double the length of the passed array, as it takes two characters to represent any
+     * given byte.
+     *
+     * @param data
+     * a byte buffer to convert to Hex characters
+     * @param toDigits
+     * the output alphabet (must be at least 16 characters)
+     * @return A char[] containing the appropriate characters from the alphabet
+     * For best results, this should be either upper- or lower-case hex.
+     * @since 1.11
+     */
+    protected fun encodeHex(data: ByteBuffer, toDigits: CharArray): CharArray {
+      return encodeHex(data.array(), toDigits)
+    }
+
+    /**
+     * Converts an array of bytes into a String representing the hexadecimal values of each byte in order. The returned
+     * String will be double the length of the passed array, as it takes two characters to represent any given byte.
+     *
+     * @param data
+     * a byte[] to convert to Hex characters
+     * @return A String containing lower-case hexadecimal characters
+     * @since 1.4
+     */
+    fun encodeHexString(data: ByteArray): String {
+      return String(encodeHex(data))
+    }
+
+    /**
+     * Converts an array of bytes into a String representing the hexadecimal values of each byte in order. The returned
+     * String will be double the length of the passed array, as it takes two characters to represent any given byte.
+     *
+     * @param data
+     * a byte[] to convert to Hex characters
+     * @param toLowerCase
+     * `true` converts to lowercase, `false` to uppercase
+     * @return A String containing lower-case hexadecimal characters
+     * @since 1.11
+     */
+    fun encodeHexString(data: ByteArray, toLowerCase: Boolean): String {
+      return String(encodeHex(data, toLowerCase))
+    }
+
+    /**
+     * Converts a byte buffer into a String representing the hexadecimal values of each byte in order. The returned
+     * String will be double the length of the passed array, as it takes two characters to represent any given byte.
+     *
+     * @param data
+     * a byte buffer to convert to Hex characters
+     * @return A String containing lower-case hexadecimal characters
+     * @since 1.11
+     */
+    fun encodeHexString(data: ByteBuffer): String {
+      return String(encodeHex(data))
+    }
+
+    /**
+     * Converts a byte buffer into a String representing the hexadecimal values of each byte in order. The returned
+     * String will be double the length of the passed array, as it takes two characters to represent any given byte.
+     *
+     * @param data
+     * a byte buffer to convert to Hex characters
+     * @param toLowerCase
+     * `true` converts to lowercase, `false` to uppercase
+     * @return A String containing lower-case hexadecimal characters
+     * @since 1.11
+     */
+    fun encodeHexString(data: ByteBuffer, toLowerCase: Boolean): String {
+      return String(encodeHex(data, toLowerCase))
+    }
+
+    /**
+     * Converts a hexadecimal character to an integer.
+     *
+     * @param ch
+     * A character to convert to an integer digit
+     * @param index
+     * The index of the character in the source
+     * @return An integer
+     * @throws DecoderException
+     * Thrown if ch is an illegal hex character
+     */
+    @Throws(DecoderException::class)
+    protected fun toDigit(ch: Char, index: Int): Int {
+      val digit = Character.digit(ch, 16)
+      if (digit == -1) {
+        throw DecoderException("Illegal hexadecimal character $ch at index $index")
+      }
+      return digit
+    }
+  }
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/helper/AnyHelper.kt b/app/src/main/java/de/kuschku/quasseldroid/util/helper/AnyHelper.kt
new file mode 100644
index 000000000..ac9f12f5b
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/helper/AnyHelper.kt
@@ -0,0 +1,3 @@
+package de.kuschku.quasseldroid.util.helper
+
+inline fun <T> T.letIf(condition: Boolean, f: (T) -> T) = if (condition) f(this) else this
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/helper/BooleanHelper.kt b/app/src/main/java/de/kuschku/quasseldroid/util/helper/BooleanHelper.kt
new file mode 100644
index 000000000..8bb2713f0
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/helper/BooleanHelper.kt
@@ -0,0 +1,10 @@
+package de.kuschku.quasseldroid.util.helper
+
+inline fun <R> Boolean.letIf(block: () -> R): R? {
+  return if (this) block() else null
+}
+
+inline fun <R> Boolean.letUnless(block: () -> R): R? {
+  return if (this) null else block()
+}
+
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/helper/CharSequenceHelper.kt b/app/src/main/java/de/kuschku/quasseldroid/util/helper/CharSequenceHelper.kt
index cefe3ead6..22c209d71 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/util/helper/CharSequenceHelper.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/helper/CharSequenceHelper.kt
@@ -157,3 +157,5 @@ fun CharSequence.lastWordIndices(cursor: Int = this.length,
     null
   }
 }
+
+inline fun <T : CharSequence> T?.notBlank(): T? = if (this.isNullOrBlank()) null else this
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/helper/GlideHelper.kt b/app/src/main/java/de/kuschku/quasseldroid/util/helper/GlideHelper.kt
new file mode 100644
index 000000000..0676bb48c
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/helper/GlideHelper.kt
@@ -0,0 +1,33 @@
+package de.kuschku.quasseldroid.util.helper
+
+import android.graphics.drawable.Drawable
+import android.widget.ImageView
+import com.bumptech.glide.RequestBuilder
+import de.kuschku.quasseldroid.GlideApp
+import de.kuschku.quasseldroid.GlideRequest
+import de.kuschku.quasseldroid.GlideRequests
+
+fun GlideRequests.loadWithFallbacks(urls: List<String>): GlideRequest<Drawable>? {
+  fun fold(url: String, fallback: RequestBuilder<Drawable>?): GlideRequest<Drawable> {
+    return this.load(url).let {
+      if (fallback != null) it.error(fallback) else it
+    }
+  }
+
+  return urls.foldRight(null, ::fold)
+}
+
+fun ImageView.loadAvatars(urls: List<String>, fallback: Drawable? = null, crop: Boolean = true) {
+  if (urls.isNotEmpty()) {
+    GlideApp.with(this)
+      .loadWithFallbacks(urls)
+      ?.letIf(crop) {
+        it.optionalCircleCrop()
+      }
+      ?.placeholder(fallback)
+      ?.into(this)
+  } else {
+    GlideApp.with(this).clear(this)
+    setImageDrawable(fallback)
+  }
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/ui/DoubleClickHelper.kt b/app/src/main/java/de/kuschku/quasseldroid/util/ui/DoubleClickHelper.kt
index 917f84c55..1cf9eac78 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/util/ui/DoubleClickHelper.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/ui/DoubleClickHelper.kt
@@ -3,22 +3,23 @@ package de.kuschku.quasseldroid.util.ui
 import android.view.GestureDetector
 import android.view.MotionEvent
 import android.view.View
-import android.widget.EditText
 
-class DoubleClickHelper(editText: EditText) : View.OnTouchListener {
+class DoubleClickHelper(view: View) : View.OnTouchListener {
   var doubleClickListener: (() -> Unit)? = null
 
   private val gestureDetector = GestureDetector(
-    editText.context, object : GestureDetector.SimpleOnGestureListener() {
-    override fun onDoubleTap(e: MotionEvent?): Boolean {
-      doubleClickListener?.invoke()
-      return true
-    }
+    view.context,
+    object : GestureDetector.SimpleOnGestureListener() {
+      override fun onDoubleTap(e: MotionEvent?): Boolean {
+        doubleClickListener?.invoke()
+        return true
+      }
 
-    override fun onDoubleTapEvent(e: MotionEvent?): Boolean {
-      return true
+      override fun onDoubleTapEvent(e: MotionEvent?): Boolean {
+        return true
+      }
     }
-  })
+  )
 
   override fun onTouch(v: View?, event: MotionEvent?) = gestureDetector.onTouchEvent(event)
 }
diff --git a/app/src/main/res/layout/fragment_info_user.xml b/app/src/main/res/layout/fragment_info_user.xml
index dc3ea1234..a58b9fef8 100644
--- a/app/src/main/res/layout/fragment_info_user.xml
+++ b/app/src/main/res/layout/fragment_info_user.xml
@@ -22,7 +22,7 @@
         android:layout_height="0dp"
         android:contentDescription="@string/label_avatar"
         android:scaleType="centerCrop"
-        app:layout_constraintDimensionRatio="H,1.77:1"
+        app:layout_constraintDimensionRatio="H,3:2"
         tools:src="@tools:sample/avatars" />
 
     </android.support.constraint.ConstraintLayout>
diff --git a/app/src/main/res/values-de/strings_preferences.xml b/app/src/main/res/values-de/strings_preferences.xml
index a3f8d9859..e483c79b6 100644
--- a/app/src/main/res/values-de/strings_preferences.xml
+++ b/app/src/main/res/values-de/strings_preferences.xml
@@ -52,8 +52,16 @@
   <string name="preference_nicks_on_new_line_title">Separate Spitznamen</string>
   <string name="preference_nicks_on_new_line_summary">Zeigt Spitznamen in einer eigenen Zeile an</string>
 
+  <string name="preference_show_realnames_title">Echtnamen anzeigen</string>
+
   <string name="preference_show_avatars_title">Avatare anzeigen</string>
 
+  <string name="preference_show_irccloud_avatars_title">IRCCloud Avatare anzeigen</string>
+  <string name="preference_show_irccloud_avatars_summary">Zeigt für Nutzer ohne Avatare den IRCCloud-Avatar an, falls verfügbar</string>
+
+  <string name="preference_show_gravatar_avatars_title">Gravatar Avatare anzeigen</string>
+  <string name="preference_show_gravatar_avatars_summary">Zeigt für Nutzer ohne Avatare den Gravatar-Avatar an, falls verfügbar</string>
+
   <string name="preference_time_at_end_title">Rechts-Ausgerichtete Zeit</string>
   <string name="preference_time_at_end_summary">Zeigt die Zeit rechts in Nachrichten an</string>
 
@@ -63,6 +71,9 @@
 
   <string name="preference_autocomplete_title">Autovervollständigung</string>
 
+  <string name="preference_autocomplete_sender_doubleclick_title">Sender-Autovervollständigung</string>
+  <string name="preference_autocomplete_sender_doubleclick_summary">Vervollständigt den Sender einer Nachricht, wenn auf diese doppelgeklickt wird</string>
+
   <string name="preference_autocomplete_button_title">Autovervollständigungsknopf</string>
   <string name="preference_autocomplete_button_summary">Zeigt einen Knopf um Namen und Chats zu vervollständigen</string>
 
diff --git a/app/src/main/res/values/strings_preferences.xml b/app/src/main/res/values/strings_preferences.xml
index 4a557e595..2538fcc46 100644
--- a/app/src/main/res/values/strings_preferences.xml
+++ b/app/src/main/res/values/strings_preferences.xml
@@ -111,9 +111,20 @@
   <string name="preference_nicks_on_new_line_title">Separate Nicknames</string>
   <string name="preference_nicks_on_new_line_summary">Shows nicknames on a separate line</string>
 
+  <string name="preference_show_realnames_key" translatable="false">show_realnames</string>
+  <string name="preference_show_realnames_title">Show Realnames</string>
+
   <string name="preference_show_avatars_key" translatable="false">show_avatars</string>
   <string name="preference_show_avatars_title">Show Avatars</string>
 
+  <string name="preference_show_irccloud_avatars_key" translatable="false">show_irccloud_avatars</string>
+  <string name="preference_show_irccloud_avatars_title">Show IRCCloud Avatars</string>
+  <string name="preference_show_irccloud_avatars_summary">Shows for users without avatar their IRCCloud fallback, if available</string>
+
+  <string name="preference_show_gravatar_avatars_key" translatable="false">show_gravatar_avatars</string>
+  <string name="preference_show_gravatar_avatars_title">Show Gravatar Avatars</string>
+  <string name="preference_show_gravatar_avatars_summary">Shows for users without avatar their Gravatar fallback, if available</string>
+
   <string name="preference_time_at_end_key" translatable="false">time_at_end</string>
   <string name="preference_time_at_end_title">Right-Aligned Timestamps</string>
   <string name="preference_time_at_end_summary">Aligns timestamps at the end of each message</string>
@@ -125,6 +136,10 @@
 
   <string name="preference_autocomplete_title">Autocomplete</string>
 
+  <string name="preference_autocomplete_sender_doubleclick_key" translatable="false">autocomplete_sender_doubleclick</string>
+  <string name="preference_autocomplete_sender_doubleclick_title">Sender Autocomplete</string>
+  <string name="preference_autocomplete_sender_doubleclick_summary">Doubleclick on a message to autocomplete its sender</string>
+
   <string name="preference_autocomplete_button_key" translatable="false">autocomplete_button</string>
   <string name="preference_autocomplete_button_title">Autocomplete Button</string>
   <string name="preference_autocomplete_button_summary">Shows a button on the left of the input line that triggers tabcomplete</string>
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
index 12b68e147..04da89343 100644
--- a/app/src/main/res/xml/preferences.xml
+++ b/app/src/main/res/xml/preferences.xml
@@ -69,17 +69,35 @@
       android:title="@string/preference_show_prefix_title" />
 
     <SwitchPreference
-      android:defaultValue="false"
+      android:defaultValue="true"
       android:key="@string/preference_nicks_on_new_line_key"
       android:summary="@string/preference_nicks_on_new_line_summary"
       android:title="@string/preference_nicks_on_new_line_title" />
 
     <SwitchPreference
-      android:defaultValue="false"
+      android:defaultValue="true"
+      android:dependency="@string/preference_nicks_on_new_line_key"
+      android:key="@string/preference_show_realnames_key"
+      android:title="@string/preference_show_realnames_title" />
+
+    <SwitchPreference
+      android:defaultValue="true"
       android:dependency="@string/preference_nicks_on_new_line_key"
       android:key="@string/preference_show_avatars_key"
       android:title="@string/preference_show_avatars_title" />
 
+    <SwitchPreference
+      android:defaultValue="false"
+      android:dependency="@string/preference_show_avatars_key"
+      android:key="@string/preference_show_irccloud_avatars_key"
+      android:title="@string/preference_show_irccloud_avatars_title" />
+
+    <SwitchPreference
+      android:defaultValue="false"
+      android:dependency="@string/preference_show_avatars_key"
+      android:key="@string/preference_show_gravatar_avatars_key"
+      android:title="@string/preference_show_gravatar_avatars_title" />
+
     <SwitchPreference
       android:defaultValue="false"
       android:key="@string/preference_hostmask_actions_key"
@@ -110,6 +128,12 @@
 
   <PreferenceCategory android:title="@string/preference_autocomplete_title">
 
+    <SwitchPreference
+      android:defaultValue="true"
+      android:key="@string/preference_autocomplete_sender_doubleclick_key"
+      android:summary="@string/preference_autocomplete_sender_doubleclick_summary"
+      android:title="@string/preference_autocomplete_sender_doubleclick_title" />
+
     <SwitchPreference
       android:defaultValue="false"
       android:key="@string/preference_autocomplete_button_key"
@@ -123,7 +147,7 @@
       android:title="@string/preference_autocomplete_doubletap_title" />
 
     <SwitchPreference
-      android:defaultValue="true"
+      android:defaultValue="false"
       android:key="@string/preference_autocomplete_auto_key"
       android:summary="@string/preference_autocomplete_auto_summary"
       android:title="@string/preference_autocomplete_auto_title" />
diff --git a/lib/src/main/java/de/kuschku/libquassel/protocol/Message.kt b/lib/src/main/java/de/kuschku/libquassel/protocol/Message.kt
index f628ca914..fadb2eb37 100644
--- a/lib/src/main/java/de/kuschku/libquassel/protocol/Message.kt
+++ b/lib/src/main/java/de/kuschku/libquassel/protocol/Message.kt
@@ -13,6 +13,8 @@ class Message(
   val bufferInfo: BufferInfo,
   val sender: String,
   val senderPrefixes: String,
+  val realName: String,
+  val avatarUrl: String,
   val content: String
 ) {
   enum class MessageType(override val bit: Int) :
diff --git a/lib/src/main/java/de/kuschku/libquassel/protocol/primitive/serializer/MessageSerializer.kt b/lib/src/main/java/de/kuschku/libquassel/protocol/primitive/serializer/MessageSerializer.kt
index 89155782c..ebe0b3ff9 100644
--- a/lib/src/main/java/de/kuschku/libquassel/protocol/primitive/serializer/MessageSerializer.kt
+++ b/lib/src/main/java/de/kuschku/libquassel/protocol/primitive/serializer/MessageSerializer.kt
@@ -10,20 +10,30 @@ import java.nio.ByteBuffer
 object MessageSerializer : Serializer<Message> {
   override fun serialize(buffer: ChainedByteBuffer, data: Message, features: QuasselFeatures) {
     IntSerializer.serialize(buffer, data.messageId, features)
-    IntSerializer.serialize(buffer, data.time.epochSecond.toInt(), features)
+    if (features.hasFeature(ExtendedFeature.LongMessageTime))
+      LongSerializer.serialize(buffer, data.time.toEpochMilli(), features)
+    else
+      IntSerializer.serialize(buffer, data.time.epochSecond.toInt(), features)
     IntSerializer.serialize(buffer, data.type.toInt(), features)
     ByteSerializer.serialize(buffer, data.flag.toByte(), features)
     BufferInfoSerializer.serialize(buffer, data.bufferInfo, features)
     StringSerializer.UTF8.serialize(buffer, data.sender, features)
     if (features.hasFeature(ExtendedFeature.SenderPrefixes))
       StringSerializer.UTF8.serialize(buffer, data.senderPrefixes, features)
+    if (features.hasFeature(ExtendedFeature.RichMessages))
+      StringSerializer.UTF8.serialize(buffer, data.realName, features)
+    if (features.hasFeature(ExtendedFeature.RichMessages))
+      StringSerializer.UTF8.serialize(buffer, data.avatarUrl, features)
     StringSerializer.UTF8.serialize(buffer, data.content, features)
   }
 
   override fun deserialize(buffer: ByteBuffer, features: QuasselFeatures): Message {
     return Message(
       messageId = IntSerializer.deserialize(buffer, features),
-      time = Instant.ofEpochSecond(IntSerializer.deserialize(buffer, features).toLong()),
+      time = if (features.hasFeature(ExtendedFeature.LongMessageTime))
+        Instant.ofEpochMilli(LongSerializer.deserialize(buffer, features))
+      else
+        Instant.ofEpochSecond(IntSerializer.deserialize(buffer, features).toLong()),
       type = Message.MessageType.of(IntSerializer.deserialize(buffer, features)),
       flag = Message.MessageFlag.of(
         ByteSerializer.deserialize(buffer, features).toInt()
@@ -32,6 +42,10 @@ object MessageSerializer : Serializer<Message> {
       sender = StringSerializer.UTF8.deserialize(buffer, features) ?: "",
       senderPrefixes = if (features.hasFeature(ExtendedFeature.SenderPrefixes))
         StringSerializer.UTF8.deserialize(buffer, features) ?: "" else "",
+      realName = if (features.hasFeature(ExtendedFeature.RichMessages))
+        StringSerializer.UTF8.deserialize(buffer, features) ?: "" else "",
+      avatarUrl = if (features.hasFeature(ExtendedFeature.RichMessages))
+        StringSerializer.UTF8.deserialize(buffer, features) ?: "" else "",
       content = StringSerializer.UTF8.deserialize(buffer, features) ?: ""
     )
   }
diff --git a/lib/src/main/java/de/kuschku/libquassel/quassel/ExtendedFeature.kt b/lib/src/main/java/de/kuschku/libquassel/quassel/ExtendedFeature.kt
index c9d0a62b3..d30e67652 100644
--- a/lib/src/main/java/de/kuschku/libquassel/quassel/ExtendedFeature.kt
+++ b/lib/src/main/java/de/kuschku/libquassel/quassel/ExtendedFeature.kt
@@ -26,10 +26,14 @@ enum class ExtendedFeature {
   /** Supports RPC call disconnectFromCore to remotely disconnect a client */
   RemoteDisconnect,
   /** Transmit features as list of strings */
-  ExtendedFeatures;
+  ExtendedFeatures,
+  /** Serialize message time as 64-bit */
+  LongMessageTime,
+  /** Real Name and Avatar URL in backlog */
+  RichMessages;
 
   companion object {
     private val map = values().associateBy(ExtendedFeature::name)
     fun of(name: String) = map[name]
   }
-}
\ No newline at end of file
+}
diff --git a/lib/src/main/java/de/kuschku/libquassel/quassel/LegacyFeature.kt b/lib/src/main/java/de/kuschku/libquassel/quassel/LegacyFeature.kt
index 9243d109f..de9c099f9 100644
--- a/lib/src/main/java/de/kuschku/libquassel/quassel/LegacyFeature.kt
+++ b/lib/src/main/java/de/kuschku/libquassel/quassel/LegacyFeature.kt
@@ -58,13 +58,14 @@ enum class LegacyFeature(override val bit: Int) :
       ExtendedFeature.VerifyServerSSL        -> LegacyFeature.VerifyServerSSL
       ExtendedFeature.CustomRateLimits       -> LegacyFeature.CustomRateLimits
       ExtendedFeature.DccFileTransfer        -> LegacyFeature.DccFileTransfer
-      ExtendedFeature.AwayFormatTimestamp    -> LegacyFeature.AwayFormatTimestamp
-      ExtendedFeature.Authenticators         -> LegacyFeature.Authenticators
-      ExtendedFeature.BufferActivitySync     -> LegacyFeature.BufferActivitySync
-      ExtendedFeature.CoreSideHighlights     -> LegacyFeature.CoreSideHighlights
-      ExtendedFeature.SenderPrefixes         -> LegacyFeature.SenderPrefixes
-      ExtendedFeature.RemoteDisconnect       -> LegacyFeature.RemoteDisconnect
-      ExtendedFeature.ExtendedFeatures       -> LegacyFeature.ExtendedFeatures
+      ExtendedFeature.AwayFormatTimestamp -> LegacyFeature.AwayFormatTimestamp
+      ExtendedFeature.Authenticators      -> LegacyFeature.Authenticators
+      ExtendedFeature.BufferActivitySync  -> LegacyFeature.BufferActivitySync
+      ExtendedFeature.CoreSideHighlights  -> LegacyFeature.CoreSideHighlights
+      ExtendedFeature.SenderPrefixes      -> LegacyFeature.SenderPrefixes
+      ExtendedFeature.RemoteDisconnect    -> LegacyFeature.RemoteDisconnect
+      ExtendedFeature.ExtendedFeatures    -> LegacyFeature.ExtendedFeatures
+      else                                -> null
     }
   }
 
diff --git a/lib/src/main/java/de/kuschku/libquassel/quassel/QuasselFeatures.kt b/lib/src/main/java/de/kuschku/libquassel/quassel/QuasselFeatures.kt
index 6bce7edbe..965f4905b 100644
--- a/lib/src/main/java/de/kuschku/libquassel/quassel/QuasselFeatures.kt
+++ b/lib/src/main/java/de/kuschku/libquassel/quassel/QuasselFeatures.kt
@@ -9,11 +9,11 @@ class QuasselFeatures(
 ) {
   constructor(legacyFeatures: Legacy_Features?, extendedFeatures: Collection<String>) : this(
     legacyFeatures?.enabledValues()?.map(Legacy_Feature::toExtended).orEmpty() union
-      extendedFeatures.mapNotNull { ExtendedFeature.of(it) },
+      extendedFeatures.mapNotNull(ExtendedFeature.Companion::of),
     extendedFeatures.filter { ExtendedFeature.of(it) == null }.toSet()
   )
 
-  fun toInt() = LegacyFeature.of(enabledFeatures.map(LegacyFeature.Companion::fromExtended))
+  fun toInt() = LegacyFeature.of(enabledFeatures.mapNotNull(LegacyFeature.Companion::fromExtended))
 
   fun toStringList() = enabledFeatures.map(ExtendedFeature::name)
 
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 e1b29a819..3209bbd9c 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
@@ -19,32 +19,38 @@ class BufferSyncer constructor(
     markerLine(buffer)
   }.distinctUntilChanged()
 
-  fun liveLastSeenMsgs(): Observable<Map<BufferId, MsgId>> = live_lastSeenMsg
+  fun liveLastSeenMsgs(): Observable<Map<BufferId, MsgId>> =
+    live_lastSeenMsg.map { _lastSeenMsg.toMap() }
 
   fun markerLine(buffer: BufferId): MsgId = _markerLines[buffer] ?: 0
-  fun liveMarkerLine(
-    buffer: BufferId): Observable<MsgId> = live_markerLines.map { markerLine(buffer) }.distinctUntilChanged()
+  fun liveMarkerLine(buffer: BufferId): Observable<MsgId> =
+    live_markerLines.map { markerLine(buffer) }.distinctUntilChanged()
 
-  fun liveMarkerLines(): Observable<Map<BufferId, MsgId>> = live_markerLines
+  fun liveMarkerLines(): Observable<Map<BufferId, MsgId>> =
+    live_markerLines.map { _markerLines.toMap() }
 
-  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 activity(buffer: BufferId): Message_Types =
+    _bufferActivities[buffer] ?: Message_Types.of()
 
-  fun liveActivities(): Observable<Map<BufferId, Message_Types>> = live_bufferActivities
+  fun liveActivity(buffer: BufferId): Observable<Message_Types> =
+    live_bufferActivities.map { activity(buffer) }.distinctUntilChanged()
+
+  fun liveActivities(): Observable<Map<BufferId, Message_Types>> =
+    live_bufferActivities.map { _bufferActivities.toMap() }
 
   fun highlightCount(buffer: BufferId): Int = _highlightCounts[buffer] ?: 0
-  fun liveHighlightCount(
-    buffer: BufferId): Observable<Int> = live_highlightCounts.map { highlightCount(buffer) }.distinctUntilChanged()
+  fun liveHighlightCount(buffer: BufferId): Observable<Int> =
+    live_highlightCounts.map { highlightCount(buffer) }.distinctUntilChanged()
 
-  fun liveHighlightCounts(): Observable<Map<BufferId, Int>> = live_highlightCounts
+  fun liveHighlightCounts(): Observable<Map<BufferId, Int>> =
+    live_highlightCounts.map { _highlightCounts.toMap() }
 
   fun bufferInfo(bufferId: BufferId) = _bufferInfos[bufferId]
-  fun liveBufferInfo(
-    bufferId: BufferId) = live_bufferInfos.map { bufferInfo(bufferId) }.distinctUntilChanged()
+  fun liveBufferInfo(bufferId: BufferId) =
+    live_bufferInfos.map { bufferInfo(bufferId) }.distinctUntilChanged()
 
-  fun bufferInfos(): Collection<BufferInfo> = _bufferInfos.values
-  fun liveBufferInfos(): Observable<Map<BufferId, BufferInfo>> = live_bufferInfos
+  fun bufferInfos(): Collection<BufferInfo> = _bufferInfos.values.toList()
+  fun liveBufferInfos(): Observable<Map<BufferId, BufferInfo>> = live_bufferInfos.map { _bufferInfos.toMap() }
 
   override fun toVariantMap(): QVariantMap = mapOf(
     "Activities" to QVariant.of(initActivities(), Type.QVariantList),
@@ -102,7 +108,7 @@ class BufferSyncer constructor(
     }.forEach { (buffer, activity) ->
       setBufferActivity(buffer, activity)
     }
-    live_bufferActivities.onNext(_bufferActivities)
+    live_bufferActivities.onNext(Unit)
   }
 
   override fun initSetHighlightCounts(data: QVariantList) {
@@ -111,7 +117,7 @@ class BufferSyncer constructor(
     }.forEach { (buffer, count) ->
       setHighlightCount(buffer, count)
     }
-    live_highlightCounts.onNext(_highlightCounts)
+    live_highlightCounts.onNext(Unit)
   }
 
   override fun initSetLastSeenMsg(data: QVariantList) {
@@ -120,7 +126,7 @@ class BufferSyncer constructor(
     }.forEach { (buffer, msgId) ->
       setLastSeenMsg(buffer, msgId)
     }
-    live_lastSeenMsg.onNext(_lastSeenMsg)
+    live_lastSeenMsg.onNext(Unit)
   }
 
   override fun initSetMarkerLines(data: QVariantList) {
@@ -129,29 +135,29 @@ class BufferSyncer constructor(
     }.forEach { (buffer, msgId) ->
       setMarkerLine(buffer, msgId)
     }
-    live_markerLines.onNext(_markerLines)
+    live_markerLines.onNext(Unit)
   }
 
   fun initSetBufferInfos(infos: QVariantList?) {
     _bufferInfos.clear()
     infos?.mapNotNull { it.value<BufferInfo>() }?.forEach { _bufferInfos[it.bufferId] = it }
-    live_bufferInfos.onNext(_bufferInfos)
+    live_bufferInfos.onNext(Unit)
   }
 
   override fun mergeBuffersPermanently(buffer1: BufferId, buffer2: BufferId) {
-    _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)
+    _lastSeenMsg.remove(buffer2);live_lastSeenMsg.onNext(Unit)
+    _markerLines.remove(buffer2);live_markerLines.onNext(Unit)
+    _bufferActivities.remove(buffer2);live_bufferActivities.onNext(Unit)
+    _highlightCounts.remove(buffer2);live_highlightCounts.onNext(Unit)
+    _bufferInfos.remove(buffer2);live_bufferInfos.onNext(Unit)
   }
 
   override fun removeBuffer(buffer: BufferId) {
-    _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)
+    _lastSeenMsg.remove(buffer);live_lastSeenMsg.onNext(Unit)
+    _markerLines.remove(buffer);live_markerLines.onNext(Unit)
+    _bufferActivities.remove(buffer);live_bufferActivities.onNext(Unit)
+    _highlightCounts.remove(buffer);live_highlightCounts.onNext(Unit)
+    _bufferInfos.remove(buffer);live_bufferInfos.onNext(Unit)
     session.backlogManager?.removeBuffer(buffer)
   }
 
@@ -159,7 +165,7 @@ class BufferSyncer constructor(
     val bufferInfo = _bufferInfos[buffer]
     if (bufferInfo != null) {
       _bufferInfos[buffer] = bufferInfo.copy(bufferName = newName)
-      live_bufferInfos.onNext(_bufferInfos)
+      live_bufferInfos.onNext(Unit)
     }
   }
 
@@ -167,7 +173,7 @@ class BufferSyncer constructor(
     val oldInfo = _bufferInfos[info.bufferId]
     if (info != oldInfo) {
       _bufferInfos[info.bufferId] = info
-      live_bufferInfos.onNext(_bufferInfos)
+      live_bufferInfos.onNext(Unit)
 
       if (oldInfo == null) {
         session.bufferViewManager?.handleBuffer(info, this)
@@ -182,7 +188,7 @@ class BufferSyncer constructor(
     val oldLastSeenMsg = lastSeenMsg(buffer)
     if (oldLastSeenMsg < msgId) {
       _lastSeenMsg[buffer] = msgId
-      live_lastSeenMsg.onNext(_lastSeenMsg)
+      live_lastSeenMsg.onNext(Unit)
       super.setLastSeenMsg(buffer, msgId)
     }
   }
@@ -192,7 +198,7 @@ class BufferSyncer constructor(
       return
 
     _markerLines[buffer] = msgId
-    live_markerLines.onNext(_markerLines)
+    live_markerLines.onNext(Unit)
     super.setMarkerLine(buffer, msgId)
   }
 
@@ -200,13 +206,13 @@ class BufferSyncer constructor(
     val flags = Message_Types.of<Message_Type>(activity)
     super.setBufferActivity(buffer, activity)
     _bufferActivities[buffer] = flags
-    live_bufferActivities.onNext(_bufferActivities)
+    live_bufferActivities.onNext(Unit)
   }
 
   override fun setHighlightCount(buffer: BufferId, count: Int) {
     super.setHighlightCount(buffer, count)
     _highlightCounts[buffer] = count
-    live_highlightCounts.onNext(_highlightCounts)
+    live_highlightCounts.onNext(Unit)
   }
 
   fun all(
@@ -236,17 +242,17 @@ class BufferSyncer constructor(
   ) = all(bufferName, bufferId, networkId, type, groupId).firstOrNull()
 
   private val _lastSeenMsg: MutableMap<BufferId, MsgId> = mutableMapOf()
-  private val live_lastSeenMsg = BehaviorSubject.createDefault(mapOf<BufferId, MsgId>())
+  private val live_lastSeenMsg = BehaviorSubject.createDefault(Unit)
 
   private val _markerLines: MutableMap<BufferId, MsgId> = mutableMapOf()
-  private val live_markerLines = BehaviorSubject.createDefault(mapOf<BufferId, MsgId>())
+  private val live_markerLines = BehaviorSubject.createDefault(Unit)
 
   private val _bufferActivities: MutableMap<BufferId, Message_Types> = mutableMapOf()
-  private val live_bufferActivities = BehaviorSubject.createDefault(mapOf<BufferId, Message_Types>())
+  private val live_bufferActivities = BehaviorSubject.createDefault(Unit)
 
   private val _highlightCounts: MutableMap<BufferId, Int> = mutableMapOf()
-  private val live_highlightCounts = BehaviorSubject.createDefault(mapOf<BufferId, Int>())
+  private val live_highlightCounts = BehaviorSubject.createDefault(Unit)
 
   private val _bufferInfos = mutableMapOf<BufferId, BufferInfo>()
-  private val live_bufferInfos = BehaviorSubject.createDefault(mapOf<BufferId, BufferInfo>())
+  private val live_bufferInfos = BehaviorSubject.createDefault(Unit)
 }
diff --git a/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/BufferViewConfig.kt b/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/BufferViewConfig.kt
index 1cee19a48..deb0381b6 100644
--- a/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/BufferViewConfig.kt
+++ b/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/BufferViewConfig.kt
@@ -58,17 +58,17 @@ class BufferViewConfig constructor(
 
   override fun initSetBufferList(buffers: QVariantList) {
     _buffers = buffers.mapNotNull { it.value<BufferId?>() }.toMutableList()
-    live_buffers.onNext(_buffers)
+    live_buffers.onNext(Unit)
   }
 
   override fun initSetRemovedBuffers(buffers: QVariantList) {
     _removedBuffers = buffers.mapNotNull { it.value<BufferId?>() }.toMutableSet()
-    live_removedBuffers.onNext(_removedBuffers)
+    live_removedBuffers.onNext(Unit)
   }
 
   override fun initSetTemporarilyRemovedBuffers(buffers: QVariantList) {
     _temporarilyRemovedBuffers = buffers.mapNotNull { it.value<BufferId?>() }.toMutableSet()
-    live_temporarilyRemovedBuffers.onNext(_temporarilyRemovedBuffers)
+    live_temporarilyRemovedBuffers.onNext(Unit)
   }
 
   override fun initSetProperties(properties: QVariantMap) {
@@ -90,16 +90,16 @@ class BufferViewConfig constructor(
 
     if (_removedBuffers.contains(bufferId)) {
       _removedBuffers.remove(bufferId)
-      live_removedBuffers.onNext(_removedBuffers)
+      live_removedBuffers.onNext(Unit)
     }
 
     if (_temporarilyRemovedBuffers.contains(bufferId)) {
       _temporarilyRemovedBuffers.remove(bufferId)
-      live_temporarilyRemovedBuffers.onNext(_temporarilyRemovedBuffers)
+      live_temporarilyRemovedBuffers.onNext(Unit)
     }
 
     _buffers.add(minOf(maxOf(pos, 0), _buffers.size), bufferId)
-    live_buffers.onNext(_buffers)
+    live_buffers.onNext(Unit)
   }
 
   override fun moveBuffer(bufferId: BufferId, pos: Int) {
@@ -119,37 +119,37 @@ class BufferViewConfig constructor(
       _buffers.add(bufferId, targetPos - 1)
     }
 
-    live_buffers.onNext(_buffers)
+    live_buffers.onNext(Unit)
   }
 
   override fun removeBuffer(bufferId: BufferId) {
     if (_buffers.contains(bufferId)) {
       _buffers.remove(bufferId)
-      live_buffers.onNext(_buffers)
+      live_buffers.onNext(Unit)
     }
 
     if (_removedBuffers.contains(bufferId)) {
       _removedBuffers.remove(bufferId)
-      live_removedBuffers.onNext(_removedBuffers)
+      live_removedBuffers.onNext(Unit)
     }
 
     _temporarilyRemovedBuffers.add(bufferId)
-    live_temporarilyRemovedBuffers.onNext(_temporarilyRemovedBuffers)
+    live_temporarilyRemovedBuffers.onNext(Unit)
   }
 
   override fun removeBufferPermanently(bufferId: BufferId) {
     if (_buffers.contains(bufferId)) {
       _buffers.remove(bufferId)
-      live_buffers.onNext(_buffers)
+      live_buffers.onNext(Unit)
     }
 
     if (_temporarilyRemovedBuffers.contains(bufferId)) {
       _temporarilyRemovedBuffers.remove(bufferId)
-      live_temporarilyRemovedBuffers.onNext(_temporarilyRemovedBuffers)
+      live_temporarilyRemovedBuffers.onNext(Unit)
     }
 
     _removedBuffers.add(bufferId)
-    live_removedBuffers.onNext(_removedBuffers)
+    live_removedBuffers.onNext(Unit)
   }
 
   fun bufferViewId() = _bufferViewId
@@ -164,14 +164,21 @@ class BufferViewConfig constructor(
   fun minimumActivity() = _minimumActivity
   fun showSearch() = _showSearch
 
-  fun buffers(): List<BufferId> = _buffers
-  fun removedBuffers(): Set<BufferId> = _removedBuffers
-  fun temporarilyRemovedBuffers(): Set<BufferId> = _temporarilyRemovedBuffers
+  fun buffers(): List<BufferId> = _buffers.toList()
+  fun removedBuffers(): Set<BufferId> = _removedBuffers.toSet()
+  fun temporarilyRemovedBuffers(): Set<BufferId> = _temporarilyRemovedBuffers.toSet()
 
-  fun liveUpdates(): Observable<BufferViewConfig> = live_config
-  fun liveBuffers(): Observable<List<BufferId>> = live_buffers
-  fun liveRemovedBuffers(): Observable<Set<BufferId>> = live_removedBuffers
-  fun liveTemporarilyRemovedBuffers(): Observable<Set<BufferId>> = live_temporarilyRemovedBuffers
+  fun liveUpdates(): Observable<BufferViewConfig> =
+    live_config.map { this }
+
+  fun liveBuffers(): Observable<List<BufferId>> =
+    live_buffers.map { buffers() }
+
+  fun liveRemovedBuffers(): Observable<Set<BufferId>> =
+    live_removedBuffers.map { removedBuffers() }
+
+  fun liveTemporarilyRemovedBuffers(): Observable<Set<BufferId>> =
+    live_temporarilyRemovedBuffers.map { temporarilyRemovedBuffers() }
 
   fun copy(): BufferViewConfig {
     val config = BufferViewConfig(this.bufferViewId(), SignalProxy.NULL)
@@ -238,60 +245,60 @@ class BufferViewConfig constructor(
   private var _bufferViewName: String = ""
     set(value) {
       field = value
-      live_config.onNext(this)
+      live_config.onNext(Unit)
     }
   private var _networkId: NetworkId = 0
     set(value) {
       field = value
-      live_config.onNext(this)
+      live_config.onNext(Unit)
     }
   private var _addNewBuffersAutomatically: Boolean = true
     set(value) {
       field = value
-      live_config.onNext(this)
+      live_config.onNext(Unit)
     }
   private var _sortAlphabetically: Boolean = true
     set(value) {
       field = value
-      live_config.onNext(this)
+      live_config.onNext(Unit)
     }
   private var _hideInactiveBuffers: Boolean = false
     set(value) {
       field = value
-      live_config.onNext(this)
+      live_config.onNext(Unit)
     }
   private var _hideInactiveNetworks: Boolean = false
     set(value) {
       field = value
-      live_config.onNext(this)
+      live_config.onNext(Unit)
     }
   private var _disableDecoration: Boolean = false
     set(value) {
       field = value
-      live_config.onNext(this)
+      live_config.onNext(Unit)
     }
   private var _allowedBufferTypes: Buffer_Types = Buffer_Type.of(*Buffer_Type.validValues)
     set(value) {
       field = value
-      live_config.onNext(this)
+      live_config.onNext(Unit)
     }
   private var _minimumActivity: Buffer_Activities = Buffer_Activities.of(0)
     set(value) {
       field = value
-      live_config.onNext(this)
+      live_config.onNext(Unit)
     }
   private var _showSearch: Boolean = false
     set(value) {
       field = value
-      live_config.onNext(this)
+      live_config.onNext(Unit)
     }
   private var _buffers: MutableList<BufferId> = mutableListOf()
   private var _removedBuffers: MutableSet<BufferId> = mutableSetOf()
   private var _temporarilyRemovedBuffers: MutableSet<BufferId> = mutableSetOf()
-  private val live_config = BehaviorSubject.createDefault(this)
-  private val live_buffers = BehaviorSubject.createDefault<List<BufferId>>(emptyList())
-  private val live_removedBuffers = BehaviorSubject.createDefault<Set<BufferId>>(emptySet())
-  private val live_temporarilyRemovedBuffers = BehaviorSubject.createDefault<Set<BufferId>>(emptySet())
+  private val live_config = BehaviorSubject.createDefault(Unit)
+  private val live_buffers = BehaviorSubject.createDefault(Unit)
+  private val live_removedBuffers = BehaviorSubject.createDefault(Unit)
+  private val live_temporarilyRemovedBuffers = BehaviorSubject.createDefault(Unit)
 
   object NameComparator : Comparator<BufferViewConfig> {
     override fun compare(a: BufferViewConfig?, b: BufferViewConfig?) =
diff --git a/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/Identity.kt b/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/Identity.kt
index 21d0ff463..2c673f0ef 100644
--- a/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/Identity.kt
+++ b/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/Identity.kt
@@ -99,7 +99,7 @@ class Identity constructor(
     super.setAutoAwayEnabled(enabled)
   }
 
-  override fun setAutoAwayReason(reason: String) {
+  override fun setAutoAwayReason(reason: String?) {
     _autoAwayReason = reason
     super.setAutoAwayReason(reason)
   }
@@ -114,7 +114,7 @@ class Identity constructor(
     super.setAutoAwayTime(time)
   }
 
-  override fun setAwayNick(awayNick: String) {
+  override fun setAwayNick(awayNick: String?) {
     _awayNick = awayNick
     super.setAwayNick(awayNick)
   }
@@ -124,7 +124,7 @@ class Identity constructor(
     super.setAwayNickEnabled(enabled)
   }
 
-  override fun setAwayReason(awayReason: String) {
+  override fun setAwayReason(awayReason: String?) {
     _awayReason = awayReason
     super.setAwayReason(awayReason)
   }
@@ -139,7 +139,7 @@ class Identity constructor(
     super.setDetachAwayEnabled(enabled)
   }
 
-  override fun setDetachAwayReason(reason: String) {
+  override fun setDetachAwayReason(reason: String?) {
     _detachAwayReason = reason
     super.setDetachAwayReason(reason)
   }
@@ -154,17 +154,17 @@ class Identity constructor(
     super.setId(id)
   }
 
-  override fun setIdent(ident: String) {
+  override fun setIdent(ident: String?) {
     _ident = ident
     super.setIdent(ident)
   }
 
-  override fun setIdentityName(name: String) {
+  override fun setIdentityName(name: String?) {
     _identityName = name
     super.setIdentityName(name)
   }
 
-  override fun setKickReason(reason: String) {
+  override fun setKickReason(reason: String?) {
     _kickReason = reason
     super.setKickReason(reason)
   }
@@ -174,17 +174,17 @@ class Identity constructor(
     super.setNicks(nicks)
   }
 
-  override fun setPartReason(reason: String) {
+  override fun setPartReason(reason: String?) {
     _partReason = reason
     super.setPartReason(reason)
   }
 
-  override fun setQuitReason(reason: String) {
+  override fun setQuitReason(reason: String?) {
     _quitReason = reason
     super.setQuitReason(reason)
   }
 
-  override fun setRealName(realName: String) {
+  override fun setRealName(realName: String?) {
     _realName = realName
     super.setRealName(realName)
   }
@@ -196,12 +196,12 @@ class Identity constructor(
       field = value
       _change.onNext(Unit)
     }
-  private var _identityName: String = "<isEmpty>"
+  private var _identityName: String? = "<empty>"
     set(value) {
       field = value
       _change.onNext(Unit)
     }
-  private var _realName: String = ""
+  private var _realName: String? = ""
     set(value) {
       field = value
       _change.onNext(Unit)
@@ -211,7 +211,7 @@ class Identity constructor(
       field = value
       _change.onNext(Unit)
     }
-  private var _awayNick: String = ""
+  private var _awayNick: String? = ""
     set(value) {
       field = value
       _change.onNext(Unit)
@@ -221,7 +221,7 @@ class Identity constructor(
       field = value
       _change.onNext(Unit)
     }
-  private var _awayReason: String = "Gone fishing."
+  private var _awayReason: String? = "Gone fishing."
     set(value) {
       field = value
       _change.onNext(Unit)
@@ -241,7 +241,7 @@ class Identity constructor(
       field = value
       _change.onNext(Unit)
     }
-  private var _autoAwayReason: String = "Not here. No, really. not here!"
+  private var _autoAwayReason: String? = "Not here. No, really. not here!"
     set(value) {
       field = value
       _change.onNext(Unit)
@@ -256,7 +256,7 @@ class Identity constructor(
       field = value
       _change.onNext(Unit)
     }
-  private var _detachAwayReason: String = "All Quassel clients vanished from the face of the earth..."
+  private var _detachAwayReason: String? = "All Quassel clients vanished from the face of the earth..."
     set(value) {
       field = value
       _change.onNext(Unit)
@@ -266,22 +266,22 @@ class Identity constructor(
       field = value
       _change.onNext(Unit)
     }
-  private var _ident: String = "quassel"
+  private var _ident: String? = "quassel"
     set(value) {
       field = value
       _change.onNext(Unit)
     }
-  private var _kickReason: String = "Kindergarten is elsewhere!"
+  private var _kickReason: String? = "Kindergarten is elsewhere!"
     set(value) {
       field = value
       _change.onNext(Unit)
     }
-  private var _partReason: String = "http://quassel-irc.org - Chat comfortably. Anywhere."
+  private var _partReason: String? = "http://quassel-irc.org - Chat comfortably. Anywhere."
     set(value) {
       field = value
       _change.onNext(Unit)
     }
-  private var _quitReason: String = "http://quassel-irc.org - Chat comfortably. Anywhere."
+  private var _quitReason: String? = "http://quassel-irc.org - Chat comfortably. Anywhere."
     set(value) {
       field = value
       _change.onNext(Unit)
diff --git a/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/interfaces/IIdentity.kt b/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/interfaces/IIdentity.kt
index d93149961..52b1389a5 100644
--- a/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/interfaces/IIdentity.kt
+++ b/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/interfaces/IIdentity.kt
@@ -22,7 +22,7 @@ interface IIdentity : ISyncableObject {
   }
 
   @Slot
-  fun setAutoAwayReason(reason: String) {
+  fun setAutoAwayReason(reason: String?) {
     SYNC("setAutoAwayReason", ARG(reason, Type.QString))
   }
 
@@ -37,7 +37,7 @@ interface IIdentity : ISyncableObject {
   }
 
   @Slot
-  fun setAwayNick(awayNick: String) {
+  fun setAwayNick(awayNick: String?) {
     SYNC("setAwayNick", ARG(awayNick, Type.QString))
   }
 
@@ -47,7 +47,7 @@ interface IIdentity : ISyncableObject {
   }
 
   @Slot
-  fun setAwayReason(awayReason: String) {
+  fun setAwayReason(awayReason: String?) {
     SYNC("setAwayReason", ARG(awayReason, Type.QString))
   }
 
@@ -62,7 +62,7 @@ interface IIdentity : ISyncableObject {
   }
 
   @Slot
-  fun setDetachAwayReason(reason: String) {
+  fun setDetachAwayReason(reason: String?) {
     SYNC("setDetachAwayReason", ARG(reason, Type.QString))
   }
 
@@ -77,17 +77,17 @@ interface IIdentity : ISyncableObject {
   }
 
   @Slot
-  fun setIdent(ident: String) {
+  fun setIdent(ident: String?) {
     SYNC("setIdent", ARG(ident, Type.QString))
   }
 
   @Slot
-  fun setIdentityName(name: String) {
+  fun setIdentityName(name: String?) {
     SYNC("setIdentityName", ARG(name, Type.QString))
   }
 
   @Slot
-  fun setKickReason(reason: String) {
+  fun setKickReason(reason: String?) {
     SYNC("setKickReason", ARG(reason, Type.QString))
   }
 
@@ -97,17 +97,17 @@ interface IIdentity : ISyncableObject {
   }
 
   @Slot
-  fun setPartReason(reason: String) {
+  fun setPartReason(reason: String?) {
     SYNC("setPartReason", ARG(reason, Type.QString))
   }
 
   @Slot
-  fun setQuitReason(reason: String) {
+  fun setQuitReason(reason: String?) {
     SYNC("setQuitReason", ARG(reason, Type.QString))
   }
 
   @Slot
-  fun setRealName(realName: String) {
+  fun setRealName(realName: String?) {
     SYNC("setRealName", ARG(realName, Type.QString))
   }
 
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 fd6a46f21..23439087d 100644
--- a/lib/src/main/java/de/kuschku/libquassel/session/Session.kt
+++ b/lib/src/main/java/de/kuschku/libquassel/session/Session.kt
@@ -47,14 +47,14 @@ class Session(
 
   override val identities = mutableMapOf<IdentityId, Identity>()
   private val live_identities = BehaviorSubject.createDefault(Unit)
-  override fun liveIdentities(): Observable<Map<IdentityId, Identity>> = live_identities.map { identities }
+  override fun liveIdentities(): Observable<Map<IdentityId, Identity>> = live_identities.map { identities.toMap() }
 
   override val ignoreListManager = IgnoreListManager(this)
   override val ircListHelper = IrcListHelper(this)
 
   override val networks = mutableMapOf<NetworkId, Network>()
   private val live_networks = BehaviorSubject.createDefault(Unit)
-  override fun liveNetworks(): Observable<Map<NetworkId, Network>> = live_networks.map { networks }
+  override fun liveNetworks(): Observable<Map<NetworkId, Network>> = live_networks.map { networks.toMap() }
 
   override val networkConfig = NetworkConfig(this)
 
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 7e0ded8ca..c10ac00b8 100644
--- a/lib/src/main/java/de/kuschku/libquassel/session/SessionManager.kt
+++ b/lib/src/main/java/de/kuschku/libquassel/session/SessionManager.kt
@@ -8,7 +8,6 @@ import de.kuschku.libquassel.util.compatibility.LoggingHandler
 import de.kuschku.libquassel.util.compatibility.LoggingHandler.Companion.log
 import de.kuschku.libquassel.util.helpers.or
 import io.reactivex.BackpressureStrategy
-import io.reactivex.Flowable
 import io.reactivex.Observable
 import io.reactivex.functions.BiFunction
 import io.reactivex.subjects.BehaviorSubject
@@ -40,8 +39,11 @@ class SessionManager(
     else
       lastSession
   }
-  val error: Flowable<HandshakeMessage>
-    get() = inProgressSession.toFlowable(BackpressureStrategy.LATEST).switchMap(ISession::error)
+  val error: Observable<HandshakeMessage>
+    get() = inProgressSession
+      .toFlowable(BackpressureStrategy.LATEST)
+      .switchMap(ISession::error)
+      .toObservable()
 
   val connectionProgress: Observable<Triple<ConnectionState, Int, Int>> = Observable.combineLatest(
     state, initStatus,
diff --git a/lib/src/main/java/de/kuschku/libquassel/util/compatibility/HandlerService.kt b/lib/src/main/java/de/kuschku/libquassel/util/compatibility/HandlerService.kt
index 837d07626..921d55c59 100644
--- a/lib/src/main/java/de/kuschku/libquassel/util/compatibility/HandlerService.kt
+++ b/lib/src/main/java/de/kuschku/libquassel/util/compatibility/HandlerService.kt
@@ -1,6 +1,12 @@
 package de.kuschku.libquassel.util.compatibility
 
+import io.reactivex.Scheduler
+
 interface HandlerService {
+  val scheduler: Scheduler
+
+  var exceptionHandler: Thread.UncaughtExceptionHandler?
+
   fun serialize(f: () -> Unit)
   fun deserialize(f: () -> Unit)
   fun write(f: () -> Unit)
@@ -8,6 +14,4 @@ interface HandlerService {
   fun backendDelayed(delayMillis: Long, f: () -> Unit)
 
   fun quit()
-
-  var exceptionHandler: Thread.UncaughtExceptionHandler?
 }
diff --git a/lib/src/main/java/de/kuschku/libquassel/util/compatibility/reference/JavaHandlerService.kt b/lib/src/main/java/de/kuschku/libquassel/util/compatibility/reference/JavaHandlerService.kt
index 32b4befe9..3d1dca343 100644
--- a/lib/src/main/java/de/kuschku/libquassel/util/compatibility/reference/JavaHandlerService.kt
+++ b/lib/src/main/java/de/kuschku/libquassel/util/compatibility/reference/JavaHandlerService.kt
@@ -1,9 +1,12 @@
 package de.kuschku.libquassel.util.compatibility.reference
 
 import de.kuschku.libquassel.util.compatibility.HandlerService
+import io.reactivex.schedulers.Schedulers
 import java.util.concurrent.Executors
 
 class JavaHandlerService : HandlerService {
+  override val scheduler = Schedulers.computation()
+
   override fun backendDelayed(delayMillis: Long, f: () -> Unit) = backend(f)
 
   private val serializeExecutor = Executors.newSingleThreadExecutor()
diff --git a/persistence/src/main/java/de/kuschku/quasseldroid/persistence/QuasselBacklogStorage.kt b/persistence/src/main/java/de/kuschku/quasseldroid/persistence/QuasselBacklogStorage.kt
index 2c79f758b..37c141583 100644
--- a/persistence/src/main/java/de/kuschku/quasseldroid/persistence/QuasselBacklogStorage.kt
+++ b/persistence/src/main/java/de/kuschku/quasseldroid/persistence/QuasselBacklogStorage.kt
@@ -29,6 +29,8 @@ class QuasselBacklogStorage(private val db: QuasselDatabase) : BacklogStorage {
           bufferId = message.bufferInfo.bufferId,
           sender = message.sender,
           senderPrefixes = message.senderPrefixes,
+          realName = message.realName,
+          avatarUrl = message.avatarUrl,
           content = message.content
         )
       )
diff --git a/persistence/src/main/java/de/kuschku/quasseldroid/persistence/QuasselDatabase.kt b/persistence/src/main/java/de/kuschku/quasseldroid/persistence/QuasselDatabase.kt
index 857f984fa..672f16b1a 100644
--- a/persistence/src/main/java/de/kuschku/quasseldroid/persistence/QuasselDatabase.kt
+++ b/persistence/src/main/java/de/kuschku/quasseldroid/persistence/QuasselDatabase.kt
@@ -16,7 +16,7 @@ import de.kuschku.quasseldroid.persistence.QuasselDatabase.Filtered
 import io.reactivex.Flowable
 import org.threeten.bp.Instant
 
-@Database(entities = [DatabaseMessage::class, Filtered::class], version = 5)
+@Database(entities = [DatabaseMessage::class, Filtered::class], version = 6)
 @TypeConverters(DatabaseMessage.MessageTypeConverters::class)
 abstract class QuasselDatabase : RoomDatabase() {
   abstract fun message(): MessageDao
@@ -31,6 +31,8 @@ abstract class QuasselDatabase : RoomDatabase() {
     var bufferId: Int,
     var sender: String,
     var senderPrefixes: String,
+    var realName: String,
+    var avatarUrl: String,
     var content: String
   ) {
     class MessageTypeConverters {
@@ -46,7 +48,7 @@ abstract class QuasselDatabase : RoomDatabase() {
         type
       )}, flag=${Message_Flag.of(
         flag
-      )}, bufferId=$bufferId, sender='$sender', senderPrefixes='$senderPrefixes', content='$content')"
+      )}, bufferId=$bufferId, sender='$sender', senderPrefixes='$senderPrefixes', realName='$realName', avatarUrl='$avatarUrl', content='$content')"
     }
   }
 
@@ -181,6 +183,12 @@ abstract class QuasselDatabase : RoomDatabase() {
                   database.execSQL("drop table message;")
                   database.execSQL("create table message (messageId INTEGER not null primary key, time INTEGER not null, type INTEGER not null, flag INTEGER not null, bufferId INTEGER not null, sender TEXT not null, senderPrefixes TEXT not null, content TEXT not null);")
                 }
+              },
+              object : Migration(5, 6) {
+                override fun migrate(database: SupportSQLiteDatabase) {
+                  database.execSQL("drop table message;")
+                  database.execSQL("create table message (messageId INTEGER not null primary key, time INTEGER not null, type INTEGER not null, flag INTEGER not null, bufferId INTEGER not null, sender TEXT not null, senderPrefixes TEXT not null, realName TEXT not null, avatarUrl TEXT not null, content TEXT not null);")
+                }
               }
             ).build()
           }
diff --git a/viewmodel/src/main/java/de/kuschku/quasseldroid/util/helper/ObservableHelper.kt b/viewmodel/src/main/java/de/kuschku/quasseldroid/util/helper/ObservableHelper.kt
index 9b6eb6569..f0863ce12 100644
--- a/viewmodel/src/main/java/de/kuschku/quasseldroid/util/helper/ObservableHelper.kt
+++ b/viewmodel/src/main/java/de/kuschku/quasseldroid/util/helper/ObservableHelper.kt
@@ -2,23 +2,28 @@ package de.kuschku.quasseldroid.util.helper
 
 import android.arch.lifecycle.LiveData
 import android.arch.lifecycle.LiveDataReactiveStreams
+import de.kuschku.libquassel.util.compatibility.HandlerService
 import io.reactivex.*
 import io.reactivex.functions.BiFunction
 import io.reactivex.schedulers.Schedulers
 
 inline fun <T> Observable<T>.toLiveData(
-  strategy: BackpressureStrategy = BackpressureStrategy.LATEST
-): LiveData<T> = LiveDataReactiveStreams.fromPublisher(
-  subscribeOn(Schedulers.computation()).toFlowable(strategy)
-)
+  strategy: BackpressureStrategy = BackpressureStrategy.LATEST,
+  handlerService: HandlerService? = null,
+  scheduler: Scheduler = handlerService?.scheduler ?: Schedulers.computation()
+): LiveData<T> =
+  LiveDataReactiveStreams.fromPublisher(subscribeOn(scheduler).toFlowable(strategy))
 
-inline fun <T> Maybe<T>.toLiveData(): LiveData<T> = LiveDataReactiveStreams.fromPublisher(
-  subscribeOn(Schedulers.computation()).toFlowable()
-)
+inline fun <T> Maybe<T>.toLiveData(
+  handlerService: HandlerService? = null,
+  scheduler: Scheduler = handlerService?.scheduler ?: Schedulers.computation()
+): LiveData<T> =
+  LiveDataReactiveStreams.fromPublisher(subscribeOn(scheduler).toFlowable())
 
-inline fun <T> Flowable<T>.toLiveData(): LiveData<T> = LiveDataReactiveStreams.fromPublisher(
-  subscribeOn(Schedulers.computation())
-)
+inline fun <T> Flowable<T>.toLiveData(
+  handlerService: HandlerService? = null,
+  scheduler: Scheduler = handlerService?.scheduler ?: Schedulers.computation()
+): LiveData<T> = LiveDataReactiveStreams.fromPublisher(subscribeOn(scheduler))
 
 inline fun <reified A, B> combineLatest(
   a: ObservableSource<A>,
diff --git a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/EditorViewModel.kt b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/EditorViewModel.kt
index f77ff9678..afb77af7c 100644
--- a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/EditorViewModel.kt
+++ b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/EditorViewModel.kt
@@ -84,10 +84,7 @@ class EditorViewModel : ViewModel() {
                         lowestMode,
                         user.realName(),
                         user.isAway(),
-                        network.support("CASEMAPPING"),
-                        Regex("[us]id(\\d+)").matchEntire(user.user())?.groupValues?.lastOrNull()?.let {
-                          "https://www.irccloud.com/avatar-redirect/$it"
-                        }
+                        network.support("CASEMAPPING")
                       )
                     }
                   }
diff --git a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/QuasselViewModel.kt b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/QuasselViewModel.kt
index 232fbd13f..576041eb9 100644
--- a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/QuasselViewModel.kt
+++ b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/QuasselViewModel.kt
@@ -20,8 +20,6 @@ import de.kuschku.libquassel.util.flag.hasFlag
 import de.kuschku.libquassel.util.helpers.*
 import de.kuschku.libquassel.util.irc.IrcCaseMappers
 import de.kuschku.quasseldroid.util.helper.combineLatest
-import de.kuschku.quasseldroid.util.helper.switchMapNotNull
-import de.kuschku.quasseldroid.util.helper.toLiveData
 import de.kuschku.quasseldroid.viewmodel.data.*
 import io.reactivex.Observable
 import io.reactivex.subjects.BehaviorSubject
@@ -41,13 +39,11 @@ class QuasselViewModel : ViewModel() {
   val expandedMessages = BehaviorSubject.createDefault(emptySet<MsgId>())
 
   val buffer = BehaviorSubject.createDefault(-1)
-  val buffer_liveData = buffer.toLiveData()
 
   val bufferViewConfigId = BehaviorSubject.createDefault(-1)
 
   val MAX_RECENT_MESSAGES = 20
   val recentlySentMessages = BehaviorSubject.createDefault(emptyList<CharSequence>())
-  val recentlySentMessages_liveData = recentlySentMessages.toLiveData()
   fun addRecentlySentMessage(message: CharSequence) {
     recentlySentMessages.onNext(
       listOf(message) + recentlySentMessages.value
@@ -58,12 +54,10 @@ class QuasselViewModel : ViewModel() {
 
   val backend = backendWrapper.switchMap { it }
   val sessionManager = backend.mapMap(Backend::sessionManager)
-  val sessionManager_liveData = sessionManager.toLiveData()
   val session = sessionManager.mapSwitchMap(SessionManager::session)
 
   val connectionProgress = sessionManager.mapSwitchMap(SessionManager::connectionProgress)
     .mapOrElse(Triple(ConnectionState.DISCONNECTED, 0, 0))
-  val connectionProgress_liveData = connectionProgress.toLiveData()
 
   val bufferViewManager = session.mapMapNullable(ISession::bufferViewManager)
 
@@ -73,9 +67,7 @@ class QuasselViewModel : ViewModel() {
     }
   }
 
-  val errors = sessionManager.toLiveData().switchMapNotNull {
-    it.orNull()?.error?.toLiveData()
-  }
+  val errors = sessionManager.mapSwitchMap(SessionManager::error)
 
   val networkConfig = session.map {
     it.map(ISession::networkConfig)
@@ -94,6 +86,12 @@ class QuasselViewModel : ViewModel() {
     it.liveBufferInfos().map(Map<BufferId, BufferInfo>::values)
   }.mapOrElse(emptyList())
 
+  val network = combineLatest(bufferSyncer,
+                              networks,
+                              buffer).map { (bufferSyncer, networks, buffer) ->
+    Optional.ofNullable(bufferSyncer.orNull()?.bufferInfo(buffer)?.let { networks[it.networkId] })
+  }
+
   /**
    * An observable of the changes of the markerline, as pairs of `(old, new)`
    */
@@ -103,7 +101,6 @@ class QuasselViewModel : ViewModel() {
       currentSession.bufferSyncer?.liveMarkerLine(currentBuffer) ?: Observable.empty()
     }
   }
-  val markerLine_liveData = markerLine.toLiveData()
 
   // Remove orElse
   val lag: Observable<Long> = session.mapSwitchMap(ISession::lag).mapOrElse(0)
@@ -198,11 +195,9 @@ class QuasselViewModel : ViewModel() {
                       network.modesToPrefixes(userModes),
                       lowestMode,
                       user.realName(),
+                      user.hostMask(),
                       user.isAway(),
-                      network.support("CASEMAPPING"),
-                      Regex("[us]id(\\d+)").matchEntire(user.user())?.groupValues?.lastOrNull()?.let {
-                        "https://www.irccloud.com/avatar-redirect/$it"
-                      }
+                      network.support("CASEMAPPING")
                     )
                   }
                 }
@@ -285,7 +280,6 @@ class QuasselViewModel : ViewModel() {
         Observable.just(SelectedBufferItem())
       }
     }
-  val selectedBuffer_liveData = selectedBuffer.toLiveData()
 
   val bufferList: Observable<Pair<BufferViewConfig?, List<BufferProps>>?> =
     combineLatest(session, bufferViewConfig, showHidden)
diff --git a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/data/AutoCompleteItem.kt b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/data/AutoCompleteItem.kt
index fe384dcd5..f77645263 100644
--- a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/data/AutoCompleteItem.kt
+++ b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/data/AutoCompleteItem.kt
@@ -22,7 +22,7 @@ sealed class AutoCompleteItem(open val name: String) : Comparable<AutoCompleteIt
     val realname: CharSequence,
     val away: Boolean,
     val networkCasemapping: String?,
-    val avatarUrl: String? = null,
+    val avatarUrls: List<String> = emptyList(),
     val fallbackDrawable: Drawable? = null,
     val displayNick: CharSequence? = null
   ) : AutoCompleteItem(nick)
@@ -33,4 +33,4 @@ sealed class AutoCompleteItem(open val name: String) : Comparable<AutoCompleteIt
     val bufferStatus: BufferStatus,
     val description: CharSequence
   ) : AutoCompleteItem(info.bufferName ?: "")
-}
\ No newline at end of file
+}
diff --git a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/data/FormattedMessage.kt b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/data/FormattedMessage.kt
index fd7c7865b..567f2b735 100644
--- a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/data/FormattedMessage.kt
+++ b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/data/FormattedMessage.kt
@@ -9,8 +9,9 @@ class FormattedMessage(
   val content: CharSequence? = null,
   val combined: CharSequence,
   val fallbackDrawable: Drawable? = null,
-  val avatarUrl: String? = null,
+  val realName: CharSequence? = null,
+  val avatarUrls: List<String> = emptyList(),
   val isSelected: Boolean,
   val isExpanded: Boolean,
   val isMarkerLine: Boolean
-)
\ No newline at end of file
+)
diff --git a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/data/IrcUserItem.kt b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/data/IrcUserItem.kt
index de3ec6672..d7f8080da 100644
--- a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/data/IrcUserItem.kt
+++ b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/data/IrcUserItem.kt
@@ -7,9 +7,10 @@ data class IrcUserItem(
   val modes: String,
   val lowestMode: Int,
   val realname: CharSequence,
+  val hostmask: String,
   val away: Boolean,
   val networkCasemapping: String?,
-  val avatarUrl: String? = null,
+  val avatarUrls: List<String> = emptyList(),
   val fallbackDrawable: Drawable? = null,
   val displayNick: CharSequence? = null
-)
\ No newline at end of file
+)
-- 
GitLab