From d9b0223e88288badc700b2d07f398805a307aba6 Mon Sep 17 00:00:00 2001
From: Janne Koschinski <janne@kuschku.de>
Date: Fri, 16 Feb 2018 23:33:34 +0100
Subject: [PATCH] Improved channel list

---
 .../quasseldroid_ng/service/QuasselService.kt |   1 +
 .../ui/chat/BufferListAdapter.kt              | 348 ++++++++++++++++--
 .../ui/chat/BufferViewConfigFragment.kt       |  83 ++++-
 .../util/helper/ContextHelper.kt              |  21 ++
 .../util/helper/LiveDataZipHelper.kt          |  66 ++++
 .../util/helper/ThemeHelper.kt                |  16 +
 app/src/main/res/drawable/ic_chevron_down.xml |  10 +
 app/src/main/res/drawable/ic_chevron_up.xml   |  10 +
 app/src/main/res/layout/activity_main.xml     |   3 +-
 app/src/main/res/layout/activity_setup.xml    |   3 +-
 .../main/res/layout/fragment_chat_list.xml    |   8 +-
 app/src/main/res/layout/fragment_messages.xml |   4 +-
 .../res/layout/setup_account_connection.xml   |   5 +-
 .../main/res/layout/setup_account_edit.xml    |   4 +-
 .../main/res/layout/setup_account_name.xml    |   5 +-
 .../main/res/layout/setup_account_user.xml    |   5 +-
 .../main/res/layout/setup_select_account.xml  |   5 +-
 app/src/main/res/layout/setup_slide.xml       |   4 +-
 app/src/main/res/layout/widget_buffer.xml     |  52 +++
 .../res/layout/widget_chatmessage_action.xml  |   5 +-
 .../res/layout/widget_chatmessage_error.xml   |   4 +-
 .../res/layout/widget_chatmessage_plain.xml   |   4 +-
 .../res/layout/widget_chatmessage_server.xml  |   4 +-
 .../main/res/layout/widget_core_account.xml   |   3 +-
 .../res/layout/widget_core_account_add.xml    |   3 +-
 app/src/main/res/layout/widget_network.xml    |  42 +++
 .../res/layout/widget_spinner_item_inline.xml |   4 +-
 .../layout/widget_spinner_item_toolbar.xml    |   3 +-
 .../quassel/syncables/IrcChannel.kt           |   7 +
 .../libquassel/quassel/syncables/IrcUser.kt   |   9 +
 .../libquassel/quassel/syncables/Network.kt   |  27 +-
 .../kuschku/libquassel/session/SignalProxy.kt |  21 ++
 .../java/de/kuschku/libquassel/util/Flag.kt   |   7 +
 .../de/kuschku/libquassel/util/LongFlag.kt    |   7 +
 .../de/kuschku/libquassel/util/ShortFlag.kt   |   7 +
 35 files changed, 748 insertions(+), 62 deletions(-)
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/LiveDataZipHelper.kt
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/ThemeHelper.kt
 create mode 100644 app/src/main/res/drawable/ic_chevron_down.xml
 create mode 100644 app/src/main/res/drawable/ic_chevron_up.xml
 create mode 100644 app/src/main/res/layout/widget_buffer.xml
 create mode 100644 app/src/main/res/layout/widget_network.xml

diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/service/QuasselService.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/service/QuasselService.kt
index a3c6e5fca..8df09d4aa 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/service/QuasselService.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/service/QuasselService.kt
@@ -30,6 +30,7 @@ class QuasselService : LifecycleService() {
 
     @SuppressLint("TrustAllX509TrustManager")
     override fun checkServerTrusted(p0: Array<out X509Certificate>?, p1: String?) = Unit
+
     override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
   }
 
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/BufferListAdapter.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/BufferListAdapter.kt
index 499ff06f5..05ea46dc9 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/BufferListAdapter.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/BufferListAdapter.kt
@@ -2,38 +2,75 @@ package de.kuschku.quasseldroid_ng.ui.chat
 
 import android.arch.lifecycle.LifecycleOwner
 import android.arch.lifecycle.LiveData
+import android.arch.lifecycle.MutableLiveData
 import android.arch.lifecycle.Observer
+import android.graphics.drawable.Drawable
+import android.support.v4.graphics.drawable.DrawableCompat
 import android.support.v7.util.DiffUtil
+import android.support.v7.widget.AppCompatImageButton
 import android.support.v7.widget.RecyclerView
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
+import android.widget.ImageView
 import android.widget.TextView
 import butterknife.BindView
 import butterknife.ButterKnife
 import de.kuschku.libquassel.protocol.BufferId
+import de.kuschku.libquassel.protocol.NetworkId
 import de.kuschku.libquassel.quassel.BufferInfo
+import de.kuschku.libquassel.quassel.syncables.interfaces.INetwork
 import de.kuschku.libquassel.util.hasFlag
