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

Implement autocomplete of channels

parent bedb39cd
No related branches found
No related tags found
No related merge requests found
...@@ -3,40 +3,46 @@ package de.kuschku.quasseldroid_ng.ui.chat ...@@ -3,40 +3,46 @@ package de.kuschku.quasseldroid_ng.ui.chat
import android.arch.lifecycle.LifecycleOwner import android.arch.lifecycle.LifecycleOwner
import android.arch.lifecycle.LiveData import android.arch.lifecycle.LiveData
import android.arch.lifecycle.Observer import android.arch.lifecycle.Observer
import android.graphics.drawable.Drawable
import android.support.v4.graphics.drawable.DrawableCompat
import android.support.v7.util.DiffUtil import android.support.v7.util.DiffUtil
import android.support.v7.widget.RecyclerView import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import butterknife.BindView import butterknife.BindView
import butterknife.ButterKnife import butterknife.ButterKnife
import de.kuschku.libquassel.quassel.BufferInfo
import de.kuschku.libquassel.quassel.syncables.interfaces.INetwork
import de.kuschku.quasseldroid_ng.R import de.kuschku.quasseldroid_ng.R
import de.kuschku.quasseldroid_ng.ui.chat.NickListAdapter.Companion.VIEWTYPE_AWAY
import de.kuschku.quasseldroid_ng.ui.chat.buffers.BufferListAdapter
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.visibleIf
import de.kuschku.quasseldroid_ng.util.irc.IrcCaseMappers
class AutoCompleteAdapter( class AutoCompleteAdapter(
lifecycleOwner: LifecycleOwner, lifecycleOwner: LifecycleOwner,
liveData: LiveData<List<NickListAdapter.IrcUserItem>?>, liveData: LiveData<List<AutoCompleteItem>?>,
runInBackground: (() -> Unit) -> Any, runInBackground: (() -> Unit) -> Any,
runOnUiThread: (Runnable) -> Any, runOnUiThread: (Runnable) -> Any,
private val clickListener: ((String) -> Unit)? = null private val clickListener: ((String) -> Unit)? = null
) : RecyclerView.Adapter<AutoCompleteAdapter.NickViewHolder>() { ) : RecyclerView.Adapter<AutoCompleteAdapter.AutoCompleteViewHolder>() {
var data = mutableListOf<NickListAdapter.IrcUserItem>() var data = mutableListOf<AutoCompleteItem>()
init { init {
liveData.observe( liveData.observe(
lifecycleOwner, Observer { it: List<NickListAdapter.IrcUserItem>? -> lifecycleOwner, Observer { it: List<AutoCompleteItem>? ->
runInBackground { runInBackground {
val list = it ?: emptyList() val list = it ?: emptyList()
val old: List<NickListAdapter.IrcUserItem> = data val old: List<AutoCompleteItem> = data
val new: List<NickListAdapter.IrcUserItem> = list val new: List<AutoCompleteItem> = list
.sortedBy { IrcCaseMappers[it.networkCasemapping].toLowerCase(it.nick) }
.sortedBy { it.lowestMode }
val result = DiffUtil.calculateDiff( val result = DiffUtil.calculateDiff(
object : DiffUtil.Callback() { object : DiffUtil.Callback() {
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
old[oldItemPosition].nick == new[newItemPosition].nick old[oldItemPosition].name == new[newItemPosition].name
override fun getOldListSize() = old.size override fun getOldListSize() = old.size
override fun getNewListSize() = new.size override fun getNewListSize() = new.size
...@@ -56,7 +62,13 @@ class AutoCompleteAdapter( ...@@ -56,7 +62,13 @@ class AutoCompleteAdapter(
) )
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = NickViewHolder( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when (viewType) {
VIEWTYPE_CHANNEL -> AutoCompleteViewHolder.ChannelViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.widget_buffer, parent, false),
clickListener = clickListener
)
VIEWTYPE_NICK_ACTIVE, VIEWTYPE_NICK_AWAY -> AutoCompleteViewHolder.NickViewHolder(
LayoutInflater.from(parent.context).inflate( LayoutInflater.from(parent.context).inflate(
when (viewType) { when (viewType) {
VIEWTYPE_AWAY -> R.layout.widget_nick_away VIEWTYPE_AWAY -> R.layout.widget_nick_away
...@@ -65,21 +77,65 @@ class AutoCompleteAdapter( ...@@ -65,21 +77,65 @@ class AutoCompleteAdapter(
), ),
clickListener = clickListener clickListener = clickListener
) )
else -> throw IllegalArgumentException(
"Invoked with wrong item type"
)
}
override fun onBindViewHolder(holder: NickViewHolder, position: Int) = holder.bind(data[position]) override fun onBindViewHolder(holder: AutoCompleteViewHolder, position: Int) =
holder.bind(data[position])
override fun getItemCount() = data.size override fun getItemCount() = data.size
override fun getItemViewType(position: Int) = if (data[position].away) { override fun getItemViewType(position: Int) = data[position].let { it ->
VIEWTYPE_AWAY when {
} else { it is AutoCompleteItem.ChannelItem -> VIEWTYPE_CHANNEL
VIEWTYPE_ACTIVE it is AutoCompleteItem.UserItem && it.away -> VIEWTYPE_NICK_AWAY
else -> VIEWTYPE_NICK_ACTIVE
}
}
sealed class AutoCompleteItem(open val name: String) : Comparable<AutoCompleteItem> {
override fun compareTo(other: AutoCompleteItem): Int {
return when {
this is AutoCompleteItem.UserItem &&
other is AutoCompleteItem.ChannelItem -> -1
this is AutoCompleteItem.ChannelItem &&
other is AutoCompleteItem.UserItem -> 1
else -> this.name.compareTo(other.name)
}
}
data class UserItem(
val nick: String,
val modes: String,
val lowestMode: Int,
val realname: CharSequence,
val away: Boolean,
val networkCasemapping: String
) : AutoCompleteItem(nick)
data class ChannelItem(
val info: BufferInfo,
val network: INetwork.NetworkInfo,
val bufferStatus: BufferListAdapter.BufferStatus,
val description: CharSequence
) : AutoCompleteItem(info.bufferName ?: "")
}
sealed class AutoCompleteViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(data: AutoCompleteItem) = when {
data is AutoCompleteItem.UserItem && this is NickViewHolder -> this.bindImpl(data)
data is AutoCompleteItem.ChannelItem && this is ChannelViewHolder -> this.bindImpl(data)
else -> throw IllegalArgumentException(
"Invoked with wrong item type"
)
} }
class NickViewHolder( class NickViewHolder(
itemView: View, itemView: View,
private val clickListener: ((String) -> Unit)? = null private val clickListener: ((String) -> Unit)? = null
) : RecyclerView.ViewHolder(itemView) { ) : AutoCompleteViewHolder(itemView) {
@BindView(R.id.modesContainer) @BindView(R.id.modesContainer)
lateinit var modesContainer: View lateinit var modesContainer: View
...@@ -92,19 +148,19 @@ class AutoCompleteAdapter( ...@@ -92,19 +148,19 @@ class AutoCompleteAdapter(
@BindView(R.id.realname) @BindView(R.id.realname)
lateinit var realname: TextView lateinit var realname: TextView
var user: String? = null var value: String? = null
init { init {
ButterKnife.bind(this, itemView) ButterKnife.bind(this, itemView)
itemView.setOnClickListener { itemView.setOnClickListener {
val nick = user val value = value
if (nick != null) if (value != null)
clickListener?.invoke(nick) clickListener?.invoke(value)
} }
} }
fun bind(data: NickListAdapter.IrcUserItem) { fun bindImpl(data: AutoCompleteItem.UserItem) {
user = data.nick value = data.name
nick.text = data.nick nick.text = data.nick
modes.text = data.modes modes.text = data.modes
...@@ -114,8 +170,64 @@ class AutoCompleteAdapter( ...@@ -114,8 +170,64 @@ class AutoCompleteAdapter(
} }
} }
class ChannelViewHolder(
itemView: View,
private val clickListener: ((String) -> Unit)? = null
) : AutoCompleteViewHolder(itemView) {
@BindView(R.id.status)
lateinit var status: ImageView
@BindView(R.id.name)
lateinit var name: TextView
@BindView(R.id.description)
lateinit var description: TextView
var value: String? = null
private val online: Drawable
private val offline: Drawable
init {
ButterKnife.bind(this, itemView)
itemView.setOnClickListener {
val value = value
if (value != null)
clickListener?.invoke(value)
}
online = itemView.context.getCompatDrawable(R.drawable.ic_status_channel).mutate()
offline = itemView.context.getCompatDrawable(R.drawable.ic_status_channel_offline).mutate()
itemView.context.theme.styledAttributes(
R.attr.colorAccent, R.attr.colorAway
) {
DrawableCompat.setTint(online, getColor(0, 0))
DrawableCompat.setTint(offline, getColor(1, 0))
}
}
fun bindImpl(data: AutoCompleteItem.ChannelItem) {
value = data.name
name.text = data.info.bufferName
description.text = data.description
description.visibleIf(data.description.isNotBlank())
status.setImageDrawable(
when (data.bufferStatus) {
BufferListAdapter.BufferStatus.ONLINE -> online
else -> offline
}
)
}
}
}
companion object { companion object {
val VIEWTYPE_ACTIVE = 0 val VIEWTYPE_CHANNEL = 0
val VIEWTYPE_AWAY = 1 val VIEWTYPE_NICK_ACTIVE = 1
val VIEWTYPE_NICK_AWAY = 2
} }
} }
\ No newline at end of file
...@@ -45,7 +45,6 @@ import de.kuschku.quasseldroid_ng.util.helper.* ...@@ -45,7 +45,6 @@ import de.kuschku.quasseldroid_ng.util.helper.*
import de.kuschku.quasseldroid_ng.util.service.ServiceBoundActivity import de.kuschku.quasseldroid_ng.util.service.ServiceBoundActivity
import de.kuschku.quasseldroid_ng.util.ui.MaterialContentLoadingProgressBar import de.kuschku.quasseldroid_ng.util.ui.MaterialContentLoadingProgressBar
import io.reactivex.subjects.BehaviorSubject import io.reactivex.subjects.BehaviorSubject
import java.util.concurrent.TimeUnit
class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenceChangeListener, class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenceChangeListener,
ActionMenuView.OnMenuItemClickListener { ActionMenuView.OnMenuItemClickListener {
...@@ -116,8 +115,9 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc ...@@ -116,8 +115,9 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
private val lastWord = BehaviorSubject.createDefault("") private val lastWord = BehaviorSubject.createDefault("")
private val textWatcher = object : TextWatcher { private val textWatcher = object : TextWatcher {
override fun afterTextChanged(s: Editable?) = override fun afterTextChanged(s: Editable?) {
lastWord.onNext(s?.lastWord(chatline.selectionStart, onlyBeforeCursor = true).toString()) lastWord.onNext(s?.lastWord(chatline.selectionStart, onlyBeforeCursor = true).toString())
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit
...@@ -142,6 +142,7 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc ...@@ -142,6 +142,7 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
viewModel = ViewModelProviders.of(this)[QuasselViewModel::class.java] viewModel = ViewModelProviders.of(this)[QuasselViewModel::class.java]
viewModel.setBackend(this.backend) viewModel.setBackend(this.backend)
viewModel.lastWord.value = lastWord
backlogSettings = Settings.backlog(this) backlogSettings = Settings.backlog(this)
inputEditor = InputEditor(chatline) inputEditor = InputEditor(chatline)
...@@ -223,21 +224,7 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc ...@@ -223,21 +224,7 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
val autocompleteAdapter = AutoCompleteAdapter( val autocompleteAdapter = AutoCompleteAdapter(
this, this,
viewModel.nickData.switchMapRx { nicks -> viewModel.autoCompleteData,
lastWord
.map { if (it.length >= 3) it else "" }
.distinctUntilChanged()
.debounce(300, TimeUnit.MILLISECONDS)
.map { input ->
if (input.isEmpty()) {
emptyList()
} else {
nicks.filter {
it.nick.contains(input, ignoreCase = true)
}.sortedBy(NickListAdapter.IrcUserItem::nick)
}
}
},
handler::post, handler::post,
::runOnUiThread, ::runOnUiThread,
inputEditor::autoComplete inputEditor::autoComplete
......
...@@ -16,6 +16,7 @@ import de.kuschku.libquassel.session.ISession ...@@ -16,6 +16,7 @@ import de.kuschku.libquassel.session.ISession
import de.kuschku.libquassel.session.SessionManager import de.kuschku.libquassel.session.SessionManager
import de.kuschku.libquassel.util.and import de.kuschku.libquassel.util.and
import de.kuschku.libquassel.util.hasFlag import de.kuschku.libquassel.util.hasFlag
import de.kuschku.quasseldroid_ng.ui.chat.AutoCompleteAdapter
import de.kuschku.quasseldroid_ng.ui.chat.NickListAdapter import de.kuschku.quasseldroid_ng.ui.chat.NickListAdapter
import de.kuschku.quasseldroid_ng.ui.chat.ToolbarFragment import de.kuschku.quasseldroid_ng.ui.chat.ToolbarFragment
import de.kuschku.quasseldroid_ng.ui.chat.buffers.BufferListAdapter import de.kuschku.quasseldroid_ng.ui.chat.buffers.BufferListAdapter
...@@ -192,6 +193,119 @@ class QuasselViewModel : ViewModel() { ...@@ -192,6 +193,119 @@ class QuasselViewModel : ViewModel() {
} }
} }
val lastWord = MutableLiveData<Observable<String>>()
val autoCompleteData: LiveData<List<AutoCompleteAdapter.AutoCompleteItem>?> = session.zip(
buffer, lastWord
).switchMapRx { (session, id, lastWordWrapper) ->
lastWordWrapper
.distinctUntilChanged()
.debounce(300, TimeUnit.MILLISECONDS)
.switchMap { lastWord ->
val bufferSyncer = session?.bufferSyncer
val bufferInfo = bufferSyncer?.bufferInfo(id)
if (bufferSyncer != null && lastWord.length >= 3) {
bufferSyncer.liveBufferInfos().switchMap { infos ->
if (bufferInfo?.type?.hasFlag(
Buffer_Type.ChannelBuffer
) == true) {
val network = session.networks[bufferInfo.networkId]
val ircChannel = network?.ircChannel(
bufferInfo.bufferName
)
if (ircChannel != null) {
ircChannel.liveIrcUsers().switchMap { users ->
val buffers: List<Observable<AutoCompleteAdapter.AutoCompleteItem.ChannelItem>?> = infos.values
.filter {
it.type.toInt() == Buffer_Type.ChannelBuffer.toInt()
}.mapNotNull { info ->
session.networks[info.networkId]?.let { info to it }
}.map<Pair<BufferInfo, Network>, Observable<AutoCompleteAdapter.AutoCompleteItem.ChannelItem>?> { (info, network) ->
network.liveIrcChannel(
info.bufferName
).switchMap { channel ->
channel.liveTopic().map { topic ->
AutoCompleteAdapter.AutoCompleteItem.ChannelItem(
info = info,
network = network.networkInfo(),
bufferStatus = when (channel) {
IrcChannel.NULL -> BufferListAdapter.BufferStatus.OFFLINE
else -> BufferListAdapter.BufferStatus.ONLINE
},
description = topic
)
}
}
}
val nicks = users.map<IrcUser, Observable<AutoCompleteAdapter.AutoCompleteItem.UserItem>?> { user ->
user.liveNick().switchMap { nick ->
user.liveRealName().switchMap { realName ->
user.liveIsAway().map { away ->
val userModes = ircChannel.userModes(
user
)
val prefixModes = network.prefixModes()
val lowestMode = userModes.mapNotNull {
prefixModes.indexOf(
it
)
}.min() ?: prefixModes.size
AutoCompleteAdapter.AutoCompleteItem.UserItem(
nick,
network.modesToPrefixes(
userModes
),
lowestMode,
realName,
away,
network.support(
"CASEMAPPING"
)
)
}
}
}
}
Observable.combineLatest(
nicks + buffers,
object :
Function<Array<Any>, List<AutoCompleteAdapter.AutoCompleteItem>> {
override fun apply(
objects: Array<Any>): List<AutoCompleteAdapter.AutoCompleteItem> {
return objects.toList() as List<AutoCompleteAdapter.AutoCompleteItem>
}
}).map { list ->
list
.filter {
it.name.contains(
lastWord,
ignoreCase = true
)
}.sorted()
}
}
} else {
Observable.just(
emptyList()
)
}
} else {
Observable.just(
emptyList()
)
}
}
} else {
Observable.just(
emptyList()
)
}
}
}
val bufferViewConfigs = bufferViewManager.switchMapRx { manager -> val bufferViewConfigs = bufferViewManager.switchMapRx { manager ->
manager.liveBufferViewConfigs().map { ids -> manager.liveBufferViewConfigs().map { ids ->
ids.mapNotNull { id -> ids.mapNotNull { id ->
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment