diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ChatActivity.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ChatActivity.kt index 3623247e33477574a980a196fe9a1cd44f4fd275..938998a313e77df8d8879533026f1219411641fa 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ChatActivity.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ChatActivity.kt @@ -115,6 +115,7 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc editor = Editor( this, + viewModel.rawAutoCompleteData, viewModel.autoCompleteData, viewModel.lastWord, findViewById(R.id.chatline), diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/Editor.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/Editor.kt index d3a9f21fb2d0ff810360e848f05c68c80fa36539..02dfc3fa2212eb339d1c11134a2c5980f2da6c99 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/Editor.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/Editor.kt @@ -17,7 +17,12 @@ import android.view.* import android.view.inputmethod.EditorInfo import butterknife.BindView import butterknife.ButterKnife +import de.kuschku.libquassel.protocol.Buffer_Type +import de.kuschku.libquassel.quassel.syncables.IrcChannel +import de.kuschku.libquassel.session.ISession import de.kuschku.libquassel.util.IrcUserUtils +import de.kuschku.libquassel.util.Optional +import de.kuschku.libquassel.util.flag.hasFlag import de.kuschku.libquassel.util.helpers.value import de.kuschku.quasseldroid.R import de.kuschku.quasseldroid.settings.AppearanceSettings @@ -30,14 +35,15 @@ import de.kuschku.quasseldroid.util.ui.ColorChooserDialog import de.kuschku.quasseldroid.util.ui.EditTextSelectionChange import de.kuschku.quasseldroid.util.ui.TextDrawable import de.kuschku.quasseldroid.viewmodel.data.AutoCompleteItem +import de.kuschku.quasseldroid.viewmodel.data.BufferStatus import io.reactivex.Observable import io.reactivex.subjects.BehaviorSubject -import java.util.concurrent.TimeUnit class Editor( // Contexts activity: AppCompatActivity, // LiveData + private val autoCompleteDataRaw: Observable<Triple<Optional<ISession>, Int, Pair<String, IntRange>>>, private val autoCompleteData: Observable<Pair<String, List<AutoCompleteItem>>>, lastWordContainer: BehaviorSubject<Observable<Pair<String, IntRange>>>, // Views @@ -165,8 +171,7 @@ class Editor( formatHandler::autoComplete ) - autoCompleteData.debounce(300, TimeUnit.MILLISECONDS) - .toLiveData().observe(activity, Observer { + autoCompleteData.toLiveData().observe(activity, Observer { val query = it?.first ?: "" val shouldShowResults = (autoCompleteSettings.auto && query.length >= 3) || (autoCompleteSettings.prefix && query.startsWith('@')) || @@ -452,12 +457,79 @@ class Editor( chatline.setSelection(selectionStart, selectionEnd) } + private fun autoCompleteDataFull(): List<AutoCompleteItem> { + return autoCompleteDataRaw.value?.let { (sessionOptional, id, lastWord) -> + val session = sessionOptional.orNull() + val bufferInfo = session?.bufferSyncer?.bufferInfo(id) + session?.networks?.let { networks -> + session.bufferSyncer?.bufferInfos()?.let { infos -> + if (bufferInfo?.type?.hasFlag(Buffer_Type.ChannelBuffer) == true) { + val network = networks[bufferInfo.networkId] + network?.ircChannel( + bufferInfo.bufferName + )?.let { ircChannel -> + val users = ircChannel.ircUsers() + val buffers = infos + .filter { + it.type.toInt() == Buffer_Type.ChannelBuffer.toInt() + }.mapNotNull { info -> + networks[info.networkId]?.let { info to it } + }.map { (info, network) -> + val channel = network.ircChannel(info.bufferName) ?: IrcChannel.NULL + AutoCompleteItem.ChannelItem( + info = info, + network = network.networkInfo(), + bufferStatus = when (channel) { + IrcChannel.NULL -> BufferStatus.OFFLINE + else -> BufferStatus.ONLINE + }, + description = channel.topic() + ) + } + val nicks = users.map { user -> + val userModes = ircChannel.userModes(user) + val prefixModes = network.prefixModes() + + val lowestMode = userModes.mapNotNull(prefixModes::indexOf).min() + ?: prefixModes.size + + AutoCompleteItem.UserItem( + user.nick(), + network.modesToPrefixes(userModes), + lowestMode, + user.realName(), + user.isAway(), + network.support("CASEMAPPING"), + Regex("[us]id(\\d+)").matchEntire(user.user())?.groupValues?.lastOrNull()?.let { + "https://www.irccloud.com/avatar-redirect/$it" + } + ) + } + + val ignoredStartingCharacters = charArrayOf( + '-', '_', '[', ']', '{', '}', '|', '`', '^', '.', '\\', '@' + ) + + (nicks + buffers).filter { + it.name.trimStart(*ignoredStartingCharacters) + .startsWith( + lastWord.first.trimStart(*ignoredStartingCharacters), + ignoreCase = true + ) + }.sorted() + } + } else null + } + } + } ?: emptyList() + } + private fun autoComplete(reverse: Boolean = false) { val originalWord = lastWord.value val previous = autocompletionState if (!originalWord.second.isEmpty()) { - val autoCompletedWords = autoCompleteData.value?.second.orEmpty() + val autoCompletedWords = autoCompleteDataFull() if (previous != null && lastWord.value.first == previous.originalWord && lastWord.value.second.start == previous.range.start) { val previousIndex = autoCompletedWords.indexOf(previous.completion) val autoCompletedWord = if (previousIndex != -1) { diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 76c7c5d82ef01d9969e8878783f1e1e70559bf08..f8a241618cd2e99b8e2eed1209725b1cee27c8d7 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -35,7 +35,7 @@ android:key="@string/preference_textsize_key" android:max="24" android:title="@string/preference_textsize_title" - robobunny:min="12" /> + robobunny:min="6" /> <SwitchPreference android:defaultValue="false" @@ -136,4 +136,4 @@ android:summary="@string/preference_show_notification_summary" android:title="@string/preference_show_notification_title" /> </PreferenceCategory> -</PreferenceScreen> \ No newline at end of file +</PreferenceScreen> diff --git a/viewmodel/build.gradle.kts b/viewmodel/build.gradle.kts index 9f832911d0f4e1e87659e086b2e03d60c7aa5eb2..ebb3b5aa66adcf86078c0cbfae6b83df2d91f24f 100644 --- a/viewmodel/build.gradle.kts +++ b/viewmodel/build.gradle.kts @@ -32,6 +32,7 @@ dependencies { } // Utility + implementation("io.reactivex.rxjava2", "rxandroid", "2.0.2") implementation("io.reactivex.rxjava2", "rxjava", "2.1.9") implementation("org.threeten", "threetenbp", "1.3.6", classifier = "no-tzdb") implementation("org.jetbrains", "annotations", "16.0.1") @@ -41,4 +42,4 @@ dependencies { implementation(project(":lib")) { exclude(group = "org.threeten", module = "threetenbp") } -} \ No newline at end of file +} diff --git a/viewmodel/src/main/java/de/kuschku/quasseldroid/util/helper/ObservableHelper.kt b/viewmodel/src/main/java/de/kuschku/quasseldroid/util/helper/ObservableHelper.kt index 64a57cb96a526926be59961c2c7b6cd3e7f3811c..9b6eb6569be05909d384730d3d14b2558b781083 100644 --- a/viewmodel/src/main/java/de/kuschku/quasseldroid/util/helper/ObservableHelper.kt +++ b/viewmodel/src/main/java/de/kuschku/quasseldroid/util/helper/ObservableHelper.kt @@ -4,14 +4,21 @@ import android.arch.lifecycle.LiveData import android.arch.lifecycle.LiveDataReactiveStreams import io.reactivex.* import io.reactivex.functions.BiFunction +import io.reactivex.schedulers.Schedulers inline fun <T> Observable<T>.toLiveData( strategy: BackpressureStrategy = BackpressureStrategy.LATEST -): LiveData<T> = LiveDataReactiveStreams.fromPublisher(toFlowable(strategy)) +): LiveData<T> = LiveDataReactiveStreams.fromPublisher( + subscribeOn(Schedulers.computation()).toFlowable(strategy) +) -inline fun <T> Maybe<T>.toLiveData(): LiveData<T> = LiveDataReactiveStreams.fromPublisher(toFlowable()) +inline fun <T> Maybe<T>.toLiveData(): LiveData<T> = LiveDataReactiveStreams.fromPublisher( + subscribeOn(Schedulers.computation()).toFlowable() +) -inline fun <T> Flowable<T>.toLiveData(): LiveData<T> = LiveDataReactiveStreams.fromPublisher(this) +inline fun <T> Flowable<T>.toLiveData(): LiveData<T> = LiveDataReactiveStreams.fromPublisher( + subscribeOn(Schedulers.computation()) +) inline fun <reified A, B> combineLatest( a: ObservableSource<A>, @@ -55,4 +62,4 @@ data class Tuple4<out A, out B, out C, out D>( val second: B, val third: C, val fourth: D -) \ No newline at end of file +) diff --git a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/QuasselViewModel.kt b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/QuasselViewModel.kt index d6e90f91c964be7c4557c87a5267f7e2fd9adff3..3a5349e2be82aa90b1ce54335ed06c683c402c3f 100644 --- a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/QuasselViewModel.kt +++ b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/QuasselViewModel.kt @@ -214,100 +214,120 @@ class QuasselViewModel : ViewModel() { val lastWord = BehaviorSubject.create<Observable<Pair<String, IntRange>>>() - val autoCompleteData: Observable<Pair<String, List<AutoCompleteItem>>> = + val rawAutoCompleteData: Observable<Triple<Optional<ISession>, Int, Pair<String, IntRange>>> = combineLatest(session, buffer, lastWord).switchMap { (sessionOptional, id, lastWordWrapper) -> lastWordWrapper .distinctUntilChanged() - .switchMap { lastWord -> - val session = sessionOptional.orNull() - val bufferSyncer = session?.bufferSyncer - val bufferInfo = bufferSyncer?.bufferInfo(id) - if (bufferSyncer != null) { - session.liveNetworks().switchMap { networks -> - bufferSyncer.liveBufferInfos().switchMap { infos -> - if (bufferInfo?.type?.hasFlag( - Buffer_Type.ChannelBuffer - ) == true) { - val network = networks[bufferInfo.networkId] - val ircChannel = network?.ircChannel( - bufferInfo.bufferName - ) - if (ircChannel != null) { - ircChannel.liveIrcUsers().switchMap { users -> - val buffers: List<Observable<AutoCompleteItem.ChannelItem>?> = infos.values - .filter { - it.type.toInt() == Buffer_Type.ChannelBuffer.toInt() - }.mapNotNull { info -> - networks[info.networkId]?.let { info to it } - }.map<Pair<BufferInfo, Network>, Observable<AutoCompleteItem.ChannelItem>?> { (info, network) -> - network.liveIrcChannel( - info.bufferName - ).switchMap { channel -> - channel.liveUpdates().map { - AutoCompleteItem.ChannelItem( - info = info, - network = network.networkInfo(), - bufferStatus = when (it) { - IrcChannel.NULL -> BufferStatus.OFFLINE - else -> BufferStatus.ONLINE - }, - description = it.topic() - ) - } - } - } - val nicks = users.map<IrcUser, Observable<AutoCompleteItem.UserItem>?> { - it.updates().map { user -> - val userModes = ircChannel.userModes(user) - val prefixModes = network.prefixModes() - - val lowestMode = userModes.mapNotNull(prefixModes::indexOf).min() - ?: prefixModes.size - - AutoCompleteItem.UserItem( - user.nick(), - network.modesToPrefixes(userModes), - lowestMode, - user.realName(), - user.isAway(), - network.support("CASEMAPPING"), - Regex("[us]id(\\d+)").matchEntire(user.user())?.groupValues?.lastOrNull()?.let { - "https://www.irccloud.com/avatar-redirect/$it" - } + .map { lastWord -> + Triple(sessionOptional, id, lastWord) + } + } + + var time = 0L + var previous: Any? = null + val autoCompleteData = rawAutoCompleteData + .distinctUntilChanged() + .debounce(300, TimeUnit.MILLISECONDS) + .map { + val now = System.currentTimeMillis() + val difference = now - time + if (difference < 300) { + println("Updated too early!: $difference") + } + time = now + if (it == previous) { + println("what the fuck") + } + previous = it + it + } + .switchMap { (sessionOptional, id, lastWord) -> + val session = sessionOptional.orNull() + val bufferSyncer = session?.bufferSyncer + val bufferInfo = bufferSyncer?.bufferInfo(id) + if (bufferSyncer != null) { + session.liveNetworks().switchMap { networks -> + bufferSyncer.liveBufferInfos().switchMap { infos -> + if (bufferInfo?.type?.hasFlag(Buffer_Type.ChannelBuffer) == true) { + val network = networks[bufferInfo.networkId] + val ircChannel = network?.ircChannel( + bufferInfo.bufferName + ) + if (ircChannel != null) { + ircChannel.liveIrcUsers().switchMap { users -> + val buffers: List<Observable<AutoCompleteItem.ChannelItem>?> = infos.values + .filter { + it.type.toInt() == Buffer_Type.ChannelBuffer.toInt() + }.mapNotNull { info -> + networks[info.networkId]?.let { info to it } + }.map { (info, network) -> + network.liveIrcChannel( + info.bufferName + ).switchMap { channel -> + channel.liveUpdates().map { + AutoCompleteItem.ChannelItem( + info = info, + network = network.networkInfo(), + bufferStatus = when (it) { + IrcChannel.NULL -> BufferStatus.OFFLINE + else -> BufferStatus.ONLINE + }, + description = it.topic() ) } } - - combineLatest<AutoCompleteItem>(nicks + buffers) - .map { list -> - val ignoredStartingCharacters = charArrayOf( - '-', '_', '[', ']', '{', '}', '|', '`', '^', '.', '\\', '@' - ) - - Pair( - lastWord.first, - list.filter { - it.name.trimStart(*ignoredStartingCharacters) - .startsWith( - lastWord.first.trimStart(*ignoredStartingCharacters), - ignoreCase = true - ) - }.sorted() - ) + } + val nicks = users.map<IrcUser, Observable<AutoCompleteItem.UserItem>?> { + it.updates().map { user -> + val userModes = ircChannel.userModes(user) + val prefixModes = network.prefixModes() + + val lowestMode = userModes.mapNotNull(prefixModes::indexOf).min() + ?: prefixModes.size + + AutoCompleteItem.UserItem( + user.nick(), + network.modesToPrefixes(userModes), + lowestMode, + user.realName(), + user.isAway(), + network.support("CASEMAPPING"), + Regex("[us]id(\\d+)").matchEntire(user.user())?.groupValues?.lastOrNull()?.let { + "https://www.irccloud.com/avatar-redirect/$it" } + ) } - } else { - Observable.just(Pair(lastWord.first, emptyList())) } - } else { - Observable.just(Pair(lastWord.first, emptyList())) + + combineLatest<AutoCompleteItem>(nicks + buffers) + .map { list -> + val ignoredStartingCharacters = charArrayOf( + '-', '_', '[', ']', '{', '}', '|', '`', '^', '.', '\\', '@' + ) + + Pair( + lastWord.first, + list.filter { + it.name.trimStart(*ignoredStartingCharacters) + .startsWith( + lastWord.first.trimStart(*ignoredStartingCharacters), + ignoreCase = true + ) + }.sorted() + ) + } } + } else { + Observable.just(Pair(lastWord.first, emptyList())) } + } else { + Observable.just(Pair(lastWord.first, emptyList())) } - } else { - Observable.just(Pair(lastWord.first, emptyList())) } } + } else { + Observable.just(Pair(lastWord.first, emptyList())) + } } val bufferViewConfigs = bufferViewManager.mapSwitchMap { manager ->