diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/AutoCompleteAdapter.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/AutoCompleteAdapter.kt index 7779e26f4b2d1392414573ed4e7bc300d42b4a52..b722544e707394d141977abece3ba21a123d9492 100644 --- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/AutoCompleteAdapter.kt +++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/AutoCompleteAdapter.kt @@ -3,40 +3,46 @@ package de.kuschku.quasseldroid_ng.ui.chat import android.arch.lifecycle.LifecycleOwner import android.arch.lifecycle.LiveData import android.arch.lifecycle.Observer +import android.graphics.drawable.Drawable +import android.support.v4.graphics.drawable.DrawableCompat import android.support.v7.util.DiffUtil import android.support.v7.widget.RecyclerView import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.ImageView import android.widget.TextView import butterknife.BindView import butterknife.ButterKnife +import de.kuschku.libquassel.quassel.BufferInfo +import de.kuschku.libquassel.quassel.syncables.interfaces.INetwork 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.irc.IrcCaseMappers class AutoCompleteAdapter( lifecycleOwner: LifecycleOwner, - liveData: LiveData<List<NickListAdapter.IrcUserItem>?>, + liveData: LiveData<List<AutoCompleteItem>?>, runInBackground: (() -> Unit) -> Any, runOnUiThread: (Runnable) -> Any, private val clickListener: ((String) -> Unit)? = null -) : RecyclerView.Adapter<AutoCompleteAdapter.NickViewHolder>() { - var data = mutableListOf<NickListAdapter.IrcUserItem>() +) : RecyclerView.Adapter<AutoCompleteAdapter.AutoCompleteViewHolder>() { + var data = mutableListOf<AutoCompleteItem>() init { liveData.observe( - lifecycleOwner, Observer { it: List<NickListAdapter.IrcUserItem>? -> + lifecycleOwner, Observer { it: List<AutoCompleteItem>? -> runInBackground { val list = it ?: emptyList() - val old: List<NickListAdapter.IrcUserItem> = data - val new: List<NickListAdapter.IrcUserItem> = list - .sortedBy { IrcCaseMappers[it.networkCasemapping].toLowerCase(it.nick) } - .sortedBy { it.lowestMode } + val old: List<AutoCompleteItem> = data + val new: List<AutoCompleteItem> = list val result = DiffUtil.calculateDiff( object : DiffUtil.Callback() { 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 getNewListSize() = new.size @@ -56,66 +62,172 @@ class AutoCompleteAdapter( ) } - 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 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( + when (viewType) { + VIEWTYPE_AWAY -> R.layout.widget_nick_away + else -> R.layout.widget_nick + }, parent, false + ), + 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 getItemViewType(position: Int) = if (data[position].away) { - VIEWTYPE_AWAY - } else { - VIEWTYPE_ACTIVE + override fun getItemViewType(position: Int) = data[position].let { it -> + when { + it is AutoCompleteItem.ChannelItem -> VIEWTYPE_CHANNEL + it is AutoCompleteItem.UserItem && it.away -> VIEWTYPE_NICK_AWAY + else -> VIEWTYPE_NICK_ACTIVE + } } - class NickViewHolder( - itemView: View, - private val clickListener: ((String) -> Unit)? = null - ) : RecyclerView.ViewHolder(itemView) { - @BindView(R.id.modesContainer) - lateinit var modesContainer: View + 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( + itemView: View, + private val clickListener: ((String) -> Unit)? = null + ) : AutoCompleteViewHolder(itemView) { + @BindView(R.id.modesContainer) + lateinit var modesContainer: View + + @BindView(R.id.modes) + lateinit var modes: TextView + + @BindView(R.id.nick) + lateinit var nick: TextView - @BindView(R.id.modes) - lateinit var modes: TextView + @BindView(R.id.realname) + lateinit var realname: TextView - @BindView(R.id.nick) - lateinit var nick: TextView + var value: String? = null - @BindView(R.id.realname) - lateinit var realname: TextView + init { + ButterKnife.bind(this, itemView) + itemView.setOnClickListener { + val value = value + if (value != null) + clickListener?.invoke(value) + } + } + + fun bindImpl(data: AutoCompleteItem.UserItem) { + value = data.name - var user: String? = null + nick.text = data.nick + modes.text = data.modes + realname.text = data.realname - init { - ButterKnife.bind(this, itemView) - itemView.setOnClickListener { - val nick = user - if (nick != null) - clickListener?.invoke(nick) + modes.visibleIf(data.modes.isNotBlank()) } } - fun bind(data: NickListAdapter.IrcUserItem) { - user = data.nick + 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)) + } + } - nick.text = data.nick - modes.text = data.modes - realname.text = data.realname + fun bindImpl(data: AutoCompleteItem.ChannelItem) { + value = data.name - modes.visibleIf(data.modes.isNotBlank()) + 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 { - val VIEWTYPE_ACTIVE = 0 - val VIEWTYPE_AWAY = 1 + val VIEWTYPE_CHANNEL = 0 + val VIEWTYPE_NICK_ACTIVE = 1 + val VIEWTYPE_NICK_AWAY = 2 } } \ No newline at end of file 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 3ee817471c44c8fb24fdc68d4264f1ed265c331f..732cc3f1319ed662c6ed878cc944cf38894884b9 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 @@ -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.ui.MaterialContentLoadingProgressBar import io.reactivex.subjects.BehaviorSubject -import java.util.concurrent.TimeUnit class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenceChangeListener, ActionMenuView.OnMenuItemClickListener { @@ -116,8 +115,9 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc private val lastWord = BehaviorSubject.createDefault("") private val textWatcher = object : TextWatcher { - override fun afterTextChanged(s: Editable?) = + override fun afterTextChanged(s: Editable?) { lastWord.onNext(s?.lastWord(chatline.selectionStart, onlyBeforeCursor = true).toString()) + } override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit @@ -142,6 +142,7 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc viewModel = ViewModelProviders.of(this)[QuasselViewModel::class.java] viewModel.setBackend(this.backend) + viewModel.lastWord.value = lastWord backlogSettings = Settings.backlog(this) inputEditor = InputEditor(chatline) @@ -223,21 +224,7 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc val autocompleteAdapter = AutoCompleteAdapter( this, - viewModel.nickData.switchMapRx { nicks -> - 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) - } - } - }, + viewModel.autoCompleteData, handler::post, ::runOnUiThread, inputEditor::autoComplete diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/viewmodel/QuasselViewModel.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/viewmodel/QuasselViewModel.kt index 60c584f2cbffde72d87aa242289b61482b7623b9..2938374049fa7e6377a8c27fb4bdd820a84bf389 100644 --- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/viewmodel/QuasselViewModel.kt +++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/viewmodel/QuasselViewModel.kt @@ -16,6 +16,7 @@ import de.kuschku.libquassel.session.ISession import de.kuschku.libquassel.session.SessionManager import de.kuschku.libquassel.util.and 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.ToolbarFragment import de.kuschku.quasseldroid_ng.ui.chat.buffers.BufferListAdapter @@ -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 -> manager.liveBufferViewConfigs().map { ids -> ids.mapNotNull { id ->