Skip to content
Snippets Groups Projects
Commit 4cf5fa57 authored by Janne Mareike Koschinski's avatar Janne Mareike Koschinski
Browse files

Improved nick list, added drawer toggle

parent acb6d5ed
Branches
Tags
No related merge requests found
Showing
with 467 additions and 45 deletions
......@@ -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) {
......
......@@ -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,6 +191,9 @@ class ChatActivity : ServiceBoundActivity() {
}
override fun onOptionsItemSelected(item: MenuItem?) = when (item?.itemId) {
android.R.id.home -> {
drawerToggle.onOptionsItemSelected(item)
}
R.id.disconnect -> {
handler.post {
getSharedPreferences(Keys.Status.NAME, Context.MODE_PRIVATE).editApply {
......
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
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
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
<?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>
......@@ -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"
......
......@@ -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"
......
......@@ -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
......
......@@ -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"
......
<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
<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
......@@ -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) {
......
......@@ -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()
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment