From 4cf5fa57f5b9bca1565ad3b2129481679d03a5b8 Mon Sep 17 00:00:00 2001
From: Janne Koschinski <janne@kuschku.de>
Date: Sat, 17 Feb 2018 15:03:47 +0100
Subject: [PATCH] Improved nick list, added drawer toggle

---
 .../ui/chat/BufferListAdapter.kt              |  28 +---
 .../quasseldroid_ng/ui/chat/ChatActivity.kt   |  13 +-
 .../ui/chat/NickListAdapter.kt                | 124 ++++++++++++++++++
 .../ui/chat/NickListFragment.kt               | 122 +++++++++++++++++
 .../quasseldroid_ng/util/helper/ViewHelper.kt |   9 ++
 app/src/main/res/drawable/bg_badge.xml        |  10 ++
 app/src/main/res/layout/activity_main.xml     |   8 +-
 .../main/res/layout/fragment_nick_list.xml    |   1 -
 app/src/main/res/layout/widget_buffer.xml     |   2 +-
 app/src/main/res/layout/widget_network.xml    |   3 +-
 app/src/main/res/layout/widget_nick.xml       |  72 ++++++++++
 app/src/main/res/layout/widget_nick_away.xml  |  74 +++++++++++
 .../primitive/serializer/StringSerializer.kt  |   4 +
 .../libquassel/quassel/syncables/Network.kt   |  42 +++---
 14 files changed, 467 insertions(+), 45 deletions(-)
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/NickListAdapter.kt
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/NickListFragment.kt
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/ViewHelper.kt
 create mode 100644 app/src/main/res/drawable/bg_badge.xml
 create mode 100644 app/src/main/res/layout/widget_nick.xml
 create mode 100644 app/src/main/res/layout/widget_nick_away.xml

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 17bc32a3d..21b8c0bca 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
@@ -17,6 +17,7 @@ import butterknife.BindView
 import butterknife.ButterKnife
 import de.kuschku.libquassel.protocol.BufferId
 import de.kuschku.libquassel.protocol.Buffer_Activity
+import de.kuschku.libquassel.protocol.Buffer_Type
 import de.kuschku.libquassel.protocol.NetworkId
 import de.kuschku.libquassel.quassel.BufferInfo
 import de.kuschku.libquassel.quassel.syncables.interfaces.INetwork
@@ -24,6 +25,7 @@ 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.visibleIf
 import de.kuschku.quasseldroid_ng.util.helper.zip
 
 class BufferListAdapter(
@@ -55,6 +57,8 @@ class BufferListAdapter(
 
         val old: List<BufferListItem> = data
         val new: List<BufferListItem> = list.sortedBy { props ->
+          !props.info.type.hasFlag(Buffer_Type.StatusBuffer)
+        }.sortedBy { props ->
           props.network.networkName
         }.map { props ->
           BufferListItem(
@@ -150,12 +154,6 @@ class BufferListAdapter(
   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,
@@ -286,11 +284,7 @@ class BufferListAdapter(
           }
         )
 
-        description.visibility = if (props.description == "") {
-          View.GONE
-        } else {
-          View.VISIBLE
-        }
+        description.visibleIf(props.description.isNotBlank())
 
         status.setImageDrawable(
           when (props.bufferStatus) {
@@ -365,11 +359,7 @@ class BufferListAdapter(
           }
         )
 
-        description.visibility = if (props.description == "") {
-          View.GONE
-        } else {
-          View.VISIBLE
-        }
+        description.visibleIf(props.description.isNotBlank())
 
         status.setImageDrawable(
           when (props.bufferStatus) {
@@ -447,11 +437,7 @@ class BufferListAdapter(
           }
         )
 
-        description.visibility = if (props.description == "") {
-          View.GONE
-        } else {
-          View.VISIBLE
-        }
+        description.visibleIf(props.description.isNotBlank())
 
         status.setImageDrawable(
           when (props.bufferStatus) {
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/ChatActivity.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/ChatActivity.kt
index 679b94aa2..1b4b2d5c4 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/ChatActivity.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/ChatActivity.kt
@@ -34,6 +34,7 @@ import de.kuschku.quasseldroid_ng.util.ui.MaterialContentLoadingProgressBar
 class ChatActivity : ServiceBoundActivity() {
   private var contentMessages: MessageListFragment? = null
   private var chatListFragment: BufferViewConfigFragment? = null
+  private var nickListFragment: NickListFragment? = null
 
   @BindView(R.id.drawerLayout)
   lateinit var drawerLayout: DrawerLayout
@@ -78,10 +79,14 @@ class ChatActivity : ServiceBoundActivity() {
     chatListFragment = supportFragmentManager.findFragmentById(
       R.id.chatListFragment
     ) as? BufferViewConfigFragment
+    nickListFragment = supportFragmentManager.findFragmentById(
+      R.id.nickListFragment
+    ) as? NickListFragment
 
     setSupportActionBar(toolbar)
 
     chatListFragment?.currentBuffer?.value = currentBuffer
+    nickListFragment?.currentBuffer?.value = currentBuffer
     contentMessages?.currentBuffer?.value = currentBuffer
 
     chatListFragment?.clickListeners?.add {
@@ -97,6 +102,7 @@ class ChatActivity : ServiceBoundActivity() {
     }
     )
 
+    supportActionBar?.setDisplayHomeAsUpEnabled(true)
     drawerToggle = ActionBarDrawerToggle(
       this,
       drawerLayout,
@@ -185,7 +191,10 @@ class ChatActivity : ServiceBoundActivity() {
   }
 
   override fun onOptionsItemSelected(item: MenuItem?) = when (item?.itemId) {
-    R.id.disconnect -> {
+    android.R.id.home -> {
+      drawerToggle.onOptionsItemSelected(item)
+    }
+    R.id.disconnect   -> {
       handler.post {
         getSharedPreferences(Keys.Status.NAME, Context.MODE_PRIVATE).editApply {
           putBoolean(Keys.Status.reconnect, false)
@@ -196,7 +205,7 @@ class ChatActivity : ServiceBoundActivity() {
       }
       true
     }
-    else            -> super.onOptionsItemSelected(item)
+    else              -> super.onOptionsItemSelected(item)
   }
 
   override fun onDestroy() {
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/NickListAdapter.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/NickListAdapter.kt
new file mode 100644
index 000000000..dea956b64
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/NickListAdapter.kt
@@ -0,0 +1,124 @@
+package de.kuschku.quasseldroid_ng.ui.chat
+
+import android.arch.lifecycle.LifecycleOwner
+import android.arch.lifecycle.LiveData
+import android.arch.lifecycle.Observer
+import android.support.v7.util.DiffUtil
+import android.support.v7.widget.RecyclerView
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import butterknife.BindView
+import butterknife.ButterKnife
+import de.kuschku.quasseldroid_ng.R
+import de.kuschku.quasseldroid_ng.util.helper.visibleIf
+
+class NickListAdapter(
+  lifecycleOwner: LifecycleOwner,
+  liveData: LiveData<List<IrcUserItem>?>,
+  runInBackground: (() -> Unit) -> Any,
+  runOnUiThread: (Runnable) -> Any,
+  private val clickListener: ((String) -> Unit)? = null
+) : RecyclerView.Adapter<NickListAdapter.NickViewHolder>() {
+  var data = mutableListOf<IrcUserItem>()
+
+  init {
+    liveData.observe(
+      lifecycleOwner, Observer { it: List<IrcUserItem>? ->
+      runInBackground {
+        val list = it ?: emptyList()
+        val old: List<IrcUserItem> = data
+        val new: List<IrcUserItem> = list.sortedBy { it.lowestMode }
+        val result = DiffUtil.calculateDiff(
+          object : DiffUtil.Callback() {
+            override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int)
+              = old[oldItemPosition].nick == new[newItemPosition].nick
+
+            override fun getOldListSize() = old.size
+            override fun getNewListSize() = new.size
+            override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int)
+              = old[oldItemPosition] == new[newItemPosition]
+          }, true
+        )
+        runOnUiThread(
+          Runnable {
+            data.clear()
+            data.addAll(new)
+            result.dispatchUpdatesTo(this@NickListAdapter)
+          }
+        )
+      }
+    }
+    )
+  }
+
+  override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = NickViewHolder(
+    LayoutInflater.from(parent.context).inflate(
+      when (viewType) {
+        VIEWTYPE_AWAY -> R.layout.widget_nick_away
+        else          -> R.layout.widget_nick
+      }, parent, false
+    ),
+    clickListener = clickListener
+  )
+
+  override fun onBindViewHolder(holder: NickViewHolder, position: Int)
+    = holder.bind(data[position])
+
+  override fun getItemCount() = data.size
+
+  override fun getItemViewType(position: Int) = if (data[position].away) {
+    VIEWTYPE_AWAY
+  } else {
+    VIEWTYPE_ACTIVE
+  }
+
+  data class IrcUserItem(
+    val nick: String,
+    val modes: String,
+    val lowestMode: Int,
+    val realname: String,
+    val away: Boolean
+  )
+
+  class NickViewHolder(
+    itemView: View,
+    private val clickListener: ((String) -> Unit)? = null
+  ) : RecyclerView.ViewHolder(itemView) {
+    @BindView(R.id.modes)
+    lateinit var modes: TextView
+
+    @BindView(R.id.nick)
+    lateinit var nick: TextView
+
+    @BindView(R.id.realname)
+    lateinit var realname: TextView
+
+    var user: String? = null
+
+    init {
+      ButterKnife.bind(this, itemView)
+      itemView.setOnClickListener {
+        val nick = user
+        if (nick != null)
+          clickListener?.invoke(nick)
+      }
+    }
+
+    fun bind(data: IrcUserItem) {
+      user = data.nick
+
+      nick.text = data.nick
+      modes.text = data.modes
+      realname.text = data.realname
+
+      modes.visibleIf(data.modes.isNotBlank())
+    }
+  }
+
+  companion object {
+    val VIEWTYPE_ACTIVE = 0
+    val VIEWTYPE_AWAY = 1
+  }
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/NickListFragment.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/NickListFragment.kt
new file mode 100644
index 000000000..39d97df1a
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/NickListFragment.kt
@@ -0,0 +1,122 @@
+package de.kuschku.quasseldroid_ng.ui.chat
+
+import android.arch.lifecycle.LiveData
+import android.arch.lifecycle.MutableLiveData
+import android.os.Bundle
+import android.support.v7.widget.DefaultItemAnimator
+import android.support.v7.widget.LinearLayoutManager
+import android.support.v7.widget.RecyclerView
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import butterknife.BindView
+import butterknife.ButterKnife
+import de.kuschku.libquassel.protocol.BufferId
+import de.kuschku.libquassel.protocol.Buffer_Type
+import de.kuschku.libquassel.session.Backend
+import de.kuschku.libquassel.session.SessionManager
+import de.kuschku.libquassel.util.hasFlag
+import de.kuschku.quasseldroid_ng.R
+import de.kuschku.quasseldroid_ng.util.AndroidHandlerThread
+import de.kuschku.quasseldroid_ng.util.helper.map
+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
+import io.reactivex.Observable.zip
+import io.reactivex.functions.BiFunction
+
+class NickListFragment : ServiceBoundFragment() {
+  private val handlerThread = AndroidHandlerThread("NickList")
+
+  @BindView(R.id.nickList)
+  lateinit var nickList: RecyclerView
+
+  val currentBuffer: MutableLiveData<LiveData<BufferId?>?> = MutableLiveData()
+  val buffer = currentBuffer.switchMap { it }
+
+  private val sessionManager: LiveData<SessionManager?>
+    = backend.map(Backend::sessionManager)
+
+  private val ircChannel: LiveData<List<NickListAdapter.IrcUserItem>?>
+    = sessionManager.switchMapRx(SessionManager::session).switchMap { session ->
+    buffer.switchMapRx {
+      val bufferSyncer = session.bufferSyncer
+      val bufferInfo = bufferSyncer?.bufferInfo(it)
+      if (bufferInfo?.type?.hasFlag(Buffer_Type.ChannelBuffer) == true) {
+        val network = session.networks[bufferInfo.networkId]
+        val ircChannel = network?.ircChannel(bufferInfo.bufferName)
+        if (ircChannel != null) {
+          Observable.combineLatest(
+            ircChannel.ircUsers().map { user ->
+              zip(
+                user.live_realName, user.live_away,
+                BiFunction<String, Boolean, Pair<String, Boolean>> { a, b -> Pair(a, b) }
+              ).map { (realName, away) ->
+                val userModes = ircChannel.userModes(user)
+                val prefixModes = network.prefixModes()
+
+                val lowestMode = userModes.mapNotNull {
+                  prefixModes.indexOf(it)
+                }.min() ?: prefixModes.size
+
+                NickListAdapter.IrcUserItem(
+                  user.nick(),
+                  network.modesToPrefixes(userModes),
+                  lowestMode,
+                  realName,
+                  away
+                )
+              }
+            }, { array: Array<Any> ->
+              array.toList() as List<NickListAdapter.IrcUserItem>
+            }
+          )
+        } else {
+          Observable.just(emptyList())
+        }
+      } else {
+        Observable.just(emptyList())
+      }
+    }
+  }
+
+  private val nicks: LiveData<List<NickListAdapter.IrcUserItem>?> = ircChannel
+
+  override fun onCreate(savedInstanceState: Bundle?) {
+    handlerThread.onCreate()
+    super.onCreate(savedInstanceState)
+  }
+
+  override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
+                            savedInstanceState: Bundle?): View? {
+    val view = inflater.inflate(R.layout.fragment_nick_list, container, false)
+    ButterKnife.bind(this, view)
+
+    nickList.adapter = NickListAdapter(
+      this,
+      nicks,
+      handlerThread::post,
+      activity!!::runOnUiThread,
+      clickListener
+    )
+
+    nickList.layoutManager = LinearLayoutManager(context)
+    nickList.itemAnimator = DefaultItemAnimator()
+
+    return view
+  }
+
+  override fun onDestroy() {
+    handlerThread.onDestroy()
+    super.onDestroy()
+  }
+
+  val clickListeners = mutableListOf<(String) -> Unit>()
+
+  private val clickListener: ((String) -> Unit)? = {
+    for (clickListener in clickListeners) {
+      clickListener.invoke(it)
+    }
+  }
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/ViewHelper.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/ViewHelper.kt
new file mode 100644
index 000000000..c7b18b2e6
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/ViewHelper.kt
@@ -0,0 +1,9 @@
+package de.kuschku.quasseldroid_ng.util.helper
+
+import android.view.View
+
+fun View.visibleIf(check: Boolean) = if (check) {
+  this.visibility = View.VISIBLE
+} else {
+  this.visibility = View.GONE
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/bg_badge.xml b/app/src/main/res/drawable/bg_badge.xml
new file mode 100644
index 000000000..da40f3172
--- /dev/null
+++ b/app/src/main/res/drawable/bg_badge.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+  <solid />
+  <padding
+    android:bottom="4dp"
+    android:left="8dp"
+    android:right="8dp"
+    android:top="4dp" />
+  <corners android:radius="16dp" />
+</shape>
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 98bd94628..4ba82b1eb 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -137,7 +137,13 @@
 
   </LinearLayout>
 
-  <include layout="@layout/fragment_nick_list" />
+  <fragment
+    android:id="@+id/nickListFragment"
+    android:name="de.kuschku.quasseldroid_ng.ui.chat.NickListFragment"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:layout_gravity="end"
+    tools:layout="@layout/fragment_nick_list" />
 
   <de.kuschku.quasseldroid_ng.util.ui.NavigationDrawerLayout
     android:layout_width="match_parent"
diff --git a/app/src/main/res/layout/fragment_nick_list.xml b/app/src/main/res/layout/fragment_nick_list.xml
index eba412b14..e7b01adea 100644
--- a/app/src/main/res/layout/fragment_nick_list.xml
+++ b/app/src/main/res/layout/fragment_nick_list.xml
@@ -3,7 +3,6 @@
   android:id="@+id/nickList"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
-  android:layout_gravity="end"
   android:background="?attr/colorBackground"
   android:clipToPadding="false"
   android:fitsSystemWindows="true"
diff --git a/app/src/main/res/layout/widget_buffer.xml b/app/src/main/res/layout/widget_buffer.xml
index f057d26ad..12fdcea15 100644
--- a/app/src/main/res/layout/widget_buffer.xml
+++ b/app/src/main/res/layout/widget_buffer.xml
@@ -34,10 +34,10 @@
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:layout_gravity="center"
+      android:fontFamily="sans-serif-medium"
       android:singleLine="true"
       android:textColor="?attr/colorTextPrimary"
       android:textSize="13sp"
-      android:textStyle="bold"
       tools:text="#quasseldroid" />
 
     <TextView
diff --git a/app/src/main/res/layout/widget_network.xml b/app/src/main/res/layout/widget_network.xml
index 0ec8859d2..8d68d7c8e 100644
--- a/app/src/main/res/layout/widget_network.xml
+++ b/app/src/main/res/layout/widget_network.xml
@@ -29,10 +29,10 @@
       android:layout_marginRight="16dp"
       android:layout_marginTop="8dp"
       android:layout_weight="1"
+      android:fontFamily="sans-serif-medium"
       android:singleLine="true"
       android:textColor="?attr/colorTextSecondary"
       android:textSize="14sp"
-      android:textStyle="bold"
       tools:text="Freenode" />
 
     <ImageView
@@ -41,7 +41,6 @@
       android:layout_height="match_parent"
       android:background="?attr/selectableItemBackgroundBorderless"
       android:clickable="true"
-      android:contentDescription="Expand"
       android:focusable="true"
       android:minWidth="72dp"
       android:paddingBottom="12dp"
diff --git a/app/src/main/res/layout/widget_nick.xml b/app/src/main/res/layout/widget_nick.xml
new file mode 100644
index 000000000..ea3530b17
--- /dev/null
+++ b/app/src/main/res/layout/widget_nick.xml
@@ -0,0 +1,72 @@
+<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="56dp"
+  android:background="?selectableItemBackground"
+  android:clickable="true"
+  android:focusable="true"
+  android:orientation="horizontal"
+  android:paddingEnd="?listPreferredItemPaddingRight"
+  android:paddingLeft="?listPreferredItemPaddingLeft"
+  android:paddingRight="?listPreferredItemPaddingRight"
+  android:paddingStart="?listPreferredItemPaddingLeft"
+  tools:background="@android:color/background_light"
+  tools:theme="@style/Theme.ChatTheme.Quassel_Light">
+
+  <FrameLayout
+    android:layout_width="wrap_content"
+    android:layout_height="match_parent"
+    android:minWidth="40dp">
+
+    <android.support.v7.widget.AppCompatTextView
+      android:id="@+id/modes"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:layout_gravity="center_vertical"
+      android:layout_marginBottom="8dp"
+      android:layout_marginTop="8dp"
+      android:background="@drawable/bg_badge"
+      android:fontFamily="monospace"
+      android:gravity="center"
+      android:minHeight="24dp"
+      android:minWidth="24dp"
+      android:textColor="?colorBackground"
+      android:textSize="12sp"
+      android:textStyle="bold"
+      app:backgroundTint="@color/colorAccent"
+      tools:text="\@" />
+  </FrameLayout>
+
+  <LinearLayout
+    android:layout_width="0dp"
+    android:layout_height="match_parent"
+    android:layout_marginLeft="16dp"
+    android:layout_marginStart="16dp"
+    android:layout_weight="1"
+    android:gravity="center_vertical|start"
+    android:orientation="vertical">
+
+    <TextView
+      android:id="@+id/nick"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:fontFamily="sans-serif-medium"
+      android:gravity="center_vertical|start"
+      android:singleLine="true"
+      android:textColor="?colorTextPrimary"
+      android:textSize="13sp"
+      tools:text="justJanne" />
+
+    <TextView
+      android:id="@+id/realname"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:fontFamily="sans-serif"
+      android:gravity="center_vertical|start"
+      android:singleLine="true"
+      android:textColor="?colorTextSecondary"
+      android:textSize="12sp"
+      tools:text="Janne Koschinski: https://kuschku.de/" />
+  </LinearLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/widget_nick_away.xml b/app/src/main/res/layout/widget_nick_away.xml
new file mode 100644
index 000000000..3e82539c8
--- /dev/null
+++ b/app/src/main/res/layout/widget_nick_away.xml
@@ -0,0 +1,74 @@
+<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="56dp"
+  android:background="?selectableItemBackground"
+  android:clickable="true"
+  android:focusable="true"
+  android:orientation="horizontal"
+  android:paddingEnd="?listPreferredItemPaddingRight"
+  android:paddingLeft="?listPreferredItemPaddingLeft"
+  android:paddingRight="?listPreferredItemPaddingRight"
+  android:paddingStart="?listPreferredItemPaddingLeft"
+  tools:background="@android:color/background_light"
+  tools:theme="@style/Theme.ChatTheme.Quassel_Light">
+
+  <FrameLayout
+    android:layout_width="wrap_content"
+    android:layout_height="match_parent"
+    android:minWidth="40dp">
+
+    <android.support.v7.widget.AppCompatTextView
+      android:id="@+id/modes"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:layout_gravity="center_vertical"
+      android:layout_marginBottom="8dp"
+      android:layout_marginTop="8dp"
+      android:background="@drawable/bg_badge"
+      android:fontFamily="monospace"
+      android:gravity="center"
+      android:minHeight="24dp"
+      android:minWidth="24dp"
+      android:textColor="?colorBackground"
+      android:textSize="12sp"
+      android:textStyle="bold"
+      app:backgroundTint="@color/colorAccent"
+      tools:text="\@" />
+  </FrameLayout>
+
+  <LinearLayout
+    android:layout_width="0dp"
+    android:layout_height="match_parent"
+    android:layout_marginLeft="16dp"
+    android:layout_marginStart="16dp"
+    android:layout_weight="1"
+    android:gravity="center_vertical|start"
+    android:orientation="vertical">
+
+    <TextView
+      android:id="@+id/nick"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:fontFamily="sans-serif-medium"
+      android:gravity="center_vertical|start"
+      android:singleLine="true"
+      android:textColor="?colorTextSecondary"
+      android:textSize="13sp"
+      android:textStyle="italic"
+      tools:text="justJanne" />
+
+    <TextView
+      android:id="@+id/realname"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:fontFamily="sans-serif"
+      android:gravity="center_vertical|start"
+      android:singleLine="true"
+      android:textColor="?colorTextSecondary"
+      android:textSize="12sp"
+      android:textStyle="italic"
+      tools:text="Janne Koschinski: https://kuschku.de/" />
+  </LinearLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/lib/src/main/java/de/kuschku/libquassel/protocol/primitive/serializer/StringSerializer.kt b/lib/src/main/java/de/kuschku/libquassel/protocol/primitive/serializer/StringSerializer.kt
index e0328ec12..9f49f4d62 100644
--- a/lib/src/main/java/de/kuschku/libquassel/protocol/primitive/serializer/StringSerializer.kt
+++ b/lib/src/main/java/de/kuschku/libquassel/protocol/primitive/serializer/StringSerializer.kt
@@ -42,6 +42,7 @@ abstract class StringSerializer(
     return buf
   }
 
+  @Synchronized
   override fun serialize(buffer: ChainedByteBuffer, data: String?, features: Quassel_Features) {
     if (data == null) {
       IntSerializer.serialize(buffer, -1, features)
@@ -58,6 +59,7 @@ abstract class StringSerializer(
     }
   }
 
+  @Synchronized
   fun serialize(data: String?): ByteBuffer = if (data == null) {
     ByteBuffer.allocate(0)
   } else {
@@ -68,6 +70,7 @@ abstract class StringSerializer(
     encoder.encode(charBuffer)
   }
 
+  @Synchronized
   fun deserializeAll(buffer: ByteBuffer): String? {
     val len = buffer.remaining()
     return if (len == -1) {
@@ -85,6 +88,7 @@ abstract class StringSerializer(
     }
   }
 
+  @Synchronized
   override fun deserialize(buffer: ByteBuffer, features: Quassel_Features): String? {
     val len = IntSerializer.deserialize(buffer, features)
     return if (len == -1) {
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 f08a638c7..680286716 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
@@ -69,13 +69,21 @@ class Network constructor(
     = prefixModes().elementAtOrNull(prefixes().indexOf(prefix))
 
   fun prefixesToModes(prefixes: String): String
-    = prefixes.mapNotNull(this::prefixToMode).joinToString()
+    = prefixes.mapNotNull {
+    prefixes().indexOf(it)
+  }.sorted().mapNotNull {
+    prefixModes().elementAtOrNull(it)
+  }.joinToString("")
 
   fun modeToPrefix(mode: Char): Char?
     = prefixes().elementAtOrNull(prefixModes().indexOf(mode))
 
   fun modesToPrefixes(modes: String): String
-    = modes.mapNotNull(this::modeToPrefix).joinToString()
+    = modes.mapNotNull {
+    prefixModes().indexOf(it)
+  }.sorted().mapNotNull {
+    prefixes().elementAtOrNull(it)
+  }.joinToString("")
 
   fun channelModeType(mode: Char): ChannelModeType {
     if (_channelModes == null)
@@ -204,31 +212,31 @@ class Network constructor(
       setUnlimitedMessageRate(info.unlimitedMessageRate)
   }
 
-  fun prefixes(): Set<Char> {
+  fun prefixes(): List<Char> {
     if (_prefixes == null)
       determinePrefixes()
-    return _prefixes!!
+    return _prefixes ?: emptyList()
   }
 
-  fun prefixModes(): Set<Char> {
+  fun prefixModes(): List<Char> {
     if (_prefixModes == null)
       determinePrefixes()
-    return _prefixModes!!
+    return _prefixModes ?: emptyList()
   }
 
   private fun determinePrefixes() {
     // seems like we have to construct them first
     val prefix = support("PREFIX")
     if (prefix.startsWith("(") && prefix.contains(")")) {
-      val (prefixes, prefixModes) = prefix.substring(1)
+      val (prefixModes, prefixes) = prefix.substring(1)
         .split(')', limit = 2)
         .map(String::toCharArray)
-        .map(CharArray::toSet)
+        .map(CharArray::toList)
       _prefixes = prefixes
       _prefixModes = prefixModes
     } else {
-      val defaultPrefixes = setOf('~', '&', '@', '%', '+')
-      val defaultPrefixModes = setOf('q', 'a', 'o', 'h', 'v')
+      val defaultPrefixes = listOf('~', '&', '@', '%', '+')
+      val defaultPrefixModes = listOf('q', 'a', 'o', 'h', 'v')
       if (prefix.isBlank()) {
         _prefixes = defaultPrefixes
         _prefixModes = defaultPrefixModes
@@ -238,8 +246,8 @@ class Network constructor(
       val (prefixes, prefixModes) = defaultPrefixes.zip(defaultPrefixModes)
         .filter { prefix.contains(it.second) }
         .unzip()
-      _prefixes = prefixes.toSet()
-      _prefixModes = prefixModes.toSet()
+      _prefixes = prefixes
+      _prefixModes = prefixModes
       // check for success
       if (prefixes.isNotEmpty())
         return
@@ -248,8 +256,8 @@ class Network constructor(
       val (prefixes2, prefixModes2) = defaultPrefixes.zip(defaultPrefixModes)
         .filter { prefix.contains(it.first) }
         .unzip()
-      _prefixes = prefixes2.toSet()
-      _prefixModes = prefixModes2.toSet()
+      _prefixes = prefixes2
+      _prefixModes = prefixModes2
       // now we've done all we've could...
     }
   }
@@ -876,8 +884,8 @@ class Network constructor(
   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 _prefixes: List<Char>? = null
+  private var _prefixModes: List<Char>? = null
   private var _channelModes: Map<ChannelModeType, Set<Char>>? = null
   // stores all known nicks for the server
   private var _ircUsers: MutableMap<String, IrcUser> = mutableMapOf()
@@ -931,4 +939,4 @@ class Network constructor(
   companion object {
     val NULL = Network(-1, SignalProxy.NULL)
   }
-}
+}
\ No newline at end of file
-- 
GitLab