From c5341e00f50c7595d1f715da32f367347c720cbd Mon Sep 17 00:00:00 2001
From: Janne Koschinski <janne@kuschku.de>
Date: Thu, 23 May 2019 18:40:27 +0200
Subject: [PATCH] Fixes archived chats screen, significantly improves its
 performance

---
 .../ui/chat/add/query/QueryCreateFragment.kt  |   2 +-
 .../ui/chat/archive/ArchiveFragment.kt        |  80 ++-
 .../ui/chat/archive/ArchiveListAdapter.kt     | 601 ++++++++++++++++++
 .../ui/chat/archive/ArchiveListItem.kt        |  39 ++
 .../ui/chat/buffers/BufferListAdapter.kt      | 155 ++---
 app/src/main/res/layout/chat_archive.xml      |  69 +-
 .../res/layout/widget_archive_placeholder.xml |  11 +
 app/src/main/res/layout/widget_header.xml     |  30 +
 app/src/main/res/values/strings.xml           |   2 +
 .../util/helper/ObservableHelper.kt           |   7 +-
 .../viewmodel/data/BufferStatus.kt            |  10 +-
 .../helper/ArchiveViewModelHelper.kt          |   7 +-
 .../viewmodel/helper/ChatViewModelHelper.kt   |   4 +-
 .../helper/QuasselViewModelHelper.kt          |  36 +-
 14 files changed, 852 insertions(+), 201 deletions(-)
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid/ui/chat/archive/ArchiveListAdapter.kt
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid/ui/chat/archive/ArchiveListItem.kt
 create mode 100644 app/src/main/res/layout/widget_archive_placeholder.xml
 create mode 100644 app/src/main/res/layout/widget_header.xml

diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/query/QueryCreateFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/query/QueryCreateFragment.kt
index fc9f7d400..95c12d4bd 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/query/QueryCreateFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/add/query/QueryCreateFragment.kt
@@ -209,7 +209,7 @@ class QueryCreateFragment : ServiceBoundFragment() {
         it.liveIrcUsers()
       }.mapOrElse(emptyList()).safeSwitchMap {
         combineLatest<IrcUserItem>(
-          it.map<IrcUser, Observable<IrcUserItem>?> {
+          it.mapNotNull<IrcUser, Observable<IrcUserItem>> {
             it.updates().map { user ->
               IrcUserItem(
                 user.network().networkId(),
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/archive/ArchiveFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/archive/ArchiveFragment.kt
index 720a49cc1..28199f556 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/archive/ArchiveFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/archive/ArchiveFragment.kt
@@ -37,7 +37,6 @@ import de.kuschku.quasseldroid.persistence.db.QuasselDatabase
 import de.kuschku.quasseldroid.persistence.models.Filtered
 import de.kuschku.quasseldroid.settings.MessageSettings
 import de.kuschku.quasseldroid.ui.chat.ChatActivity
-import de.kuschku.quasseldroid.ui.chat.buffers.BufferListAdapter
 import de.kuschku.quasseldroid.util.helper.toLiveData
 import de.kuschku.quasseldroid.util.service.ServiceBoundFragment
 import de.kuschku.quasseldroid.util.ui.presenter.BufferContextPresenter
@@ -47,11 +46,8 @@ import de.kuschku.quasseldroid.viewmodel.helper.ArchiveViewModelHelper
 import javax.inject.Inject
 
 class ArchiveFragment : ServiceBoundFragment() {
-  @BindView(R.id.list_temporary)
-  lateinit var listTemporary: RecyclerView
-
-  @BindView(R.id.list_permanently)
-  lateinit var listPermanently: RecyclerView
+  @BindView(R.id.list)
+  lateinit var list: RecyclerView
 
   @Inject
   lateinit var modelHelper: ArchiveViewModelHelper
@@ -68,9 +64,7 @@ class ArchiveFragment : ServiceBoundFragment() {
   @Inject
   lateinit var messageSettings: MessageSettings
 
-  private lateinit var listTemporaryAdapter: BufferListAdapter
-
-  private lateinit var listPermanentlyAdapter: BufferListAdapter
+  private lateinit var listAdapter: ArchiveListAdapter
 
   private var actionMode: ActionMode? = null
 
@@ -112,8 +106,7 @@ class ArchiveFragment : ServiceBoundFragment() {
 
     override fun onDestroyActionMode(mode: ActionMode?) {
       actionMode = null
-      listTemporaryAdapter.unselectAll()
-      listPermanentlyAdapter.unselectAll()
+      listAdapter.unselectAll()
     }
   }
 
@@ -125,27 +118,16 @@ class ArchiveFragment : ServiceBoundFragment() {
     val chatlistId = arguments?.getInt("chatlist_id", -1) ?: -1
     modelHelper.archive.bufferViewConfigId.onNext(chatlistId)
 
-    listTemporaryAdapter = BufferListAdapter(
+    listAdapter = ArchiveListAdapter(
       messageSettings,
       modelHelper.archive.selectedBufferId,
       modelHelper.archive.temporarilyExpandedNetworks
     )
-    listTemporaryAdapter.setOnClickListener(::clickListener)
-    listTemporaryAdapter.setOnLongClickListener(::longClickListener)
-    listTemporary.adapter = listTemporaryAdapter
-    listTemporary.layoutManager = LinearLayoutManager(listTemporary.context)
-    listTemporary.itemAnimator = DefaultItemAnimator()
-
-    listPermanentlyAdapter = BufferListAdapter(
-      messageSettings,
-      modelHelper.archive.selectedBufferId,
-      modelHelper.archive.permanentlyExpandedNetworks
-    )
-    listPermanentlyAdapter.setOnClickListener(::clickListener)
-    listPermanentlyAdapter.setOnLongClickListener(::longClickListener)
-    listPermanently.adapter = listPermanentlyAdapter
-    listPermanently.layoutManager = LinearLayoutManager(listPermanently.context)
-    listPermanently.itemAnimator = DefaultItemAnimator()
+    listAdapter.setOnClickListener(::clickListener)
+    listAdapter.setOnLongClickListener(::longClickListener)
+    list.adapter = listAdapter
+    list.layoutManager = LinearLayoutManager(list.context)
+    list.itemAnimator = DefaultItemAnimator()
 
     val filtered = combineLatest(
       database.filtered().listenRx(accountId).toObservable().map {
@@ -154,20 +136,36 @@ class ArchiveFragment : ServiceBoundFragment() {
       accountDatabase.accounts().listenDefaultFiltered(accountId, 0).toObservable()
     )
 
-    fun processArchiveBufferList(bufferListType: BufferHiddenState, showHandle: Boolean) =
-      modelHelper.processArchiveBufferList(bufferListType, showHandle, filtered).map { buffers ->
-        bufferPresenter.render(buffers)
+    combineLatest(
+      modelHelper.processArchiveBufferList(BufferHiddenState.HIDDEN_TEMPORARY, false, filtered),
+      modelHelper.processArchiveBufferList(BufferHiddenState.HIDDEN_PERMANENT, false, filtered)
+    ).map { (temporary, permanently) ->
+      listOf(ArchiveListItem.Header(
+        title = getString(R.string.label_temporarily_archived),
+        content = getString(R.string.label_temporarily_archived_long)
+      )) + temporary.map {
+        ArchiveListItem.Buffer(it.copy(
+          props = bufferPresenter.render(it.props)
+        ))
+      }.ifEmpty {
+        listOf(ArchiveListItem.Placeholder(
+          content = getString(R.string.label_temporarily_archived_empty)
+        ))
+      } + listOf(ArchiveListItem.Header(
+        title = getString(R.string.label_permanently_archived),
+        content = getString(R.string.label_permanently_archived_long)
+      )) + permanently.map {
+        ArchiveListItem.Buffer(it.copy(
+          props = bufferPresenter.render(it.props)
+        ))
+      }.ifEmpty {
+        listOf(ArchiveListItem.Placeholder(
+          content = getString(R.string.label_permanently_archived_empty)
+        ))
       }
-
-    processArchiveBufferList(BufferHiddenState.HIDDEN_TEMPORARY, false)
-      .toLiveData().observe(this, Observer { processedList ->
-        listTemporaryAdapter.submitList(processedList)
-      })
-
-    processArchiveBufferList(BufferHiddenState.HIDDEN_PERMANENT, false)
-      .toLiveData().observe(this, Observer { processedList ->
-        listPermanentlyAdapter.submitList(processedList)
-      })
+    }.toLiveData().observe(this, Observer { processedList ->
+      listAdapter.submitList(processedList)
+    })
 
     modelHelper.selectedBuffer.toLiveData().observe(this, Observer { buffer ->
       actionMode?.let {
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/archive/ArchiveListAdapter.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/archive/ArchiveListAdapter.kt
new file mode 100644
index 000000000..cfd355e21
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/archive/ArchiveListAdapter.kt
@@ -0,0 +1,601 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2019 Janne Mareike Koschinski
+ * Copyright (c) 2019 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * 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.ui.chat.archive
+
+import android.graphics.drawable.Drawable
+import android.view.LayoutInflater
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.RecyclerView
+import butterknife.BindView
+import butterknife.ButterKnife
+import de.kuschku.libquassel.protocol.*
+import de.kuschku.libquassel.quassel.BufferInfo
+import de.kuschku.libquassel.util.flag.hasFlag
+import de.kuschku.quasseldroid.R
+import de.kuschku.quasseldroid.settings.MessageSettings
+import de.kuschku.quasseldroid.util.helper.*
+import de.kuschku.quasseldroid.util.lists.ListAdapter
+import de.kuschku.quasseldroid.util.ui.fastscroll.views.FastScrollRecyclerView
+import de.kuschku.quasseldroid.viewmodel.data.BufferListItem
+import de.kuschku.quasseldroid.viewmodel.data.BufferStatus
+import io.reactivex.subjects.BehaviorSubject
+
+class ArchiveListAdapter(
+  private val messageSettings: MessageSettings,
+  private val selectedBuffer: BehaviorSubject<BufferId>,
+  private val expandedNetworks: BehaviorSubject<Map<NetworkId, Boolean>>
+) : ListAdapter<ArchiveListItem, ArchiveListAdapter.ArchiveViewHolder>(
+  object : DiffUtil.ItemCallback<ArchiveListItem>() {
+    override fun areItemsTheSame(oldItem: ArchiveListItem, newItem: ArchiveListItem) = when {
+      oldItem is ArchiveListItem.Buffer &&
+      newItem is ArchiveListItem.Buffer      ->
+        oldItem.item.props.info.bufferId == newItem.item.props.info.bufferId
+      oldItem is ArchiveListItem.Header &&
+      newItem is ArchiveListItem.Header      ->
+        oldItem == newItem
+      oldItem is ArchiveListItem.Placeholder &&
+      newItem is ArchiveListItem.Placeholder ->
+        oldItem == newItem
+      else                                   ->
+        false
+    }
+
+    override fun areContentsTheSame(oldItem: ArchiveListItem, newItem: ArchiveListItem) =
+      oldItem == newItem
+  }
+), FastScrollRecyclerView.SectionedAdapter {
+  override fun getSectionName(position: Int) = when (val item = getItem(position)) {
+    is ArchiveListItem.Header      ->
+      item.title
+    is ArchiveListItem.Placeholder ->
+      ""
+    is ArchiveListItem.Buffer      ->
+      item.item.props.network.networkName
+  }
+
+  private var clickListener: ((BufferId) -> Unit)? = null
+  fun setOnClickListener(listener: ((BufferId) -> Unit)?) {
+    this.clickListener = listener
+  }
+
+  private var longClickListener: ((BufferId) -> Unit)? = null
+  fun setOnLongClickListener(listener: ((BufferId) -> Unit)?) {
+    this.longClickListener = listener
+  }
+
+  private var dragListener: ((ArchiveViewHolder) -> Unit)? = null
+  fun setOnDragListener(listener: ((ArchiveViewHolder) -> Unit)?) {
+    dragListener = listener
+  }
+
+  private var updateFinishedListener: ((List<ArchiveListItem>) -> Unit)? = null
+  fun setOnUpdateFinishedListener(listener: ((List<ArchiveListItem>) -> Unit)?) {
+    this.updateFinishedListener = listener
+  }
+
+  override fun onUpdateFinished(list: List<ArchiveListItem>) {
+    this.updateFinishedListener?.invoke(list)
+  }
+
+  fun expandListener(networkId: NetworkId, expand: Boolean) {
+    expandedNetworks.onNext(expandedNetworks.value.orEmpty() + Pair(networkId, expand))
+  }
+
+  fun unselectAll() {
+    selectedBuffer.onNext(BufferId.MAX_VALUE)
+  }
+
+  override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ArchiveViewHolder {
+    val viewType = ViewType(viewType.toUInt())
+    return when (viewType.type) {
+      ArchiveListItem.Type.HEADER      -> ArchiveViewHolder.HeaderViewHolder(
+        LayoutInflater.from(parent.context).inflate(
+          R.layout.widget_header, parent, false
+        )
+      )
+      ArchiveListItem.Type.PLACEHOLDER -> ArchiveViewHolder.PlaceholderViewHolder(
+        LayoutInflater.from(parent.context).inflate(
+          R.layout.widget_archive_placeholder, parent, false
+        )
+      )
+      ArchiveListItem.Type.BUFFER      -> when (viewType.bufferType?.enabledValues()?.firstOrNull()) {
+        BufferInfo.Type.ChannelBuffer -> ArchiveViewHolder.BufferViewHolder.ChannelBuffer(
+          LayoutInflater.from(parent.context).inflate(
+            R.layout.widget_buffer, parent, false
+          ),
+          clickListener = clickListener,
+          longClickListener = longClickListener,
+          dragListener = dragListener
+        )
+        BufferInfo.Type.QueryBuffer   -> ArchiveViewHolder.BufferViewHolder.QueryBuffer(
+          LayoutInflater.from(parent.context).inflate(
+            if (viewType.bufferStatus == BufferStatus.AWAY) R.layout.widget_buffer_away
+            else R.layout.widget_buffer, parent, false
+          ),
+          clickListener = clickListener,
+          longClickListener = longClickListener,
+          dragListener = dragListener
+        )
+        BufferInfo.Type.GroupBuffer   -> ArchiveViewHolder.BufferViewHolder.GroupBuffer(
+          LayoutInflater.from(parent.context).inflate(
+            R.layout.widget_buffer, parent, false
+          ),
+          clickListener = clickListener,
+          longClickListener = longClickListener,
+          dragListener = dragListener
+        )
+        BufferInfo.Type.StatusBuffer  -> ArchiveViewHolder.BufferViewHolder.StatusBuffer(
+          LayoutInflater.from(parent.context).inflate(
+            R.layout.widget_network, parent, false
+          ),
+          clickListener = clickListener,
+          longClickListener = longClickListener,
+          expansionListener = ::expandListener
+        )
+        else                          ->
+          throw IllegalArgumentException("No such viewType: $viewType")
+      }
+    }
+  }
+
+  override fun onBindViewHolder(holder: ArchiveViewHolder, position: Int) {
+    when (val item = getItem(position)) {
+      is ArchiveListItem.Header      ->
+        (holder as? ArchiveViewHolder.HeaderViewHolder)?.bind(item)
+      is ArchiveListItem.Placeholder ->
+        (holder as? ArchiveViewHolder.PlaceholderViewHolder)?.bind(item)
+      is ArchiveListItem.Buffer      ->
+        (holder as? ArchiveViewHolder.BufferViewHolder)?.bind(item.item, messageSettings)
+    }
+  }
+
+  data class ViewType(
+    val type: ArchiveListItem.Type,
+    val bufferStatus: BufferStatus?,
+    val bufferType: Buffer_Types?
+  ) {
+    constructor(item: ArchiveListItem) : this(
+      type = item.type,
+      bufferStatus = (item as? ArchiveListItem.Buffer)?.item?.props?.bufferStatus,
+      bufferType = (item as? ArchiveListItem.Buffer)?.item?.props?.info?.type
+    )
+
+    constructor(viewType: UInt) : this(
+      type = ArchiveListItem.Type.of(viewType.shr(24).and(0xFFu).toUByte())
+             ?: throw IllegalArgumentException("Unknown View Type"),
+      bufferStatus = BufferStatus.of(viewType.shr(16).and(0xFFu).toUByte()),
+      bufferType = Buffer_Type.of(viewType.and(0xFFFFu).toUShort())
+    )
+
+    fun compute(): UInt {
+      val typeValue = type.value
+      val bufferStatusValue = bufferStatus?.value ?: 0xFFu
+      val bufferTypeValue = bufferType?.value ?: 0xFFu
+      return typeValue.toUInt().shl(24) +
+             bufferStatusValue.toUInt().shl(16) +
+             bufferTypeValue
+    }
+  }
+
+  override fun getItemViewType(position: Int) = ViewType(getItem(position)).compute().toInt()
+
+  sealed class ArchiveViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+    class HeaderViewHolder(
+      itemView: View
+    ) : ArchiveViewHolder(itemView) {
+      @BindView(R.id.title)
+      lateinit var title: TextView
+
+      @BindView(R.id.content)
+      lateinit var content: TextView
+
+      init {
+        ButterKnife.bind(this, itemView)
+      }
+
+      fun bind(item: ArchiveListItem.Header) {
+        title.text = item.title
+        content.text = item.content
+      }
+    }
+
+    class PlaceholderViewHolder(
+      itemView: View
+    ) : ArchiveViewHolder(itemView) {
+      @BindView(R.id.content)
+      lateinit var content: TextView
+
+      init {
+        ButterKnife.bind(this, itemView)
+      }
+
+      fun bind(item: ArchiveListItem.Placeholder) {
+        content.text = item.content
+      }
+    }
+
+    sealed class BufferViewHolder(itemView: View) : ArchiveViewHolder(itemView) {
+      abstract fun bind(item: BufferListItem, messageSettings: MessageSettings)
+
+      class StatusBuffer(
+        itemView: View,
+        private val clickListener: ((BufferId) -> Unit)? = null,
+        private val longClickListener: ((BufferId) -> Unit)? = null,
+        private val expansionListener: ((NetworkId, Boolean) -> Unit)? = null
+      ) : BufferViewHolder(itemView) {
+        @BindView(R.id.status)
+        lateinit var status: ImageView
+
+        @BindView(R.id.name)
+        lateinit var name: TextView
+
+        var bufferId: BufferId? = null
+        var networkId: NetworkId? = null
+
+        private var none: Int = 0
+        private var activity: Int = 0
+        private var message: Int = 0
+        private var highlight: Int = 0
+
+        private var expanded: Boolean = false
+
+        init {
+          ButterKnife.bind(this, itemView)
+          itemView.setOnClickListener {
+            val buffer = bufferId
+            if (buffer != null)
+              clickListener?.invoke(buffer)
+          }
+
+          itemView.setOnLongClickListener {
+            val buffer = bufferId
+            if (buffer != null) {
+              longClickListener?.invoke(buffer)
+              true
+            } else {
+              false
+            }
+          }
+
+          status.setOnClickListener {
+            val network = networkId
+            if (network != null)
+              expansionListener?.invoke(network, !expanded)
+          }
+
+          itemView.context.theme.styledAttributes(
+            R.attr.colorTextSecondary, R.attr.colorTintActivity, R.attr.colorTintMessage,
+            R.attr.colorTintHighlight
+          ) {
+            none = getColor(0, 0)
+            activity = getColor(1, 0)
+            message = getColor(2, 0)
+            highlight = getColor(3, 0)
+          }
+        }
+
+        override fun bind(item: BufferListItem, messageSettings: MessageSettings) {
+          name.text = item.props.network.networkName
+          bufferId = item.props.info.bufferId
+          networkId = item.props.info.networkId
+
+          name.setTextColor(
+            when {
+              item.props.bufferActivity.hasFlag(Buffer_Activity.Highlight)     -> highlight
+              item.props.bufferActivity.hasFlag(Buffer_Activity.NewMessage)    -> message
+              item.props.bufferActivity.hasFlag(Buffer_Activity.OtherActivity) -> activity
+              else                                                             -> none
+            }
+          )
+
+          this.expanded = item.state.networkExpanded
+
+          itemView.isSelected = item.state.selected
+
+          if (item.state.networkExpanded) {
+            status.setImageResource(R.drawable.ic_chevron_up)
+          } else {
+            status.setImageResource(R.drawable.ic_chevron_down)
+          }
+        }
+      }
+
+      class GroupBuffer(
+        itemView: View,
+        private val clickListener: ((BufferId) -> Unit)? = null,
+        private val longClickListener: ((BufferId) -> Unit)? = null,
+        private val dragListener: ((BufferViewHolder) -> 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
+
+        @BindView(R.id.handle)
+        lateinit var handle: View
+
+        var bufferId: BufferId? = null
+
+        private val online: Drawable?
+        private val offline: Drawable?
+
+        private var none: Int = 0
+        private var activity: Int = 0
+        private var message: Int = 0
+        private var highlight: Int = 0
+
+        init {
+          ButterKnife.bind(this, itemView)
+          itemView.setOnClickListener {
+            val buffer = bufferId
+            if (buffer != null)
+              clickListener?.invoke(buffer)
+          }
+
+          itemView.setOnLongClickListener {
+            val buffer = bufferId
+            if (buffer != null) {
+              longClickListener?.invoke(buffer)
+              true
+            } else {
+              false
+            }
+          }
+
+          handle.setOnTouchListener { _, event ->
+            if (event.action == MotionEvent.ACTION_DOWN) {
+              dragListener?.invoke(this)
+            }
+            false
+          }
+
+          online = itemView.context.getVectorDrawableCompat(R.drawable.ic_status)?.mutate()
+          offline = itemView.context.getVectorDrawableCompat(R.drawable.ic_status_offline)?.mutate()
+
+          itemView.context.theme.styledAttributes(
+            R.attr.colorAccent, R.attr.colorAway,
+            R.attr.colorTextPrimary, R.attr.colorTintActivity, R.attr.colorTintMessage,
+            R.attr.colorTintHighlight
+          ) {
+            online?.tint(getColor(0, 0))
+            offline?.tint(getColor(1, 0))
+
+            none = getColor(2, 0)
+            activity = getColor(3, 0)
+            message = getColor(4, 0)
+            highlight = getColor(5, 0)
+          }
+        }
+
+        override fun bind(item: BufferListItem, messageSettings: MessageSettings) {
+          bufferId = item.props.info.bufferId
+
+          name.text = item.props.info.bufferName
+          description.text = item.props.description
+
+          name.setTextColor(
+            when {
+              item.props.bufferActivity.hasFlag(Buffer_Activity.Highlight)     -> highlight
+              item.props.bufferActivity.hasFlag(Buffer_Activity.NewMessage)    -> message
+              item.props.bufferActivity.hasFlag(Buffer_Activity.OtherActivity) -> activity
+              else                                                             -> none
+            }
+          )
+
+          itemView.isSelected = item.state.selected
+
+          handle.visibleIf(item.state.showHandle)
+
+          description.visibleIf(item.props.description.isNotBlank())
+
+          status.setImageDrawable(
+            when (item.props.bufferStatus) {
+              BufferStatus.ONLINE -> online
+              else                -> offline
+            }
+          )
+        }
+      }
+
+      class ChannelBuffer(
+        itemView: View,
+        private val clickListener: ((BufferId) -> Unit)? = null,
+        private val longClickListener: ((BufferId) -> Unit)? = null,
+        private val dragListener: ((BufferViewHolder) -> 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
+
+        @BindView(R.id.handle)
+        lateinit var handle: View
+
+        var bufferId: BufferId? = null
+
+        private var none: Int = 0
+        private var activity: Int = 0
+        private var message: Int = 0
+        private var highlight: Int = 0
+
+        init {
+          ButterKnife.bind(this, itemView)
+          itemView.setOnClickListener {
+            val buffer = bufferId
+            if (buffer != null)
+              clickListener?.invoke(buffer)
+          }
+
+          itemView.setOnLongClickListener {
+            val buffer = bufferId
+            if (buffer != null) {
+              longClickListener?.invoke(buffer)
+              true
+            } else {
+              false
+            }
+          }
+
+          handle.setOnTouchListener { _, event ->
+            if (event.action == MotionEvent.ACTION_DOWN) {
+              dragListener?.invoke(this)
+            }
+            false
+          }
+
+          itemView.context.theme.styledAttributes(
+            R.attr.colorTextPrimary, R.attr.colorTintActivity, R.attr.colorTintMessage,
+            R.attr.colorTintHighlight
+          ) {
+            none = getColor(0, 0)
+            activity = getColor(1, 0)
+            message = getColor(2, 0)
+            highlight = getColor(3, 0)
+          }
+        }
+
+        override fun bind(item: BufferListItem, messageSettings: MessageSettings) {
+          bufferId = item.props.info.bufferId
+
+          name.text = item.props.info.bufferName
+          description.text = item.props.description
+
+          name.setTextColor(
+            when {
+              item.props.bufferActivity.hasFlag(Buffer_Activity.Highlight)     -> highlight
+              item.props.bufferActivity.hasFlag(Buffer_Activity.NewMessage)    -> message
+              item.props.bufferActivity.hasFlag(Buffer_Activity.OtherActivity) -> activity
+              else                                                             -> none
+            }
+          )
+
+          itemView.isSelected = item.state.selected
+
+          handle.visibleIf(item.state.showHandle)
+
+          description.visibleIf(item.props.description.isNotBlank())
+
+          status.setImageDrawable(item.props.fallbackDrawable)
+        }
+      }
+
+      class QueryBuffer(
+        itemView: View,
+        private val clickListener: ((BufferId) -> Unit)? = null,
+        private val longClickListener: ((BufferId) -> Unit)? = null,
+        private val dragListener: ((BufferViewHolder) -> 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
+
+        @BindView(R.id.handle)
+        lateinit var handle: View
+
+        var bufferId: BufferId? = null
+
+        private var none: Int = 0
+        private var activity: Int = 0
+        private var message: Int = 0
+        private var highlight: Int = 0
+
+        init {
+          ButterKnife.bind(this, itemView)
+          itemView.setOnClickListener {
+            val buffer = bufferId
+            if (buffer != null)
+              clickListener?.invoke(buffer)
+          }
+
+          itemView.setOnLongClickListener {
+            val buffer = bufferId
+            if (buffer != null) {
+              longClickListener?.invoke(buffer)
+              true
+            } else {
+              false
+            }
+          }
+
+          handle.setOnTouchListener { _, event ->
+            if (event.action == MotionEvent.ACTION_DOWN) {
+              dragListener?.invoke(this)
+            }
+            false
+          }
+
+          itemView.context.theme.styledAttributes(
+            R.attr.colorTextPrimary, R.attr.colorTintActivity, R.attr.colorTintMessage,
+            R.attr.colorTintHighlight
+          ) {
+            none = getColor(0, 0)
+            activity = getColor(1, 0)
+            message = getColor(2, 0)
+            highlight = getColor(3, 0)
+          }
+        }
+
+        override fun bind(item: BufferListItem, messageSettings: MessageSettings) {
+          bufferId = item.props.info.bufferId
+
+          name.text = item.props.info.bufferName
+          description.text = item.props.description
+
+          name.setTextColor(
+            when {
+              item.props.bufferActivity.hasFlag(Buffer_Activity.Highlight)     -> highlight
+              item.props.bufferActivity.hasFlag(Buffer_Activity.NewMessage)    -> message
+              item.props.bufferActivity.hasFlag(Buffer_Activity.OtherActivity) -> activity
+              else                                                             -> none
+            }
+          )
+
+          itemView.isSelected = item.state.selected
+
+          handle.visibleIf(item.state.showHandle)
+
+          description.visibleIf(item.props.description.isNotBlank())
+
+          status.loadAvatars(item.props.avatarUrls,
+                             item.props.fallbackDrawable,
+                             crop = !messageSettings.squareAvatars)
+        }
+      }
+    }
+  }
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/archive/ArchiveListItem.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/archive/ArchiveListItem.kt
new file mode 100644
index 000000000..946051b5f
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/archive/ArchiveListItem.kt
@@ -0,0 +1,39 @@
+package de.kuschku.quasseldroid.ui.chat.archive
+
+import de.kuschku.quasseldroid.viewmodel.data.BufferListItem
+
+sealed class ArchiveListItem(val type: Type) {
+  data class Header(
+    val title: String,
+    val content: String
+  ) : ArchiveListItem(Type.HEADER)
+
+  data class Placeholder(
+    val content: String
+  ) : ArchiveListItem(Type.PLACEHOLDER)
+
+  data class Buffer(
+    val item: BufferListItem
+  ) : ArchiveListItem(Type.BUFFER)
+
+  override fun equals(other: Any?): Boolean {
+    if (this === other) return true
+    if (other !is ArchiveListItem) return false
+    return true
+  }
+
+  override fun hashCode(): Int {
+    return javaClass.hashCode()
+  }
+
+  enum class Type(val value: UByte) {
+    HEADER(0u),
+    PLACEHOLDER(1u),
+    BUFFER(2u);
+
+    companion object {
+      private val map = values().associateBy { it.value }
+      fun of(value: UByte) = map[value]
+    }
+  }
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferListAdapter.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferListAdapter.kt
index 128d769dd..5fd727f29 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferListAdapter.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferListAdapter.kt
@@ -30,9 +30,7 @@ import androidx.recyclerview.widget.DiffUtil
 import androidx.recyclerview.widget.RecyclerView
 import butterknife.BindView
 import butterknife.ButterKnife
-import de.kuschku.libquassel.protocol.BufferId
-import de.kuschku.libquassel.protocol.Buffer_Activity
-import de.kuschku.libquassel.protocol.NetworkId
+import de.kuschku.libquassel.protocol.*
 import de.kuschku.libquassel.quassel.BufferInfo
 import de.kuschku.libquassel.util.flag.hasFlag
 import de.kuschku.quasseldroid.R
@@ -41,8 +39,6 @@ import de.kuschku.quasseldroid.util.helper.*
 import de.kuschku.quasseldroid.util.lists.ListAdapter
 import de.kuschku.quasseldroid.util.ui.fastscroll.views.FastScrollRecyclerView
 import de.kuschku.quasseldroid.viewmodel.data.BufferListItem
-import de.kuschku.quasseldroid.viewmodel.data.BufferProps
-import de.kuschku.quasseldroid.viewmodel.data.BufferState
 import de.kuschku.quasseldroid.viewmodel.data.BufferStatus
 import io.reactivex.subjects.BehaviorSubject
 
@@ -94,10 +90,9 @@ class BufferListAdapter(
   }
 
   override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BufferViewHolder {
-    val bufferType = viewType and 0xFFFF
-    val bufferStatus = BufferStatus.of(((viewType ushr 16) and 0xFFFF).toShort())
-    return when (bufferType) {
-      BufferInfo.Type.ChannelBuffer.toInt() -> BufferViewHolder.ChannelBuffer(
+    val viewType = ViewType(viewType.toUInt())
+    return when (viewType.bufferType?.enabledValues()?.firstOrNull()) {
+      BufferInfo.Type.ChannelBuffer -> BufferViewHolder.ChannelBuffer(
         LayoutInflater.from(parent.context).inflate(
           R.layout.widget_buffer, parent, false
         ),
@@ -105,17 +100,16 @@ class BufferListAdapter(
         longClickListener = longClickListener,
         dragListener = dragListener
       )
-      BufferInfo.Type.QueryBuffer.toInt()   -> BufferViewHolder.QueryBuffer(
+      BufferInfo.Type.QueryBuffer   -> BufferViewHolder.QueryBuffer(
         LayoutInflater.from(parent.context).inflate(
-          if (bufferStatus == BufferStatus.AWAY) R.layout.widget_buffer_away
-          else R.layout.widget_buffer
-          , parent, false
+          if (viewType.bufferStatus == BufferStatus.AWAY) R.layout.widget_buffer_away
+          else R.layout.widget_buffer, parent, false
         ),
         clickListener = clickListener,
         longClickListener = longClickListener,
         dragListener = dragListener
       )
-      BufferInfo.Type.GroupBuffer.toInt()   -> BufferViewHolder.GroupBuffer(
+      BufferInfo.Type.GroupBuffer   -> BufferViewHolder.GroupBuffer(
         LayoutInflater.from(parent.context).inflate(
           R.layout.widget_buffer, parent, false
         ),
@@ -123,7 +117,7 @@ class BufferListAdapter(
         longClickListener = longClickListener,
         dragListener = dragListener
       )
-      BufferInfo.Type.StatusBuffer.toInt()  -> BufferViewHolder.StatusBuffer(
+      BufferInfo.Type.StatusBuffer  -> BufferViewHolder.StatusBuffer(
         LayoutInflater.from(parent.context).inflate(
           R.layout.widget_network, parent, false
         ),
@@ -131,21 +125,40 @@ class BufferListAdapter(
         longClickListener = longClickListener,
         expansionListener = ::expandListener
       )
-      else                                  -> throw IllegalArgumentException(
-        "No such viewType: ${viewType.toString(16)}"
-      )
+      else                          ->
+        throw IllegalArgumentException("No such viewType: $viewType")
     }
   }
 
   override fun onBindViewHolder(holder: BufferViewHolder, position: Int) =
-    holder.bind(getItem(position).props, getItem(position).state, messageSettings)
-
-  override fun getItemViewType(position: Int) = getItem(position).let {
-    (it.props.bufferStatus.ordinal shl 16) + (it.props.info.type.toInt() and 0xFFFF)
+    holder.bind(getItem(position), messageSettings)
+
+  data class ViewType(
+    val bufferStatus: BufferStatus?,
+    val bufferType: Buffer_Types?
+  ) {
+    constructor(item: BufferListItem) : this(
+      bufferStatus = item.props.bufferStatus,
+      bufferType = item.props.info.type
+    )
+
+    constructor(viewType: UInt) : this(
+      bufferStatus = BufferStatus.of(viewType.shr(16).and(0xFFu).toUByte()),
+      bufferType = Buffer_Type.of(viewType.and(0xFFFFu).toUShort())
+    )
+
+    fun compute(): UInt {
+      val bufferStatusValue = bufferStatus?.value ?: 0xFFu
+      val bufferTypeValue = bufferType?.value ?: 0xFFu
+      return bufferStatusValue.toUInt().shl(16) +
+             bufferTypeValue
+    }
   }
 
+  override fun getItemViewType(position: Int) = ViewType(getItem(position)).compute().toInt()
+
   abstract class BufferViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
-    abstract fun bind(props: BufferProps, state: BufferState, messageSettings: MessageSettings)
+    abstract fun bind(item: BufferListItem, messageSettings: MessageSettings)
 
     class StatusBuffer(
       itemView: View,
@@ -204,25 +217,25 @@ class BufferListAdapter(
         }
       }
 
-      override fun bind(props: BufferProps, state: BufferState, messageSettings: MessageSettings) {
-        name.text = props.network.networkName
-        bufferId = props.info.bufferId
-        networkId = props.info.networkId
+      override fun bind(item: BufferListItem, messageSettings: MessageSettings) {
+        name.text = item.props.network.networkName
+        bufferId = item.props.info.bufferId
+        networkId = item.props.info.networkId
 
         name.setTextColor(
           when {
-            props.bufferActivity.hasFlag(Buffer_Activity.Highlight)     -> highlight
-            props.bufferActivity.hasFlag(Buffer_Activity.NewMessage)    -> message
-            props.bufferActivity.hasFlag(Buffer_Activity.OtherActivity) -> activity
-            else                                                        -> none
+            item.props.bufferActivity.hasFlag(Buffer_Activity.Highlight)     -> highlight
+            item.props.bufferActivity.hasFlag(Buffer_Activity.NewMessage)    -> message
+            item.props.bufferActivity.hasFlag(Buffer_Activity.OtherActivity) -> activity
+            else                                                             -> none
           }
         )
 
-        this.expanded = state.networkExpanded
+        this.expanded = item.state.networkExpanded
 
-        itemView.isSelected = state.selected
+        itemView.isSelected = item.state.selected
 
-        if (state.networkExpanded) {
+        if (item.state.networkExpanded) {
           status.setImageResource(R.drawable.ic_chevron_up)
         } else {
           status.setImageResource(R.drawable.ic_chevron_down)
@@ -301,29 +314,29 @@ class BufferListAdapter(
         }
       }
 
-      override fun bind(props: BufferProps, state: BufferState, messageSettings: MessageSettings) {
-        bufferId = props.info.bufferId
+      override fun bind(item: BufferListItem, messageSettings: MessageSettings) {
+        bufferId = item.props.info.bufferId
 
-        name.text = props.info.bufferName
-        description.text = props.description
+        name.text = item.props.info.bufferName
+        description.text = item.props.description
 
         name.setTextColor(
           when {
-            props.bufferActivity.hasFlag(Buffer_Activity.Highlight)     -> highlight
-            props.bufferActivity.hasFlag(Buffer_Activity.NewMessage)    -> message
-            props.bufferActivity.hasFlag(Buffer_Activity.OtherActivity) -> activity
-            else                                                        -> none
+            item.props.bufferActivity.hasFlag(Buffer_Activity.Highlight)     -> highlight
+            item.props.bufferActivity.hasFlag(Buffer_Activity.NewMessage)    -> message
+            item.props.bufferActivity.hasFlag(Buffer_Activity.OtherActivity) -> activity
+            else                                                             -> none
           }
         )
 
-        itemView.isSelected = state.selected
+        itemView.isSelected = item.state.selected
 
-        handle.visibleIf(state.showHandle)
+        handle.visibleIf(item.state.showHandle)
 
-        description.visibleIf(props.description.isNotBlank())
+        description.visibleIf(item.props.description.isNotBlank())
 
         status.setImageDrawable(
-          when (props.bufferStatus) {
+          when (item.props.bufferStatus) {
             BufferStatus.ONLINE -> online
             else                -> offline
           }
@@ -392,28 +405,28 @@ class BufferListAdapter(
         }
       }
 
-      override fun bind(props: BufferProps, state: BufferState, messageSettings: MessageSettings) {
-        bufferId = props.info.bufferId
+      override fun bind(item: BufferListItem, messageSettings: MessageSettings) {
+        bufferId = item.props.info.bufferId
 
-        name.text = props.info.bufferName
-        description.text = props.description
+        name.text = item.props.info.bufferName
+        description.text = item.props.description
 
         name.setTextColor(
           when {
-            props.bufferActivity.hasFlag(Buffer_Activity.Highlight)     -> highlight
-            props.bufferActivity.hasFlag(Buffer_Activity.NewMessage)    -> message
-            props.bufferActivity.hasFlag(Buffer_Activity.OtherActivity) -> activity
-            else                                                        -> none
+            item.props.bufferActivity.hasFlag(Buffer_Activity.Highlight)     -> highlight
+            item.props.bufferActivity.hasFlag(Buffer_Activity.NewMessage)    -> message
+            item.props.bufferActivity.hasFlag(Buffer_Activity.OtherActivity) -> activity
+            else                                                             -> none
           }
         )
 
-        itemView.isSelected = state.selected
+        itemView.isSelected = item.state.selected
 
-        handle.visibleIf(state.showHandle)
+        handle.visibleIf(item.state.showHandle)
 
-        description.visibleIf(props.description.isNotBlank())
+        description.visibleIf(item.props.description.isNotBlank())
 
-        status.setImageDrawable(props.fallbackDrawable)
+        status.setImageDrawable(item.props.fallbackDrawable)
       }
     }
 
@@ -478,29 +491,29 @@ class BufferListAdapter(
         }
       }
 
-      override fun bind(props: BufferProps, state: BufferState, messageSettings: MessageSettings) {
-        bufferId = props.info.bufferId
+      override fun bind(item: BufferListItem, messageSettings: MessageSettings) {
+        bufferId = item.props.info.bufferId
 
-        name.text = props.info.bufferName
-        description.text = props.description
+        name.text = item.props.info.bufferName
+        description.text = item.props.description
 
         name.setTextColor(
           when {
-            props.bufferActivity.hasFlag(Buffer_Activity.Highlight)     -> highlight
-            props.bufferActivity.hasFlag(Buffer_Activity.NewMessage)    -> message
-            props.bufferActivity.hasFlag(Buffer_Activity.OtherActivity) -> activity
-            else                                                        -> none
+            item.props.bufferActivity.hasFlag(Buffer_Activity.Highlight)     -> highlight
+            item.props.bufferActivity.hasFlag(Buffer_Activity.NewMessage)    -> message
+            item.props.bufferActivity.hasFlag(Buffer_Activity.OtherActivity) -> activity
+            else                                                             -> none
           }
         )
 
-        itemView.isSelected = state.selected
+        itemView.isSelected = item.state.selected
 
-        handle.visibleIf(state.showHandle)
+        handle.visibleIf(item.state.showHandle)
 
-        description.visibleIf(props.description.isNotBlank())
+        description.visibleIf(item.props.description.isNotBlank())
 
-        status.loadAvatars(props.avatarUrls,
-                           props.fallbackDrawable,
+        status.loadAvatars(item.props.avatarUrls,
+                           item.props.fallbackDrawable,
                            crop = !messageSettings.squareAvatars)
       }
     }
diff --git a/app/src/main/res/layout/chat_archive.xml b/app/src/main/res/layout/chat_archive.xml
index f5d3bdfd1..880e94188 100644
--- a/app/src/main/res/layout/chat_archive.xml
+++ b/app/src/main/res/layout/chat_archive.xml
@@ -18,71 +18,10 @@
   -->
 
 
-<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
-  xmlns:app="http://schemas.android.com/apk/res-auto"
+<de.kuschku.quasseldroid.util.ui.fastscroll.views.FastScrollRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
-  android:orientation="vertical"
-  android:scrollbars="vertical">
-
-  <LinearLayout
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:orientation="vertical">
-
-    <LinearLayout style="@style/Widget.CoreSettings.PrimaryItemGroupHeader">
-
-      <androidx.appcompat.widget.AppCompatImageView
-        style="@style/Widget.CoreSettings.PrimaryItemIcon"
-        app:srcCompat="@drawable/ic_clock" />
-
-      <TextView
-        style="@style/Widget.CoreSettings.PrimaryItemSwitch"
-        android:text="@string/label_temporarily_archived" />
-    </LinearLayout>
-
-    <TextView
-      style="@style/Widget.CoreSettings.TextView"
-      android:layout_marginStart="72dp"
-      android:layout_marginLeft="72dp"
-      android:layout_marginEnd="16dp"
-      android:layout_marginRight="16dp"
-      android:text="@string/label_temporarily_archived_long" />
-
-    <androidx.recyclerview.widget.RecyclerView
-      android:id="@+id/list_temporary"
-      android:layout_width="match_parent"
-      android:layout_height="wrap_content"
-      android:layout_marginStart="56dp"
-      android:layout_marginLeft="56dp"
-      tools:listitem="@layout/widget_buffer" />
-
-    <LinearLayout style="@style/Widget.CoreSettings.PrimaryItemGroupHeader">
-
-      <androidx.appcompat.widget.AppCompatImageView
-        style="@style/Widget.CoreSettings.PrimaryItemIcon"
-        app:srcCompat="@drawable/ic_eye_off" />
-
-      <TextView
-        style="@style/Widget.CoreSettings.PrimaryItemSwitch"
-        android:text="@string/label_permanently_archived" />
-    </LinearLayout>
-
-    <TextView
-      style="@style/Widget.CoreSettings.TextView"
-      android:layout_marginStart="72dp"
-      android:layout_marginLeft="72dp"
-      android:layout_marginEnd="16dp"
-      android:layout_marginRight="16dp"
-      android:text="@string/label_permanently_archived_long" />
-
-    <androidx.recyclerview.widget.RecyclerView
-      android:id="@+id/list_permanently"
-      android:layout_width="match_parent"
-      android:layout_height="wrap_content"
-      android:layout_marginStart="56dp"
-      android:layout_marginLeft="56dp"
-      tools:listitem="@layout/widget_buffer" />
-  </LinearLayout>
-</androidx.core.widget.NestedScrollView>
+  android:id="@+id/list"
+  style="@style/Widget.FastScroller"
+  tools:listitem="@layout/widget_buffer" />
diff --git a/app/src/main/res/layout/widget_archive_placeholder.xml b/app/src/main/res/layout/widget_archive_placeholder.xml
new file mode 100644
index 000000000..aa4a6e279
--- /dev/null
+++ b/app/src/main/res/layout/widget_archive_placeholder.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+  xmlns:tools="http://schemas.android.com/tools"
+  android:id="@+id/content"
+  style="@style/Widget.CoreSettings.TextView"
+  android:paddingStart="72dp"
+  android:paddingLeft="72dp"
+  android:paddingEnd="16dp"
+  android:paddingRight="16dp"
+  android:textStyle="italic"
+  tools:text="@string/label_temporarily_archived_empty" />
diff --git a/app/src/main/res/layout/widget_header.xml b/app/src/main/res/layout/widget_header.xml
new file mode 100644
index 000000000..40a965aa7
--- /dev/null
+++ b/app/src/main/res/layout/widget_header.xml
@@ -0,0 +1,30 @@
+<?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:orientation="vertical">
+
+  <LinearLayout style="@style/Widget.CoreSettings.PrimaryItemGroupHeader">
+
+    <androidx.appcompat.widget.AppCompatImageView
+      style="@style/Widget.CoreSettings.PrimaryItemIcon"
+      app:srcCompat="@drawable/ic_clock" />
+
+    <TextView
+      android:id="@+id/title"
+      style="@style/Widget.CoreSettings.PrimaryItemSwitch"
+      tools:text="@string/label_temporarily_archived" />
+  </LinearLayout>
+
+  <TextView
+    android:id="@+id/content"
+    style="@style/Widget.CoreSettings.TextView"
+    android:layout_marginStart="72dp"
+    android:layout_marginLeft="72dp"
+    android:layout_marginEnd="16dp"
+    android:layout_marginRight="16dp"
+    tools:text="@string/label_temporarily_archived_long" />
+
+</LinearLayout>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index e64a3fffb..a06e2a8dd 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -104,6 +104,7 @@
   <string name="label_password_old">Old Password</string>
   <string name="label_password_repeat">Repeat Password</string>
   <string name="label_permanently_archived">Permanently Archived</string>
+  <string name="label_permanently_archived_empty">You have no permanently archived chats</string>
   <string name="label_permanently_archived_long">Permanently archived chats stay hidden until you manually choose to un-archive them.</string>
   <string name="label_placeholder_message">Write a message…</string>
   <string name="label_placeholder_topic">Describe the channel topic…</string>
@@ -134,6 +135,7 @@
   <string name="label_sort">Sort</string>
   <string name="label_source">Source</string>
   <string name="label_temporarily_archived">Temporarily Archived</string>
+  <string name="label_temporarily_archived_empty">You have no temporarily archived chats</string>
   <string name="label_temporarily_archived_long">Chats which are temporarily archived will be shown again once they get a new message.</string>
   <string name="label_topic">Channel Topic</string>
   <string name="label_translators">Translators</string>
diff --git a/lib/src/main/java/de/kuschku/libquassel/util/helper/ObservableHelper.kt b/lib/src/main/java/de/kuschku/libquassel/util/helper/ObservableHelper.kt
index 682618b75..baec16d9a 100644
--- a/lib/src/main/java/de/kuschku/libquassel/util/helper/ObservableHelper.kt
+++ b/lib/src/main/java/de/kuschku/libquassel/util/helper/ObservableHelper.kt
@@ -125,8 +125,11 @@ inline fun <reified A, B, C, D, E, F> combineLatest(
     Tuple6(it[0], it[1], it[2], it[3], it[4], it[5]) as Tuple6<A, B, C, D, E, F>
   }
 
-inline fun <reified T> combineLatest(sources: Iterable<ObservableSource<out T>?>) =
-  Observable.combineLatest(sources) { t -> t.toList() as List<T> }
+inline fun <reified T> combineLatest(
+  sources: Collection<ObservableSource<out T>>
+): Observable<List<T>> =
+  if (sources.isEmpty()) Observable.just(emptyList())
+  else Observable.combineLatest(sources) { t -> t.toList() as List<T> }
 
 inline operator fun <T, U> Observable<T>.invoke(f: (T) -> U?) =
   blockingLatest().firstOrNull()?.let(f)
diff --git a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/data/BufferStatus.kt b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/data/BufferStatus.kt
index 93c47f586..3e4e824d0 100644
--- a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/data/BufferStatus.kt
+++ b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/data/BufferStatus.kt
@@ -19,13 +19,13 @@
 
 package de.kuschku.quasseldroid.viewmodel.data
 
-enum class BufferStatus(val value: Short) {
-  ONLINE(0),
-  AWAY(1),
-  OFFLINE(2);
+enum class BufferStatus(val value: UByte) {
+  ONLINE(0u),
+  AWAY(1u),
+  OFFLINE(2u);
 
   companion object {
     private val map = values().associateBy { it.value }
-    fun of(value: Short) = map[value]
+    fun of(value: UByte) = map[value]
   }
 }
diff --git a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/helper/ArchiveViewModelHelper.kt b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/helper/ArchiveViewModelHelper.kt
index 08ec7fc98..c2d34d484 100644
--- a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/helper/ArchiveViewModelHelper.kt
+++ b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/helper/ArchiveViewModelHelper.kt
@@ -45,7 +45,12 @@ open class ArchiveViewModelHelper @Inject constructor(
     showHandle: Boolean,
     filtered: Observable<Pair<Map<BufferId, Int>, Int>>
   ) = filterBufferList(
-    processRawBufferList(bufferViewConfig, filtered, bufferListType = bufferListType),
+    processRawBufferList(
+      bufferViewConfig,
+      filtered,
+      bufferListType = bufferListType,
+      showAllNetworks = false
+    ),
     when (bufferListType) {
       BufferHiddenState.VISIBLE          -> archive.visibleExpandedNetworks
       BufferHiddenState.HIDDEN_TEMPORARY -> archive.temporarilyExpandedNetworks
diff --git a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/helper/ChatViewModelHelper.kt b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/helper/ChatViewModelHelper.kt
index 7dfb92e14..223670166 100644
--- a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/helper/ChatViewModelHelper.kt
+++ b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/helper/ChatViewModelHelper.kt
@@ -137,7 +137,7 @@ open class ChatViewModelHelper @Inject constructor(
           network?.liveIrcChannel(bufferInfo.bufferName)?.switchMapNullable(IrcChannel.NULL) { ircChannel ->
             ircChannel?.liveIrcUsers()?.safeSwitchMap { users ->
               combineLatest<IrcUserItem>(
-                users.map<IrcUser, Observable<IrcUserItem>?> {
+                users.mapNotNull<IrcUser, Observable<IrcUserItem>> {
                   it.updates().map { user ->
                     val userModes = ircChannel.userModes(user)
                     val prefixModes = network.prefixModes()
@@ -158,7 +158,7 @@ open class ChatViewModelHelper @Inject constructor(
                       network.support("CASEMAPPING")
                     )
                   }
-                }
+                }.toList()
               )
             } ?: Observable.just(emptyList())
           } ?: Observable.just(emptyList())
diff --git a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/helper/QuasselViewModelHelper.kt b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/helper/QuasselViewModelHelper.kt
index f57a19ac5..89ad8851c 100644
--- a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/helper/QuasselViewModelHelper.kt
+++ b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/helper/QuasselViewModelHelper.kt
@@ -137,8 +137,7 @@ open class QuasselViewModelHelper @Inject constructor(
     filtered: Map<BufferId, Int>,
     defaultFiltered: Int,
     bufferSearch: String = ""
-  ) =
-    ids.asSequence().mapNotNull { id ->
+  ): Sequence<Observable<BufferProps>> = ids.asSequence().mapNotNull { id ->
       bufferSyncer.bufferInfo(id)
     }.filter {
       bufferSearch.isBlank() ||
@@ -156,7 +155,7 @@ open class QuasselViewModelHelper @Inject constructor(
       } else {
         it to network
       }
-    }.map<Pair<BufferInfo, Network>, Observable<BufferProps>?> { (info, network) ->
+    }.mapNotNull<Pair<BufferInfo, Network>, Observable<BufferProps>> { (info, network) ->
       bufferSyncer.liveActivity(info.bufferId).safeSwitchMap { activity ->
         bufferSyncer.liveHighlightCount(info.bufferId).map { highlights ->
           Pair(activity, highlights)
@@ -284,7 +283,8 @@ open class QuasselViewModelHelper @Inject constructor(
     bufferViewConfig: Observable<Optional<BufferViewConfig>>,
     filteredTypes: Observable<Pair<Map<BufferId, Int>, Int>>,
     bufferSearch: Observable<String> = Observable.just(""),
-    bufferListType: BufferHiddenState = BufferHiddenState.VISIBLE
+    bufferListType: BufferHiddenState = BufferHiddenState.VISIBLE,
+    showAllNetworks: Boolean = true
   ): Observable<Pair<BufferViewConfig?, List<BufferProps>>> =
     combineLatest(connectedSession, bufferViewConfig, filteredTypes, bufferSearch)
       .safeSwitchMap { (sessionOptional, configOptional, rawFiltered, bufferSearch) ->
@@ -316,18 +316,28 @@ open class QuasselViewModelHelper @Inject constructor(
                     )
 
                   fun missingStatusBuffers(
-                    list: Collection<BufferId>): Sequence<Observable<BufferProps>?> {
-                    val totalNetworks = networks.keys
-                    val wantedNetworks = if (!currentConfig.networkId().isValidId()) totalNetworks
-                    else listOf(currentConfig.networkId())
-
-                    val availableNetworks = list.asSequence().mapNotNull { id ->
+                    list: Collection<BufferId>
+                  ): Sequence<Observable<BufferProps>> {
+                    val buffers = list.asSequence().mapNotNull { id ->
                       bufferSyncer.bufferInfo(id)
-                    }.filter {
+                    }
+
+                    val totalNetworks =
+                      if (showAllNetworks) networks.keys
+                      else buffers.filter {
+                        !it.type.hasFlag(Buffer_Type.StatusBuffer)
+                      }.map {
+                        it.networkId
+                      }.toList()
+
+                    val availableNetworks = buffers.filter {
                       it.type.hasFlag(Buffer_Type.StatusBuffer)
                     }.map {
                       it.networkId
-                    }
+                    }.toList()
+
+                    val wantedNetworks = if (!currentConfig.networkId().isValidId()) totalNetworks
+                    else listOf(currentConfig.networkId())
 
                     val missingNetworks = wantedNetworks - availableNetworks
 
@@ -339,7 +349,7 @@ open class QuasselViewModelHelper @Inject constructor(
                       networks[it]
                     }.filter {
                       !config.hideInactiveNetworks() || it.isConnected()
-                    }.map<Network, Observable<BufferProps>?> { network ->
+                    }.mapNotNull<Network, Observable<BufferProps>> { network ->
                       network.liveNetworkInfo().safeSwitchMap { networkInfo ->
                         network.liveConnectionState().map { connectionState ->
                           BufferProps(
-- 
GitLab