+import de.kuschku.quasseldroid_ng.R
+import de.kuschku.quasseldroid_ng.util.helper.getCompatDrawable
+import de.kuschku.quasseldroid_ng.util.helper.styledAttributes
+import de.kuschku.quasseldroid_ng.util.helper.zip
 
 class BufferListAdapter(
   lifecycleOwner: LifecycleOwner,
-  liveData: LiveData<List<BufferInfo>?>,
+  liveData: LiveData<List<BufferProps>?>,
   runInBackground: (() -> Unit) -> Any,
   runOnUiThread: (Runnable) -> Any,
   private val clickListener: ((BufferId) -> Unit)? = null
 ) : RecyclerView.Adapter<BufferListAdapter.BufferViewHolder>() {
-  var data = mutableListOf<BufferInfo>()
+  var data = mutableListOf<BufferListItem>()
+
+  var collapsedNetworks = MutableLiveData<Set<NetworkId>>()
+
+  fun expandListener(networkId: NetworkId) {
+    if (collapsedNetworks.value.orEmpty().contains(networkId))
+      collapsedNetworks.postValue(collapsedNetworks.value.orEmpty() - networkId)
+    else
+      collapsedNetworks.postValue(collapsedNetworks.value.orEmpty() + networkId)
+  }
 
   init {
-    liveData.observe(
-      lifecycleOwner, Observer { list: List<BufferInfo>? ->
+    collapsedNetworks.value = emptySet()
+
+    liveData.zip(collapsedNetworks).observe(
+      lifecycleOwner, Observer { it: Pair<List<BufferProps>?, Set<NetworkId>>? ->
       runInBackground {
-        val old = data
-        val new = list?.sortedBy(BufferInfo::networkId) ?: emptyList()
+        val list = it?.first ?: emptyList()
+        val collapsedNetworks = it?.second ?: emptySet()
+
+        val old: List<BufferListItem> = data
+        val new: List<BufferListItem> = list.sortedBy { props ->
+          props.network.networkName
+        }.map { props ->
+          BufferListItem(
+            props,
+            BufferState(
+              networkExpanded = !collapsedNetworks.contains(props.network.networkId)
+            )
+          )
+        }.filter { (props, state) ->
+          props.info.type.hasFlag(BufferInfo.Type.StatusBuffer) || state.networkExpanded
+        }
+
         val result = DiffUtil.calculateDiff(
           object : DiffUtil.Callback() {
             override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int)
-              = old[oldItemPosition].bufferId == new[newItemPosition].bufferId
+              = old[oldItemPosition].props.info.bufferId == new[newItemPosition].props.info.bufferId
 
             override fun getOldListSize() = old.size
             override fun getNewListSize() = new.size
@@ -54,40 +91,287 @@ class BufferListAdapter(
     )
   }
 
-  override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = BufferViewHolder(
-    LayoutInflater.from(parent.context).inflate(android.R.layout.simple_list_item_1, parent, false),
-    clickListener = clickListener
-  )
+  override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when (viewType) {
+    BufferInfo.Type.ChannelBuffer.toInt() -> BufferViewHolder.ChannelBuffer(
+      LayoutInflater.from(parent.context).inflate(
+        R.layout.widget_buffer, parent, false
+      ),
+      clickListener = clickListener
+    )
+    BufferInfo.Type.QueryBuffer.toInt()   -> BufferViewHolder.QueryBuffer(
+      LayoutInflater.from(parent.context).inflate(
+        R.layout.widget_buffer, parent, false
+      ),
+      clickListener = clickListener
+    )
+    BufferInfo.Type.GroupBuffer.toInt()   -> BufferViewHolder.GroupBuffer(
+      LayoutInflater.from(parent.context).inflate(
+        R.layout.widget_buffer, parent, false
+      ),
+      clickListener = clickListener
+    )
+    BufferInfo.Type.StatusBuffer.toInt()  -> BufferViewHolder.StatusBuffer(
+      LayoutInflater.from(parent.context).inflate(
+        R.layout.widget_network, parent, false
+      ),
+      clickListener = clickListener,
+      expansionListener = ::expandListener
+    )
+    else                                  -> throw IllegalArgumentException(
+      "No such viewType: $viewType"
+    )
+  }
 
   override fun onBindViewHolder(holder: BufferViewHolder, position: Int)
-    = holder.bind(data[position])
+    = holder.bind(data[position].props, data[position].state)
 
   override fun getItemCount() = data.size
 
-  class BufferViewHolder(
-    itemView: View,
-    private val clickListener: ((BufferId) -> Unit)? = null
-  ) : RecyclerView.ViewHolder(itemView) {
-    @BindView(android.R.id.text1)
-    lateinit var text: TextView
-
-    var bufferId: BufferId? = null
-
-    init {
-      ButterKnife.bind(this, itemView)
-      itemView.setOnClickListener {
-        val buffer = bufferId
-        if (buffer != null)
-          clickListener?.invoke(buffer)
+  override fun getItemViewType(position: Int) = data[position].props.info.type.toInt()
+
+  data class BufferListItem(
+    val props: BufferProps,
+    val state: BufferState
+  )
+
+  data class BufferProps(
+    val info: BufferInfo,
+    val network: INetwork.NetworkInfo,
+    val bufferStatus: BufferStatus,
+    val description: String
+  )
+
+  data class BufferState(
+    val networkExpanded: Boolean
+  )
+
+  abstract class BufferViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+    abstract fun bind(props: BufferProps, state: BufferState)
+
+    fun <T> status(target: T, actual: T) = if (target == actual) {
+      View.VISIBLE
+    } else {
+      View.GONE
+    }
+
+    class StatusBuffer(
+      itemView: View,
+      private val clickListener: ((BufferId) -> Unit)? = null,
+      private val expansionListener: ((NetworkId) -> Unit)? = null
+    ) : BufferViewHolder(itemView) {
+      @BindView(R.id.status)
+      lateinit var status: AppCompatImageButton
+
+      @BindView(R.id.name)
+      lateinit var name: TextView
+
+      var bufferId: BufferId? = null
+      var networkId: NetworkId? = null
+
+      init {
+        ButterKnife.bind(this, itemView)
+        itemView.setOnClickListener {
+          val buffer = bufferId
+          if (buffer != null)
+            clickListener?.invoke(buffer)
+        }
+
+        status.setOnClickListener {
+          val network = networkId
+          if (network != null)
+            expansionListener?.invoke(network)
+        }
+      }
+
+      override fun bind(props: BufferProps, state: BufferState) {
+        name.text = props.network.networkName
+        bufferId = props.info.bufferId
+        networkId = props.info.networkId
+
+        if (state.networkExpanded) {
+          status.setImageDrawable(itemView.context.getCompatDrawable(R.drawable.ic_chevron_up))
+        } else {
+          status.setImageDrawable(itemView.context.getCompatDrawable(R.drawable.ic_chevron_down))
+        }
+      }
+    }
+
+    class GroupBuffer(
+      itemView: View,
+      private val clickListener: ((BufferId) -> Unit)? = null
+    ) : BufferViewHolder(itemView) {
+      @BindView(R.id.status)
+      lateinit var status: ImageView
+
+      @BindView(R.id.name)
+      lateinit var name: TextView
+
+      @BindView(R.id.description)
+      lateinit var description: TextView
+
+      var bufferId: BufferId? = null
+
+      private val online: Drawable
+      private val offline: Drawable
+
+      init {
+        ButterKnife.bind(this, itemView)
+        itemView.setOnClickListener {
+          val buffer = bufferId
+          if (buffer != null)
+            clickListener?.invoke(buffer)
+        }
+
+        online = itemView.context.getCompatDrawable(R.drawable.ic_status)
+        offline = itemView.context.getCompatDrawable(R.drawable.ic_status_offline)
+
+        itemView.context.theme.styledAttributes(R.attr.colorAccent, R.attr.colorAway) {
+          DrawableCompat.setTint(online, getColor(0, 0))
+          DrawableCompat.setTint(offline, getColor(1, 0))
+        }
+      }
+
+      override fun bind(props: BufferProps, state: BufferState) {
+        bufferId = props.info.bufferId
+
+        name.text = props.info.bufferName
+        description.text = props.description
+
+        description.visibility = if (props.description == "") {
+          View.GONE
+        } else {
+          View.VISIBLE
+        }
+
+        status.setImageDrawable(
+          when (props.bufferStatus) {
+            BufferStatus.ONLINE -> online
+            else                -> offline
+          }
+        )
+      }
+    }
+
+    class ChannelBuffer(
+      itemView: View,
+      private val clickListener: ((BufferId) -> Unit)? = null
+    ) : BufferViewHolder(itemView) {
+      @BindView(R.id.status)
+      lateinit var status: ImageView
+
+      @BindView(R.id.name)
+      lateinit var name: TextView
+
+      @BindView(R.id.description)
+      lateinit var description: TextView
+
+      var bufferId: BufferId? = null
+
+      private val online: Drawable
+      private val offline: Drawable
+
+      init {
+        ButterKnife.bind(this, itemView)
+        itemView.setOnClickListener {
+          val buffer = bufferId
+          if (buffer != null)
+            clickListener?.invoke(buffer)
+        }
+
+        online = itemView.context.getCompatDrawable(R.drawable.ic_status_channel)
+        offline = itemView.context.getCompatDrawable(R.drawable.ic_status_channel_offline)
+
+        itemView.context.theme.styledAttributes(R.attr.colorAccent, R.attr.colorAway) {
+          DrawableCompat.setTint(online, getColor(0, 0))
+          DrawableCompat.setTint(offline, getColor(1, 0))
+        }
+      }
+
+      override fun bind(props: BufferProps, state: BufferState) {
+        bufferId = props.info.bufferId
+
+        name.text = props.info.bufferName
+        description.text = props.description
+
+        description.visibility = if (props.description == "") {
+          View.GONE
+        } else {
+          View.VISIBLE
+        }
+
+        status.setImageDrawable(
+          when (props.bufferStatus) {
+            BufferStatus.ONLINE -> online
+            else                -> offline
+          }
+        )
       }
     }
 
-    fun bind(info: BufferInfo) {
-      text.text = when {
-        info.type.hasFlag(BufferInfo.Type.StatusBuffer) -> "Network ${info.networkId}"
-        else                                            -> "${info.networkId}/${info.bufferName}"
+    class QueryBuffer(
+      itemView: View,
+      private val clickListener: ((BufferId) -> Unit)? = null
+    ) : BufferViewHolder(itemView) {
+      @BindView(R.id.status)
+      lateinit var status: ImageView
+
+      @BindView(R.id.name)
+      lateinit var name: TextView
+
+      @BindView(R.id.description)
+      lateinit var description: TextView
+
+      var bufferId: BufferId? = null
+
+      private val online: Drawable
+      private val away: Drawable
+      private val offline: Drawable
+
+      init {
+        ButterKnife.bind(this, itemView)
+        itemView.setOnClickListener {
+          val buffer = bufferId
+          if (buffer != null)
+            clickListener?.invoke(buffer)
+        }
+
+        online = itemView.context.getCompatDrawable(R.drawable.ic_status)
+        away = itemView.context.getCompatDrawable(R.drawable.ic_status)
+        offline = itemView.context.getCompatDrawable(R.drawable.ic_status_offline)
+
+        itemView.context.theme.styledAttributes(R.attr.colorAccent, R.attr.colorAway) {
+          DrawableCompat.setTint(online, getColor(0, 0))
+          DrawableCompat.setTint(away, getColor(1, 0))
+          DrawableCompat.setTint(offline, getColor(1, 0))
+        }
+      }
+
+      override fun bind(props: BufferProps, state: BufferState) {
+        bufferId = props.info.bufferId
+
+        name.text = props.info.bufferName
+        description.text = props.description
+
+        description.visibility = if (props.description == "") {
+          View.GONE
+        } else {
+          View.VISIBLE
+        }
+
+        status.setImageDrawable(
+          when (props.bufferStatus) {
+            BufferStatus.ONLINE -> online
+            BufferStatus.AWAY   -> away
+            else                -> offline
+          }
+        )
       }
-      bufferId = info.bufferId
     }
   }
+
+  enum class BufferStatus {
+    ONLINE,
+    AWAY,
+    OFFLINE
+  }
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/BufferViewConfigFragment.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/BufferViewConfigFragment.kt
index af325bf39..3deed0c90 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/BufferViewConfigFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/BufferViewConfigFragment.kt
@@ -12,9 +12,8 @@ import butterknife.BindView
 import butterknife.ButterKnife
 import de.kuschku.libquassel.protocol.BufferId
 import de.kuschku.libquassel.protocol.NetworkId
-import de.kuschku.libquassel.quassel.syncables.BufferViewConfig
-import de.kuschku.libquassel.quassel.syncables.BufferViewManager
-import de.kuschku.libquassel.quassel.syncables.Network
+import de.kuschku.libquassel.quassel.BufferInfo
+import de.kuschku.libquassel.quassel.syncables.*
 import de.kuschku.libquassel.session.Backend
 import de.kuschku.libquassel.session.ISession
 import de.kuschku.libquassel.session.SessionManager
@@ -25,6 +24,7 @@ import de.kuschku.quasseldroid_ng.util.helper.or
 import de.kuschku.quasseldroid_ng.util.helper.switchMap
 import de.kuschku.quasseldroid_ng.util.helper.switchMapRx
 import de.kuschku.quasseldroid_ng.util.service.ServiceBoundFragment
+import io.reactivex.Observable
 
 class BufferViewConfigFragment : ServiceBoundFragment() {
   private val handlerThread = AndroidHandlerThread("ChatList")
@@ -74,11 +74,78 @@ class BufferViewConfigFragment : ServiceBoundFragment() {
 
   private val bufferIdList = selectedBufferViewConfig.switchMapRx(BufferViewConfig::live_buffers)
 
-  private val bufferList = sessionManager.switchMap { manager ->
-    bufferIdList.map { ids ->
-      ids.mapNotNull {
-        manager.bufferSyncer?.bufferInfo(it)
-      }
+  private val bufferList: LiveData<List<BufferListAdapter.BufferProps>?> = sessionManager.switchMap { manager ->
+    bufferIdList.switchMapRx { ids ->
+      Observable.combineLatest(
+        ids.mapNotNull { id ->
+          manager.bufferSyncer?.bufferInfo(
+            id
+          )
+        }.mapNotNull {
+          val network = manager.networks[it.networkId]
+          if (network == null) {
+            null
+          } else {
+            it to network
+          }
+        }.map { (info, network) ->
+          when (info.type.toInt()) {
+            BufferInfo.Type.QueryBuffer.toInt()   -> {
+              network.liveIrcUser(info.bufferName).distinctUntilChanged().switchMap { user ->
+                user.live_away.switchMap { away ->
+                  user.live_realName.map { realName ->
+                    BufferListAdapter.BufferProps(
+                      info = info,
+                      network = network.networkInfo(),
+                      bufferStatus = when {
+                        user == IrcUser.NULL -> BufferListAdapter.BufferStatus.OFFLINE
+                        away                 -> BufferListAdapter.BufferStatus.AWAY
+                        else                 -> BufferListAdapter.BufferStatus.ONLINE
+                      },
+                      description = realName
+                    )
+                  }
+                }
+              }
+            }
+            BufferInfo.Type.ChannelBuffer.toInt() -> {
+              network.liveIrcChannel(info.bufferName).distinctUntilChanged().switchMap { channel ->
+                channel.live_topic.map { topic ->
+                  BufferListAdapter.BufferProps(
+                    info = info,
+                    network = network.networkInfo(),
+                    bufferStatus = when (channel) {
+                      IrcChannel.NULL -> BufferListAdapter.BufferStatus.OFFLINE
+                      else            -> BufferListAdapter.BufferStatus.ONLINE
+                    },
+                    description = topic
+                  )
+                }
+              }
+            }
+            BufferInfo.Type.StatusBuffer.toInt()  -> {
+              network.liveConnectionState.map {
+                BufferListAdapter.BufferProps(
+                  info = info,
+                  network = network.networkInfo(),
+                  bufferStatus = BufferListAdapter.BufferStatus.OFFLINE,
+                  description = ""
+                )
+              }
+            }
+            else                                  -> Observable.just(
+              BufferListAdapter.BufferProps(
+                info = info,
+                network = network.networkInfo(),
+                bufferStatus = BufferListAdapter.BufferStatus.OFFLINE,
+                description = ""
+              )
+            )
+          }
+        }, { array: Array<Any> ->
+          array.toList() as List<BufferListAdapter.BufferProps>
+        }
+      )
     }
   }
 
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/ContextHelper.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/ContextHelper.kt
index 70dab9704..1fe35d03f 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/ContextHelper.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/ContextHelper.kt
@@ -1,7 +1,11 @@
 package de.kuschku.quasseldroid_ng.util.helper
 
 import android.content.Context
+import android.graphics.drawable.Drawable
 import android.os.Build
+import android.support.annotation.ColorInt
+import android.support.annotation.ColorRes
+import android.support.annotation.DrawableRes
 
 fun Context.getStatusBarHeight(): Int {
   var result = 0
@@ -17,3 +21,20 @@ inline fun <reified T> Context.systemService(): T = if (Build.VERSION.SDK_INT >=
 } else {
   getSystemService(T::class.java.simpleName) as T
 }
+
+fun Context.getCompatDrawable(@DrawableRes id: Int): Drawable {
+  return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+    this.resources.getDrawable(id, this.theme)
+  } else {
+    this.resources.getDrawable(id)
+  }
+}
+
+@ColorInt
+fun Context.getCompatColor(@ColorRes id: Int): Int {
+  return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+    this.resources.getColor(id, this.theme)
+  } else {
+    this.resources.getColor(id)
+  }
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/LiveDataZipHelper.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/LiveDataZipHelper.kt
new file mode 100644
index 000000000..c9045e619
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/LiveDataZipHelper.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2017 Mitchell Skaggs, Keturah Gadson, Ethan Holtgrieve, Nathan Skelton,
+ * Pattonville School District
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package de.kuschku.quasseldroid_ng.util.helper
+
+import android.arch.lifecycle.LiveData
+import android.arch.lifecycle.MediatorLiveData
+
+/**
+ * This function creates a [LiveData] of a [Pair] of the two types provided. The resulting LiveData
+ * is updated whenever either input LiveData updates and both LiveData have updated at least once
+ * before.
+ *
+ * If the zip of A and B is C, and A and B are updated in this pattern: `AABA`, C would be updated
+ * twice (once with the second A value and first B value, and once with the third A value and first
+ * B value).
+ *
+ * @param a the first LiveData
+ * @param b the second LiveData
+ * @author Mitchell Skaggs
+ */
+fun <A, B> zipLiveData(a: LiveData<A>, b: LiveData<B>): LiveData<Pair<A, B>> {
+  return MediatorLiveData<Pair<A, B>>().apply {
+    var lastA: A? = null
+    var lastB: B? = null
+
+    fun update() {
+      val localLastA = lastA
+      val localLastB = lastB
+      if (localLastA != null && localLastB != null)
+        this.value = Pair(localLastA, localLastB)
+    }
+
+    addSource(a) {
+      lastA = it
+      update()
+    }
+    addSource(b) {
+      lastB = it
+      update()
+    }
+  }
+}
+
+/**
+ * This is merely an extension function for [zipLiveData].
+ *
+ * @see zipLiveData
+ * @author Mitchell Skaggs
+ */
+fun <A, B> LiveData<A>.zip(b: LiveData<B>): LiveData<Pair<A, B>> = zipLiveData(this, b)
\ No newline at end of file
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/ThemeHelper.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/ThemeHelper.kt
new file mode 100644
index 000000000..452086db1
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/ThemeHelper.kt
@@ -0,0 +1,16 @@
+package de.kuschku.quasseldroid_ng.util.helper
+
+import android.content.res.Resources
+import android.content.res.TypedArray
+
+inline fun Resources.Theme.styledAttributes(vararg attributes: Int, f: TypedArray.() -> Unit) {
+  this.obtainStyledAttributes(attributes).use {
+    it.apply(f)
+  }
+}
+
+inline fun <R> TypedArray.use(block: (TypedArray) -> R): R {
+  val result = block(this)
+  recycle()
+  return result
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_chevron_down.xml b/app/src/main/res/drawable/ic_chevron_down.xml
new file mode 100644
index 000000000..e86d4f638
--- /dev/null
+++ b/app/src/main/res/drawable/ic_chevron_down.xml
@@ -0,0 +1,10 @@
+<!-- drawable/chevron_down.xml -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+  android:width="24dp"
+  android:height="24dp"
+  android:viewportHeight="24"
+  android:viewportWidth="24">
+  <path
+    android:fillColor="#000"
+    android:pathData="M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z" />
+</vector>
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_chevron_up.xml b/app/src/main/res/drawable/ic_chevron_up.xml
new file mode 100644
index 000000000..092abec33
--- /dev/null
+++ b/app/src/main/res/drawable/ic_chevron_up.xml
@@ -0,0 +1,10 @@
+<!-- drawable/chevron_up.xml -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+  android:width="24dp"
+  android:height="24dp"
+  android:viewportHeight="24"
+  android:viewportWidth="24">
+  <path
+    android:fillColor="#000"
+    android:pathData="M7.41,15.41L12,10.83L16.59,15.41L18,14L12,8L6,14L7.41,15.41Z" />
+</vector>
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 1094421ed..98bd94628 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -4,7 +4,8 @@
   android:id="@+id/drawerLayout"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
-  android:fitsSystemWindows="true">
+  android:fitsSystemWindows="true"
+  tools:theme="@style/Theme.ChatTheme.Quassel_Light">
 
   <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
diff --git a/app/src/main/res/layout/activity_setup.xml b/app/src/main/res/layout/activity_setup.xml
index a0a74dc86..81175f085 100644
--- a/app/src/main/res/layout/activity_setup.xml
+++ b/app/src/main/res/layout/activity_setup.xml
@@ -2,7 +2,8 @@
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
-  android:layout_height="match_parent">
+  android:layout_height="match_parent"
+  tools:theme="@style/Theme.SetupTheme">
 
   <android.support.v4.view.ViewPager
     android:id="@+id/view_pager"
diff --git a/app/src/main/res/layout/fragment_chat_list.xml b/app/src/main/res/layout/fragment_chat_list.xml
index 228f6f5e9..1dc6351ef 100644
--- a/app/src/main/res/layout/fragment_chat_list.xml
+++ b/app/src/main/res/layout/fragment_chat_list.xml
@@ -1,8 +1,11 @@
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
+  xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
-  android:orientation="vertical">
+  android:orientation="vertical"
+  tools:background="@android:color/background_light"
+  tools:theme="@style/Theme.ChatTheme.Quassel_Light">
 
   <android.support.design.widget.AppBarLayout
     android:layout_width="match_parent"
@@ -30,5 +33,6 @@
   <android.support.v7.widget.RecyclerView
     android:id="@+id/chatList"
     android:layout_width="match_parent"
-    android:layout_height="match_parent" />
+    android:layout_height="match_parent"
+    tools:listitem="@layout/widget_buffer" />
 </LinearLayout>
diff --git a/app/src/main/res/layout/fragment_messages.xml b/app/src/main/res/layout/fragment_messages.xml
index ac1b5a37a..2470d8d7d 100644
--- a/app/src/main/res/layout/fragment_messages.xml
+++ b/app/src/main/res/layout/fragment_messages.xml
@@ -4,7 +4,9 @@
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
-  android:background="?attr/colorBackground">
+  android:background="?attr/colorBackground"
+  tools:background="@android:color/background_light"
+  tools:theme="@style/Theme.ChatTheme.Quassel_Light">
 
   <android.support.v7.widget.RecyclerView
     android:id="@+id/messages"
diff --git a/app/src/main/res/layout/setup_account_connection.xml b/app/src/main/res/layout/setup_account_connection.xml
index 499b64e16..162ac4321 100644
--- a/app/src/main/res/layout/setup_account_connection.xml
+++ b/app/src/main/res/layout/setup_account_connection.xml
@@ -2,10 +2,13 @@
 
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
+  xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:orientation="vertical"
-  android:padding="32dp">
+  android:padding="32dp"
+  tools:background="@android:color/background_light"
+  tools:theme="@style/Theme.SetupTheme">
 
   <android.support.design.widget.TextInputLayout
     android:id="@+id/hostWrapper"
diff --git a/app/src/main/res/layout/setup_account_edit.xml b/app/src/main/res/layout/setup_account_edit.xml
index 715e30623..bfc7e62d9 100644
--- a/app/src/main/res/layout/setup_account_edit.xml
+++ b/app/src/main/res/layout/setup_account_edit.xml
@@ -2,7 +2,9 @@
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
-  android:layout_height="match_parent">
+  android:layout_height="match_parent"
+  tools:background="@android:color/background_light"
+  tools:theme="@style/Theme.SetupTheme">
 
   <LinearLayout
     android:layout_width="match_parent"
diff --git a/app/src/main/res/layout/setup_account_name.xml b/app/src/main/res/layout/setup_account_name.xml
index 591099361..0bf1ecfcd 100644
--- a/app/src/main/res/layout/setup_account_name.xml
+++ b/app/src/main/res/layout/setup_account_name.xml
@@ -1,9 +1,12 @@
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
+  xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:orientation="vertical"
-  android:padding="32dp">
+  android:padding="32dp"
+  tools:background="@android:color/background_light"
+  tools:theme="@style/Theme.SetupTheme">
 
   <android.support.design.widget.TextInputLayout
     android:id="@+id/nameWrapper"
diff --git a/app/src/main/res/layout/setup_account_user.xml b/app/src/main/res/layout/setup_account_user.xml
index 4468f2c97..e75a654c6 100644
--- a/app/src/main/res/layout/setup_account_user.xml
+++ b/app/src/main/res/layout/setup_account_user.xml
@@ -1,9 +1,12 @@
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
+  xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:orientation="vertical"
-  android:padding="32dp">
+  android:padding="32dp"
+  tools:background="@android:color/background_light"
+  tools:theme="@style/Theme.SetupTheme">
 
   <android.support.design.widget.TextInputLayout
     android:id="@+id/userWrapper"
diff --git a/app/src/main/res/layout/setup_select_account.xml b/app/src/main/res/layout/setup_select_account.xml
index 7961fd8dd..b2e81ee86 100644
--- a/app/src/main/res/layout/setup_select_account.xml
+++ b/app/src/main/res/layout/setup_select_account.xml
@@ -1,4 +1,7 @@
 <android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
+  xmlns:tools="http://schemas.android.com/tools"
   android:id="@+id/account_list"
   android:layout_width="match_parent"
-  android:layout_height="match_parent" />
+  android:layout_height="match_parent"
+  tools:background="@android:color/background_light"
+  tools:theme="@style/Theme.SetupTheme" />
diff --git a/app/src/main/res/layout/setup_slide.xml b/app/src/main/res/layout/setup_slide.xml
index 54045a751..46f172494 100644
--- a/app/src/main/res/layout/setup_slide.xml
+++ b/app/src/main/res/layout/setup_slide.xml
@@ -2,7 +2,9 @@
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
-  android:layout_height="match_parent">
+  android:layout_height="match_parent"
+  tools:background="@android:color/background_light"
+  tools:theme="@style/Theme.SetupTheme">
 
   <android.support.design.widget.AppBarLayout
     android:layout_width="match_parent"
diff --git a/app/src/main/res/layout/widget_buffer.xml b/app/src/main/res/layout/widget_buffer.xml
new file mode 100644
index 000000000..4e1e095d1
--- /dev/null
+++ b/app/src/main/res/layout/widget_buffer.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+  xmlns:tools="http://schemas.android.com/tools"
+  android:layout_width="match_parent"
+  android:layout_height="wrap_content"
+  android:minHeight="48dp"
+  android:paddingBottom="8dp"
+  android:paddingLeft="16dp"
+  android:paddingRight="16dp"
+  android:paddingTop="8dp"
+  tools:background="@android:color/background_light"
+  tools:theme="@style/Theme.ChatTheme.Quassel_Light">
+
+  <ImageView
+    android:id="@+id/status"
+    android:layout_width="24dp"
+    android:layout_height="24dp"
+    android:layout_gravity="center"
+    android:layout_marginEnd="32dp"
+    android:layout_marginRight="32dp"
+    tools:src="@drawable/ic_status_channel"
+    tools:tint="?attr/colorAccent" />
+
+  <LinearLayout
+    android:layout_width="0dip"
+    android:layout_height="wrap_content"
+    android:layout_gravity="center"
+    android:layout_weight="1"
+    android:orientation="vertical">
+
+    <TextView
+      android:id="@+id/name"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:layout_gravity="center"
+      android:singleLine="true"
+      android:textColor="?attr/colorForeground"
+      android:textSize="13sp"
+      android:textStyle="bold"
+      tools:text="#quasseldroid" />
+
+    <TextView
+      android:id="@+id/description"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:layout_gravity="center"
+      android:singleLine="true"
+      android:textColor="?attr/colorForegroundSecondary"
+      android:textSize="12sp"
+      tools:text="QuasselDroid is an Android client for #quassel ♥ justJanne's much improved version: https://dl.kuschku.de/releases/quasseldroid/ ♥ http://github.com/sandsmark/QuasselDroid ♥ Quasseldroid  on play  https://market.android.com/details?id=com.iskrembilen.quasseldroid ♥ Sign up for beta: https://plus.google.com/communities/104094956084217666662" />
+  </LinearLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/widget_chatmessage_action.xml b/app/src/main/res/layout/widget_chatmessage_action.xml
index 754adfd98..13146566a 100644
--- a/app/src/main/res/layout/widget_chatmessage_action.xml
+++ b/app/src/main/res/layout/widget_chatmessage_action.xml
@@ -20,6 +20,7 @@
   -->
 
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+  xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:clickable="true"
@@ -32,7 +33,9 @@
   android:paddingRight="@dimen/message_horizontal"
   android:paddingStart="@dimen/message_horizontal"
   android:paddingTop="@dimen/message_vertical"
-  android:textAppearance="?android:attr/textAppearanceListItemSmall">
+  android:textAppearance="?android:attr/textAppearanceListItemSmall"
+  tools:background="@android:color/background_light"
+  tools:theme="@style/Theme.ChatTheme.Quassel_Light">
 
   <TextView
     android:id="@+id/time"
diff --git a/app/src/main/res/layout/widget_chatmessage_error.xml b/app/src/main/res/layout/widget_chatmessage_error.xml
index a19b7e87f..ac45d3d6e 100644
--- a/app/src/main/res/layout/widget_chatmessage_error.xml
+++ b/app/src/main/res/layout/widget_chatmessage_error.xml
@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+  xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:background="?attr/colorBackgroundSecondary"
@@ -13,7 +14,8 @@
   android:paddingRight="@dimen/message_horizontal"
   android:paddingStart="@dimen/message_horizontal"
   android:paddingTop="@dimen/message_vertical"
-  android:textAppearance="?android:attr/textAppearanceListItemSmall">
+  android:textAppearance="?android:attr/textAppearanceListItemSmall"
+  tools:theme="@style/Theme.ChatTheme.Quassel_Light">
 
   <TextView
     android:id="@+id/time"
diff --git a/app/src/main/res/layout/widget_chatmessage_plain.xml b/app/src/main/res/layout/widget_chatmessage_plain.xml
index 705534482..1f52573c3 100644
--- a/app/src/main/res/layout/widget_chatmessage_plain.xml
+++ b/app/src/main/res/layout/widget_chatmessage_plain.xml
@@ -13,7 +13,9 @@
   android:paddingRight="@dimen/message_horizontal"
   android:paddingStart="@dimen/message_horizontal"
   android:paddingTop="@dimen/message_vertical"
-  android:textAppearance="?android:attr/textAppearanceListItemSmall">
+  android:textAppearance="?android:attr/textAppearanceListItemSmall"
+  tools:background="@android:color/background_light"
+  tools:theme="@style/Theme.ChatTheme.Quassel_Light">
 
   <TextView
     android:id="@+id/time"
diff --git a/app/src/main/res/layout/widget_chatmessage_server.xml b/app/src/main/res/layout/widget_chatmessage_server.xml
index a9ac1112f..48b076cb0 100644
--- a/app/src/main/res/layout/widget_chatmessage_server.xml
+++ b/app/src/main/res/layout/widget_chatmessage_server.xml
@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+  xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:background="?attr/colorBackgroundSecondary"
@@ -13,7 +14,8 @@
   android:paddingRight="@dimen/message_horizontal"
   android:paddingStart="@dimen/message_horizontal"
   android:paddingTop="@dimen/message_vertical"
-  android:textAppearance="?android:attr/textAppearanceListItemSmall">
+  android:textAppearance="?android:attr/textAppearanceListItemSmall"
+  tools:theme="@style/Theme.ChatTheme.Quassel_Light">
 
   <TextView
     android:id="@+id/time"
diff --git a/app/src/main/res/layout/widget_core_account.xml b/app/src/main/res/layout/widget_core_account.xml
index 0a8e02ba0..041147810 100644
--- a/app/src/main/res/layout/widget_core_account.xml
+++ b/app/src/main/res/layout/widget_core_account.xml
@@ -9,7 +9,8 @@
   android:focusableInTouchMode="false"
   android:orientation="horizontal"
   android:paddingLeft="16dp"
-  android:paddingRight="16dp">
+  android:paddingRight="16dp"
+  tools:theme="@style/Theme.ChatTheme.Quassel_Light">
 
   <LinearLayout
     android:layout_width="48dp"
diff --git a/app/src/main/res/layout/widget_core_account_add.xml b/app/src/main/res/layout/widget_core_account_add.xml
index f6c839109..464c45b4e 100644
--- a/app/src/main/res/layout/widget_core_account_add.xml
+++ b/app/src/main/res/layout/widget_core_account_add.xml
@@ -9,7 +9,8 @@
   android:focusableInTouchMode="false"
   android:orientation="horizontal"
   android:paddingLeft="16dp"
-  android:paddingRight="16dp">
+  android:paddingRight="16dp"
+  tools:theme="@style/Theme.ChatTheme.Quassel_Light">
 
   <ImageView
     android:layout_width="32dp"
diff --git a/app/src/main/res/layout/widget_network.xml b/app/src/main/res/layout/widget_network.xml
new file mode 100644
index 000000000..974a81e40
--- /dev/null
+++ b/app/src/main/res/layout/widget_network.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+  xmlns:app="http://schemas.android.com/apk/res-auto"
+  xmlns:tools="http://schemas.android.com/tools"
+  android:layout_width="match_parent"
+  android:layout_height="wrap_content"
+  android:minHeight="48dp"
+  tools:background="@android:color/background_light"
+  tools:theme="@style/Theme.ChatTheme.Quassel_Light">
+
+  <TextView
+    android:id="@+id/name"
+    android:layout_width="0dip"
+    android:layout_height="wrap_content"
+    android:layout_gravity="center"
+    android:layout_marginBottom="8dp"
+    android:layout_marginLeft="16dp"
+    android:layout_marginRight="16dp"
+    android:layout_marginTop="8dp"
+    android:layout_weight="1"
+    android:singleLine="true"
+    android:textColor="?attr/colorForeground"
+    android:textSize="14sp"
+    android:textStyle="bold"
+    tools:text="Freenode" />
+
+  <android.support.v7.widget.AppCompatImageButton
+    android:id="@+id/status"
+    style="?attr/buttonStyleSmall"
+    android:layout_width="wrap_content"
+    android:layout_height="match_parent"
+    android:background="@null"
+    android:foreground="?attr/selectableItemBackgroundBorderless"
+    android:minWidth="72dp"
+    android:paddingBottom="12dp"
+    android:paddingEnd="16dp"
+    android:paddingStart="16dp"
+    android:paddingTop="12dp"
+    android:scaleType="fitEnd"
+    android:tint="?attr/colorForegroundSecondary"
+    app:srcCompat="@drawable/ic_chevron_down" />
+</LinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/widget_spinner_item_inline.xml b/app/src/main/res/layout/widget_spinner_item_inline.xml
index b21fc8158..bdbc09345 100644
--- a/app/src/main/res/layout/widget_spinner_item_inline.xml
+++ b/app/src/main/res/layout/widget_spinner_item_inline.xml
@@ -1,7 +1,9 @@
 <TextView xmlns:android="http://schemas.android.com/apk/res/android"
+  xmlns:tools="http://schemas.android.com/tools"
   android:id="@android:id/text1"
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:gravity="center_vertical"
   android:minHeight="48dp"
-  android:textAppearance="?android:attr/textAppearanceListItemSmall" />
+  android:textAppearance="?android:attr/textAppearanceListItemSmall"
+  tools:theme="@style/Theme.ChatTheme.Quassel_Light" />
diff --git a/app/src/main/res/layout/widget_spinner_item_toolbar.xml b/app/src/main/res/layout/widget_spinner_item_toolbar.xml
index fa2ad16dc..5c14c6cad 100644
--- a/app/src/main/res/layout/widget_spinner_item_toolbar.xml
+++ b/app/src/main/res/layout/widget_spinner_item_toolbar.xml
@@ -8,4 +8,5 @@
   android:paddingLeft="16dp"
   android:paddingRight="16dp"
   android:textAppearance="?android:attr/textAppearanceListItemSmall"
-  tools:text="All Chats" />
+  tools:text="All Chats"
+  tools:theme="@style/Theme.ChatTheme.Quassel_Light" />
diff --git a/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/IrcChannel.kt b/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/IrcChannel.kt
index 9d4a65d46..34acb8ce5 100644
--- a/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/IrcChannel.kt
+++ b/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/IrcChannel.kt
@@ -6,6 +6,7 @@ import de.kuschku.libquassel.quassel.syncables.interfaces.IIrcChannel
 import de.kuschku.libquassel.quassel.syncables.interfaces.INetwork
 import de.kuschku.libquassel.session.SignalProxy
 import de.kuschku.libquassel.util.helpers.getOr
+import io.reactivex.subjects.BehaviorSubject
 import java.nio.charset.Charset
 
 class IrcChannel(
@@ -165,6 +166,7 @@ class IrcChannel(
     if (_topic == topic)
       return
     _topic = topic
+    live_topic.onNext(topic)
     super.setTopic(topic)
   }
 
@@ -312,6 +314,7 @@ class IrcChannel(
 
   private var _name: String = name
   private var _topic: String = ""
+  val live_topic = BehaviorSubject.createDefault("")
   private var _password: String = ""
   private var _encrypted: Boolean = false
   private var _userModes: MutableMap<IrcUser, String> = mutableMapOf()
@@ -322,4 +325,8 @@ class IrcChannel(
   private var _B_channelModes: MutableMap<Char, String> = mutableMapOf()
   private var _C_channelModes: MutableMap<Char, String> = mutableMapOf()
   private var _D_channelModes: MutableSet<Char> = mutableSetOf()
+
+  companion object {
+    val NULL = IrcChannel("", Network.NULL, SignalProxy.NULL)
+  }
 }
diff --git a/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/IrcUser.kt b/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/IrcUser.kt
index cc0d9d2c8..7069bc149 100644
--- a/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/IrcUser.kt
+++ b/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/IrcUser.kt
@@ -7,6 +7,7 @@ import de.kuschku.libquassel.protocol.valueOr
 import de.kuschku.libquassel.quassel.syncables.interfaces.IIrcUser
 import de.kuschku.libquassel.session.SignalProxy
 import de.kuschku.libquassel.util.irc.HostmaskHelper
+import io.reactivex.subjects.BehaviorSubject
 import org.threeten.bp.Instant
 import java.nio.charset.Charset
 
@@ -127,6 +128,7 @@ class IrcUser(
   override fun setRealName(realName: String) {
     if (_realName != realName) {
       _realName = realName
+      live_realName.onNext(realName)
       super.setRealName(realName)
     }
   }
@@ -141,6 +143,7 @@ class IrcUser(
   override fun setAway(away: Boolean) {
     if (_away != away) {
       _away = away
+      live_away.onNext(away)
       super.setAway(away)
     }
   }
@@ -272,9 +275,11 @@ class IrcUser(
   private var _user: String = HostmaskHelper.user(hostmask)
   private var _host: String = HostmaskHelper.host(hostmask)
   private var _realName: String = ""
+  val live_realName = BehaviorSubject.createDefault("")
   private var _account: String = ""
   private var _awayMessage: String = ""
   private var _away: Boolean = false
+  val live_away = BehaviorSubject.createDefault(false)
   private var _server: String = ""
   private var _idleTime: Instant = Instant.EPOCH
   private var _idleTimeSet: Instant = Instant.EPOCH
@@ -289,4 +294,8 @@ class IrcUser(
   private var _network: Network = network
   private var _codecForEncoding: Charset? = null
   private var _codecForDecoding: Charset? = null
+
+  companion object {
+    val NULL = IrcUser("", Network.NULL, SignalProxy.NULL)
+  }
 }
diff --git a/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/Network.kt b/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/Network.kt
index 146d07605..2e672826d 100644
--- a/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/Network.kt
+++ b/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/Network.kt
@@ -9,6 +9,7 @@ import de.kuschku.libquassel.session.SignalProxy
 import de.kuschku.libquassel.util.helpers.getOr
 import de.kuschku.libquassel.util.helpers.serializeString
 import de.kuschku.libquassel.util.irc.HostmaskHelper
+import io.reactivex.subjects.BehaviorSubject
 import java.nio.ByteBuffer
 import java.nio.charset.Charset
 import java.util.*
@@ -327,13 +328,15 @@ class Network constructor(
       _ircUsers[nick] = ircUser
       val mask = ircUser.hostMask()
       super.addIrcUser(mask)
+      live_ircUsers.onNext(_ircUsers)
       ircUser
     } else {
       user
     }
   }
 
-  fun ircUser(nickName: String?) = _ircUsers[nickName]
+  fun ircUser(nickName: String?) = _ircUsers[nickName?.toLowerCase(Locale.ENGLISH)]
+  fun liveIrcUser(nickName: String?) = live_ircUsers.map { ircUser(nickName) ?: IrcUser.NULL }
   fun ircUsers() = _ircUsers.values.toList()
   fun ircUserCount(): UInt = _ircUsers.size
   fun newIrcChannel(channelName: String, initData: QVariantMap = emptyMap()): IrcChannel {
@@ -347,6 +350,7 @@ class Network constructor(
       }
       proxy.synchronize(ircChannel)
       _ircChannels[channelName.toLowerCase(Locale.ENGLISH)] = ircChannel
+      live_ircChannels.onNext(_ircChannels)
       super.addIrcChannel(channelName)
       return ircChannel
     } else {
@@ -354,9 +358,15 @@ class Network constructor(
     }
   }
 
-  fun ircChannel(channelName: String) = _ircChannels[channelName]
+  fun ircChannel(channelName: String?) = _ircChannels[channelName?.toLowerCase(Locale.ENGLISH)]
+  fun liveIrcChannel(channelName: String?) = live_ircChannels.map {
+    ircChannel(
+      channelName
+    ) ?: IrcChannel.NULL
+  }
+
   fun ircChannels() = _ircChannels.values.toList()
-  fun ircChanenlCount(): UInt = _ircChannels.size
+  fun ircChannelCount(): UInt = _ircChannels.size
   fun codecForServer(): String = _codecForServer.name()
   fun codecForEncoding(): String = _codecForEncoding.name()
   fun codecForDecoding(): String = _codecForDecoding.name()
@@ -838,14 +848,18 @@ class Network constructor(
   fun removeChansAndUsers() {
     _ircUsers.clear()
     _ircChannels.clear()
+    live_ircChannels.onNext(_ircChannels)
+    live_ircUsers.onNext(_ircUsers)
   }
 
   fun removeIrcUser(user: IrcUser) {
     _ircUsers.remove(user.nick())
+    live_ircUsers.onNext(_ircUsers)
   }
 
   fun removeIrcChannel(channel: IrcChannel) {
     _ircChannels.remove(channel.name())
+    live_ircChannels.onNext(_ircChannels)
   }
 
   private var _networkId: NetworkId = networkId
@@ -856,13 +870,16 @@ class Network constructor(
   private var _currentServer: String = ""
   private var _connected: Boolean = false
   private var _connectionState: ConnectionState = ConnectionState.Disconnected
+  val liveConnectionState = BehaviorSubject.createDefault(ConnectionState.Disconnected)
   private var _prefixes: Set<Char>? = null
   private var _prefixModes: Set<Char>? = null
   private var _channelModes: Map<ChannelModeType, Set<Char>>? = null
   // stores all known nicks for the server
   private var _ircUsers: MutableMap<String, IrcUser> = mutableMapOf()
+  private val live_ircUsers = BehaviorSubject.createDefault(emptyMap<String, IrcUser>())
   // stores all known channels
   private var _ircChannels: MutableMap<String, IrcChannel> = mutableMapOf()
+  private val live_ircChannels = BehaviorSubject.createDefault(emptyMap<String, IrcChannel>())
   // stores results from RPL_ISUPPORT
   private var _supports: MutableMap<String, String> = mutableMapOf()
   /**
@@ -905,4 +922,8 @@ class Network constructor(
   private var _codecForDecoding: Charset = Charsets.UTF_8
   /** when this is active handle305 and handle306 don't trigger any output */
   private var _autoAwayActive: Boolean = false
+
+  companion object {
+    val NULL = Network(-1, SignalProxy.NULL)
+  }
 }
diff --git a/lib/src/main/java/de/kuschku/libquassel/session/SignalProxy.kt b/lib/src/main/java/de/kuschku/libquassel/session/SignalProxy.kt
index de9118c29..bb83b858d 100644
--- a/lib/src/main/java/de/kuschku/libquassel/session/SignalProxy.kt
+++ b/lib/src/main/java/de/kuschku/libquassel/session/SignalProxy.kt
@@ -43,4 +43,25 @@ interface SignalProxy {
   fun synchronize(syncableObject: ISyncableObject?, baseInit: Boolean)
   fun synchronize(syncableObject: ISyncableObject?) = synchronize(syncableObject, false)
   fun stopSynchronize(syncableObject: ISyncableObject?)
+
+  companion object {
+    val NULL = object : SignalProxy {
+      override fun dispatch(message: SignalProxyMessage) = Unit
+      override fun dispatch(message: HandshakeMessage) = Unit
+      override fun callSync(type: String, instance: String, slot: String,
+                            params: List<QVariant_>) = Unit
+
+      override fun callRpc(slot: String, params: List<QVariant_>) = Unit
+      override fun shouldSync(type: String, instance: String, slot: String) = false
+      override fun shouldRpc(slot: String) = false
+      override fun network(id: NetworkId) = null
+      override fun identity(id: IdentityId) = null
+      override fun renameObject(syncableObject: ISyncableObject, newName: String,
+                                oldName: String) = Unit
+
+      override fun renameObject(className: String, newName: String, oldName: String) = Unit
+      override fun synchronize(syncableObject: ISyncableObject?, baseInit: Boolean) = Unit
+      override fun stopSynchronize(syncableObject: ISyncableObject?) = Unit
+    }
+  }
 }
diff --git a/lib/src/main/java/de/kuschku/libquassel/util/Flag.kt b/lib/src/main/java/de/kuschku/libquassel/util/Flag.kt
index aff3f65f3..e670ed8b4 100644
--- a/lib/src/main/java/de/kuschku/libquassel/util/Flag.kt
+++ b/lib/src/main/java/de/kuschku/libquassel/util/Flag.kt
@@ -2,6 +2,13 @@ package de.kuschku.libquassel.util
 
 interface Flag<T> where T : Enum<T>, T : Flag<T> {
   val bit: Int
+  fun toByte() = bit.toByte()
+  fun toChar() = bit.toChar()
+  fun toDouble() = bit.toDouble()
+  fun toFloat() = bit.toFloat()
+  fun toInt() = bit
+  fun toLong() = bit.toLong()
+  fun toShort() = bit.toShort()
 }
 
 data class Flags<E>(
diff --git a/lib/src/main/java/de/kuschku/libquassel/util/LongFlag.kt b/lib/src/main/java/de/kuschku/libquassel/util/LongFlag.kt
index 6617ff225..9dd681cb1 100644
--- a/lib/src/main/java/de/kuschku/libquassel/util/LongFlag.kt
+++ b/lib/src/main/java/de/kuschku/libquassel/util/LongFlag.kt
@@ -2,6 +2,13 @@ package de.kuschku.libquassel.util
 
 interface LongFlag<T> where T : Enum<T>, T : LongFlag<T> {
   val bit: Long
+  fun toByte() = bit.toByte()
+  fun toChar() = bit.toChar()
+  fun toDouble() = bit.toDouble()
+  fun toFloat() = bit.toFloat()
+  fun toInt() = bit.toInt()
+  fun toLong() = bit
+  fun toShort() = bit.toShort()
 }
 
 data class LongFlags<E>(
diff --git a/lib/src/main/java/de/kuschku/libquassel/util/ShortFlag.kt b/lib/src/main/java/de/kuschku/libquassel/util/ShortFlag.kt
index ec88fd70e..25b668398 100644
--- a/lib/src/main/java/de/kuschku/libquassel/util/ShortFlag.kt
+++ b/lib/src/main/java/de/kuschku/libquassel/util/ShortFlag.kt
@@ -6,6 +6,13 @@ import kotlin.experimental.xor
 
 interface ShortFlag<T> where T : Enum<T>, T : ShortFlag<T> {
   val bit: Short
+  fun toByte() = bit.toByte()
+  fun toChar() = bit.toChar()
+  fun toDouble() = bit.toDouble()
+  fun toFloat() = bit.toFloat()
+  fun toInt() = bit.toInt()
+  fun toLong() = bit.toLong()
+  fun toShort() = bit
 }
 
 data class ShortFlags<E>(
-- 
GitLab