diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3ed98b035c39c6bcf19d71cd2c4765251e40454d..7b426b435e23d3b9f5f74803822ac5128f78947b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -34,7 +34,7 @@ android { minSdkVersion(16) targetSdkVersion(27) - applicationId = "de.kuschku.quasseldroid_ng" + applicationId = "de.kuschku.quasseldroid" versionCode = cmd("git", "rev-list", "--count", "HEAD")?.toIntOrNull() ?: 1 versionName = cmd("git", "describe", "--always", "--tags", "HEAD") ?: "1.0.0" @@ -83,7 +83,7 @@ android { } dependencies { - implementation(kotlin("stdlib", "1.2.30")) + implementation(kotlin("stdlib", "1.2.31")) // App Compat withVersion("27.1.0") { @@ -136,6 +136,7 @@ dependencies { // UI implementation("me.zhanghai.android.materialprogressbar", "library", "1.4.2") implementation("com.afollestad.material-dialogs", "core", "0.9.6.0") + implementation("com.stepstone.stepper", "material-stepper", "4.3.1") implementation(project(":slidingpanel")) // Quality Assurance diff --git a/app/src/main/java/de/kuschku/quasseldroid/service/QuasselService.kt b/app/src/main/java/de/kuschku/quasseldroid/service/QuasselService.kt index 50faea8d0ab34621d8dfb2db6120b88378cbe148..0dbd525c08339ab5bc7b0a02946dc3308ff5f310 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/service/QuasselService.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/service/QuasselService.kt @@ -69,7 +69,7 @@ class QuasselService : DaggerLifecycleService(), private var progress = Triple(ConnectionState.DISCONNECTED, 0, 0) private fun updateNotificationStatus() { - if (connectionSettings?.showNotification == true) { + if (connectionSettings.showNotification) { val notificationHandle = notificationManager.notificationBackground() this.notificationHandle = notificationHandle updateNotification(notificationHandle) @@ -268,12 +268,15 @@ class QuasselService : DaggerLifecycleService(), sessionManager.state.filter { it == ConnectionState.DISCONNECTED || it == ConnectionState.CLOSED }, connectivity, BiFunction { a: ConnectionState, b: Unit -> a to b }) + .distinctUntilChanged() .delay(200, TimeUnit.MILLISECONDS) - .throttleFirst(1, TimeUnit.SECONDS) + .throttleFirst(5, TimeUnit.SECONDS) .toLiveData() .observe( this, Observer { - sessionManager.reconnect(true) + sessionManager.ifDisconnected { + sessionManager.reconnect(true) + } }) sharedPreferences(Keys.Status.NAME, Context.MODE_PRIVATE) { 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 c80fc117ce4e012fb2f7f874b3aa819e49c5dde0..b8d174a5f87d1097adecf61970bf2751409a5eb6 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 @@ -38,6 +38,7 @@ import de.kuschku.quasseldroid.ui.chat.input.Editor import de.kuschku.quasseldroid.ui.chat.input.MessageHistoryAdapter import de.kuschku.quasseldroid.ui.settings.SettingsActivity import de.kuschku.quasseldroid.util.helper.invoke +import de.kuschku.quasseldroid.util.helper.toLiveData import de.kuschku.quasseldroid.util.service.ServiceBoundActivity import de.kuschku.quasseldroid.util.ui.MaterialContentLoadingProgressBar import de.kuschku.quasseldroid.viewmodel.QuasselViewModel @@ -100,11 +101,11 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc ButterKnife.bind(this) viewModel = ViewModelProviders.of(this)[QuasselViewModel::class.java] - viewModel.setBackend(this.backend) + viewModel.backendWrapper.onNext(this.backend) editor = Editor( this, - viewModel.autoCompleteData, + viewModel.autoCompleteData.toLiveData(), viewModel.lastWord, findViewById(R.id.chatline), findViewById(R.id.send), @@ -146,11 +147,12 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc historyPanel.panelState = SlidingUpPanelLayout.PanelState.COLLAPSED } msgHistory.adapter = messageHistoryAdapter - viewModel.recentlySentMessages.observe(this, Observer(messageHistoryAdapter::submitList)) + viewModel.recentlySentMessages_liveData.observe(this, + Observer(messageHistoryAdapter::submitList)) setSupportActionBar(toolbar) - viewModel.buffer.observe(this, Observer { + viewModel.buffer_liveData.observe(this, Observer { if (it != null && drawerLayout.isDrawerOpen(Gravity.START)) { drawerLayout.closeDrawer(Gravity.START, true) } @@ -168,7 +170,7 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc drawerToggle.syncState() } - viewModel.errors.observe(this, Observer { + viewModel.errors_liveData.observe(this, Observer { when (it) { is HandshakeMessage.ClientInitReject -> MaterialDialog.Builder(this) @@ -194,7 +196,7 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc } }) - viewModel.connectionProgress.observe(this, Observer { it -> + viewModel.connectionProgress_liveData.observe(this, Observer { it -> val (state, progress, max) = it ?: Triple(ConnectionState.DISCONNECTED, 0, 0) when (state) { ConnectionState.CONNECTED, @@ -239,7 +241,7 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc override fun onRestoreInstanceState(savedInstanceState: Bundle?) { super.onRestoreInstanceState(savedInstanceState) - viewModel.buffer.value = savedInstanceState?.getInt("OPEN_BUFFER", -1) ?: -1 + viewModel.buffer.onNext(savedInstanceState?.getInt("OPEN_BUFFER", -1) ?: -1) } @TargetApi(Build.VERSION_CODES.LOLLIPOP) @@ -248,7 +250,7 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc super.onRestoreInstanceState(savedInstanceState, persistentState) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { val fallback = persistentState?.getInt("OPEN_BUFFER", -1) ?: -1 - viewModel.buffer.value = savedInstanceState?.getInt("OPEN_BUFFER", fallback) ?: fallback + viewModel.buffer.onNext(savedInstanceState?.getInt("OPEN_BUFFER", fallback) ?: fallback) } } @@ -263,47 +265,49 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc } R.id.filter_messages -> { - viewModel.buffer { buffer -> - val filtered = Message_Type.of(database.filtered().get(accountId, buffer) ?: 0) - val flags = intArrayOf( - Message.MessageType.Join.bit or Message.MessageType.NetsplitJoin.bit, - Message.MessageType.Part.bit, - Message.MessageType.Quit.bit or Message.MessageType.NetsplitQuit.bit, - Message.MessageType.Nick.bit, - Message.MessageType.Mode.bit, - Message.MessageType.Topic.bit - ) - val selectedIndices = flags.withIndex().mapNotNull { (index, flag) -> - if ((filtered and flag).isNotEmpty()) { - index - } else { - null - } - }.toTypedArray() - - MaterialDialog.Builder(this) - .title(R.string.label_filter_messages) - .items(R.array.message_filter_types) - .itemsIds(flags) - .itemsCallbackMultiChoice(selectedIndices, { _, _, _ -> false }) - .positiveText(R.string.label_select_multiple) - .negativeText(R.string.label_cancel) - .onPositive { dialog, _ -> - val selected = dialog.selectedIndices ?: emptyArray() - runInBackground { - val newlyFiltered = selected - .map { flags[it] } - .fold(Message_Type.of()) { acc, i -> acc or i } - - database.filtered().replace( - QuasselDatabase.Filtered(accountId, buffer, newlyFiltered.value) - ) + runInBackground { + viewModel.buffer { buffer -> + val filtered = Message_Type.of(database.filtered().get(accountId, buffer) ?: 0) + val flags = intArrayOf( + Message.MessageType.Join.bit or Message.MessageType.NetsplitJoin.bit, + Message.MessageType.Part.bit, + Message.MessageType.Quit.bit or Message.MessageType.NetsplitQuit.bit, + Message.MessageType.Nick.bit, + Message.MessageType.Mode.bit, + Message.MessageType.Topic.bit + ) + val selectedIndices = flags.withIndex().mapNotNull { (index, flag) -> + if ((filtered and flag).isNotEmpty()) { + index + } else { + null } - }.negativeColorAttr(R.attr.colorTextPrimary) - .backgroundColorAttr(R.attr.colorBackgroundCard) - .contentColorAttr(R.attr.colorTextPrimary) - .build() - .show() + }.toTypedArray() + + MaterialDialog.Builder(this) + .title(R.string.label_filter_messages) + .items(R.array.message_filter_types) + .itemsIds(flags) + .itemsCallbackMultiChoice(selectedIndices, { _, _, _ -> false }) + .positiveText(R.string.label_select_multiple) + .negativeText(R.string.label_cancel) + .onPositive { dialog, _ -> + val selected = dialog.selectedIndices ?: emptyArray() + runInBackground { + val newlyFiltered = selected + .map { flags[it] } + .fold(Message_Type.of()) { acc, i -> acc or i } + + database.filtered().replace( + QuasselDatabase.Filtered(accountId, buffer, newlyFiltered.value) + ) + } + }.negativeColorAttr(R.attr.colorTextPrimary) + .backgroundColorAttr(R.attr.colorBackgroundCard) + .contentColorAttr(R.attr.colorTextPrimary) + .build() + .show() + } } true } diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ToolbarFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ToolbarFragment.kt index e6961292f39af686181c242df05e71d2755aa425..a54b42ca7f10f649df11cb9ed49b6f621d920930 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ToolbarFragment.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ToolbarFragment.kt @@ -14,8 +14,9 @@ import de.kuschku.libquassel.protocol.Buffer_Type import de.kuschku.libquassel.util.hasFlag import de.kuschku.quasseldroid.R import de.kuschku.quasseldroid.settings.AppearanceSettings +import de.kuschku.quasseldroid.util.helper.combineLatest +import de.kuschku.quasseldroid.util.helper.toLiveData import de.kuschku.quasseldroid.util.helper.visibleIf -import de.kuschku.quasseldroid.util.helper.zip import de.kuschku.quasseldroid.util.irc.format.IrcFormatDeserializer import de.kuschku.quasseldroid.util.service.ServiceBoundFragment import de.kuschku.quasseldroid.util.ui.SpanFormatter @@ -58,38 +59,41 @@ class ToolbarFragment : ServiceBoundFragment() { viewModel = ViewModelProviders.of(activity!!)[QuasselViewModel::class.java] } - override fun onCreateView(inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle?): View? { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { val view = inflater.inflate(R.layout.fragment_toolbar, container, false) ButterKnife.bind(this, view) - viewModel.bufferData.zip(viewModel.isSecure, viewModel.lag).observe( - this, Observer { - if (it != null) { - val (data, isSecure, lag) = it - if (data?.info?.type?.hasFlag(Buffer_Type.StatusBuffer) == true) { - this.title = data.network?.networkName - } else { - this.title = data?.info?.bufferName - } + combineLatest(viewModel.bufferData, viewModel.isSecure, viewModel.lag).toLiveData() + .observe(this, Observer { + if (it != null) { + val (data, isSecure, lag) = it + if (data?.info?.type?.hasFlag(Buffer_Type.StatusBuffer) == true) { + this.title = data.network?.networkName + } else { + this.title = data?.info?.bufferName + } - if (lag == 0L || !appearanceSettings.showLag) { - this.subtitle = colorizeDescription(data?.description) - } else { - val description = colorizeDescription(data?.description) - if (description.isNullOrBlank()) { - this.subtitle = "Lag: ${lag}ms" + if (lag == 0L || !appearanceSettings.showLag) { + this.subtitle = colorizeDescription(data?.description) } else { - this.subtitle = SpanFormatter.format( - "Lag: %dms | %s", - lag, - colorizeDescription(data?.description) - ) + val description = colorizeDescription(data?.description) + if (description.isNullOrBlank()) { + this.subtitle = "Lag: ${lag}ms" + } else { + this.subtitle = SpanFormatter.format( + "Lag: %dms | %s", + lag, + colorizeDescription(data?.description) + ) + } } } } - }) + ) return view } diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferListAdapter.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferListAdapter.kt index 190c4ac470be3dccfc96702e20c69a90314909b3..e720a15720dd592c6197703f1287d622aa4f8424 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferListAdapter.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferListAdapter.kt @@ -2,7 +2,6 @@ package de.kuschku.quasseldroid.ui.chat.buffers import android.arch.lifecycle.LifecycleOwner import android.arch.lifecycle.LiveData -import android.arch.lifecycle.MutableLiveData import android.arch.lifecycle.Observer import android.graphics.drawable.Drawable import android.support.v4.graphics.drawable.DrawableCompat @@ -22,20 +21,18 @@ import de.kuschku.libquassel.protocol.NetworkId import de.kuschku.libquassel.quassel.BufferInfo import de.kuschku.libquassel.util.hasFlag import de.kuschku.quasseldroid.R -import de.kuschku.quasseldroid.util.helper.getCompatDrawable -import de.kuschku.quasseldroid.util.helper.styledAttributes -import de.kuschku.quasseldroid.util.helper.visibleIf -import de.kuschku.quasseldroid.util.helper.zip +import de.kuschku.quasseldroid.util.helper.* import de.kuschku.quasseldroid.viewmodel.data.BufferListItem import de.kuschku.quasseldroid.viewmodel.data.BufferProps import de.kuschku.quasseldroid.viewmodel.data.BufferState import de.kuschku.quasseldroid.viewmodel.data.BufferStatus +import io.reactivex.subjects.BehaviorSubject class BufferListAdapter( lifecycleOwner: LifecycleOwner, liveData: LiveData<List<BufferProps>?>, - private val selectedBuffer: MutableLiveData<BufferId>, - private val collapsedNetworks: MutableLiveData<Set<NetworkId>>, + private val selectedBuffer: BehaviorSubject<BufferId>, + private val collapsedNetworks: BehaviorSubject<Set<NetworkId>>, runInBackground: (() -> Unit) -> Any, runOnUiThread: (Runnable) -> Any, private val clickListener: ((BufferId) -> Unit)? = null, @@ -45,25 +42,25 @@ class BufferListAdapter( fun expandListener(networkId: NetworkId) { if (collapsedNetworks.value.orEmpty().contains(networkId)) - collapsedNetworks.postValue(collapsedNetworks.value.orEmpty() - networkId) + collapsedNetworks.onNext(collapsedNetworks.value.orEmpty() - networkId) else - collapsedNetworks.postValue(collapsedNetworks.value.orEmpty() + networkId) + collapsedNetworks.onNext(collapsedNetworks.value.orEmpty() + networkId) } fun toggleSelection(buffer: BufferId) { if (selectedBuffer.value == buffer) { - selectedBuffer.value = -1 + selectedBuffer.onNext(-1) } else { - selectedBuffer.value = buffer + selectedBuffer.onNext(buffer) } } fun unselectAll() { - selectedBuffer.value = -1 + selectedBuffer.onNext(-1) } init { - liveData.zip(collapsedNetworks, selectedBuffer).observe( + liveData.zip(collapsedNetworks.toLiveData(), selectedBuffer.toLiveData()).observe( lifecycleOwner, Observer { it: Triple<List<BufferProps>?, Set<NetworkId>, BufferId>? -> runInBackground { val list = it?.first ?: emptyList() diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferViewConfigFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferViewConfigFragment.kt index 97338e7ae25f1450eb24985ba10cb3acc05e3bdd..714e484ade6dad537465ee9b682c1e530271cc27 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferViewConfigFragment.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferViewConfigFragment.kt @@ -19,11 +19,13 @@ import de.kuschku.libquassel.protocol.Buffer_Type import de.kuschku.libquassel.protocol.Message_Type import de.kuschku.libquassel.quassel.syncables.interfaces.INetwork import de.kuschku.libquassel.util.hasFlag +import de.kuschku.libquassel.util.helpers.value import de.kuschku.libquassel.util.minus import de.kuschku.quasseldroid.R import de.kuschku.quasseldroid.persistence.QuasselDatabase import de.kuschku.quasseldroid.settings.AppearanceSettings import de.kuschku.quasseldroid.util.helper.map +import de.kuschku.quasseldroid.util.helper.toLiveData import de.kuschku.quasseldroid.util.helper.zip import de.kuschku.quasseldroid.util.irc.format.IrcFormatDeserializer import de.kuschku.quasseldroid.util.service.ServiceBoundFragment @@ -126,16 +128,16 @@ class BufferViewConfigFragment : ServiceBoundFragment() { } R.id.action_unhide -> { bufferSyncer?.let { - bufferViewConfig?.requestAddBuffer(info, bufferSyncer) + bufferViewConfig?.orNull()?.requestAddBuffer(info, bufferSyncer) } true } R.id.action_hide_temp -> { - bufferViewConfig?.requestRemoveBuffer(info.bufferId) + bufferViewConfig?.orNull()?.requestRemoveBuffer(info.bufferId) true } R.id.action_hide_perm -> { - bufferViewConfig?.requestRemoveBufferPermanently(info.bufferId) + bufferViewConfig?.orNull()?.requestRemoveBufferPermanently(info.bufferId) true } else -> false @@ -168,27 +170,29 @@ class BufferViewConfigFragment : ServiceBoundFragment() { viewModel = ViewModelProviders.of(activity!!)[QuasselViewModel::class.java] } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?): View? { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { val view = inflater.inflate(R.layout.fragment_chat_list, container, false) ButterKnife.bind(this, view) - val adapter = BufferViewConfigAdapter(this, viewModel.bufferViewConfigs) + val adapter = BufferViewConfigAdapter(this, viewModel.bufferViewConfigs.toLiveData()) chatListSpinner.adapter = adapter chatListSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onNothingSelected(p0: AdapterView<*>?) { - viewModel.setBufferViewConfigId(null) + viewModel.bufferViewConfigId.onNext(-1) } override fun onItemSelected(p0: AdapterView<*>?, p1: View?, p2: Int, p3: Long) { - viewModel.setBufferViewConfigId(adapter.getItem(p2)?.bufferViewId()) + viewModel.bufferViewConfigId.onNext(adapter.getItem(p2)?.bufferViewId() ?: -1) } } listAdapter = BufferListAdapter( this, - viewModel.bufferList.zip(database.filtered().listen(accountId)).map { + viewModel.bufferList.toLiveData().zip(database.filtered().listen(accountId)).map { val (data, activityList) = it val (config, list) = data ?: Pair(null, emptyList()) val minimumActivity = config?.minimumActivity() ?: Buffer_Activity.NONE @@ -225,7 +229,7 @@ class BufferViewConfigFragment : ServiceBoundFragment() { ) chatList.adapter = listAdapter - viewModel.selectedBuffer.observe(this, Observer { buffer -> + viewModel.selectedBuffer_liveData.observe(this, Observer { buffer -> if (buffer != null) { val menu = actionMode?.menu if (menu != null) { @@ -298,7 +302,7 @@ class BufferViewConfigFragment : ServiceBoundFragment() { when (item.itemId) { R.id.action_show_hidden -> { item.isChecked = !item.isChecked - viewModel.showHidden.value = item.isChecked + viewModel.showHidden.onNext(item.isChecked) true } else -> false @@ -314,7 +318,7 @@ class BufferViewConfigFragment : ServiceBoundFragment() { if (actionMode != null) { longClickListener?.invoke(it) } else { - viewModel.buffer.value = it + viewModel.buffer.onNext(it) } } 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 61987afdefc1ca0b417005c0538264035ed22023..ab3ceea63e448fd933f653552b1961f1d1aad7e5 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 @@ -1,7 +1,6 @@ package de.kuschku.quasseldroid.ui.chat.input import android.arch.lifecycle.LiveData -import android.arch.lifecycle.MutableLiveData import android.arch.lifecycle.Observer import android.support.v7.app.AppCompatActivity import android.support.v7.widget.* @@ -25,8 +24,8 @@ class Editor( // Contexts activity: AppCompatActivity, // LiveData - private val autoCompleteData: LiveData<Pair<String, List<AutoCompleteItem>>?>, - lastWordContainer: MutableLiveData<Observable<Pair<String, IntRange>>>, + private val autoCompleteData: LiveData<Pair<String, List<AutoCompleteItem>>>, + lastWordContainer: BehaviorSubject<Observable<Pair<String, IntRange>>>, // Views val chatline: AppCompatEditText, send: AppCompatImageButton, @@ -138,7 +137,7 @@ class Editor( } } - lastWordContainer.value = lastWord + lastWordContainer.onNext(lastWord) activity.menuInflater.inflate(formatHandler.menu, formattingMenu.menu) formattingMenu.menu.retint(activity) diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageListFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageListFragment.kt index 2be7de09c64e223b837b0b0b61e2bae2c4fbea8f..47fe8b8075bbe9df07cfa75b888a835d50dc5dd8 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageListFragment.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageListFragment.kt @@ -16,6 +16,7 @@ import butterknife.ButterKnife import de.kuschku.libquassel.protocol.BufferId import de.kuschku.libquassel.protocol.MsgId import de.kuschku.libquassel.quassel.syncables.BufferSyncer +import de.kuschku.libquassel.util.helpers.value import de.kuschku.quasseldroid.R import de.kuschku.quasseldroid.persistence.QuasselDatabase import de.kuschku.quasseldroid.settings.AppearanceSettings @@ -66,8 +67,10 @@ class MessageListFragment : ServiceBoundFragment() { override fun onItemAtEndLoaded(itemAtEnd: QuasselDatabase.DatabaseMessage) = loadMore() } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?): View? { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { val view = inflater.inflate(R.layout.fragment_messages, container, false) ButterKnife.bind(this, view) @@ -90,7 +93,7 @@ class MessageListFragment : ServiceBoundFragment() { } }) - val data = viewModel.buffer.switchMapNotNull { buffer -> + val data = viewModel.buffer_liveData.switchMapNotNull { buffer -> database.filtered().listen(accountId, buffer).switchMapNotNull { filtered -> LivePagedListBuilder( database.message().findByBufferIdPaged(buffer, filtered), @@ -104,11 +107,11 @@ class MessageListFragment : ServiceBoundFragment() { } } - val lastMessageId = viewModel.buffer.switchMapNotNull { + val lastMessageId = viewModel.buffer_liveData.switchMapNotNull { database.message().lastMsgId(it) } - viewModel.sessionManager.zip(lastMessageId).observe( + viewModel.sessionManager_liveData.zip(lastMessageId).observe( this, Observer { runInBackground { val session = it?.first @@ -121,7 +124,7 @@ class MessageListFragment : ServiceBoundFragment() { } }) - viewModel.markerLine.observe(this, Observer { + viewModel.markerLine_liveData.observe(this, Observer { adapter.markerLinePosition = it adapter.notifyDataSetChanged() }) @@ -131,15 +134,8 @@ class MessageListFragment : ServiceBoundFragment() { val firstVisibleItemPosition = linearLayoutManager.findFirstVisibleItemPosition() val firstVisibleMessageId = adapter[firstVisibleItemPosition]?.messageId runInBackground { - val buffer = viewModel.buffer.value ?: -1 - if (buffer != lastBuffer) { - backend.value?.sessionManager()?.bufferSyncer?.let { bufferSyncer -> - onBufferChange(lastBuffer, buffer, firstVisibleMessageId, bufferSyncer) - } - lastBuffer = buffer - adapter.clearCache() - } adapter.submitList(list) + if (firstVisibleItemPosition < 2) { activity?.runOnUiThread { messageList.scrollToPosition(0) } runInBackgroundDelayed(16) { @@ -148,6 +144,15 @@ class MessageListFragment : ServiceBoundFragment() { } } } + + val buffer = viewModel.buffer.value ?: -1 + if (buffer != lastBuffer) { + backend.value.orNull()?.sessionManager()?.bufferSyncer?.let { bufferSyncer -> + onBufferChange(lastBuffer, buffer, firstVisibleMessageId, bufferSyncer) + } + lastBuffer = buffer + adapter.clearCache() + } } }) scrollDown.hide() @@ -161,8 +166,10 @@ class MessageListFragment : ServiceBoundFragment() { bufferSyncer.requestSetLastSeenMsg(buffer, lastMessageId) } - private fun onBufferChange(previous: BufferId?, current: BufferId, lastMessageId: MsgId?, - bufferSyncer: BufferSyncer) { + private fun onBufferChange( + previous: BufferId?, current: BufferId, lastMessageId: MsgId?, + bufferSyncer: BufferSyncer + ) { if (previous != null && lastMessageId != null) { bufferSyncer.requestSetMarkerLine(previous, lastMessageId) } @@ -187,13 +194,15 @@ class MessageListFragment : ServiceBoundFragment() { private fun loadMore() { runInBackground { viewModel.buffer { bufferId -> - viewModel.sessionManager()?.backlogManager?.requestBacklog( - bufferId = bufferId, - last = database.message().findFirstByBufferId( - bufferId - )?.messageId ?: -1, - limit = backlogSettings.dynamicAmount - ) + viewModel.sessionManager { + it.backlogManager?.requestBacklog( + bufferId = bufferId, + last = database.message().findFirstByBufferId( + bufferId + )?.messageId ?: -1, + limit = backlogSettings.dynamicAmount + ) + } } } } diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/nicks/NickListFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/nicks/NickListFragment.kt index a429a77a2083a1288f1a7bd2a71de2e9c2db04fa..242998f2776a4d693259de7edd2d88b8ebb0be8e 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/nicks/NickListFragment.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/nicks/NickListFragment.kt @@ -14,7 +14,7 @@ import butterknife.ButterKnife import de.kuschku.libquassel.util.irc.IrcCaseMappers import de.kuschku.quasseldroid.R import de.kuschku.quasseldroid.settings.AppearanceSettings -import de.kuschku.quasseldroid.util.helper.map +import de.kuschku.quasseldroid.util.helper.toLiveData import de.kuschku.quasseldroid.util.irc.format.IrcFormatDeserializer import de.kuschku.quasseldroid.util.service.ServiceBoundFragment import de.kuschku.quasseldroid.viewmodel.QuasselViewModel @@ -65,7 +65,7 @@ class NickListFragment : ServiceBoundFragment() { }.sortedBy { it.lowestMode } - }.observe(this, Observer(nickListAdapter::submitList)) + }.toLiveData().observe(this, Observer(nickListAdapter::submitList)) return view } diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/service/BackendServiceConnection.kt b/app/src/main/java/de/kuschku/quasseldroid/util/service/BackendServiceConnection.kt index 45f4f2f5911afd535504ae14b5c7ccff0ef1d34e..35a72d7c63337a8855a86dc80c19dfaac96be099 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/util/service/BackendServiceConnection.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/util/service/BackendServiceConnection.kt @@ -1,23 +1,24 @@ package de.kuschku.quasseldroid.util.service -import android.arch.lifecycle.MutableLiveData import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.ServiceConnection import android.os.IBinder import de.kuschku.libquassel.session.Backend +import de.kuschku.libquassel.util.Optional import de.kuschku.quasseldroid.service.QuasselService +import io.reactivex.subjects.BehaviorSubject class BackendServiceConnection : ServiceConnection { - val backend = MutableLiveData<Backend?>() + val backend = BehaviorSubject.createDefault(Optional.empty<Backend>()) var context: Context? = null override fun onServiceDisconnected(component: ComponentName?) { when (component) { ComponentName(context, QuasselService::class.java) -> { - backend.value = null + backend.onNext(Optional.empty()) } } } @@ -26,7 +27,7 @@ class BackendServiceConnection : ServiceConnection { when (component) { ComponentName(context, QuasselService::class.java) -> if (binder is QuasselService.QuasselBinder) { - backend.value = binder.backend + backend.onNext(Optional.of(binder.backend)) } } } diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/service/ServiceBoundActivity.kt b/app/src/main/java/de/kuschku/quasseldroid/util/service/ServiceBoundActivity.kt index 0d6757ba6d61e7f371216a32b2f9a6ff781d2752..d5e987d835c28e0a78fae283cb9f494d403c216c 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/util/service/ServiceBoundActivity.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/util/service/ServiceBoundActivity.kt @@ -1,7 +1,6 @@ package de.kuschku.quasseldroid.util.service import android.app.Activity -import android.arch.lifecycle.LiveData import android.content.Context import android.content.Intent import android.content.SharedPreferences @@ -16,6 +15,7 @@ import dagger.android.DispatchingAndroidInjector import dagger.android.HasFragmentInjector import dagger.android.support.HasSupportFragmentInjector import de.kuschku.libquassel.session.Backend +import de.kuschku.libquassel.util.Optional import de.kuschku.libquassel.util.compatibility.LoggingHandler import de.kuschku.libquassel.util.compatibility.LoggingHandler.Companion.log import de.kuschku.quasseldroid.Keys @@ -24,9 +24,9 @@ import de.kuschku.quasseldroid.settings.AppearanceSettings import de.kuschku.quasseldroid.settings.ConnectionSettings import de.kuschku.quasseldroid.settings.Settings import de.kuschku.quasseldroid.ui.setup.accounts.selection.AccountSelectionActivity -import de.kuschku.quasseldroid.util.helper.invoke import de.kuschku.quasseldroid.util.helper.sharedPreferences import de.kuschku.quasseldroid.util.helper.updateRecentsHeaderIfExisting +import io.reactivex.subjects.BehaviorSubject import javax.inject.Inject abstract class ServiceBoundActivity : AppCompatActivity(), @@ -39,7 +39,7 @@ abstract class ServiceBoundActivity : AppCompatActivity(), protected val recentsHeaderColor: Int = R.color.colorPrimary private val connection = BackendServiceConnection() - protected val backend: LiveData<Backend?> + protected val backend: BehaviorSubject<Optional<Backend>> get() = connection.backend @@ -58,13 +58,13 @@ abstract class ServiceBoundActivity : AppCompatActivity(), } protected fun runInBackground(f: () -> Unit) { - connection.backend { + connection.backend.value.ifPresent { it.sessionManager().handlerService.backend(f) } } protected fun runInBackgroundDelayed(delayMillis: Long, f: () -> Unit) { - connection.backend { + connection.backend.value.ifPresent { it.sessionManager().handlerService.backendDelayed(delayMillis, f) } } diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/service/ServiceBoundFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/util/service/ServiceBoundFragment.kt index e2e582f8d147886f9b54740de9c4aeebbadad205..bb01640e58ec3723d8a4c476edb291692ea49ca5 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/util/service/ServiceBoundFragment.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/util/service/ServiceBoundFragment.kt @@ -1,27 +1,26 @@ package de.kuschku.quasseldroid.util.service -import android.arch.lifecycle.LiveData import android.content.Context import android.os.Bundle import dagger.android.support.DaggerFragment import de.kuschku.libquassel.session.Backend +import de.kuschku.libquassel.util.Optional import de.kuschku.quasseldroid.Keys -import de.kuschku.quasseldroid.util.helper.invoke +import io.reactivex.subjects.BehaviorSubject abstract class ServiceBoundFragment : DaggerFragment() { - private var connection = BackendServiceConnection() - - protected val backend: LiveData<Backend?> + private val connection = BackendServiceConnection() + protected val backend: BehaviorSubject<Optional<Backend>> get() = connection.backend protected fun runInBackground(f: () -> Unit) { - connection.backend { + connection.backend.value.ifPresent { it.sessionManager().handlerService.backend(f) } } protected fun runInBackgroundDelayed(delayMillis: Long, f: () -> Unit) { - connection.backend { + connection.backend.value.ifPresent { it.sessionManager().handlerService.backendDelayed(delayMillis, f) } } diff --git a/app/src/main/res/layout/widget_chatmessage_action.xml b/app/src/main/res/layout/widget_chatmessage_action.xml index 43dc3ce6c0c3bda44dba857ed6ac535b2bc9def2..1270a6d77986a435546914430be3a4f3d67dad75 100644 --- a/app/src/main/res/layout/widget_chatmessage_action.xml +++ b/app/src/main/res/layout/widget_chatmessage_action.xml @@ -1,30 +1,8 @@ -<?xml version="1.0" encoding="utf-8"?><!-- - ~ QuasselDroid - Quassel client for Android - ~ Copyright (C) 2016 Janne Koschinski - ~ Copyright (C) 2016 Ken Børge Viktil - ~ Copyright (C) 2016 Magnus Fjell - ~ Copyright (C) 2016 Martin Sandsmark <martin.sandsmark@kde.org> - ~ - ~ This program is free software: you can redistribute it and/or modify it - ~ under the terms of the GNU General Public License as published by the Free - ~ Software Foundation, either version 3 of the License, or (at your option) - ~ any later version. - ~ - ~ This program is distributed in the hope that it will be useful, - ~ but WITHOUT ANY WARRANTY; without even the implied warranty of - ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - ~ GNU General Public License for more details. - ~ - ~ You should have received a copy of the GNU General Public License along - ~ with this program. If not, see <http://www.gnu.org/licenses/>. - --> - +<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:clickable="true" - android:focusable="true" android:orientation="vertical" android:textAppearance="?android:attr/textAppearanceListItemSmall"> diff --git a/app/src/main/res/layout/widget_chatmessage_error.xml b/app/src/main/res/layout/widget_chatmessage_error.xml index 9b8eac026f9133171996190ef6e38b707fe341c5..cbcd73b12de21b16cdb4954b5db780a72c0c1931 100644 --- a/app/src/main/res/layout/widget_chatmessage_error.xml +++ b/app/src/main/res/layout/widget_chatmessage_error.xml @@ -3,8 +3,6 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:clickable="true" - android:focusable="true" android:orientation="vertical" android:textAppearance="?android:attr/textAppearanceListItemSmall"> diff --git a/app/src/main/res/layout/widget_chatmessage_info.xml b/app/src/main/res/layout/widget_chatmessage_info.xml index 0748dd14c9a74f76a7f7ed3f44919772606f04b6..ba643efbbe7f4574df29c72f349322ba529e2e10 100644 --- a/app/src/main/res/layout/widget_chatmessage_info.xml +++ b/app/src/main/res/layout/widget_chatmessage_info.xml @@ -3,8 +3,6 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:clickable="true" - android:focusable="true" android:orientation="vertical" android:textAppearance="?android:attr/textAppearanceListItemSmall"> diff --git a/app/src/main/res/layout/widget_chatmessage_notice.xml b/app/src/main/res/layout/widget_chatmessage_notice.xml index facbf98b298215e434b981ce39342d80292e5349..6bcf8f7cbac80d5a68bda2e0104d5ba4e09d5d2b 100644 --- a/app/src/main/res/layout/widget_chatmessage_notice.xml +++ b/app/src/main/res/layout/widget_chatmessage_notice.xml @@ -3,8 +3,6 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:clickable="true" - android:focusable="true" android:orientation="vertical" android:textAppearance="?android:attr/textAppearanceListItemSmall"> diff --git a/app/src/main/res/layout/widget_chatmessage_plain.xml b/app/src/main/res/layout/widget_chatmessage_plain.xml index 6828a4424a1e38d8324d098652e02176a5ec8061..476865332c167682107583f55db2fd4f7e7ae74a 100644 --- a/app/src/main/res/layout/widget_chatmessage_plain.xml +++ b/app/src/main/res/layout/widget_chatmessage_plain.xml @@ -3,8 +3,6 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:clickable="true" - android:focusable="true" android:orientation="vertical" android:textAppearance="?android:attr/textAppearanceListItemSmall"> diff --git a/app/src/main/res/layout/widget_chatmessage_server.xml b/app/src/main/res/layout/widget_chatmessage_server.xml index fef073da00f2d51f19fe48c33e458907707cc789..00003711c80e2ea2d9ebcb280b9d0a7134959931 100644 --- a/app/src/main/res/layout/widget_chatmessage_server.xml +++ b/app/src/main/res/layout/widget_chatmessage_server.xml @@ -3,8 +3,6 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:clickable="true" - android:focusable="true" android:orientation="vertical" android:textAppearance="?android:attr/textAppearanceListItemSmall"> diff --git a/build.gradle.kts b/build.gradle.kts index 8a0c5cb83d91d54564192987bece64b4ca07befe..4976af8e5165407aa2d7cc5d685f318f077b626d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,7 +5,7 @@ buildscript { } dependencies { classpath("com.android.tools.build:gradle:3.0.1") - withVersion("1.2.30") { + withVersion("1.2.31") { classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$version") classpath("org.jetbrains.kotlin:kotlin-android-extensions:$version") } diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index 8c2b7c1a60beafb58b56e0b60dc5f09af07ecb04..53138ef030003af6f550d44f322607d07d42c4f7 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -10,7 +10,7 @@ plugins { } dependencies { - implementation(kotlin("stdlib", "1.2.30")) + implementation(kotlin("stdlib", "1.2.31")) withVersion("27.1.0") { implementation("com.android.support", "support-annotations", version) 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 9a2e22becec211391bc756f51a506ac4b5666d79..58e46597c600b4ac1b57ffc5b12096540df14f99 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 @@ -8,7 +8,7 @@ import java.nio.CharBuffer import java.nio.charset.Charset import java.nio.charset.CharsetDecoder import java.nio.charset.CharsetEncoder -import java.util.concurrent.atomic.AtomicReference +import kotlin.concurrent.getOrSet abstract class StringSerializer( private val encoder: CharsetEncoder, @@ -26,14 +26,17 @@ abstract class StringSerializer( } ) - private val thread = AtomicReference<Thread>() - private val charBuffer = CharBuffer.allocate(1024) + private val charBuffer = ThreadLocal<CharBuffer>() object UTF16 : StringSerializer(Charsets.UTF_16BE) object UTF8 : StringSerializer(Charsets.UTF_8) object C : StringSerializer(Charsets.ISO_8859_1, trailingNullByte = true) private inline fun charBuffer(len: Int): CharBuffer { + val charBuffer = charBuffer.getOrSet { + CharBuffer.allocate(1024) + } + val buf = if (len >= 1024) CharBuffer.allocate(len) else @@ -43,96 +46,75 @@ abstract class StringSerializer( return buf } - private inline fun <T> preventThreadRaces(f: () -> T): T { - val currentThread = Thread.currentThread() - if (!thread.compareAndSet(null, currentThread)) { - throw RuntimeException("Illegal Thread access!") - } - val result: T = f() - if (!thread.compareAndSet(currentThread, null)) { - throw RuntimeException("Illegal Thread access!") - } - return result - } - override fun serialize(buffer: ChainedByteBuffer, data: String?, features: Quassel_Features) = - preventThreadRaces { - try { - if (data == null) { - IntSerializer.serialize(buffer, -1, features) - } else { - val charBuffer = charBuffer(data.length) - charBuffer.put(data) - charBuffer.flip() - encoder.reset() - val byteBuffer = encoder.encode(charBuffer) - IntSerializer.serialize(buffer, byteBuffer.remaining() + trailingNullBytes, features) - buffer.put(byteBuffer) - for (i in 0 until trailingNullBytes) - buffer.put(0) - } - } catch (e: Throwable) { - throw RuntimeException(data, e) - } - } - - fun serialize(data: String?): ByteBuffer = preventThreadRaces { try { if (data == null) { - ByteBuffer.allocate(0) + IntSerializer.serialize(buffer, -1, features) } else { val charBuffer = charBuffer(data.length) charBuffer.put(data) charBuffer.flip() encoder.reset() - encoder.encode(charBuffer) + val byteBuffer = encoder.encode(charBuffer) + IntSerializer.serialize(buffer, byteBuffer.remaining() + trailingNullBytes, features) + buffer.put(byteBuffer) + for (i in 0 until trailingNullBytes) + buffer.put(0) } } catch (e: Throwable) { throw RuntimeException(data, e) } + + fun serialize(data: String?): ByteBuffer = try { + if (data == null) { + ByteBuffer.allocate(0) + } else { + val charBuffer = charBuffer(data.length) + charBuffer.put(data) + charBuffer.flip() + encoder.reset() + encoder.encode(charBuffer) + } + } catch (e: Throwable) { + throw RuntimeException(data, e) } - fun deserializeAll(buffer: ByteBuffer): String? = preventThreadRaces { - try { - val len = buffer.remaining() - if (len == -1) { - null - } else { - val limit = buffer.limit() - buffer.limit(buffer.position() + len - trailingNullBytes) - val charBuffer = charBuffer(len) - decoder.reset() - decoder.decode(buffer, charBuffer, true) - buffer.limit(limit) - buffer.position(buffer.position() + trailingNullBytes) - charBuffer.flip() - charBuffer.toString() - } - } catch (e: Throwable) { - buffer.hexDump() - throw RuntimeException(e) + fun deserializeAll(buffer: ByteBuffer): String? = try { + val len = buffer.remaining() + if (len == -1) { + null + } else { + val limit = buffer.limit() + buffer.limit(buffer.position() + len - trailingNullBytes) + val charBuffer = charBuffer(len) + decoder.reset() + decoder.decode(buffer, charBuffer, true) + buffer.limit(limit) + buffer.position(buffer.position() + trailingNullBytes) + charBuffer.flip() + charBuffer.toString() } + } catch (e: Throwable) { + buffer.hexDump() + throw RuntimeException(e) } - override fun deserialize(buffer: ByteBuffer, features: Quassel_Features): String? = - preventThreadRaces { - try { - val len = IntSerializer.deserialize(buffer, features) - if (len == -1) { - null - } else { - val limit = buffer.limit() - buffer.limit(buffer.position() + Math.max(0, len - trailingNullBytes)) - val charBuffer = charBuffer(len) - decoder.decode(buffer, charBuffer, true) - buffer.limit(limit) - buffer.position(buffer.position() + trailingNullBytes) - charBuffer.flip() - charBuffer.toString() - } - } catch (e: Throwable) { - buffer.hexDump() - throw RuntimeException(e) - } + override fun deserialize(buffer: ByteBuffer, features: Quassel_Features): String? = try { + val len = IntSerializer.deserialize(buffer, features) + if (len == -1) { + null + } else { + val limit = buffer.limit() + buffer.limit(buffer.position() + Math.max(0, len - trailingNullBytes)) + val charBuffer = charBuffer(len) + decoder.decode(buffer, charBuffer, true) + buffer.limit(limit) + buffer.position(buffer.position() + trailingNullBytes) + charBuffer.flip() + charBuffer.toString() } + } catch (e: Throwable) { + buffer.hexDump() + throw RuntimeException(e) + } } diff --git a/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/IrcUser.kt b/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/IrcUser.kt index 06d7c06bab8fc1a8c0e6e85197ac9207db25c0c2..84fa55d9ce29dab56a5980e6855a0b3e6204ca89 100644 --- a/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/IrcUser.kt +++ b/lib/src/main/java/de/kuschku/libquassel/quassel/syncables/IrcUser.kt @@ -68,70 +68,30 @@ class IrcUser( setUserModes(properties["userModes"].valueOr(this::userModes)) } - fun nick() = _nick - fun liveNick(): Observable<String> = live_nick + fun updates(): Observable<IrcUser> = hasChangedNotification.map { this } + fun nick() = _nick fun user() = _user - fun liveUser(): Observable<String> = live_user - fun host() = _host - fun liveHost(): Observable<String> = live_host - fun realName() = _realName - fun liveRealName(): Observable<String> = live_realName - fun account() = _account - fun liveAccount(): Observable<String> = live_account - fun hostMask() = "${nick()}!${user()}@${host()}" - fun liveHostMask() = liveNick().switchMap { nick -> - liveUser().switchMap { user -> - liveHost().map { host -> - "$nick!$user@$host" - } - } - } - fun isAway() = _away - fun liveIsAway(): Observable<Boolean> = live_away - fun awayMessage() = _awayMessage - fun liveAwayMessage(): Observable<String> = live_awayMessage - fun server() = _server - fun liveServer(): Observable<String> = live_server - fun idleTime(): Instant { if (Instant.now().epochSecond - _idleTimeSet.epochSecond > 1200) _idleTime = Instant.EPOCH return _idleTime } - - fun liveIdleTime(): Observable<Instant> = live_idleTime - fun loginTime() = _loginTime - fun liveLoginTime(): Observable<Instant> = live_loginTime - fun ircOperator() = _ircOperator - fun liveIrcOperator(): Observable<String> = live_ircOperator - fun lastAwayMessage() = _lastAwayMessage - fun liveLastAwayMessage(): Observable<Int> = live_lastAwayMessage - fun whoisServiceReply() = _whoisServiceReply - fun liveWhoisServiceReply(): Observable<String> = live_whoisServiceReply - fun suserHost() = _suserHost - fun liveSuserHost(): Observable<String> = live_suserHost - fun encrypted() = _encrypted - fun liveEncrypted(): Observable<Boolean> = live_encrypted - fun network() = _network - fun userModes() = _userModes - fun liveUserModes(): Observable<String> = live_userModes - fun channels() = _channels.map(IrcChannel::name) fun codecForEncoding() = _codecForEncoding fun codecForDecoding() = _codecForDecoding @@ -313,92 +273,106 @@ class IrcUser( renameObject(identifier) } - private val live_nick = BehaviorSubject.createDefault(HostmaskHelper.nick(hostmask)) - private var _nick: String - get() = live_nick.value - set(value) = live_nick.onNext(value) - - private val live_user = BehaviorSubject.createDefault(HostmaskHelper.user(hostmask)) - private var _user: String - get() = live_user.value - set(value) = live_user.onNext(value) - - private val live_host = BehaviorSubject.createDefault(HostmaskHelper.host(hostmask)) - private var _host: String - get() = live_host.value - set(value) = live_host.onNext(value) - - private val live_realName = BehaviorSubject.createDefault("") - private var _realName: String - get() = live_realName.value - set(value) = live_realName.onNext(value) - - private val live_account = BehaviorSubject.createDefault("") - private var _account: String - get() = live_account.value - set(value) = live_account.onNext(value) - - private val live_awayMessage = BehaviorSubject.createDefault("") - private var _awayMessage: String - get() = live_awayMessage.value - set(value) = live_awayMessage.onNext(value) - - private val live_away = BehaviorSubject.createDefault(false) - private var _away: Boolean - get() = live_away.value - set(value) = live_away.onNext(value) - - private val live_server = BehaviorSubject.createDefault("") - private var _server: String - get() = live_server.value - set(value) = live_server.onNext(value) - - private val live_idleTime = BehaviorSubject.createDefault(Instant.EPOCH) - private var _idleTime: Instant - get() = live_idleTime.value - set(value) = live_idleTime.onNext(value) - - private val live_idleTimeSet = BehaviorSubject.createDefault(Instant.EPOCH) - private var _idleTimeSet: Instant - get() = live_idleTimeSet.value - set(value) = live_idleTimeSet.onNext(value) - - private val live_loginTime = BehaviorSubject.createDefault(Instant.EPOCH) - private var _loginTime: Instant - get() = live_loginTime.value - set(value) = live_loginTime.onNext(value) - - private val live_ircOperator = BehaviorSubject.createDefault("") - private var _ircOperator: String - get() = live_ircOperator.value - set(value) = live_ircOperator.onNext(value) - - private val live_lastAwayMessage = BehaviorSubject.createDefault(0) - private var _lastAwayMessage: Int - get() = live_lastAwayMessage.value - set(value) = live_lastAwayMessage.onNext(value) - - private val live_whoisServiceReply = BehaviorSubject.createDefault("") - private var _whoisServiceReply: String - get() = live_whoisServiceReply.value - set(value) = live_whoisServiceReply.onNext(value) - - private val live_suserHost = BehaviorSubject.createDefault("") - private var _suserHost: String - get() = live_suserHost.value - set(value) = live_suserHost.onNext(value) - - private val live_encrypted = BehaviorSubject.createDefault(false) - private var _encrypted: Boolean - get() = live_encrypted.value - set(value) = live_encrypted.onNext(value) + private val hasChangedNotification = BehaviorSubject.createDefault(Unit) + private var _nick: String = HostmaskHelper.nick(hostMask()) + set(value) { + field = value + hasChangedNotification.onNext(Unit) + } + + private var _user: String = HostmaskHelper.user(hostMask()) + + private var _host: String = HostmaskHelper.host(hostMask()) + set(value) { + field = value + hasChangedNotification.onNext(Unit) + } + + private var _realName: String = "" + set(value) { + field = value + hasChangedNotification.onNext(Unit) + } + + private var _account: String = "" + set(value) { + field = value + hasChangedNotification.onNext(Unit) + } + + private var _awayMessage: String = "" + set(value) { + field = value + hasChangedNotification.onNext(Unit) + } + + private var _away: Boolean = false + set(value) { + field = value + hasChangedNotification.onNext(Unit) + } + + private var _server: String = "" + set(value) { + field = value + hasChangedNotification.onNext(Unit) + } + + private var _idleTime: Instant = Instant.EPOCH + set(value) { + field = value + hasChangedNotification.onNext(Unit) + } + + private var _idleTimeSet: Instant = Instant.EPOCH + set(value) { + field = value + hasChangedNotification.onNext(Unit) + } + + private var _loginTime: Instant = Instant.EPOCH + set(value) { + field = value + hasChangedNotification.onNext(Unit) + } + + private var _ircOperator: String = "" + set(value) { + field = value + hasChangedNotification.onNext(Unit) + } + + private var _lastAwayMessage: Int = 0 + set(value) { + field = value + hasChangedNotification.onNext(Unit) + } + + private var _whoisServiceReply: String = "" + set(value) { + field = value + hasChangedNotification.onNext(Unit) + } + + private var _suserHost: String = "" + set(value) { + field = value + hasChangedNotification.onNext(Unit) + } + + private var _encrypted: Boolean = false + set(value) { + field = value + hasChangedNotification.onNext(Unit) + } private var _channels: MutableSet<IrcChannel> = mutableSetOf() - private val live_userModes = BehaviorSubject.createDefault("") - private var _userModes: String - get() = live_userModes.value - set(value) = live_userModes.onNext(value) + private var _userModes: String = "" + set(value) { + field = value + hasChangedNotification.onNext(Unit) + } private var _network: Network = network private var _codecForEncoding: Charset? = null @@ -407,4 +381,4 @@ class IrcUser( companion object { val NULL = IrcUser("", Network.NULL, SignalProxy.NULL) } -} +} \ No newline at end of file diff --git a/lib/src/main/java/de/kuschku/libquassel/session/ProtocolHandler.kt b/lib/src/main/java/de/kuschku/libquassel/session/ProtocolHandler.kt index b1703df15cc9606119d2f7ae5d195d964842f938..428cbdfb05df6bba395e0a5f6b4c3b35557e48b8 100644 --- a/lib/src/main/java/de/kuschku/libquassel/session/ProtocolHandler.kt +++ b/lib/src/main/java/de/kuschku/libquassel/session/ProtocolHandler.kt @@ -15,6 +15,8 @@ import java.io.Closeable @Suppress("LeakingThis") abstract class ProtocolHandler : SignalProxy, AuthHandler, Closeable { + protected var closed = false + private val objectStorage: ObjectStorage = ObjectStorage(this) protected open var rpcHandler: RpcHandler? = null @@ -33,6 +35,8 @@ abstract class ProtocolHandler : SignalProxy, AuthHandler, Closeable { private var totalInitCount = 0 override fun handle(f: SignalProxyMessage): Boolean { + if (closed) return true + try { if (!super<SignalProxy>.handle(f)) { log(DEBUG, "ProtocolHandler", "No receiver registered for $f") @@ -44,6 +48,8 @@ abstract class ProtocolHandler : SignalProxy, AuthHandler, Closeable { } override fun handle(f: HandshakeMessage): Boolean { + if (closed) return true + try { if (!super<AuthHandler>.handle(f)) { log(DEBUG, "ProtocolHandler", "No receiver registered for $f") @@ -177,6 +183,8 @@ abstract class ProtocolHandler : SignalProxy, AuthHandler, Closeable { } override fun close() { + closed = true + objectStorage.clear() toInit.clear() } diff --git a/lib/src/main/java/de/kuschku/libquassel/session/Session.kt b/lib/src/main/java/de/kuschku/libquassel/session/Session.kt index 4cf4a7c1b4842e0284230cc456ee3a5a2642e507..d3b160dbe4184f68a9ec077da193b9c86e00334a 100644 --- a/lib/src/main/java/de/kuschku/libquassel/session/Session.kt +++ b/lib/src/main/java/de/kuschku/libquassel/session/Session.kt @@ -7,8 +7,7 @@ import de.kuschku.libquassel.quassel.QuasselFeature import de.kuschku.libquassel.quassel.syncables.* import de.kuschku.libquassel.util.compatibility.HandlerService import de.kuschku.libquassel.util.compatibility.LoggingHandler.Companion.log -import de.kuschku.libquassel.util.compatibility.LoggingHandler.LogLevel.DEBUG -import de.kuschku.libquassel.util.compatibility.LoggingHandler.LogLevel.INFO +import de.kuschku.libquassel.util.compatibility.LoggingHandler.LogLevel.* import de.kuschku.libquassel.util.hasFlag import io.reactivex.subjects.BehaviorSubject import org.threeten.bp.Instant @@ -54,6 +53,7 @@ class Session( override val lag = BehaviorSubject.createDefault(0L) init { + log(ERROR, "DEBUG", "created session:", RuntimeException()) coreConnection.start() } @@ -159,11 +159,15 @@ class Session( } override fun dispatch(message: SignalProxyMessage) { + if (closed) return + log(DEBUG, "Session", "> $message") coreConnection.dispatch(message) } override fun dispatch(message: HandshakeMessage) { + if (closed) return + log(DEBUG, "Session", "> $message") coreConnection.dispatch(message) } @@ -172,13 +176,13 @@ class Session( override fun identity(id: IdentityId): Identity? = identities[id] override fun close() { + super.close() + coreConnection.close() certManagers.clear() identities.clear() networks.clear() - - super.close() } fun join() { diff --git a/lib/src/main/java/de/kuschku/libquassel/session/SessionManager.kt b/lib/src/main/java/de/kuschku/libquassel/session/SessionManager.kt index 0333859a47a24cc8d0f5924af775465422b74773..9294077913652e754e5a0c4666f10eef9aa2c824 100644 --- a/lib/src/main/java/de/kuschku/libquassel/session/SessionManager.kt +++ b/lib/src/main/java/de/kuschku/libquassel/session/SessionManager.kt @@ -84,7 +84,7 @@ class SessionManager(offlineSession: ISession, log(LoggingHandler.LogLevel.INFO, "Session", "Session created") state.subscribe { - if (state == ConnectionState.CONNECTED) { + if (it == ConnectionState.CONNECTED) { lastSession.close() } } diff --git a/lib/src/main/java/de/kuschku/libquassel/util/Optional.kt b/lib/src/main/java/de/kuschku/libquassel/util/Optional.kt new file mode 100644 index 0000000000000000000000000000000000000000..390581dccb365cb615a6db858e60553660713001 --- /dev/null +++ b/lib/src/main/java/de/kuschku/libquassel/util/Optional.kt @@ -0,0 +1,58 @@ +package de.kuschku.libquassel.util + +import java.io.Serializable + +interface Optional<T> : Serializable { + fun get(): T + fun isPresent(): Boolean + fun ifPresent(consumer: (T) -> Unit) + fun filter(predicate: (T) -> Boolean): Optional<T> + fun <U> map(mapper: (T) -> U): Optional<U> + fun <U> flatMap(mapper: (T) -> Optional<U>): Optional<U> + fun orElse(other: T): T + fun orNull(): T? + fun <X : Throwable> orElseThrow(supplier: () -> X): T + override fun equals(other: Any?): Boolean + override fun hashCode(): Int + override fun toString(): String + + private class Present<T>(private val value: T) : Optional<T> { + override fun get() = value + override fun isPresent() = true + override fun ifPresent(consumer: (T) -> Unit) = consumer(value) + override fun filter(predicate: (T) -> Boolean) = if (predicate(value)) this else empty<T>() + override fun <U> map(mapper: (T) -> U) = ofNullable(mapper(value)) + override fun <U> flatMap(mapper: (T) -> Optional<U>) = mapper(value) + override fun orElse(other: T) = value + override fun orNull(): T? = value + override fun <X : Throwable> orElseThrow(supplier: () -> X) = value + override fun equals(other: Any?) = (other as? Present<*>)?.value == value + override fun hashCode() = value?.hashCode() ?: 0 + override fun toString() = "Optional[$value]" + } + + private class Absent<T> : Optional<T> { + override fun get() = throw NoSuchElementException("No value present") + override fun isPresent() = false + override fun ifPresent(consumer: (T) -> Unit) = Unit + override fun filter(predicate: (T) -> Boolean) = this + override fun <U> map(mapper: (T) -> U) = empty<U>() + override fun <U> flatMap(mapper: (T) -> Optional<U>) = empty<U>() + override fun orElse(other: T) = other + override fun orNull(): T? = null + override fun <X : Throwable> orElseThrow(supplier: () -> X) = throw supplier() + override fun equals(other: Any?) = other === this + override fun hashCode() = 0 + override fun toString() = "Optional.empty" + } + + companion object { + private val absent = Absent<Any?>() + + @Suppress("UNCHECKED_CAST") + fun <T> empty(): Optional<T> = absent as Absent<T> + + fun <T> of(value: T): Optional<T> = Present(value) + fun <T> ofNullable(value: T?): Optional<T> = value?.let(::Present) ?: empty() + } +} \ No newline at end of file diff --git a/lib/src/main/java/de/kuschku/libquassel/util/helpers/ObservableHelper.kt b/lib/src/main/java/de/kuschku/libquassel/util/helpers/ObservableHelper.kt index edc47fcedb6323e9bad8a75fd4204eddbb6fb805..90facf688a16091b7ceb37602854030f387c7f7c 100644 --- a/lib/src/main/java/de/kuschku/libquassel/util/helpers/ObservableHelper.kt +++ b/lib/src/main/java/de/kuschku/libquassel/util/helpers/ObservableHelper.kt @@ -7,3 +7,6 @@ fun <T> Observable<T>.or(default: T): T = try { } catch (_: Throwable) { default } + +val <T> Observable<T>.value + get() = this.blockingLatest().firstOrNull() \ No newline at end of file diff --git a/malheur/build.gradle.kts b/malheur/build.gradle.kts index 474d61213e8e08415a89312cf5aa7e720bbbac00..ae8c5953619a9d423cba923603dddaa8f9507a77 100644 --- a/malheur/build.gradle.kts +++ b/malheur/build.gradle.kts @@ -17,7 +17,7 @@ android { } dependencies { - implementation(kotlin("stdlib", "1.2.30")) + implementation(kotlin("stdlib", "1.2.31")) implementation("com.google.code.gson", "gson", "2.8.2") } diff --git a/persistence/build.gradle.kts b/persistence/build.gradle.kts index c470a79b3bb77d3f319d09b50e6853a2253452a4..8613c62d8ece9fd0b338b739d6544be9a9aca1cd 100644 --- a/persistence/build.gradle.kts +++ b/persistence/build.gradle.kts @@ -23,7 +23,7 @@ android { } dependencies { - implementation(kotlin("stdlib", "1.2.30")) + implementation(kotlin("stdlib", "1.2.31")) // App Compat withVersion("27.1.0") { diff --git a/viewmodel/build.gradle.kts b/viewmodel/build.gradle.kts index 0be2deb0b9626e83365891960030a72173e0ea3f..1b1afedb6578471f16ba94606c6b7f3cf806e743 100644 --- a/viewmodel/build.gradle.kts +++ b/viewmodel/build.gradle.kts @@ -17,7 +17,7 @@ android { } dependencies { - implementation(kotlin("stdlib", "1.2.30")) + implementation(kotlin("stdlib", "1.2.31")) // App Compat withVersion("27.1.0") { 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 891516836e2feffc0fd708f248a2adc0e6ab4cbb..2e725b3625b3d2084dd9e1a198312d366d4d995d 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 @@ -5,10 +5,27 @@ import android.arch.lifecycle.LiveDataReactiveStreams import io.reactivex.BackpressureStrategy import io.reactivex.Observable import io.reactivex.ObservableSource +import io.reactivex.functions.BiFunction inline fun <T> Observable<T>.toLiveData( strategy: BackpressureStrategy = BackpressureStrategy.LATEST ): LiveData<T> = LiveDataReactiveStreams.fromPublisher(toFlowable(strategy)) +inline fun <reified A, B> combineLatest( + a: ObservableSource<A>, + b: ObservableSource<B> +): Observable<Pair<A, B>> = Observable.combineLatest(a, b, BiFunction(::Pair)) + +inline fun <reified A, B, C> combineLatest( + a: ObservableSource<A>, + b: ObservableSource<B>, + c: ObservableSource<C> +): Observable<Triple<A, B, C>> = Observable.combineLatest(listOf(a, b, c), { (t0, t1, t2) -> + Triple(t0, t1, t2) as Triple<A, B, C> +}) + inline fun <reified T> combineLatest(sources: Iterable<ObservableSource<out T>?>) = - Observable.combineLatest(sources) { t -> t.toList() as List<T> } \ No newline at end of file + Observable.combineLatest(sources) { t -> t.toList() as List<T> } + +inline operator fun <T, U> Observable<T>.invoke(f: (T) -> U?) = + blockingLatest().firstOrNull()?.let(f) \ 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 e2500ca47102bbf74c2eb39123766c4a185cc310..1eb9a5b0ce6f702d6b1062a2b52a25c2dd87971d 100644 --- a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/QuasselViewModel.kt +++ b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/QuasselViewModel.kt @@ -1,101 +1,89 @@ package de.kuschku.quasseldroid.viewmodel -import android.arch.lifecycle.LiveData -import android.arch.lifecycle.MutableLiveData import android.arch.lifecycle.ViewModel import de.kuschku.libquassel.protocol.BufferId 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.BufferViewConfig -import de.kuschku.libquassel.quassel.syncables.IrcChannel -import de.kuschku.libquassel.quassel.syncables.IrcUser -import de.kuschku.libquassel.quassel.syncables.Network +import de.kuschku.libquassel.quassel.syncables.* import de.kuschku.libquassel.session.Backend import de.kuschku.libquassel.session.ISession import de.kuschku.libquassel.session.SessionManager +import de.kuschku.libquassel.util.Optional import de.kuschku.libquassel.util.and import de.kuschku.libquassel.util.hasFlag -import de.kuschku.quasseldroid.util.helper.* +import de.kuschku.quasseldroid.util.helper.combineLatest +import de.kuschku.quasseldroid.util.helper.toLiveData import de.kuschku.quasseldroid.viewmodel.data.* import io.reactivex.Observable +import io.reactivex.functions.Function +import io.reactivex.subjects.BehaviorSubject import java.util.concurrent.TimeUnit class QuasselViewModel : ViewModel() { - private val backendWrapper = MutableLiveData<LiveData<Backend?>>() - fun setBackend(backendWrapper: LiveData<Backend?>) { - this.backendWrapper.value = backendWrapper - } + val backendWrapper = BehaviorSubject.createDefault(Observable.empty<Optional<Backend>>()) - val buffer = MutableLiveData<BufferId>() + val buffer = BehaviorSubject.createDefault(-1) + val buffer_liveData = buffer.toLiveData() - private val bufferViewConfigId = MutableLiveData<Int?>() - fun getBufferViewConfigId(): LiveData<Int?> = bufferViewConfigId - fun setBufferViewConfigId(bufferViewConfig: Int?) { - this.bufferViewConfigId.value = bufferViewConfig - } + val bufferViewConfigId = BehaviorSubject.createDefault(-1) val MAX_RECENT_MESSAGES = 20 - val recentlySentMessages = MutableLiveData<List<CharSequence>>() + val recentlySentMessages = BehaviorSubject.createDefault(emptyList<CharSequence>()) + val recentlySentMessages_liveData = recentlySentMessages.toLiveData() fun addRecentlySentMessage(message: CharSequence) { - recentlySentMessages.value = - listOf(message) + - recentlySentMessages.value.orEmpty() + recentlySentMessages.onNext( + listOf(message) + recentlySentMessages.value .filter { it != message } .take(MAX_RECENT_MESSAGES - 1) + ) } val backend = backendWrapper.switchMap { it } - val sessionManager = backend.map(Backend::sessionManager) - val session = sessionManager.switchMapRx(SessionManager::session) + val sessionManager = backend + .filter(Optional<Backend>::isPresent) + .map(Optional<Backend>::get) + .map(Backend::sessionManager) + val sessionManager_liveData = sessionManager.toLiveData() + val session = sessionManager.switchMap(SessionManager::session) - val connectionProgress = sessionManager.switchMapRx(SessionManager::connectionProgress) + val connectionProgress = sessionManager.switchMap(SessionManager::connectionProgress) + val connectionProgress_liveData = connectionProgress.toLiveData() - private val bufferViewManager = session.map(ISession::bufferViewManager) + val bufferViewManager = session.map { Optional.ofNullable(it.bufferViewManager) } + .filter(Optional<BufferViewManager>::isPresent) + .map(Optional<BufferViewManager>::get) val bufferViewConfig = bufferViewManager.switchMap { manager -> - bufferViewConfigId.map { id -> - manager.bufferViewConfig(id) - } + bufferViewConfigId.map(Function<Int, Optional<BufferViewConfig>> { id -> + Optional.ofNullable(manager.bufferViewConfig(id)) + }) } - val errors = session.switchMapRx(ISession::error) + val errors = session.switchMap(ISession::error) + val errors_liveData = errors.toLiveData() - private var lastMarkerLine = -1 /** * An observable of the changes of the markerline, as pairs of `(old, new)` */ val markerLine = session.switchMap { currentSession -> - buffer.switchMapRx { currentBuffer -> + buffer.switchMap { currentBuffer -> // Get a stream of the latest marker line - val raw = currentSession.bufferSyncer?.liveMarkerLine(currentBuffer) - - // Turn it into a pair of changes - val changes = raw?.map { - val previous = lastMarkerLine - if (it != lastMarkerLine) - lastMarkerLine = it - previous to it - } - - // Only return when there was an actual change - val distinct = changes?.filter { - it.first != it.second - } - - distinct + val raw = currentSession.bufferSyncer?.liveMarkerLine(currentBuffer) ?: Observable.empty() + raw.scan(Pair(-1, -1)) { (_, previous), next -> Pair(previous, next) } } } + val markerLine_liveData = markerLine.toLiveData() - val lag: LiveData<Long?> = session.switchMapRx(ISession::lag) + val lag: Observable<Long> = session.switchMap(ISession::lag) - val isSecure: LiveData<Boolean?> = session.switchMapRx { session -> - session.state.map { _ -> - session.sslSession != null - } + val isSecure: Observable<Boolean> = session.switchMap { session -> + session.state.map { _ -> + session.sslSession != null } + } - val bufferData = session.zip(buffer).switchMapRx { (session, id) -> + val bufferData = combineLatest(session, buffer).switchMap { (session, id) -> val bufferSyncer = session?.bufferSyncer if (bufferSyncer != null) { bufferSyncer.liveBufferInfos().switchMap { @@ -106,12 +94,12 @@ class QuasselViewModel : ViewModel() { } else { when (info.type.toInt()) { BufferInfo.Type.QueryBuffer.toInt() -> { - network.liveIrcUser(info.bufferName).switchMap { user -> - user.liveRealName().map { realName -> + network.liveIrcUser(info.bufferName).switchMap { + it.updates().map { user -> BufferData( info = info, network = network.networkInfo(), - description = realName + description = user.realName() ) } } @@ -150,162 +138,139 @@ class QuasselViewModel : ViewModel() { } } - val nickData: LiveData<List<IrcUserItem>?> = session.zip( - buffer - ).switchMapRx { (session, buffer) -> - val bufferSyncer = session?.bufferSyncer - val bufferInfo = bufferSyncer?.bufferInfo(buffer) - 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 -> - combineLatest<IrcUserItem>( - users.map<IrcUser, Observable<IrcUserItem>?> { 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 - - IrcUserItem( - nick, - network.modesToPrefixes(userModes), - lowestMode, - realName, - away, - network.support("CASEMAPPING") - ) - } + val nickData: Observable<List<IrcUserItem>> = combineLatest(session, buffer) + .switchMap { (session, buffer) -> + val bufferSyncer = session?.bufferSyncer + val bufferInfo = bufferSyncer?.bufferInfo(buffer) + 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 -> + combineLatest<IrcUserItem>( + users.map<IrcUser, Observable<IrcUserItem>?> { + it.updates().map { user -> + val userModes = ircChannel.userModes(user) + val prefixModes = network.prefixModes() + + val lowestMode = userModes.mapNotNull { + prefixModes.indexOf(it) + }.min() ?: prefixModes.size + + IrcUserItem( + user.nick(), + network.modesToPrefixes(userModes), + lowestMode, + user.realName(), + user.isAway(), + network.support("CASEMAPPING") + ) } } - }) + ) + } + } else { + Observable.just(emptyList()) } } else { Observable.just(emptyList()) } - } else { - Observable.just(emptyList()) } - } - val lastWord = MutableLiveData<Observable<Pair<String, IntRange>>>() - - val autoCompleteData: LiveData<Pair<String, List<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) { - 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<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<AutoCompleteItem.ChannelItem>?> { (info, network) -> - network.liveIrcChannel( - info.bufferName - ).switchMap { channel -> - channel.liveTopic().map { topic -> - AutoCompleteItem.ChannelItem( - info = info, - network = network.networkInfo(), - bufferStatus = when (channel) { - IrcChannel.NULL -> BufferStatus.OFFLINE - else -> BufferStatus.ONLINE - }, - description = topic - ) + val lastWord = BehaviorSubject.create<Observable<Pair<String, IntRange>>>() + + val autoCompleteData: Observable<Pair<String, List<AutoCompleteItem>>> = + combineLatest(session, buffer, lastWord).switchMap { (session, id, lastWordWrapper) -> + lastWordWrapper + .distinctUntilChanged() + .debounce(300, TimeUnit.MILLISECONDS) + .switchMap { lastWord -> + val bufferSyncer = session?.bufferSyncer + val bufferInfo = bufferSyncer?.bufferInfo(id) + if (bufferSyncer != null) { + 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<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<AutoCompleteItem.ChannelItem>?> { (info, network) -> + network.liveIrcChannel( + info.bufferName + ).switchMap { channel -> + channel.liveTopic().map { topic -> + AutoCompleteItem.ChannelItem( + info = info, + network = network.networkInfo(), + bufferStatus = when (channel) { + IrcChannel.NULL -> BufferStatus.OFFLINE + else -> BufferStatus.ONLINE + }, + description = 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") + ) + } } - val nicks = users.map<IrcUser, Observable<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 + combineLatest<AutoCompleteItem>(nicks + buffers) + .map { list -> + val ignoredStartingCharacters = charArrayOf( + '-', '_', '[', ']', '{', '}', '|', '`', '^', '.', '\\' + ) - AutoCompleteItem.UserItem( - nick, - network.modesToPrefixes( - userModes - ), - lowestMode, - realName, - away, - network.support( - "CASEMAPPING" - ) - ) - } + Pair( + lastWord.first, + list.filter { + it.name.trimStart(*ignoredStartingCharacters) + .startsWith( + lastWord.first.trimStart(*ignoredStartingCharacters), + ignoreCase = true + ) + }.sorted() + ) } - } } - - 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()) - ) + 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.switchMapRx { manager -> + val bufferViewConfigs = bufferViewManager.switchMap { manager -> manager.liveBufferViewConfigs().map { ids -> ids.mapNotNull { id -> manager.bufferViewConfig(id) @@ -313,193 +278,185 @@ class QuasselViewModel : ViewModel() { } } - val showHidden = MutableLiveData<Boolean>() - val collapsedNetworks = MutableLiveData<Set<NetworkId>>() - val selectedBufferId = MutableLiveData<BufferId>() - val selectedBuffer = session.zip( - selectedBufferId, bufferViewConfig - ).switchMapRx { (session, buffer, bufferViewConfig) -> - val bufferSyncer = session?.bufferSyncer - if (bufferSyncer != null && bufferViewConfig != null) { - val hiddenState = when { - bufferViewConfig.removedBuffers().contains(buffer) -> - BufferHiddenState.HIDDEN_PERMANENT - bufferViewConfig.temporarilyRemovedBuffers().contains(buffer) -> - BufferHiddenState.HIDDEN_TEMPORARY - else -> - BufferHiddenState.VISIBLE - } + val showHidden = BehaviorSubject.createDefault(false) + val collapsedNetworks = BehaviorSubject.createDefault(emptySet<NetworkId>()) + val selectedBufferId = BehaviorSubject.createDefault(-1) + val selectedBuffer = combineLatest(session, selectedBufferId, bufferViewConfig) + .switchMap { (session, buffer, bufferViewConfigOptional) -> + val bufferSyncer = session.bufferSyncer + val bufferViewConfig = bufferViewConfigOptional.orNull() + if (bufferSyncer != null && bufferViewConfig != null) { + val hiddenState = when { + bufferViewConfig.removedBuffers().contains(buffer) -> + BufferHiddenState.HIDDEN_PERMANENT + bufferViewConfig.temporarilyRemovedBuffers().contains(buffer) -> + BufferHiddenState.HIDDEN_TEMPORARY + else -> + BufferHiddenState.VISIBLE + } - val info = bufferSyncer.bufferInfo(buffer) - if (info != null) { - val network = session.networks[info.networkId] - when (info.type.enabledValues().firstOrNull()) { - Buffer_Type.StatusBuffer -> { - network?.liveConnectionState?.map { - SelectedBufferItem( - info, - connectionState = it, - hiddenState = hiddenState - ) + val info = bufferSyncer.bufferInfo(buffer) + if (info != null) { + val network = session.networks[info.networkId] + when (info.type.enabledValues().firstOrNull()) { + Buffer_Type.StatusBuffer -> { + network?.liveConnectionState?.map { + SelectedBufferItem( + info, + connectionState = it, + hiddenState = hiddenState + ) + } ?: Observable.just(SelectedBufferItem(info, hiddenState = hiddenState)) } - } - Buffer_Type.ChannelBuffer -> { - network?.liveIrcChannel(info.bufferName)?.map { - SelectedBufferItem( - info, - joined = it != IrcChannel.NULL, - hiddenState = hiddenState - ) + Buffer_Type.ChannelBuffer -> { + network?.liveIrcChannel(info.bufferName)?.map { + SelectedBufferItem( + info, + joined = it != IrcChannel.NULL, + hiddenState = hiddenState + ) + } ?: Observable.just(SelectedBufferItem(info, hiddenState = hiddenState)) } + else -> + Observable.just(SelectedBufferItem(info, hiddenState = hiddenState)) } - else -> - Observable.just(SelectedBufferItem(info, hiddenState = hiddenState)) + } else { + Observable.just(SelectedBufferItem()) } } else { - Observable.just(SelectedBufferItem(info, hiddenState = hiddenState)) + Observable.just(SelectedBufferItem()) } - } else { - Observable.just(SelectedBufferItem()) } - } + val selectedBuffer_liveData = selectedBuffer.toLiveData() - val bufferList: LiveData<Pair<BufferViewConfig?, List<BufferProps>>?> = session.zip( - bufferViewConfig, showHidden - ).switchMapRx { (session, config, showHiddenRaw) -> - val bufferSyncer = session?.bufferSyncer - val showHidden = showHiddenRaw ?: false - if (bufferSyncer != null && config != null) { - config.live_config.debounce(16, TimeUnit.MILLISECONDS).switchMap { currentConfig -> - combineLatest<Collection<BufferId>>( - listOf( - config.live_buffers, - config.live_temporarilyRemovedBuffers, - config.live_removedBuffers - ) - ).switchMap { (ids, temp, perm) -> - fun transformIds(ids: Collection<BufferId>, state: BufferHiddenState) = - ids.mapNotNull { id -> - bufferSyncer.bufferInfo(id) - }.filter { - currentConfig.networkId() <= 0 || currentConfig.networkId() == it.networkId - }.filter { - (currentConfig.allowedBufferTypes() and it.type).isNotEmpty() || - it.type.hasFlag(Buffer_Type.StatusBuffer) - }.mapNotNull { - val network = session.networks[it.networkId] - if (network == null) { - null - } else { - it to network - } - }.map<Pair<BufferInfo, Network>, Observable<BufferProps>?> { (info, network) -> - bufferSyncer.liveActivity(info.bufferId).switchMap { activity -> - bufferSyncer.liveHighlightCount(info.bufferId).map { highlights -> - activity to highlights - } - }.switchMap { (activity, highlights) -> - when (info.type.toInt()) { - BufferInfo.Type.QueryBuffer.toInt() -> { - network.liveIrcUser(info.bufferName).switchMap { user -> - user.liveIsAway().switchMap { away -> - user.liveRealName().map { realName -> + val bufferList: Observable<Pair<BufferViewConfig?, List<BufferProps>>?> = + combineLatest(session, bufferViewConfig, showHidden) + .switchMap { (session, configOptional, showHiddenRaw) -> + val bufferSyncer = session?.bufferSyncer + val showHidden = showHiddenRaw ?: false + val config = configOptional.orNull() + if (bufferSyncer != null && config != null) { + config.live_config + .debounce(16, TimeUnit.MILLISECONDS) + .switchMap { currentConfig -> + combineLatest<Collection<BufferId>>( + listOf( + config.live_buffers, + config.live_temporarilyRemovedBuffers, + config.live_removedBuffers + ) + ).switchMap { (ids, temp, perm) -> + fun transformIds(ids: Collection<BufferId>, state: BufferHiddenState) = + ids.mapNotNull { id -> + bufferSyncer.bufferInfo(id) + }.filter { + currentConfig.networkId() <= 0 || currentConfig.networkId() == it.networkId + }.filter { + (currentConfig.allowedBufferTypes() and it.type).isNotEmpty() || + it.type.hasFlag(Buffer_Type.StatusBuffer) + }.mapNotNull { + val network = session.networks[it.networkId] + if (network == null) { + null + } else { + it to network + } + }.map<Pair<BufferInfo, Network>, Observable<BufferProps>?> { (info, network) -> + bufferSyncer.liveActivity(info.bufferId).switchMap { activity -> + bufferSyncer.liveHighlightCount(info.bufferId).map { highlights -> + activity to highlights + } + }.switchMap { (activity, highlights) -> + when (info.type.toInt()) { + BufferInfo.Type.QueryBuffer.toInt() -> { + network.liveIrcUser(info.bufferName).switchMap { + it.updates().map { user -> + BufferProps( + info = info, + network = network.networkInfo(), + bufferStatus = when { + user == IrcUser.NULL -> BufferStatus.OFFLINE + user.isAway() -> BufferStatus.AWAY + else -> BufferStatus.ONLINE + }, + description = user.realName(), + activity = activity, + highlights = highlights, + hiddenState = state + ) + } + } + } + BufferInfo.Type.ChannelBuffer.toInt() -> { + network.liveIrcChannel(info.bufferName).switchMap { channel -> + channel.liveTopic().map { topic -> + BufferProps( + info = info, + network = network.networkInfo(), + bufferStatus = when (channel) { + IrcChannel.NULL -> BufferStatus.OFFLINE + else -> BufferStatus.ONLINE + }, + description = topic, + activity = activity, + highlights = highlights, + hiddenState = state + ) + } + } + } + BufferInfo.Type.StatusBuffer.toInt() -> { + network.liveConnectionState.map { + BufferProps( + info = info, + network = network.networkInfo(), + bufferStatus = BufferStatus.OFFLINE, + description = "", + activity = activity, + highlights = highlights, + hiddenState = state + ) + } + } + else -> Observable.just( BufferProps( info = info, network = network.networkInfo(), - bufferStatus = when { - user == IrcUser.NULL -> BufferStatus.OFFLINE - away -> BufferStatus.AWAY - else -> BufferStatus.ONLINE - }, - description = realName, + bufferStatus = BufferStatus.OFFLINE, + description = "", activity = activity, highlights = highlights, hiddenState = state ) - } - } - } - } - BufferInfo.Type.ChannelBuffer.toInt() -> { - network.liveIrcChannel( - info.bufferName - ).switchMap { channel -> - channel.liveTopic().map { topic -> - BufferProps( - info = info, - network = network.networkInfo(), - bufferStatus = when (channel) { - IrcChannel.NULL -> BufferStatus.OFFLINE - else -> BufferStatus.ONLINE - }, - description = topic, - activity = activity, - highlights = highlights, - hiddenState = state ) } } } - BufferInfo.Type.StatusBuffer.toInt() -> { - network.liveConnectionState.map { - BufferProps( - info = info, - network = network.networkInfo(), - bufferStatus = BufferStatus.OFFLINE, - description = "", - activity = activity, - highlights = highlights, - hiddenState = state - ) - } + + bufferSyncer.liveBufferInfos().switchMap { + val buffers = if (showHidden) { + transformIds(ids, BufferHiddenState.VISIBLE) + + transformIds(temp, BufferHiddenState.HIDDEN_TEMPORARY) + + transformIds(perm, BufferHiddenState.HIDDEN_PERMANENT) + } else { + transformIds(ids, BufferHiddenState.VISIBLE) } - else -> Observable.just( - BufferProps( - info = info, - network = network.networkInfo(), - bufferStatus = BufferStatus.OFFLINE, - description = "", - activity = activity, - highlights = highlights, - hiddenState = state + + combineLatest<BufferProps>(buffers).map { list -> + Pair<BufferViewConfig?, List<BufferProps>>( + config, + list.filter { + (!config.hideInactiveBuffers()) || + it.bufferStatus != BufferStatus.OFFLINE || + it.info.type.hasFlag(Buffer_Type.StatusBuffer) + } ) - ) + } } } } - - bufferSyncer.liveBufferInfos().switchMap { - val buffers = if (showHidden) { - transformIds(ids, BufferHiddenState.VISIBLE) + - transformIds(temp, BufferHiddenState.HIDDEN_TEMPORARY) + - transformIds(perm, BufferHiddenState.HIDDEN_PERMANENT) - } else { - transformIds(ids, BufferHiddenState.VISIBLE) - } - - combineLatest<BufferProps>(buffers).map { list -> - Pair<BufferViewConfig?, List<BufferProps>>( - config, - list.filter { - (!config.hideInactiveBuffers()) || - it.bufferStatus != BufferStatus.OFFLINE || - it.info.type.hasFlag(Buffer_Type.StatusBuffer) - }) - } - } + } else { + Observable.just(Pair<BufferViewConfig?, List<BufferProps>>(null, emptyList())) } } - } else { - Observable.just( - Pair<BufferViewConfig?, List<BufferProps>>(null, emptyList()) - ) - } - } - - init { - showHidden.postValue(false) - selectedBufferId.postValue(-1) - collapsedNetworks.value = emptySet() - recentlySentMessages.value = emptyList() - } }