diff --git a/changelog.d/4219.misc b/changelog.d/4219.misc new file mode 100644 index 0000000000..69950e0915 --- /dev/null +++ b/changelog.d/4219.misc @@ -0,0 +1 @@ +Finish migration from RxJava to Flow \ No newline at end of file diff --git a/dependencies.gradle b/dependencies.gradle index 47090d4732..7c6c4bcc47 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -17,7 +17,7 @@ def arrow = "0.8.2" def markwon = "4.6.2" def moshi = "1.12.0" def lifecycle = "2.2.0" -def rxBinding = "3.1.0" +def flowBinding = "1.2.0" def epoxy = "4.6.2" def mavericks = "2.4.0" def glide = "4.12.0" @@ -41,7 +41,8 @@ ext.libs = [ jetbrains : [ 'coroutinesCore' : "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinCoroutines", 'coroutinesAndroid' : "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinCoroutines", - 'coroutinesRx2' : "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$kotlinCoroutines" + 'coroutinesRx2' : "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$kotlinCoroutines", + 'coroutinesTest' : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutines" ], androidx : [ 'appCompat' : "androidx.appcompat:appcompat:1.3.1", @@ -102,7 +103,6 @@ ext.libs = [ 'epoxyProcessor' : "com.airbnb.android:epoxy-processor:$epoxy", 'epoxyPaging' : "com.airbnb.android:epoxy-paging:$epoxy", 'mavericks' : "com.airbnb.android:mavericks:$mavericks", - 'mavericksRx' : "com.airbnb.android:mavericks-rxjava2:$mavericks", 'mavericksTesting' : "com.airbnb.android:mavericks-testing:$mavericks" ], mockk : [ @@ -115,13 +115,13 @@ ext.libs = [ 'bigImageViewer' : "com.github.piasy:BigImageViewer:$bigImageViewer", 'glideImageLoader' : "com.github.piasy:GlideImageLoader:$bigImageViewer", 'progressPieIndicator' : "com.github.piasy:ProgressPieIndicator:$bigImageViewer", - 'glideImageViewFactory' : "com.github.piasy:GlideImageViewFactory:$bigImageViewer" + 'glideImageViewFactory' : "com.github.piasy:GlideImageViewFactory:$bigImageViewer", + 'flowBinding' : "io.github.reactivecircus.flowbinding:flowbinding-android:$flowBinding", + 'flowBindingAppcompat' : "io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowBinding", + 'flowBindingMaterial' : "io.github.reactivecircus.flowbinding:flowbinding-material:$flowBinding" ], jakewharton : [ - 'timber' : "com.jakewharton.timber:timber:5.0.1", - 'rxbinding' : "com.jakewharton.rxbinding3:rxbinding:$rxBinding", - 'rxbindingAppcompat' : "com.jakewharton.rxbinding3:rxbinding-appcompat:$rxBinding", - 'rxbindingMaterial' : "com.jakewharton.rxbinding3:rxbinding-material:$rxBinding" + 'timber' : "com.jakewharton.timber:timber:5.0.1" ], jsonwebtoken: [ 'jjwtApi' : "io.jsonwebtoken:jjwt-api:$jjwt", diff --git a/docs/rx_flow_migration.md b/docs/rx_flow_migration.md new file mode 100644 index 0000000000..a438b0f6fb --- /dev/null +++ b/docs/rx_flow_migration.md @@ -0,0 +1,41 @@ +Useful links: +- https://github.com/ReactiveCircus/FlowBinding +- https://ivanisidrowu.github.io/kotlin/2020/08/09/Kotlin-Flow-Migration-And-Testing.html + + +Rx is now completely removed from Element dependencies. +Some examples of the changes: + +``` + sharedActionViewModel + .observe() + .subscribe { handleQuickActions(it) } + .disposeOnDestroyView() + ``` + +became + + ``` + sharedActionViewModel + .stream() + .onEach { handleQuickActions(it) } + .launchIn(viewLifecycleOwner.lifecycleScope) + +``` + +Inside fragment use +``` +launchIn(viewLifecycleOwner.lifecycleScope) +``` +Inside activity use +``` +launchIn(lifecycleScope) +``` +Inside viewModel use +``` +launchIn(viewModelScope) +``` + +Also be aware that when using these scopes the coroutine is launched on Dispatchers.Main by default. + + diff --git a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt index 13fd097bcd..2a0abd3d24 100644 --- a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt +++ b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt @@ -27,6 +27,7 @@ import org.matrix.android.sdk.api.session.group.GroupSummaryQueryParams import org.matrix.android.sdk.api.session.group.model.GroupSummary import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.pushers.Pusher +import org.matrix.android.sdk.api.session.room.RoomSortOrder import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams import org.matrix.android.sdk.api.session.room.accountdata.RoomAccountDataEvent import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState @@ -44,10 +45,10 @@ import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo class FlowSession(private val session: Session) { - fun liveRoomSummaries(queryParams: RoomSummaryQueryParams): Flow> { - return session.getRoomSummariesLive(queryParams).asFlow() + fun liveRoomSummaries(queryParams: RoomSummaryQueryParams, sortOrder: RoomSortOrder = RoomSortOrder.NONE): Flow> { + return session.getRoomSummariesLive(queryParams, sortOrder).asFlow() .startWith(session.coroutineDispatchers.io) { - session.getRoomSummaries(queryParams) + session.getRoomSummaries(queryParams, sortOrder) } } diff --git a/vector/build.gradle b/vector/build.gradle index e45cc3fa49..f82aac9247 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -105,7 +105,6 @@ def buildNumber = System.env.BUILDKITE_BUILD_NUMBER as Integer ?: 0 android { - // Due to a bug introduced in Android gradle plugin 3.6.0, we have to specify the ndk version to use // Ref: https://issuetracker.google.com/issues/144111441 ndkVersion "21.3.6528147" @@ -333,7 +332,6 @@ configurations { dependencies { implementation project(":matrix-sdk-android") - implementation project(":matrix-sdk-android-rx") implementation project(":matrix-sdk-android-flow") implementation project(":diff-match-patch") implementation project(":multipicker") @@ -374,23 +372,16 @@ dependencies { // Phone number https://github.com/google/libphonenumber implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.36' - // rx - implementation libs.rx.rxKotlin - implementation libs.rx.rxAndroid - implementation 'com.jakewharton.rxrelay2:rxrelay:2.1.1' - // RXBinding - implementation libs.jakewharton.rxbinding - implementation libs.jakewharton.rxbindingAppcompat - implementation libs.jakewharton.rxbindingMaterial + // FlowBinding + implementation libs.github.flowBinding + implementation libs.github.flowBindingAppcompat + implementation libs.github.flowBindingMaterial implementation libs.airbnb.epoxy implementation libs.airbnb.epoxyGlide kapt libs.airbnb.epoxyProcessor implementation libs.airbnb.epoxyPaging implementation libs.airbnb.mavericks - //TODO: remove when entirely migrated to Flow - implementation libs.airbnb.mavericksRx - // Work implementation libs.androidx.work @@ -471,7 +462,7 @@ dependencies { gplayImplementation 'com.google.android.gms:play-services-oss-licenses:17.0.0' implementation "androidx.emoji:emoji-appcompat:1.1.0" - implementation ('com.github.BillCarsonFr:JsonViewer:0.7') + implementation('com.github.BillCarsonFr:JsonViewer:0.7') // WebRTC // org.webrtc:google-webrtc is for development purposes only @@ -512,6 +503,9 @@ dependencies { // Plant Timber tree for test testImplementation libs.tests.timberJunitRule testImplementation libs.airbnb.mavericksTesting + testImplementation(libs.jetbrains.coroutinesTest) { + exclude group: "org.jetbrains.kotlinx", module: "kotlinx-coroutines-debug" + } // Activate when you want to check for leaks, from time to time. //debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.3' @@ -525,6 +519,9 @@ dependencies { androidTestImplementation libs.androidx.espressoIntents androidTestImplementation libs.tests.kluent androidTestImplementation libs.androidx.coreTesting + androidTestImplementation(libs.jetbrains.coroutinesTest) { + exclude group: "org.jetbrains.kotlinx", module: "kotlinx-coroutines-debug" + } // Plant Timber tree for test androidTestImplementation libs.tests.timberJunitRule // "The one who serves a great Espresso" diff --git a/vector/src/main/assets/open_source_licenses.html b/vector/src/main/assets/open_source_licenses.html index 19daf3359b..529b7da2f1 100755 --- a/vector/src/main/assets/open_source_licenses.html +++ b/vector/src/main/assets/open_source_licenses.html @@ -279,25 +279,9 @@ SOFTWARE. Copyright 2012 The Dagger Authors
  • - rxkotlin + FlowBinding
    - Copyright io.reactivex. -
  • -
  • - rxandroid -
    - Copyright io.reactivex. -
  • -
  • - rxrelay -
    - Copyright 2014 Netflix, Inc. - Copyright 2015 Jake Wharton -
  • -
  • - rxbinding -
    - Copyright (C) 2015 Jake Wharton + Copyright 2019 Yang Chen
  • Epoxy diff --git a/vector/src/main/java/im/vector/app/AppStateHandler.kt b/vector/src/main/java/im/vector/app/AppStateHandler.kt index 30078963f4..b6d41ce35d 100644 --- a/vector/src/main/java/im/vector/app/AppStateHandler.kt +++ b/vector/src/main/java/im/vector/app/AppStateHandler.kt @@ -24,8 +24,13 @@ import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.utils.BehaviorDataSource import im.vector.app.features.session.coroutineScope import im.vector.app.features.ui.UiStateRepository -import io.reactivex.disposables.CompositeDisposable +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.Session @@ -54,10 +59,10 @@ class AppStateHandler @Inject constructor( private val activeSessionHolder: ActiveSessionHolder ) : LifecycleObserver { - private val compositeDisposable = CompositeDisposable() + private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) private val selectedSpaceDataSource = BehaviorDataSource>(Option.empty()) - val selectedRoomGroupingObservable = selectedSpaceDataSource.observe() + val selectedRoomGroupingObservable = selectedSpaceDataSource.stream() fun getCurrentRoomGroupingMethod(): RoomGroupingMethod? { // XXX we should somehow make it live :/ just a work around @@ -105,9 +110,9 @@ class AppStateHandler @Inject constructor( } private fun observeActiveSession() { - sessionDataSource.observe() + sessionDataSource.stream() .distinctUntilChanged() - .subscribe { + .onEach { // sessionDataSource could already return a session while activeSession holder still returns null it.orNull()?.let { session -> if (uiStateRepository.isGroupingMethodSpace(session.sessionId)) { @@ -116,9 +121,8 @@ class AppStateHandler @Inject constructor( setCurrentGroup(uiStateRepository.getSelectedGroup(session.sessionId), session) } } - }.also { - compositeDisposable.add(it) } + .launchIn(coroutineScope) } fun safeActiveSpaceId(): String? { @@ -136,7 +140,7 @@ class AppStateHandler @Inject constructor( @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) fun entersBackground() { - compositeDisposable.clear() + coroutineScope.coroutineContext.cancelChildren() val session = activeSessionHolder.getSafeActiveSession() ?: return when (val currentMethod = selectedSpaceDataSource.currentValue?.orNull() ?: RoomGroupingMethod.BySpace(null)) { is RoomGroupingMethod.BySpace -> { diff --git a/vector/src/main/java/im/vector/app/VectorApplication.kt b/vector/src/main/java/im/vector/app/VectorApplication.kt index d9027231da..80b397231b 100644 --- a/vector/src/main/java/im/vector/app/VectorApplication.kt +++ b/vector/src/main/java/im/vector/app/VectorApplication.kt @@ -43,7 +43,6 @@ import dagger.hilt.android.HiltAndroidApp import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.extensions.configureAndStart import im.vector.app.core.extensions.startSyncing -import im.vector.app.core.rx.RxConfig import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.configuration.VectorConfiguration import im.vector.app.features.disclaimer.doNotShowDisclaimerDialog @@ -93,7 +92,6 @@ class VectorApplication : @Inject lateinit var versionProvider: VersionProvider @Inject lateinit var notificationUtils: NotificationUtils @Inject lateinit var appStateHandler: AppStateHandler - @Inject lateinit var rxConfig: RxConfig @Inject lateinit var popupAlertManager: PopupAlertManager @Inject lateinit var pinLocker: PinLocker @Inject lateinit var callManager: WebRtcCallManager @@ -118,7 +116,6 @@ class VectorApplication : appContext = this invitesAcceptor.initialize() vectorUncaughtExceptionHandler.activate(this) - rxConfig.setupRxPlugin() // Remove Log handler statically added by Jitsi Timber.forest() diff --git a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt index bf72dcb076..4a77a78c3d 100644 --- a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt @@ -111,8 +111,8 @@ import im.vector.app.features.roomprofile.members.RoomMemberListFragment import im.vector.app.features.roomprofile.notifications.RoomNotificationSettingsFragment import im.vector.app.features.roomprofile.permissions.RoomPermissionsFragment import im.vector.app.features.roomprofile.settings.RoomSettingsFragment -import im.vector.app.features.roomprofile.settings.joinrule.RoomJoinRuleChooseRestrictedFragment import im.vector.app.features.roomprofile.settings.joinrule.RoomJoinRuleFragment +import im.vector.app.features.roomprofile.settings.joinrule.advanced.RoomJoinRuleChooseRestrictedFragment import im.vector.app.features.roomprofile.uploads.RoomUploadsFragment import im.vector.app.features.roomprofile.uploads.files.RoomUploadsFilesFragment import im.vector.app.features.roomprofile.uploads.media.RoomUploadsMediaFragment diff --git a/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt b/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt index e89a060022..a3d8f39b30 100644 --- a/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt @@ -137,6 +137,6 @@ object VectorStaticModule { @Provides @JvmStatic fun providesCoroutineDispatchers(): CoroutineDispatchers { - return CoroutineDispatchers(io = Dispatchers.IO) + return CoroutineDispatchers(io = Dispatchers.IO, computation = Dispatchers.Default) } } diff --git a/vector/src/main/java/im/vector/app/core/dispatchers/CoroutineDispatchers.kt b/vector/src/main/java/im/vector/app/core/dispatchers/CoroutineDispatchers.kt index c489290a55..008ca4a9ce 100644 --- a/vector/src/main/java/im/vector/app/core/dispatchers/CoroutineDispatchers.kt +++ b/vector/src/main/java/im/vector/app/core/dispatchers/CoroutineDispatchers.kt @@ -19,4 +19,6 @@ package im.vector.app.core.dispatchers import kotlinx.coroutines.CoroutineDispatcher import javax.inject.Inject -data class CoroutineDispatchers @Inject constructor(val io: CoroutineDispatcher) +data class CoroutineDispatchers @Inject constructor( + val io: CoroutineDispatcher, + val computation: CoroutineDispatcher) diff --git a/vector/src/main/java/im/vector/app/core/flow/TimingOperators.kt b/vector/src/main/java/im/vector/app/core/flow/TimingOperators.kt new file mode 100644 index 0000000000..621a80d96e --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/flow/TimingOperators.kt @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.flow + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.ClosedReceiveChannelException +import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.channels.produce +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.consumeAsFlow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.isActive +import kotlinx.coroutines.selects.select + +@ExperimentalCoroutinesApi +fun Flow.chunk(durationInMillis: Long): Flow> { + require(durationInMillis > 0) { "Duration should be greater than 0" } + return flow { + coroutineScope { + val events = ArrayList() + val ticker = fixedPeriodTicker(durationInMillis) + try { + val upstreamValues = produce(capacity = Channel.CONFLATED) { + collect { value -> send(value) } + } + while (isActive) { + var hasTimedOut = false + select { + upstreamValues.onReceive { + events.add(it) + } + ticker.onReceive { + hasTimedOut = true + } + } + if (hasTimedOut && events.isNotEmpty()) { + emit(events.toList()) + events.clear() + } + } + } catch (e: ClosedReceiveChannelException) { + // drain remaining events + if (events.isNotEmpty()) emit(events.toList()) + } finally { + ticker.cancel() + } + } + } +} + +@ExperimentalCoroutinesApi +fun Flow.throttleFirst(windowDuration: Long): Flow = flow { + var windowStartTime = System.currentTimeMillis() + var emitted = false + collect { value -> + val currentTime = System.currentTimeMillis() + val delta = currentTime - windowStartTime + if (delta >= windowDuration) { + windowStartTime += delta / windowDuration * windowDuration + emitted = false + } + if (!emitted) { + emit(value) + emitted = true + } + } +} + +fun tickerFlow(scope: CoroutineScope, delayMillis: Long, initialDelayMillis: Long = delayMillis): Flow { + return scope.fixedPeriodTicker(delayMillis, initialDelayMillis).consumeAsFlow() +} + +private fun CoroutineScope.fixedPeriodTicker(delayMillis: Long, initialDelayMillis: Long = delayMillis): ReceiveChannel { + require(delayMillis >= 0) { "Expected non-negative delay, but has $delayMillis ms" } + require(initialDelayMillis >= 0) { "Expected non-negative initial delay, but has $initialDelayMillis ms" } + return produce(capacity = 0) { + delay(initialDelayMillis) + while (true) { + channel.send(Unit) + delay(delayMillis) + } + } +} diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt index 4d06dbe6a2..7fe939bef3 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt @@ -39,12 +39,12 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentFactory import androidx.fragment.app.FragmentManager import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope import androidx.viewbinding.ViewBinding import com.airbnb.mvrx.MavericksView import com.bumptech.glide.util.Util import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.snackbar.Snackbar -import com.jakewharton.rxbinding3.view.clicks import dagger.hilt.android.EntryPointAccessors import im.vector.app.BuildConfig import im.vector.app.R @@ -59,6 +59,7 @@ import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.extensions.restart import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.extensions.singletonEntryPoint +import im.vector.app.core.flow.throttleFirst import im.vector.app.core.utils.toast import im.vector.app.features.MainActivity import im.vector.app.features.MainActivityArgs @@ -77,13 +78,12 @@ import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.themes.ActivityOtherThemes import im.vector.app.features.themes.ThemeUtils import im.vector.app.receivers.DebugReceiver -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.disposables.CompositeDisposable -import io.reactivex.disposables.Disposable +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.failure.GlobalError +import reactivecircus.flowbinding.android.view.clicks import timber.log.Timber -import java.util.concurrent.TimeUnit import javax.inject.Inject abstract class VectorBaseActivity : AppCompatActivity(), MavericksView { @@ -104,13 +104,12 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver protected fun VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) { viewEvents - .observe() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { + .stream() + .onEach { hideWaitingView() observer(it) } - .disposeOnDestroy() + .launchIn(lifecycleScope) } /* ========================================================================================== @@ -119,10 +118,9 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver protected fun View.debouncedClicks(onClicked: () -> Unit) { clicks() - .throttleFirst(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { onClicked() } - .disposeOnDestroy() + .throttleFirst(300) + .onEach { onClicked() } + .launchIn(lifecycleScope) } /* ========================================================================================== @@ -133,6 +131,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver private lateinit var sessionListener: SessionListener protected lateinit var bugReporter: BugReporter private lateinit var pinLocker: PinLocker + @Inject lateinit var rageShake: RageShake lateinit var navigator: Navigator @@ -150,7 +149,6 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver // For debug only private var debugReceiver: DebugReceiver? = null - private val uiDisposables = CompositeDisposable() private val restorables = ArrayList() override fun attachBaseContext(base: Context) { @@ -175,10 +173,6 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver return this } - protected fun Disposable.disposeOnDestroy() { - uiDisposables.add(this) - } - @CallSuper override fun onCreate(savedInstanceState: Bundle?) { Timber.i("onCreate Activity ${javaClass.simpleName}") @@ -302,8 +296,6 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver override fun onDestroy() { super.onDestroy() Timber.i("onDestroy Activity ${javaClass.simpleName}") - - uiDisposables.dispose() } private val pinStartForActivityResult = registerStartForActivityResult { activityResult -> diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt index 711b2b144b..20697c6d11 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt @@ -26,21 +26,21 @@ import android.view.ViewGroup import android.widget.FrameLayout import androidx.annotation.CallSuper import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope import androidx.viewbinding.ViewBinding import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.MavericksView import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment -import com.jakewharton.rxbinding3.view.clicks import dagger.hilt.android.EntryPointAccessors import im.vector.app.core.di.ActivityEntryPoint +import im.vector.app.core.flow.throttleFirst import im.vector.app.core.utils.DimensionConverter -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.disposables.CompositeDisposable -import io.reactivex.disposables.Disposable +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import reactivecircus.flowbinding.android.view.clicks import timber.log.Timber -import java.util.concurrent.TimeUnit /** * Add Mavericks capabilities, handle DI and bindings. @@ -108,14 +108,12 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomShe @CallSuper override fun onDestroyView() { - uiDisposables.clear() _binding = null super.onDestroyView() } @CallSuper override fun onDestroy() { - uiDisposables.dispose() super.onDestroy() } @@ -164,27 +162,15 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomShe arguments = args?.let { Bundle().apply { putParcelable(Mavericks.KEY_ARG, it) } } } - /* ========================================================================================== - * Disposable - * ========================================================================================== */ - - private val uiDisposables = CompositeDisposable() - - protected fun Disposable.disposeOnDestroyView(): Disposable { - uiDisposables.add(this) - return this - } - /* ========================================================================================== * Views * ========================================================================================== */ protected fun View.debouncedClicks(onClicked: () -> Unit) { clicks() - .throttleFirst(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { onClicked() } - .disposeOnDestroyView() + .throttleFirst(300) + .onEach { onClicked() } + .launchIn(viewLifecycleOwner.lifecycleScope) } /* ========================================================================================== @@ -193,11 +179,10 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomShe protected fun VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) { viewEvents - .observe() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { + .stream() + .onEach { observer(it) } - .disposeOnDestroyView() + .launchIn(viewLifecycleOwner.lifecycleScope) } } diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt index d3c66ec61d..f4e1fe84e1 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt @@ -29,12 +29,12 @@ import androidx.annotation.MainThread import androidx.appcompat.app.AlertDialog import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope import androidx.viewbinding.ViewBinding import com.airbnb.mvrx.MavericksView import com.bumptech.glide.util.Util.assertMainThread import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.jakewharton.rxbinding3.view.clicks import dagger.hilt.android.EntryPointAccessors import im.vector.app.R import im.vector.app.core.di.ActivityEntryPoint @@ -42,13 +42,13 @@ import im.vector.app.core.dialogs.UnrecognizedCertificateDialog import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.extensions.singletonEntryPoint import im.vector.app.core.extensions.toMvRxBundle +import im.vector.app.core.flow.throttleFirst import im.vector.app.features.navigation.Navigator import im.vector.lib.ui.styles.dialogs.MaterialProgressDialog -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.disposables.CompositeDisposable -import io.reactivex.disposables.Disposable +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import reactivecircus.flowbinding.android.view.clicks import timber.log.Timber -import java.util.concurrent.TimeUnit abstract class VectorBaseFragment : Fragment(), MavericksView { @@ -148,7 +148,6 @@ abstract class VectorBaseFragment : Fragment(), MavericksView @CallSuper override fun onDestroyView() { Timber.i("onDestroyView Fragment ${javaClass.simpleName}") - uiDisposables.clear() _binding = null super.onDestroyView() } @@ -156,7 +155,6 @@ abstract class VectorBaseFragment : Fragment(), MavericksView @CallSuper override fun onDestroy() { Timber.i("onDestroy Fragment ${javaClass.simpleName}") - uiDisposables.dispose() super.onDestroy() } @@ -221,29 +219,18 @@ abstract class VectorBaseFragment : Fragment(), MavericksView } } - /* ========================================================================================== - * Disposable - * ========================================================================================== */ - - private val uiDisposables = CompositeDisposable() - - protected fun Disposable.disposeOnDestroyView() { - uiDisposables.add(this) - } - /* ========================================================================================== * ViewEvents * ========================================================================================== */ protected fun VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) { viewEvents - .observe() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { + .stream() + .onEach { dismissLoadingDialog() observer(it) } - .disposeOnDestroyView() + .launchIn(viewLifecycleOwner.lifecycleScope) } /* ========================================================================================== @@ -252,10 +239,9 @@ abstract class VectorBaseFragment : Fragment(), MavericksView protected fun View.debouncedClicks(onClicked: () -> Unit) { clicks() - .throttleFirst(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { onClicked() } - .disposeOnDestroyView() + .throttleFirst(300) + .onEach { onClicked() } + .launchIn(viewLifecycleOwner.lifecycleScope) } /* ========================================================================================== diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorViewModel.kt b/vector/src/main/java/im/vector/app/core/platform/VectorViewModel.kt index 6e7c24d4e9..c9d58f9545 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorViewModel.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorViewModel.kt @@ -16,53 +16,17 @@ package im.vector.app.core.platform -import com.airbnb.mvrx.Async -import com.airbnb.mvrx.BaseMvRxViewModel -import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.Loading import com.airbnb.mvrx.MavericksState -import com.airbnb.mvrx.Success +import com.airbnb.mvrx.MavericksViewModel import im.vector.app.core.utils.DataSource import im.vector.app.core.utils.PublishDataSource -import io.reactivex.Observable -import io.reactivex.Single abstract class VectorViewModel(initialState: S) : - BaseMvRxViewModel(initialState) { - - interface Factory { - fun create(state: S): BaseMvRxViewModel - } + MavericksViewModel(initialState) { // Used to post transient events to the View protected val _viewEvents = PublishDataSource() val viewEvents: DataSource = _viewEvents - /** - * This method does the same thing as the execute function, but it doesn't subscribe to the stream - * so you can use this in a switchMap or a flatMap - */ - // False positive - @Suppress("USELESS_CAST", "NULLABLE_TYPE_PARAMETER_AGAINST_NOT_NULL_TYPE_PARAMETER") - fun Single.toAsync(stateReducer: S.(Async) -> S): Single> { - setState { stateReducer(Loading()) } - return map { Success(it) as Async } - .onErrorReturn { Fail(it) } - .doOnSuccess { setState { stateReducer(it) } } - } - - /** - * This method does the same thing as the execute function, but it doesn't subscribe to the stream - * so you can use this in a switchMap or a flatMap - */ - // False positive - @Suppress("USELESS_CAST", "NULLABLE_TYPE_PARAMETER_AGAINST_NOT_NULL_TYPE_PARAMETER") - fun Observable.toAsync(stateReducer: S.(Async) -> S): Observable> { - setState { stateReducer(Loading()) } - return map { Success(it) as Async } - .onErrorReturn { Fail(it) } - .doOnNext { setState { stateReducer(it) } } - } - abstract fun handle(action: VA) } diff --git a/vector/src/main/java/im/vector/app/core/rx/RxConfig.kt b/vector/src/main/java/im/vector/app/core/rx/RxConfig.kt deleted file mode 100644 index ab0ffb7802..0000000000 --- a/vector/src/main/java/im/vector/app/core/rx/RxConfig.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.app.core.rx - -import im.vector.app.features.settings.VectorPreferences -import io.reactivex.plugins.RxJavaPlugins -import timber.log.Timber -import javax.inject.Inject - -class RxConfig @Inject constructor( - private val vectorPreferences: VectorPreferences -) { - - /** - * Make sure unhandled Rx error does not crash the app in production - */ - fun setupRxPlugin() { - RxJavaPlugins.setErrorHandler { throwable -> - Timber.e(throwable, "RxError") - // is InterruptedException -> fine, some blocking code was interrupted by a dispose call - if (throwable !is InterruptedException) { - // Avoid crash in production, except if user wants it - if (vectorPreferences.failFast()) { - throw throwable - } - } - } - } -} diff --git a/vector/src/main/java/im/vector/app/core/utils/CountUpTimer.kt b/vector/src/main/java/im/vector/app/core/utils/CountUpTimer.kt index a029f4eec9..b58d0fb3f6 100644 --- a/vector/src/main/java/im/vector/app/core/utils/CountUpTimer.kt +++ b/vector/src/main/java/im/vector/app/core/utils/CountUpTimer.kt @@ -16,23 +16,36 @@ package im.vector.app.core.utils -import io.reactivex.Observable -import java.util.concurrent.TimeUnit +import im.vector.app.core.flow.tickerFlow +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicLong class CountUpTimer(private val intervalInMs: Long = 1_000) { + private val coroutineScope = CoroutineScope(Dispatchers.Main) private val elapsedTime: AtomicLong = AtomicLong() private val resumed: AtomicBoolean = AtomicBoolean(false) - private val disposable = Observable.interval(intervalInMs / 10, TimeUnit.MILLISECONDS) - .filter { resumed.get() } - .map { elapsedTime.addAndGet(intervalInMs / 10) } - .filter { it % intervalInMs == 0L } - .subscribe { - tickListener?.onTick(it) - } + init { + startCounter() + } + + private fun startCounter() { + tickerFlow(coroutineScope, intervalInMs / 10) + .filter { resumed.get() } + .map { elapsedTime.addAndGet(intervalInMs / 10) } + .filter { it % intervalInMs == 0L } + .onEach { + tickListener?.onTick(it) + }.launchIn(coroutineScope) + } var tickListener: TickListener? = null @@ -49,7 +62,7 @@ class CountUpTimer(private val intervalInMs: Long = 1_000) { } fun stop() { - disposable.dispose() + coroutineScope.cancel() } interface TickListener { diff --git a/vector/src/main/java/im/vector/app/core/utils/DataSource.kt b/vector/src/main/java/im/vector/app/core/utils/DataSource.kt index fc4ee330bb..f83eda68e9 100644 --- a/vector/src/main/java/im/vector/app/core/utils/DataSource.kt +++ b/vector/src/main/java/im/vector/app/core/utils/DataSource.kt @@ -16,13 +16,12 @@ package im.vector.app.core.utils -import com.jakewharton.rxrelay2.BehaviorRelay -import com.jakewharton.rxrelay2.PublishRelay -import io.reactivex.Observable -import io.reactivex.android.schedulers.AndroidSchedulers +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow interface DataSource { - fun observe(): Observable + fun stream(): Flow } interface MutableDataSource : DataSource { @@ -34,25 +33,17 @@ interface MutableDataSource : DataSource { */ open class BehaviorDataSource(private val defaultValue: T? = null) : MutableDataSource { - private val behaviorRelay = createRelay() + private val mutableFlow = MutableSharedFlow(replay = 1) val currentValue: T? - get() = behaviorRelay.value + get() = mutableFlow.replayCache.firstOrNull() - override fun observe(): Observable { - return behaviorRelay.hide().observeOn(AndroidSchedulers.mainThread()) + override fun stream(): Flow { + return mutableFlow } override fun post(value: T) { - behaviorRelay.accept(value!!) - } - - private fun createRelay(): BehaviorRelay { - return if (defaultValue == null) { - BehaviorRelay.create() - } else { - BehaviorRelay.createDefault(defaultValue) - } + mutableFlow.tryEmit(value) } } @@ -61,13 +52,13 @@ open class BehaviorDataSource(private val defaultValue: T? = null) : MutableD */ open class PublishDataSource : MutableDataSource { - private val publishRelay = PublishRelay.create() + private val mutableFlow = MutableSharedFlow(replay = 0, extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) - override fun observe(): Observable { - return publishRelay.hide().observeOn(AndroidSchedulers.mainThread()) + override fun stream(): Flow { + return mutableFlow } override fun post(value: T) { - publishRelay.accept(value!!) + mutableFlow.tryEmit(value) } } diff --git a/vector/src/main/java/im/vector/app/core/utils/DefaultSubscriber.kt b/vector/src/main/java/im/vector/app/core/utils/DefaultSubscriber.kt deleted file mode 100644 index a82e5a4e03..0000000000 --- a/vector/src/main/java/im/vector/app/core/utils/DefaultSubscriber.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.app.core.utils - -import io.reactivex.Completable -import io.reactivex.Single -import io.reactivex.disposables.Disposable -import io.reactivex.internal.functions.Functions -import timber.log.Timber - -fun Single.subscribeLogError(): Disposable { - return subscribe(Functions.emptyConsumer(), { Timber.e(it) }) -} - -fun Completable.subscribeLogError(): Disposable { - return subscribe({}, { Timber.e(it) }) -} diff --git a/vector/src/main/java/im/vector/app/features/call/CallControlsBottomSheet.kt b/vector/src/main/java/im/vector/app/features/call/CallControlsBottomSheet.kt index b4f49db781..e38b53c858 100644 --- a/vector/src/main/java/im/vector/app/features/call/CallControlsBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/call/CallControlsBottomSheet.kt @@ -39,7 +39,7 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment(), CallContro setSupportActionBar(views.callToolbar) configureCallViews() - callViewModel.subscribe(this) { + callViewModel.onEach { renderState(it) } @@ -141,12 +143,11 @@ class VectorCallActivity : VectorBaseActivity(), CallContro } callViewModel.viewEvents - .observe() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { + .stream() + .onEach { handleViewEvents(it) } - .disposeOnDestroy() + .launchIn(lifecycleScope) callViewModel.onEach(VectorCallViewState::callId, VectorCallViewState::isVideoCall) { _, isVideoCall -> if (isVideoCall) { diff --git a/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt b/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt index 3fcefc9c8e..0fdfea8bff 100644 --- a/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt @@ -68,7 +68,7 @@ class VectorJitsiActivity : VectorBaseActivity(), JitsiMee override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - jitsiViewModel.subscribe(this) { + jitsiViewModel.onEach { renderState(it) } diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt index e632d00790..bbb158f6e4 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt @@ -19,8 +19,10 @@ package im.vector.app.features.call.webrtc import android.content.Context import android.hardware.camera2.CameraManager import androidx.core.content.getSystemService +import im.vector.app.core.flow.chunk import im.vector.app.core.services.CallService import im.vector.app.core.utils.CountUpTimer +import im.vector.app.core.utils.PublishDataSource import im.vector.app.core.utils.TextUtils.formatDuration import im.vector.app.features.call.CameraEventsHandlerAdapter import im.vector.app.features.call.CameraProxy @@ -35,14 +37,16 @@ import im.vector.app.features.call.utils.awaitSetLocalDescription import im.vector.app.features.call.utils.awaitSetRemoteDescription import im.vector.app.features.call.utils.mapToCallCandidate import im.vector.app.features.session.coroutineScope -import io.reactivex.disposables.Disposable -import io.reactivex.subjects.PublishSubject -import io.reactivex.subjects.ReplaySubject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.extensions.orFalse @@ -85,7 +89,6 @@ import org.webrtc.VideoTrack import timber.log.Timber import java.lang.ref.WeakReference import java.util.concurrent.CopyOnWriteArrayList -import java.util.concurrent.TimeUnit import javax.inject.Provider import kotlin.coroutines.CoroutineContext @@ -157,7 +160,7 @@ class WebRtcCall( private var currentCaptureFormat: CaptureFormat = CaptureFormat.HD private var cameraAvailabilityCallback: CameraManager.AvailabilityCallback? = null - private val timer = CountUpTimer(Duration.ofSeconds(1).toMillis()).apply { + private val timer = CountUpTimer(1000L).apply { tickListener = object : CountUpTimer.TickListener { override fun onTick(milliseconds: Long) { val formattedDuration = formatDuration(Duration.ofMillis(milliseconds)) @@ -197,26 +200,33 @@ class WebRtcCall( private var localSurfaceRenderers: MutableList> = ArrayList() private var remoteSurfaceRenderers: MutableList> = ArrayList() - private val iceCandidateSource: PublishSubject = PublishSubject.create() - private val iceCandidateDisposable = iceCandidateSource - .buffer(300, TimeUnit.MILLISECONDS) - .subscribe { - // omit empty :/ - if (it.isNotEmpty()) { - Timber.tag(loggerTag.value).v("Sending local ice candidates to call") - // it.forEach { peerConnection?.addIceCandidate(it) } - mxCall.sendLocalCallCandidates(it.mapToCallCandidate()) - } - } + private val localIceCandidateSource = PublishDataSource() + private var localIceCandidateJob: Job? = null - private val remoteCandidateSource: ReplaySubject = ReplaySubject.create() - private var remoteIceCandidateDisposable: Disposable? = null + private val remoteCandidateSource: MutableSharedFlow = MutableSharedFlow(replay = Int.MAX_VALUE) + private var remoteIceCandidateJob: Job? = null init { + setupLocalIceCanditate() mxCall.addListener(this) } - fun onIceCandidate(iceCandidate: IceCandidate) = iceCandidateSource.onNext(iceCandidate) + private fun setupLocalIceCanditate() { + sessionScope?.let { + localIceCandidateJob = localIceCandidateSource.stream() + .chunk(300) + .onEach { + // omit empty :/ + if (it.isNotEmpty()) { + Timber.tag(loggerTag.value).v("Sending local ice candidates to call") + // it.forEach { peerConnection?.addIceCandidate(it) } + mxCall.sendLocalCallCandidates(it.mapToCallCandidate()) + } + }.launchIn(it) + } + } + + fun onIceCandidate(iceCandidate: IceCandidate) = localIceCandidateSource.post(iceCandidate) fun onRenegotiationNeeded(restartIce: Boolean) { sessionScope?.launch(dispatcher) { @@ -438,12 +448,15 @@ class WebRtcCall( createLocalStream() attachViewRenderersInternal() Timber.tag(loggerTag.value).v("remoteCandidateSource $remoteCandidateSource") - remoteIceCandidateDisposable = remoteCandidateSource.subscribe({ - Timber.tag(loggerTag.value).v("adding remote ice candidate $it") - peerConnection?.addIceCandidate(it) - }, { - Timber.tag(loggerTag.value).v("failed to add remote ice candidate $it") - }) + remoteIceCandidateJob = remoteCandidateSource + .onEach { + Timber.tag(loggerTag.value).v("adding remote ice candidate $it") + peerConnection?.addIceCandidate(it) + } + .catch { + Timber.tag(loggerTag.value).v("failed to add remote ice candidate $it") + } + .launchIn(this) // Now we wait for negotiation callback } @@ -488,12 +501,13 @@ class WebRtcCall( mxCall.accept(it.description) } Timber.tag(loggerTag.value).v("remoteCandidateSource $remoteCandidateSource") - remoteIceCandidateDisposable = remoteCandidateSource.subscribe({ - Timber.tag(loggerTag.value).v("adding remote ice candidate $it") - peerConnection?.addIceCandidate(it) - }, { - Timber.tag(loggerTag.value).v("failed to add remote ice candidate $it") - }) + remoteIceCandidateJob = remoteCandidateSource + .onEach { + Timber.tag(loggerTag.value).v("adding remote ice candidate $it") + peerConnection?.addIceCandidate(it) + }.catch { + Timber.tag(loggerTag.value).v("failed to add remote ice candidate $it") + }.launchIn(this) } private suspend fun getTurnServer(): TurnServerResponse? { @@ -761,8 +775,8 @@ class WebRtcCall( videoCapturer?.stopCapture() videoCapturer?.dispose() videoCapturer = null - remoteIceCandidateDisposable?.dispose() - iceCandidateDisposable?.dispose() + remoteIceCandidateJob?.cancel() + localIceCandidateJob?.cancel() peerConnection?.close() peerConnection?.dispose() localAudioSource?.dispose() @@ -852,7 +866,7 @@ class WebRtcCall( } Timber.tag(loggerTag.value).v("onCallIceCandidateReceived for call ${mxCall.callId} sdp: ${it.candidate}") val iceCandidate = IceCandidate(it.sdpMid, it.sdpMLineIndex, it.candidate) - remoteCandidateSource.onNext(iceCandidate) + remoteCandidateSource.emit(iceCandidate) } } } diff --git a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt index d79ad308de..95c6105912 100644 --- a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt +++ b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt @@ -21,10 +21,9 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.withState -import com.jakewharton.rxbinding3.widget.checkedChanges -import com.jakewharton.rxbinding3.widget.textChanges import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.hideKeyboard @@ -37,9 +36,13 @@ import im.vector.app.features.userdirectory.UserListAction import im.vector.app.features.userdirectory.UserListSharedAction import im.vector.app.features.userdirectory.UserListSharedActionViewModel import im.vector.app.features.userdirectory.UserListViewModel +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.user.model.User -import java.util.concurrent.TimeUnit +import reactivecircus.flowbinding.android.widget.checkedChanges +import reactivecircus.flowbinding.android.widget.textChanges import javax.inject.Inject class ContactsBookFragment @Inject constructor( @@ -83,21 +86,21 @@ class ContactsBookFragment @Inject constructor( private fun setupOnlyBoundContactsView() { views.phoneBookOnlyBoundContacts.checkedChanges() - .subscribe { + .onEach { contactsBookViewModel.handle(ContactsBookAction.OnlyBoundContacts(it)) } - .disposeOnDestroyView() + .launchIn(viewLifecycleOwner.lifecycleScope) } private fun setupFilterView() { views.phoneBookFilter .textChanges() .skipInitialValue() - .debounce(300, TimeUnit.MILLISECONDS) - .subscribe { + .debounce(300) + .onEach { contactsBookViewModel.handle(ContactsBookAction.FilterWith(it.toString())) } - .disposeOnDestroyView() + .launchIn(viewLifecycleOwner.lifecycleScope) } override fun onDestroyView() { diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt index 28da72714a..3ff989da5a 100644 --- a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt +++ b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt @@ -22,6 +22,7 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.view.View +import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.Async import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading @@ -46,6 +47,8 @@ import im.vector.app.features.userdirectory.UserListFragment import im.vector.app.features.userdirectory.UserListFragmentArgs import im.vector.app.features.userdirectory.UserListSharedAction import im.vector.app.features.userdirectory.UserListSharedActionViewModel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure import java.net.HttpURLConnection @@ -64,8 +67,8 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() { sharedActionViewModel = viewModelProvider.get(UserListSharedActionViewModel::class.java) sharedActionViewModel - .observe() - .subscribe { action -> + .stream() + .onEach { action -> when (action) { UserListSharedAction.Close -> finish() UserListSharedAction.GoBack -> onBackPressed() @@ -74,7 +77,7 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() { UserListSharedAction.AddByQrCode -> openAddByQrCode() }.exhaustive } - .disposeOnDestroy() + .launchIn(lifecycleScope) if (isFirstCreation()) { addFragment( R.id.container, diff --git a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageActivity.kt b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageActivity.kt index 61c8ab8f0a..bb854aca26 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageActivity.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageActivity.kt @@ -63,7 +63,7 @@ class SharedSecureStorageActivity : viewModel.observeViewEvents { observeViewEvents(it) } - viewModel.subscribe(this) { renderState(it) } + viewModel.onEach { renderState(it) } } override fun onDestroy() { diff --git a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStorageKeyFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStorageKeyFragment.kt index 1ba0198cb4..c49291d6a2 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStorageKeyFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStorageKeyFragment.kt @@ -22,17 +22,19 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.inputmethod.EditorInfo +import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.activityViewModel -import com.jakewharton.rxbinding3.widget.editorActionEvents -import com.jakewharton.rxbinding3.widget.textChanges import im.vector.app.R import im.vector.app.core.extensions.registerStartForActivityResult +import im.vector.app.core.flow.throttleFirst import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.utils.startImportTextFromFileIntent import im.vector.app.databinding.FragmentSsssAccessFromKeyBinding -import io.reactivex.android.schedulers.AndroidSchedulers +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.extensions.tryOrNull -import java.util.concurrent.TimeUnit +import reactivecircus.flowbinding.android.widget.editorActionEvents +import reactivecircus.flowbinding.android.widget.textChanges import javax.inject.Inject class SharedSecuredStorageKeyFragment @Inject constructor() : VectorBaseFragment() { @@ -48,22 +50,21 @@ class SharedSecuredStorageKeyFragment @Inject constructor() : VectorBaseFragment views.ssssRestoreWithKeyText.text = getString(R.string.enter_secret_storage_input_key) views.ssssKeyEnterEdittext.editorActionEvents() - .throttleFirst(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { + .throttleFirst(300) + .onEach { if (it.actionId == EditorInfo.IME_ACTION_DONE) { submit() } } - .disposeOnDestroyView() + .launchIn(viewLifecycleOwner.lifecycleScope) views.ssssKeyEnterEdittext.textChanges() .skipInitialValue() - .subscribe { + .onEach { views.ssssKeyEnterTil.error = null views.ssssKeySubmit.isEnabled = it.isNotBlank() } - .disposeOnDestroyView() + .launchIn(viewLifecycleOwner.lifecycleScope) views.ssssKeyUseFile.debouncedClicks { startImportTextFromFileIntent(requireContext(), importFileStartForActivityResult) } diff --git a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt index 800b02a936..c93e562d77 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt @@ -22,15 +22,17 @@ import android.view.View import android.view.ViewGroup import android.view.inputmethod.EditorInfo import androidx.core.text.toSpannable +import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.activityViewModel -import com.jakewharton.rxbinding3.widget.editorActionEvents -import com.jakewharton.rxbinding3.widget.textChanges import im.vector.app.R +import im.vector.app.core.flow.throttleFirst import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider import im.vector.app.databinding.FragmentSsssAccessFromPassphraseBinding -import io.reactivex.android.schedulers.AndroidSchedulers -import java.util.concurrent.TimeUnit +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import reactivecircus.flowbinding.android.widget.editorActionEvents +import reactivecircus.flowbinding.android.widget.textChanges import javax.inject.Inject class SharedSecuredStoragePassphraseFragment @Inject constructor( @@ -60,21 +62,20 @@ class SharedSecuredStoragePassphraseFragment @Inject constructor( // .colorizeMatchingText(key, colorProvider.getColorFromAttribute(android.R.attr.textColorLink)) views.ssssPassphraseEnterEdittext.editorActionEvents() - .throttleFirst(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { + .throttleFirst(300) + .onEach { if (it.actionId == EditorInfo.IME_ACTION_DONE) { submit() } } - .disposeOnDestroyView() + .launchIn(viewLifecycleOwner.lifecycleScope) views.ssssPassphraseEnterEdittext.textChanges() - .subscribe { + .onEach { views.ssssPassphraseEnterTil.error = null views.ssssPassphraseSubmit.isEnabled = it.isNotBlank() } - .disposeOnDestroyView() + .launchIn(viewLifecycleOwner.lifecycleScope) views.ssssPassphraseReset.views.bottomSheetActionClickableZone.debouncedClicks { sharedViewModel.handle(SharedSecureStorageAction.ForgotResetAll) diff --git a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStorageResetAllFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStorageResetAllFragment.kt index 670e5c610a..200b2b73c2 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStorageResetAllFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStorageResetAllFragment.kt @@ -55,7 +55,7 @@ class SharedSecuredStorageResetAllFragment @Inject constructor() : } } - sharedViewModel.subscribe(this) { state -> + sharedViewModel.onEach { state -> views.ssssResetOtherDevices.setTextOrHide( state.activeDeviceCount .takeIf { it > 0 } diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapConfirmPassphraseFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapConfirmPassphraseFragment.kt index 4aeb822bbd..940a4d9af3 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapConfirmPassphraseFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapConfirmPassphraseFragment.kt @@ -22,16 +22,18 @@ import android.view.View import android.view.ViewGroup import android.view.inputmethod.EditorInfo import androidx.core.view.isGone +import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.withState -import com.jakewharton.rxbinding3.widget.editorActionEvents -import com.jakewharton.rxbinding3.widget.textChanges import im.vector.app.R import im.vector.app.core.extensions.hideKeyboard +import im.vector.app.core.flow.throttleFirst import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentBootstrapEnterPassphraseBinding -import io.reactivex.android.schedulers.AndroidSchedulers -import java.util.concurrent.TimeUnit +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import reactivecircus.flowbinding.android.widget.editorActionEvents +import reactivecircus.flowbinding.android.widget.textChanges import javax.inject.Inject class BootstrapConfirmPassphraseFragment @Inject constructor() : @@ -58,21 +60,20 @@ class BootstrapConfirmPassphraseFragment @Inject constructor() : } views.ssssPassphraseEnterEdittext.editorActionEvents() - .throttleFirst(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { + .throttleFirst(300) + .onEach { if (it.actionId == EditorInfo.IME_ACTION_DONE) { submit() } } - .disposeOnDestroyView() + .launchIn(viewLifecycleOwner.lifecycleScope) views.ssssPassphraseEnterEdittext.textChanges() - .subscribe { + .onEach { views.ssssPassphraseEnterTil.error = null - sharedViewModel.handle(BootstrapActions.UpdateConfirmCandidatePassphrase(it?.toString() ?: "")) + sharedViewModel.handle(BootstrapActions.UpdateConfirmCandidatePassphrase(it.toString())) } - .disposeOnDestroyView() + .launchIn(viewLifecycleOwner.lifecycleScope) sharedViewModel.observeViewEvents { // when (it) { diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapEnterPassphraseFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapEnterPassphraseFragment.kt index f43ddb8888..77fb5ab3a6 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapEnterPassphraseFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapEnterPassphraseFragment.kt @@ -21,16 +21,18 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.inputmethod.EditorInfo +import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.withState -import com.jakewharton.rxbinding3.widget.editorActionEvents -import com.jakewharton.rxbinding3.widget.textChanges import im.vector.app.R +import im.vector.app.core.flow.throttleFirst import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentBootstrapEnterPassphraseBinding import im.vector.app.features.settings.VectorLocale -import io.reactivex.android.schedulers.AndroidSchedulers -import java.util.concurrent.TimeUnit +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import reactivecircus.flowbinding.android.widget.editorActionEvents +import reactivecircus.flowbinding.android.widget.textChanges import javax.inject.Inject class BootstrapEnterPassphraseFragment @Inject constructor() : @@ -53,22 +55,21 @@ class BootstrapEnterPassphraseFragment @Inject constructor() : views.ssssPassphraseEnterEdittext.setText(it.passphrase ?: "") } views.ssssPassphraseEnterEdittext.editorActionEvents() - .throttleFirst(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { + .throttleFirst(300) + .onEach { if (it.actionId == EditorInfo.IME_ACTION_DONE) { submit() } } - .disposeOnDestroyView() + .launchIn(viewLifecycleOwner.lifecycleScope) views.ssssPassphraseEnterEdittext.textChanges() - .subscribe { + .onEach { // ssss_passphrase_enter_til.error = null - sharedViewModel.handle(BootstrapActions.UpdateCandidatePassphrase(it?.toString() ?: "")) + sharedViewModel.handle(BootstrapActions.UpdateCandidatePassphrase(it.toString())) // ssss_passphrase_submit.isEnabled = it.isNotBlank() } - .disposeOnDestroyView() + .launchIn(viewLifecycleOwner.lifecycleScope) sharedViewModel.observeViewEvents { // when (it) { diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapMigrateBackupFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapMigrateBackupFragment.kt index b40a194a15..5d0f3bbeae 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapMigrateBackupFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapMigrateBackupFragment.kt @@ -27,22 +27,24 @@ import android.view.ViewGroup import android.view.inputmethod.EditorInfo import androidx.core.text.toSpannable import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.withState -import com.jakewharton.rxbinding3.widget.editorActionEvents -import com.jakewharton.rxbinding3.widget.textChanges import im.vector.app.R import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.registerStartForActivityResult +import im.vector.app.core.flow.throttleFirst import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider import im.vector.app.core.utils.colorizeMatchingText import im.vector.app.core.utils.startImportTextFromFileIntent import im.vector.app.databinding.FragmentBootstrapMigrateBackupBinding -import io.reactivex.android.schedulers.AndroidSchedulers +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.internal.crypto.keysbackup.util.isValidRecoveryKey -import java.util.concurrent.TimeUnit +import reactivecircus.flowbinding.android.widget.editorActionEvents +import reactivecircus.flowbinding.android.widget.textChanges import javax.inject.Inject class BootstrapMigrateBackupFragment @Inject constructor( @@ -63,22 +65,21 @@ class BootstrapMigrateBackupFragment @Inject constructor( views.bootstrapMigrateEditText.setText(it.passphrase ?: "") } views.bootstrapMigrateEditText.editorActionEvents() - .throttleFirst(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { + .throttleFirst(300) + .onEach { if (it.actionId == EditorInfo.IME_ACTION_DONE) { submit() } } - .disposeOnDestroyView() + .launchIn(viewLifecycleOwner.lifecycleScope) views.bootstrapMigrateEditText.textChanges() .skipInitialValue() - .subscribe { + .onEach { views.bootstrapRecoveryKeyEnterTil.error = null // sharedViewModel.handle(BootstrapActions.UpdateCandidatePassphrase(it?.toString() ?: "")) } - .disposeOnDestroyView() + .launchIn(viewLifecycleOwner.lifecycleScope) // sharedViewModel.observeViewEvents {} views.bootstrapMigrateContinueButton.debouncedClicks { submit() } diff --git a/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolActivity.kt b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolActivity.kt index 772ef99931..2c7a15e6ad 100644 --- a/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolActivity.kt +++ b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolActivity.kt @@ -66,7 +66,7 @@ class RoomDevToolActivity : SimpleFragmentActivity(), FragmentManager.OnBackStac override fun initUiAndData() { super.initUiAndData() - viewModel.subscribe(this) { + viewModel.onEach { renderState(it) } diff --git a/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolEditFragment.kt b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolEditFragment.kt index 9af51f67b3..dd0bd174af 100644 --- a/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolEditFragment.kt +++ b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolEditFragment.kt @@ -20,12 +20,15 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.withState -import com.jakewharton.rxbinding3.widget.textChanges import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentDevtoolsEditorBinding +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import reactivecircus.flowbinding.android.widget.textChanges import javax.inject.Inject class RoomDevToolEditFragment @Inject constructor() : @@ -44,10 +47,10 @@ class RoomDevToolEditFragment @Inject constructor() : } views.editText.textChanges() .skipInitialValue() - .subscribe { + .onEach { sharedViewModel.handle(RoomDevToolAction.UpdateContentText(it.toString())) } - .disposeOnDestroyView() + .launchIn(viewLifecycleOwner.lifecycleScope) } override fun onResume() { diff --git a/vector/src/main/java/im/vector/app/features/discovery/change/SetIdentityServerFragment.kt b/vector/src/main/java/im/vector/app/features/discovery/change/SetIdentityServerFragment.kt index 15e4e65d3b..fcea2e92b1 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/change/SetIdentityServerFragment.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/change/SetIdentityServerFragment.kt @@ -24,10 +24,10 @@ import android.view.inputmethod.EditorInfo import androidx.appcompat.app.AppCompatActivity import androidx.core.text.toSpannable import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.jakewharton.rxbinding3.widget.textChanges import im.vector.app.R import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.registerStartForActivityResult @@ -37,7 +37,10 @@ import im.vector.app.core.resources.ColorProvider import im.vector.app.core.utils.colorizeMatchingText import im.vector.app.databinding.FragmentSetIdentityServerBinding import im.vector.app.features.discovery.DiscoverySharedViewModel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.session.terms.TermsService +import reactivecircus.flowbinding.android.widget.textChanges import javax.inject.Inject class SetIdentityServerFragment @Inject constructor( @@ -90,11 +93,11 @@ class SetIdentityServerFragment @Inject constructor( views.identityServerSetDefaultAlternativeTextInput .textChanges() - .subscribe { + .onEach { views.identityServerSetDefaultAlternativeTil.error = null views.identityServerSetDefaultAlternativeSubmit.isEnabled = it.isNotEmpty() } - .disposeOnDestroyView() + .launchIn(viewLifecycleOwner.lifecycleScope) views.identityServerSetDefaultSubmit.debouncedClicks { viewModel.handle(SetIdentityServerAction.UseDefaultIdentityServer) diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index 1ad08ac8e9..16c0655d85 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -73,6 +73,8 @@ import im.vector.app.features.spaces.share.ShareSpaceBottomSheet import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.workers.signout.ServerBackupStatusViewModel import im.vector.app.push.fcm.FcmHelper +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.session.initsync.SyncStatusService @@ -178,8 +180,8 @@ class HomeActivity : } sharedActionViewModel - .observe() - .subscribe { sharedAction -> + .stream() + .onEach { sharedAction -> when (sharedAction) { is HomeActivitySharedAction.OpenDrawer -> views.drawerLayout.openDrawer(GravityCompat.START) is HomeActivitySharedAction.CloseDrawer -> views.drawerLayout.closeDrawer(GravityCompat.START) @@ -222,7 +224,7 @@ class HomeActivity : } }.exhaustive } - .disposeOnDestroy() + .launchIn(lifecycleScope) val args = intent.getParcelableExtra(Mavericks.KEY_ARG) @@ -243,13 +245,12 @@ class HomeActivity : is HomeActivityViewEvents.OnCrossSignedInvalidated -> handleCrossSigningInvalidated(it) }.exhaustive } - homeActivityViewModel.subscribe(this) { renderState(it) } + homeActivityViewModel.onEach { renderState(it) } - shortcutsHandler.observeRoomsAndBuildShortcuts() - .disposeOnDestroy() + shortcutsHandler.observeRoomsAndBuildShortcuts(lifecycleScope) if (!vectorPreferences.didPromoteNewRestrictedFeature()) { - promoteRestrictedViewModel.subscribe(this) { + promoteRestrictedViewModel.onEach { if (it.activeSpaceSummary != null && !it.activeSpaceSummary.isPublic && it.activeSpaceSummary.otherMemberIds.isNotEmpty()) { // It's a private space with some members show this once diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt index 80351a437e..55d8e2e09a 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt @@ -299,7 +299,7 @@ class HomeDetailFragment @Inject constructor( private fun setupKeysBackupBanner() { serverBackupStatusViewModel - .subscribe(this) { + .onEach { when (val banState = it.bannerState.invoke()) { is BannerState.Setup -> views.homeKeysBackupBanner.render(KeysBackupBanner.State.Setup(banState.numberOfKeys), false) BannerState.BackingUp -> views.homeKeysBackupBanner.render(KeysBackupBanner.State.BackingUp, false) diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt index 73e50ad5f1..1c1d012cc8 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt @@ -27,6 +27,7 @@ import im.vector.app.RoomGroupingMethod import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.singletonEntryPoint +import im.vector.app.core.flow.throttleFirst import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.call.dialpad.DialPadLookup import im.vector.app.features.call.lookup.CallProtocolsChecker @@ -36,10 +37,13 @@ import im.vector.app.features.invite.AutoAcceptInvites import im.vector.app.features.invite.showInvites import im.vector.app.features.settings.VectorDataStore import im.vector.app.features.ui.UiStateRepository -import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.matrix.android.sdk.api.query.ActiveSpaceFilter import org.matrix.android.sdk.api.query.RoomCategoryFilter @@ -50,9 +54,7 @@ import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.flow.flow -import org.matrix.android.sdk.rx.asObservable import timber.log.Timber -import java.util.concurrent.TimeUnit /** * View model used to update the home bottom bar notification counts, observe the sync state and @@ -66,7 +68,7 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho private val directRoomHelper: DirectRoomHelper, private val appStateHandler: AppStateHandler, private val autoAcceptInvites: AutoAcceptInvites) : - VectorViewModel(initialState), + VectorViewModel(initialState), CallProtocolsChecker.Listener { @AssistedFactory @@ -194,18 +196,15 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho private fun observeRoomGroupingMethod() { appStateHandler.selectedRoomGroupingObservable - .subscribe { - setState { - copy( - roomGroupingMethod = it.orNull() ?: RoomGroupingMethod.BySpace(null) - ) - } + .setOnEach { + copy( + roomGroupingMethod = it.orNull() ?: RoomGroupingMethod.BySpace(null) + ) } - .disposeOnClear() } private fun observeRoomSummaries() { - appStateHandler.selectedRoomGroupingObservable.distinctUntilChanged().switchMap { + appStateHandler.selectedRoomGroupingObservable.distinctUntilChanged().flatMapLatest { // we use it as a trigger to all changes in room, but do not really load // the actual models session.getPagedRoomSummariesLive( @@ -213,11 +212,10 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho memberships = Membership.activeMemberships() }, sortOrder = RoomSortOrder.NONE - ).asObservable() + ).asFlow() } - .observeOn(Schedulers.computation()) - .throttleFirst(300, TimeUnit.MILLISECONDS) - .subscribe { + .throttleFirst(300) + .onEach { when (val groupingMethod = appStateHandler.getCurrentRoomGroupingMethod()) { is RoomGroupingMethod.ByLegacyGroup -> { // TODO!! @@ -274,6 +272,6 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho } } } - .disposeOnClear() + .launchIn(viewModelScope) } } diff --git a/vector/src/main/java/im/vector/app/features/home/PromoteRestrictedViewModel.kt b/vector/src/main/java/im/vector/app/features/home/PromoteRestrictedViewModel.kt index 218574c03e..77ee23f732 100644 --- a/vector/src/main/java/im/vector/app/features/home/PromoteRestrictedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/PromoteRestrictedViewModel.kt @@ -29,6 +29,7 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel +import kotlinx.coroutines.flow.distinctUntilChanged import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toModel diff --git a/vector/src/main/java/im/vector/app/features/home/ShortcutsHandler.kt b/vector/src/main/java/im/vector/app/features/home/ShortcutsHandler.kt index 612e2dcf87..a84a721a31 100644 --- a/vector/src/main/java/im/vector/app/features/home/ShortcutsHandler.kt +++ b/vector/src/main/java/im/vector/app/features/home/ShortcutsHandler.kt @@ -22,54 +22,62 @@ import android.os.Build import androidx.core.content.getSystemService import androidx.core.content.pm.ShortcutManagerCompat import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.dispatchers.CoroutineDispatchers import im.vector.app.features.pin.PinCodeStore import im.vector.app.features.pin.PinCodeStoreListener -import io.reactivex.disposables.Disposable -import io.reactivex.disposables.Disposables +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart import org.matrix.android.sdk.api.session.room.RoomSortOrder import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams -import org.matrix.android.sdk.rx.asObservable +import org.matrix.android.sdk.flow.flow import timber.log.Timber +import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject class ShortcutsHandler @Inject constructor( private val context: Context, + private val appDispatchers: CoroutineDispatchers, private val shortcutCreator: ShortcutCreator, private val activeSessionHolder: ActiveSessionHolder, private val pinCodeStore: PinCodeStore ) : PinCodeStoreListener { + private val isRequestPinShortcutSupported = ShortcutManagerCompat.isRequestPinShortcutSupported(context) // Value will be set correctly if necessary - private var hasPinCode = true + private var hasPinCode = AtomicBoolean(true) - fun observeRoomsAndBuildShortcuts(): Disposable { + fun observeRoomsAndBuildShortcuts(coroutineScope: CoroutineScope): Job { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) { // No op - return Disposables.empty() + return Job() } - - hasPinCode = pinCodeStore.getEncodedPin() != null - - val session = activeSessionHolder.getSafeActiveSession() ?: return Disposables.empty() - return session.getRoomSummariesLive( + hasPinCode.set(pinCodeStore.getEncodedPin() != null) + val session = activeSessionHolder.getSafeActiveSession() ?: return Job() + return session.flow().liveRoomSummaries( roomSummaryQueryParams { memberships = listOf(Membership.JOIN) }, sortOrder = RoomSortOrder.PRIORITY_AND_ACTIVITY ) - .asObservable() - .doOnSubscribe { pinCodeStore.addListener(this) } - .doFinally { pinCodeStore.removeListener(this) } - .subscribe { rooms -> + .onStart { pinCodeStore.addListener(this@ShortcutsHandler) } + .onCompletion { pinCodeStore.removeListener(this@ShortcutsHandler) } + .onEach { rooms -> // Remove dead shortcuts (i.e. deleted rooms) removeDeadShortcut(rooms.map { it.roomId }) // Create shortcuts createShortcuts(rooms) } + .flowOn(appDispatchers.computation) + .launchIn(coroutineScope) } private fun removeDeadShortcut(roomIds: List) { @@ -89,7 +97,7 @@ class ShortcutsHandler @Inject constructor( } private fun createShortcuts(rooms: List) { - if (hasPinCode) { + if (hasPinCode.get()) { // No shortcut in this case (privacy) ShortcutManagerCompat.removeAllDynamicShortcuts(context) } else { @@ -127,7 +135,7 @@ class ShortcutsHandler @Inject constructor( } override fun onPinSetUpChange(isConfigured: Boolean) { - hasPinCode = isConfigured + hasPinCode.set(isConfigured) if (isConfigured) { // Remove shortcuts immediately ShortcutManagerCompat.removeAllDynamicShortcuts(context) diff --git a/vector/src/main/java/im/vector/app/features/home/UnreadMessagesSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/home/UnreadMessagesSharedViewModel.kt index 5bdbc95b48..6c0ae71cfa 100644 --- a/vector/src/main/java/im/vector/app/features/home/UnreadMessagesSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/UnreadMessagesSharedViewModel.kt @@ -16,6 +16,7 @@ package im.vector.app.features.home +import androidx.lifecycle.asFlow import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.MavericksViewModelFactory import dagger.assisted.Assisted @@ -25,13 +26,17 @@ import im.vector.app.AppStateHandler import im.vector.app.RoomGroupingMethod import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory +import im.vector.app.core.flow.throttleFirst import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.invite.AutoAcceptInvites import im.vector.app.features.settings.VectorPreferences -import io.reactivex.Observable -import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOn import org.matrix.android.sdk.api.query.ActiveSpaceFilter import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.room.RoomSortOrder @@ -39,8 +44,6 @@ import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.api.session.room.spaceSummaryQueryParams import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount -import org.matrix.android.sdk.rx.asObservable -import java.util.concurrent.TimeUnit data class UnreadMessagesState( val homeSpaceUnread: RoomAggregateNotificationCount = RoomAggregateNotificationCount(0, 0), @@ -57,7 +60,7 @@ class UnreadMessagesSharedViewModel @AssistedInject constructor(@Assisted initia private val vectorPreferences: VectorPreferences, appStateHandler: AppStateHandler, private val autoAcceptInvites: AutoAcceptInvites) : - VectorViewModel(initialState) { + VectorViewModel(initialState) { @AssistedFactory interface Factory : MavericksAssistedViewModelFactory { @@ -75,8 +78,8 @@ class UnreadMessagesSharedViewModel @AssistedInject constructor(@Assisted initia this.memberships = listOf(Membership.JOIN) this.activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(null) }, sortOrder = RoomSortOrder.NONE - ).asObservable() - .throttleFirst(300, TimeUnit.MILLISECONDS) + ).asFlow() + .throttleFirst(300) .execute { val counts = session.getNotificationCountForRooms( roomSummaryQueryParams { @@ -103,91 +106,91 @@ class UnreadMessagesSharedViewModel @AssistedInject constructor(@Assisted initia ) } - Observable.combineLatest( + combine( appStateHandler.selectedRoomGroupingObservable.distinctUntilChanged(), - appStateHandler.selectedRoomGroupingObservable.switchMap { + appStateHandler.selectedRoomGroupingObservable.flatMapLatest { session.getPagedRoomSummariesLive( roomSummaryQueryParams { this.memberships = Membership.activeMemberships() }, sortOrder = RoomSortOrder.NONE - ).asObservable() - .throttleFirst(300, TimeUnit.MILLISECONDS) - .observeOn(Schedulers.computation()) - }, - { groupingMethod, _ -> - when (groupingMethod.orNull()) { - is RoomGroupingMethod.ByLegacyGroup -> { - // currently not supported - CountInfo( - RoomAggregateNotificationCount(0, 0), - RoomAggregateNotificationCount(0, 0) - ) - } - is RoomGroupingMethod.BySpace -> { - val selectedSpace = appStateHandler.safeActiveSpaceId() - - val inviteCount = if (autoAcceptInvites.hideInvites) { - 0 - } else { - session.getRoomSummaries( - roomSummaryQueryParams { this.memberships = listOf(Membership.INVITE) } - ).size - } - - val spaceInviteCount = if (autoAcceptInvites.hideInvites) { - 0 - } else { - session.getRoomSummaries( - spaceSummaryQueryParams { - this.memberships = listOf(Membership.INVITE) - } - ).size - } - - val totalCount = session.getNotificationCountForRooms( - roomSummaryQueryParams { - this.memberships = listOf(Membership.JOIN) - this.activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(null).takeIf { - !vectorPreferences.prefSpacesShowAllRoomInHome() - } ?: ActiveSpaceFilter.None - } - ) - - val counts = RoomAggregateNotificationCount( - totalCount.notificationCount + inviteCount, - totalCount.highlightCount + inviteCount - ) - val rootCounts = session.spaceService().getRootSpaceSummaries() - .filter { - // filter out current selection - it.roomId != selectedSpace - } - - CountInfo( - homeCount = counts, - otherCount = RoomAggregateNotificationCount( - notificationCount = rootCounts.fold(0, { acc, rs -> acc + rs.notificationCount }) + - (counts.notificationCount.takeIf { selectedSpace != null } ?: 0) + - spaceInviteCount, - highlightCount = rootCounts.fold(0, { acc, rs -> acc + rs.highlightCount }) + - (counts.highlightCount.takeIf { selectedSpace != null } ?: 0) + - spaceInviteCount - ) - ) - } - null -> { - CountInfo( - RoomAggregateNotificationCount(0, 0), - RoomAggregateNotificationCount(0, 0) - ) - } - } + ).asFlow() + .throttleFirst(300) } - ).execute { - copy( - homeSpaceUnread = it.invoke()?.homeCount ?: RoomAggregateNotificationCount(0, 0), - otherSpacesUnread = it.invoke()?.otherCount ?: RoomAggregateNotificationCount(0, 0) - ) + ) { groupingMethod, _ -> + when (groupingMethod.orNull()) { + is RoomGroupingMethod.ByLegacyGroup -> { + // currently not supported + CountInfo( + RoomAggregateNotificationCount(0, 0), + RoomAggregateNotificationCount(0, 0) + ) + } + is RoomGroupingMethod.BySpace -> { + val selectedSpace = appStateHandler.safeActiveSpaceId() + + val inviteCount = if (autoAcceptInvites.hideInvites) { + 0 + } else { + session.getRoomSummaries( + roomSummaryQueryParams { this.memberships = listOf(Membership.INVITE) } + ).size + } + + val spaceInviteCount = if (autoAcceptInvites.hideInvites) { + 0 + } else { + session.getRoomSummaries( + spaceSummaryQueryParams { + this.memberships = listOf(Membership.INVITE) + } + ).size + } + + val totalCount = session.getNotificationCountForRooms( + roomSummaryQueryParams { + this.memberships = listOf(Membership.JOIN) + this.activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(null).takeIf { + !vectorPreferences.prefSpacesShowAllRoomInHome() + } ?: ActiveSpaceFilter.None + } + ) + + val counts = RoomAggregateNotificationCount( + totalCount.notificationCount + inviteCount, + totalCount.highlightCount + inviteCount + ) + val rootCounts = session.spaceService().getRootSpaceSummaries() + .filter { + // filter out current selection + it.roomId != selectedSpace + } + + CountInfo( + homeCount = counts, + otherCount = RoomAggregateNotificationCount( + notificationCount = rootCounts.fold(0, { acc, rs -> acc + rs.notificationCount }) + + (counts.notificationCount.takeIf { selectedSpace != null } ?: 0) + + spaceInviteCount, + highlightCount = rootCounts.fold(0, { acc, rs -> acc + rs.highlightCount }) + + (counts.highlightCount.takeIf { selectedSpace != null } ?: 0) + + spaceInviteCount + ) + ) + } + null -> { + CountInfo( + RoomAggregateNotificationCount(0, 0), + RoomAggregateNotificationCount(0, 0) + ) + } + } } + .flowOn(Dispatchers.Default) + .execute { + copy( + homeSpaceUnread = it.invoke()?.homeCount ?: RoomAggregateNotificationCount(0, 0), + otherSpacesUnread = it.invoke()?.otherCount ?: RoomAggregateNotificationCount(0, 0) + ) + } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt index ba53f75eca..415ca7bc04 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt @@ -24,6 +24,7 @@ import androidx.core.view.GravityCompat import androidx.drawerlayout.widget.DrawerLayout import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager +import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.viewModel import com.google.android.material.appbar.MaterialToolbar @@ -40,6 +41,8 @@ import im.vector.app.features.navigation.Navigator import im.vector.app.features.room.RequireActiveMembershipAction import im.vector.app.features.room.RequireActiveMembershipViewEvents import im.vector.app.features.room.RequireActiveMembershipViewModel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach @AndroidEntryPoint class RoomDetailActivity : @@ -97,13 +100,13 @@ class RoomDetailActivity : sharedActionViewModel = viewModelProvider.get(RoomDetailSharedActionViewModel::class.java) sharedActionViewModel - .observe() - .subscribe { sharedAction -> + .stream() + .onEach { sharedAction -> when (sharedAction) { is RoomDetailSharedAction.SwitchToRoom -> switchToRoom(sharedAction) } } - .disposeOnDestroy() + .launchIn(lifecycleScope) requireActiveMembershipViewModel.observeViewEvents { when (it) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 75edee5d55..d3af9c4196 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -67,8 +67,6 @@ import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.jakewharton.rxbinding3.view.focusChanges -import com.jakewharton.rxbinding3.widget.textChanges import com.vanniktech.emoji.EmojiPopup import im.vector.app.R import im.vector.app.core.dialogs.ConfirmationDialogBuilder @@ -185,6 +183,10 @@ import im.vector.app.features.widgets.WidgetActivity import im.vector.app.features.widgets.WidgetArgs import im.vector.app.features.widgets.WidgetKind import im.vector.app.features.widgets.permissions.RoomWidgetPermissionBottomSheet +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize import nl.dionsegijn.konfetti.models.Shape @@ -216,10 +218,11 @@ import org.matrix.android.sdk.api.util.MimeTypes import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode +import reactivecircus.flowbinding.android.view.focusChanges +import reactivecircus.flowbinding.android.widget.textChanges import timber.log.Timber import java.net.URL import java.util.UUID -import java.util.concurrent.TimeUnit import javax.inject.Inject @Parcelize @@ -366,11 +369,11 @@ class RoomDetailFragment @Inject constructor( } sharedActionViewModel - .observe() - .subscribe { + .stream() + .onEach { handleActions(it) } - .disposeOnDestroyView() + .launchIn(viewLifecycleOwner.lifecycleScope) knownCallsViewModel .liveKnownCalls @@ -1358,19 +1361,19 @@ class RoomDetailFragment @Inject constructor( private fun observerUserTyping() { views.composerLayout.views.composerEditText.textChanges() .skipInitialValue() - .debounce(300, TimeUnit.MILLISECONDS) + .debounce(300) .map { it.isNotEmpty() } - .subscribe { + .onEach { Timber.d("Typing: User is typing: $it") textComposerViewModel.handle(TextComposerAction.UserIsTyping(it)) } - .disposeOnDestroyView() + .launchIn(viewLifecycleOwner.lifecycleScope) views.composerLayout.views.composerEditText.focusChanges() - .subscribe { + .onEach { roomDetailViewModel.handle(RoomDetailAction.ComposerFocusChange(it)) } - .disposeOnDestroyView() + .launchIn(viewLifecycleOwner.lifecycleScope) } private fun sendUri(uri: Uri): Boolean { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 03bde7d4cc..d846a1d1f8 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -27,16 +27,17 @@ import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext -import com.jakewharton.rxrelay2.BehaviorRelay import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.extensions.exhaustive +import im.vector.app.core.flow.chunk import im.vector.app.core.mvrx.runCatchingToAsync import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider +import im.vector.app.core.utils.BehaviorDataSource import im.vector.app.features.attachments.toContentAttachmentData import im.vector.app.features.call.conference.ConferenceEvent import im.vector.app.features.call.conference.JitsiActiveConferenceHolder @@ -56,13 +57,14 @@ import im.vector.app.features.session.coroutineScope import im.vector.app.features.settings.VectorDataStore import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.voice.VoicePlayerHelper -import io.reactivex.rxkotlin.subscribeBy import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @@ -98,7 +100,6 @@ import org.matrix.android.sdk.flow.flow import org.matrix.android.sdk.flow.unwrap import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode import timber.log.Timber -import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean class RoomDetailViewModel @AssistedInject constructor( @@ -123,8 +124,8 @@ class RoomDetailViewModel @AssistedInject constructor( private val room = session.getRoom(initialState.roomId)!! private val eventId = initialState.eventId - private val invisibleEventsObservable = BehaviorRelay.create() - private val visibleEventsObservable = BehaviorRelay.create() + private val invisibleEventsSource = BehaviorDataSource() + private val visibleEventsSource = BehaviorDataSource() private var timelineEvents = MutableSharedFlow>(0) val timeline = timelineFactory.createTimeline(viewModelScope, room, eventId) @@ -562,7 +563,7 @@ class RoomDetailViewModel @AssistedInject constructor( } private fun handleEventInvisible(action: RoomDetailAction.TimelineEventTurnsInvisible) { - invisibleEventsObservable.accept(action) + invisibleEventsSource.post(action) } fun getMember(userId: String): RoomMemberSummary? { @@ -711,12 +712,12 @@ class RoomDetailViewModel @AssistedInject constructor( private fun handleEventVisible(action: RoomDetailAction.TimelineEventTurnsVisible) { viewModelScope.launch(Dispatchers.Default) { if (action.event.root.sendState.isSent()) { // ignore pending/local events - visibleEventsObservable.accept(action) + visibleEventsSource.post(action) } // We need to update this with the related m.replace also (to move read receipt) action.event.annotations?.editSummary?.sourceEvents?.forEach { room.getTimeLineEvent(it)?.let { event -> - visibleEventsObservable.accept(RoomDetailAction.TimelineEventTurnsVisible(event)) + visibleEventsSource.post(RoomDetailAction.TimelineEventTurnsVisible(event)) } } @@ -864,11 +865,13 @@ class RoomDetailViewModel @AssistedInject constructor( private fun observeEventDisplayedActions() { // We are buffering scroll events for one second // and keep the most recent one to set the read receipt on. - visibleEventsObservable - .buffer(1, TimeUnit.SECONDS) + + visibleEventsSource + .stream() + .chunk(1000) .filter { it.isNotEmpty() } - .subscribeBy(onNext = { actions -> - val bufferedMostRecentDisplayedEvent = actions.maxByOrNull { it.event.displayIndex }?.event ?: return@subscribeBy + .onEach { actions -> + val bufferedMostRecentDisplayedEvent = actions.maxByOrNull { it.event.displayIndex }?.event ?: return@onEach val globalMostRecentDisplayedEvent = mostRecentDisplayedEvent if (trackUnreadMessages.get()) { if (globalMostRecentDisplayedEvent == null) { @@ -882,8 +885,9 @@ class RoomDetailViewModel @AssistedInject constructor( tryOrNull { room.setReadReceipt(eventId) } } } - }) - .disposeOnClear() + } + .flowOn(Dispatchers.Default) + .launchIn(viewModelScope) } private fun handleMarkAllAsRead() { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt index e80f25de2f..3d7c4c71f9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt @@ -104,7 +104,7 @@ class TextComposerViewModel @AssistedInject constructor( } private fun subscribeToStateInternal() { - selectSubscribe(TextComposerViewState::sendMode, TextComposerViewState::canSendMessage, TextComposerViewState::isVoiceRecording) { _, _, _ -> + onEach(TextComposerViewState::sendMode, TextComposerViewState::canSendMessage, TextComposerViewState::isVoiceRecording) { _, _, _ -> updateIsSendButtonVisibility(false) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt index 1f4d67db03..686d767850 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt @@ -31,18 +31,16 @@ import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.home.room.detail.timeline.action.TimelineEventFragmentArgs -import io.reactivex.Observable -import io.reactivex.Single +import kotlinx.coroutines.flow.map import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.room.model.ReactionAggregatedSummary -import org.matrix.android.sdk.rx.RxRoom -import org.matrix.android.sdk.rx.unwrap +import org.matrix.android.sdk.flow.flow +import org.matrix.android.sdk.flow.unwrap data class DisplayReactionsViewState( val eventId: String, val roomId: String, val mapReactionKeyToMemberList: Async> = Uninitialized) : - MavericksState { + MavericksState { constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId) } @@ -81,39 +79,31 @@ class ViewReactionsViewModel @AssistedInject constructor(@Assisted } private fun observeEventAnnotationSummaries() { - RxRoom(room) + room.flow() .liveAnnotationSummary(eventId) .unwrap() - .flatMapSingle { summaries -> - Observable - .fromIterable(summaries.reactionsSummary) - // .filter { reactionAggregatedSummary -> isSingleEmoji(reactionAggregatedSummary.key) } - .toReactionInfoList() + .map { annotationsSummary -> + annotationsSummary.reactionsSummary + .flatMap { reactionsSummary -> + reactionsSummary.sourceEvents.map { + val event = room.getTimeLineEvent(it) + ?: throw RuntimeException("Your eventId is not valid") + ReactionInfo( + event.root.eventId!!, + reactionsSummary.key, + event.root.senderId ?: "", + event.senderInfo.disambiguatedDisplayName, + dateFormatter.format(event.root.originServerTs, DateFormatKind.DEFAULT_DATE_AND_TIME) + + ) + } + } } .execute { copy(mapReactionKeyToMemberList = it) } } - private fun Observable.toReactionInfoList(): Single> { - return flatMap { summary -> - Observable - .fromIterable(summary.sourceEvents) - .map { - val event = room.getTimeLineEvent(it) - ?: throw RuntimeException("Your eventId is not valid") - ReactionInfo( - event.root.eventId!!, - summary.key, - event.root.senderId ?: "", - event.senderInfo.disambiguatedDisplayName, - dateFormatter.format(event.root.originServerTs, DateFormatKind.DEFAULT_DATE_AND_TIME) - - ) - } - }.toList() - } - override fun handle(action: EmptyAction) { // No op } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListDisplayModeFilter.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListDisplayModeFilter.kt index b8cdac19cf..94a79f5fbd 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListDisplayModeFilter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListDisplayModeFilter.kt @@ -16,8 +16,8 @@ package im.vector.app.features.home.room.list +import androidx.core.util.Predicate import im.vector.app.features.home.RoomListDisplayMode -import io.reactivex.functions.Predicate import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomSummary diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt index 1c173e12e8..0e049e22b1 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt @@ -23,6 +23,7 @@ import android.view.View import android.view.ViewGroup import androidx.core.content.ContextCompat import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -49,6 +50,8 @@ import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedA import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel import im.vector.app.features.home.room.list.widget.NotifsFabMenuView import im.vector.app.features.notifications.NotificationDrawerManager +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.extensions.orTrue import org.matrix.android.sdk.api.session.room.model.RoomSummary @@ -118,9 +121,9 @@ class RoomListFragment @Inject constructor( views.createChatFabMenu.listener = this sharedActionViewModel - .observe() - .subscribe { handleQuickActions(it) } - .disposeOnDestroyView() + .stream() + .onEach { handleQuickActions(it) } + .launchIn(viewLifecycleOwner.lifecycleScope) roomListViewModel.onEach(RoomListViewState::roomMembershipChanges) { ms -> // it's for invites local echo diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListNameFilter.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListNameFilter.kt index 274cf7c869..edd4c86060 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListNameFilter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListNameFilter.kt @@ -16,7 +16,7 @@ package im.vector.app.features.home.room.list -import io.reactivex.functions.Predicate +import androidx.core.util.Predicate import org.matrix.android.sdk.api.session.room.model.RoomSummary import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilder.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilder.kt index 2b3152f8cf..c98f613c40 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilder.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilder.kt @@ -20,6 +20,4 @@ import im.vector.app.features.home.RoomListDisplayMode interface RoomListSectionBuilder { fun buildSections(mode: RoomListDisplayMode): List - - fun dispose() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderGroup.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderGroup.kt index f101669af3..58db2a4030 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderGroup.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderGroup.kt @@ -17,6 +17,7 @@ package im.vector.app.features.home.room.list import androidx.annotation.StringRes +import androidx.lifecycle.asFlow import im.vector.app.AppStateHandler import im.vector.app.R import im.vector.app.RoomGroupingMethod @@ -24,17 +25,21 @@ import im.vector.app.core.resources.StringProvider import im.vector.app.features.home.RoomListDisplayMode import im.vector.app.features.invite.AutoAcceptInvites import im.vector.app.features.invite.showInvites -import io.reactivex.disposables.CompositeDisposable -import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.query.RoomCategoryFilter import org.matrix.android.sdk.api.query.RoomTagQueryFilter import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams import org.matrix.android.sdk.api.session.room.UpdatableLivePageResult import org.matrix.android.sdk.api.session.room.model.Membership -import org.matrix.android.sdk.rx.asObservable class RoomListSectionBuilderGroup( + private val coroutineScope: CoroutineScope, private val session: Session, private val stringProvider: StringProvider, private val appStateHandler: AppStateHandler, @@ -42,8 +47,6 @@ class RoomListSectionBuilderGroup( private val onUpdatable: (UpdatableLivePageResult) -> Unit ) : RoomListSectionBuilder { - private val disposables = CompositeDisposable() - override fun buildSections(mode: RoomListDisplayMode): List { val activeGroupAwareQueries = mutableListOf() val sections = mutableListOf() @@ -103,16 +106,14 @@ class RoomListSectionBuilderGroup( appStateHandler.selectedRoomGroupingObservable .distinctUntilChanged() - .subscribe { groupingMethod -> + .onEach { groupingMethod -> val selectedGroupId = (groupingMethod.orNull() as? RoomGroupingMethod.ByLegacyGroup)?.groupSummary?.groupId activeGroupAwareQueries.onEach { updater -> updater.updateQuery { query -> query.copy(activeGroupId = selectedGroupId) } } - }.also { - disposables.add(it) - } + }.launchIn(coroutineScope) return sections } @@ -251,15 +252,14 @@ class RoomListSectionBuilderGroup( }.livePagedList .let { livePagedList -> // use it also as a source to update count - livePagedList.asObservable() - .observeOn(Schedulers.computation()) - .subscribe { + livePagedList.asFlow() + .onEach { sections.find { it.sectionName == name } ?.notificationCount ?.postValue(session.getNotificationCountForRooms(roomQueryParams)) - }.also { - disposables.add(it) } + .flowOn(Dispatchers.Default) + .launchIn(coroutineScope) sections.add( RoomsSection( @@ -280,8 +280,4 @@ class RoomListSectionBuilderGroup( .build() .let { block(it) } } - - override fun dispose() { - disposables.dispose() - } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderSpace.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderSpace.kt index 7063281853..bde324e57b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderSpace.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderSpace.kt @@ -19,6 +19,7 @@ package im.vector.app.features.home.room.list import androidx.annotation.StringRes import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.asFlow import androidx.lifecycle.liveData import androidx.paging.PagedList import com.airbnb.mvrx.Async @@ -29,12 +30,15 @@ import im.vector.app.features.home.RoomListDisplayMode import im.vector.app.features.invite.AutoAcceptInvites import im.vector.app.features.invite.showInvites import im.vector.app.space -import io.reactivex.Observable -import io.reactivex.disposables.CompositeDisposable -import io.reactivex.rxkotlin.Observables -import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.query.ActiveSpaceFilter import org.matrix.android.sdk.api.query.RoomCategoryFilter @@ -44,7 +48,7 @@ import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams import org.matrix.android.sdk.api.session.room.UpdatableLivePageResult import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount -import org.matrix.android.sdk.rx.asObservable +import timber.log.Timber class RoomListSectionBuilderSpace( private val session: Session, @@ -57,8 +61,6 @@ class RoomListSectionBuilderSpace( private val onlyOrphansInHome: Boolean = false ) : RoomListSectionBuilder { - private val disposables = CompositeDisposable() - private val pagedListConfig = PagedList.Config.Builder() .setPageSize(10) .setInitialLoadSizeHint(20) @@ -132,14 +134,12 @@ class RoomListSectionBuilderSpace( appStateHandler.selectedRoomGroupingObservable .distinctUntilChanged() - .subscribe { groupingMethod -> + .onEach { groupingMethod -> val selectedSpace = groupingMethod.orNull()?.space() activeSpaceAwareQueries.onEach { updater -> updater.updateForSpaceId(selectedSpace?.roomId) } - }.also { - disposables.add(it) - } + }.launchIn(viewModelScope) return sections } @@ -221,13 +221,13 @@ class RoomListSectionBuilderSpace( } // add suggested rooms - val suggestedRoomsObservable = // MutableLiveData>() + val suggestedRoomsFlow = // MutableLiveData>() appStateHandler.selectedRoomGroupingObservable .distinctUntilChanged() - .switchMap { groupingMethod -> + .flatMapLatest { groupingMethod -> val selectedSpace = groupingMethod.orNull()?.space() if (selectedSpace == null) { - Observable.just(emptyList()) + flowOf(emptyList()) } else { liveData(context = viewModelScope.coroutineContext + Dispatchers.IO) { val spaceSum = tryOrNull { @@ -240,24 +240,23 @@ class RoomListSectionBuilderSpace( session.getRoomSummary(it.childRoomId)?.membership?.isActive() != true } emit(filtered) - }.asObservable() + }.asFlow() } } val liveSuggestedRooms = MutableLiveData() - Observables.combineLatest( - suggestedRoomsObservable, - suggestedRoomJoiningState.asObservable() + combine( + suggestedRoomsFlow, + suggestedRoomJoiningState.asFlow() ) { rooms, joinStates -> SuggestedRoomInfo( rooms, joinStates ) - }.subscribe { + }.onEach { liveSuggestedRooms.postValue(it) - }.also { - disposables.add(it) - } + }.launchIn(viewModelScope) + sections.add( RoomsSection( sectionName = stringProvider.getString(R.string.suggested_header), @@ -373,9 +372,9 @@ class RoomListSectionBuilderSpace( }.livePagedList .let { livePagedList -> // use it also as a source to update count - livePagedList.asObservable() - .observeOn(Schedulers.computation()) - .subscribe { + livePagedList.asFlow() + .onEach { + Timber.v("Thread space list: ${Thread.currentThread()}") sections.find { it.sectionName == name } ?.notificationCount ?.postValue( @@ -387,9 +386,9 @@ class RoomListSectionBuilderSpace( ) } ) - }.also { - disposables.add(it) } + .flowOn(Dispatchers.Default) + .launchIn(viewModelScope) sections.add( RoomsSection( @@ -432,8 +431,4 @@ class RoomListSectionBuilderSpace( RoomListViewModel.SpaceFilterStrategy.NONE -> this } } - - override fun dispose() { - disposables.dispose() - } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt index b38f2565b6..36f6dcab34 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt @@ -135,6 +135,7 @@ class RoomListViewModel @AssistedInject constructor( ) } else { RoomListSectionBuilderGroup( + viewModelScope, session, stringProvider, appStateHandler, @@ -335,9 +336,4 @@ class RoomListViewModel @AssistedInject constructor( _viewEvents.post(value) } } - - override fun onCleared() { - super.onCleared() - roomListSectionBuilder.dispose() - } } diff --git a/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomActivity.kt b/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomActivity.kt index 6f4aff0041..1bf1c12a48 100644 --- a/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomActivity.kt +++ b/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomActivity.kt @@ -21,6 +21,7 @@ import android.content.Intent import android.os.Bundle import android.os.Parcelable import android.view.View +import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.viewModel import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -41,6 +42,8 @@ import im.vector.app.features.userdirectory.UserListFragment import im.vector.app.features.userdirectory.UserListFragmentArgs import im.vector.app.features.userdirectory.UserListSharedAction import im.vector.app.features.userdirectory.UserListSharedActionViewModel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.failure.Failure import java.net.HttpURLConnection @@ -63,8 +66,8 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity() { sharedActionViewModel = viewModelProvider.get(UserListSharedActionViewModel::class.java) sharedActionViewModel - .observe() - .subscribe { sharedAction -> + .stream() + .onEach { sharedAction -> when (sharedAction) { UserListSharedAction.Close -> finish() UserListSharedAction.GoBack -> onBackPressed() @@ -75,7 +78,7 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity() { } } } - .disposeOnDestroy() + .launchIn(lifecycleScope) if (isFirstCreation()) { addFragment( R.id.container, diff --git a/vector/src/main/java/im/vector/app/features/invite/InvitesAcceptor.kt b/vector/src/main/java/im/vector/app/features/invite/InvitesAcceptor.kt index 09eff756d5..73876bf6e9 100644 --- a/vector/src/main/java/im/vector/app/features/invite/InvitesAcceptor.kt +++ b/vector/src/main/java/im/vector/app/features/invite/InvitesAcceptor.kt @@ -18,11 +18,14 @@ package im.vector.app.features.invite import im.vector.app.ActiveSessionDataSource import im.vector.app.features.session.coroutineScope -import io.reactivex.disposables.Disposable +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -51,7 +54,8 @@ class InvitesAcceptor @Inject constructor( private val autoAcceptInvites: AutoAcceptInvites ) : Session.Listener { - private lateinit var activeSessionDisposable: Disposable + private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) + private val shouldRejectRoomIds = mutableSetOf() private val activeSessionIds = mutableSetOf() private val semaphore = Semaphore(1) @@ -61,13 +65,14 @@ class InvitesAcceptor @Inject constructor( } private fun observeActiveSession() { - activeSessionDisposable = sessionDataSource.observe() + sessionDataSource.stream() .distinctUntilChanged() - .subscribe { + .onEach { it.orNull()?.let { session -> onSessionActive(session) } } + .launchIn(coroutineScope) } private fun onSessionActive(session: Session) { diff --git a/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt index 0ca076ccd7..a357caf8fa 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt @@ -85,10 +85,9 @@ open class LoginActivity : VectorBaseActivity(), ToolbarCo addFirstFragment() } - loginViewModel - .subscribe(this) { - updateWithState(it) - } + loginViewModel.onEach { + updateWithState(it) + } loginViewModel.observeViewEvents { handleLoginViewEvents(it) } diff --git a/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt index 60f02cb2c6..0978621f28 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt @@ -25,21 +25,24 @@ import android.view.inputmethod.EditorInfo import androidx.autofill.HintConstants import androidx.core.text.isDigitsOnly import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success -import com.jakewharton.rxbinding3.widget.textChanges import im.vector.app.R import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.hidePassword import im.vector.app.core.extensions.toReducedUrl import im.vector.app.databinding.FragmentLoginBinding -import io.reactivex.Observable -import io.reactivex.rxkotlin.subscribeBy +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.failure.isInvalidPassword +import reactivecircus.flowbinding.android.widget.textChanges import javax.inject.Inject /** @@ -224,20 +227,18 @@ class LoginFragment @Inject constructor() : AbstractSSOLoginFragment - isLoginNotEmpty && isPasswordNotEmpty - } - ) - .subscribeBy { + combine( + views.loginField.textChanges().map { it.trim().isNotEmpty() }, + views.passwordField.textChanges().map { it.isNotEmpty() } + ) { isLoginNotEmpty, isPasswordNotEmpty -> + isLoginNotEmpty && isPasswordNotEmpty + } + .onEach { views.loginFieldTil.error = null views.passwordFieldTil.error = null views.loginSubmit.isEnabled = it } - .disposeOnDestroyView() + .launchIn(viewLifecycleOwner.lifecycleScope) } private fun forgetPasswordClicked() { diff --git a/vector/src/main/java/im/vector/app/features/login/LoginGenericTextInputFormFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginGenericTextInputFormFragment.kt index 925a5c05ab..150cb700cb 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginGenericTextInputFormFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginGenericTextInputFormFragment.kt @@ -25,19 +25,22 @@ import android.view.View import android.view.ViewGroup import androidx.autofill.HintConstants import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.args import com.google.i18n.phonenumbers.NumberParseException import com.google.i18n.phonenumbers.PhoneNumberUtil -import com.jakewharton.rxbinding3.widget.textChanges import im.vector.app.R import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.isEmail import im.vector.app.core.extensions.setTextOrHide import im.vector.app.databinding.FragmentLoginGenericTextInputFormBinding +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.auth.registration.RegisterThreePid import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.is401 +import reactivecircus.flowbinding.android.widget.textChanges import javax.inject.Inject enum class TextInputFormFragmentMode { @@ -93,10 +96,10 @@ class LoginGenericTextInputFormFragment @Inject constructor() : AbstractLoginFra private fun setupTil() { views.loginGenericTextInputFormTextInput.textChanges() - .subscribe { + .onEach { views.loginGenericTextInputFormTil.error = null } - .disposeOnDestroyView() + .launchIn(viewLifecycleOwner.lifecycleScope) } private fun setupUi() { @@ -195,10 +198,10 @@ class LoginGenericTextInputFormFragment @Inject constructor() : AbstractLoginFra private fun setupSubmitButton() { views.loginGenericTextInputFormSubmit.isEnabled = false views.loginGenericTextInputFormTextInput.textChanges() - .subscribe { + .onEach { views.loginGenericTextInputFormSubmit.isEnabled = isInputValid(it) } - .disposeOnDestroyView() + .launchIn(viewLifecycleOwner.lifecycleScope) } private fun isInputValid(input: CharSequence): Boolean { diff --git a/vector/src/main/java/im/vector/app/features/login/LoginResetPasswordFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginResetPasswordFragment.kt index cc113b1eed..a06a569f44 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginResetPasswordFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginResetPasswordFragment.kt @@ -20,19 +20,22 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.jakewharton.rxbinding3.widget.textChanges import im.vector.app.R import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.hidePassword import im.vector.app.core.extensions.isEmail import im.vector.app.core.extensions.toReducedUrl import im.vector.app.databinding.FragmentLoginResetPasswordBinding -import io.reactivex.Observable -import io.reactivex.rxkotlin.subscribeBy +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import reactivecircus.flowbinding.android.widget.textChanges import javax.inject.Inject /** @@ -59,21 +62,18 @@ class LoginResetPasswordFragment @Inject constructor() : AbstractLoginFragment - isEmail && isPasswordNotEmpty - } - ) - .subscribeBy { + combine( + views.resetPasswordEmail.textChanges().map { it.isEmail() }, + views.passwordField.textChanges().map { it.isNotEmpty() } + ) { isEmail, isPasswordNotEmpty -> + isEmail && isPasswordNotEmpty + } + .onEach { views.resetPasswordEmailTil.error = null views.passwordFieldTil.error = null views.resetPasswordSubmit.isEnabled = it } - .disposeOnDestroyView() + .launchIn(viewLifecycleOwner.lifecycleScope) } private fun submit() { diff --git a/vector/src/main/java/im/vector/app/features/login/LoginServerUrlFormFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginServerUrlFormFragment.kt index ebe82b1163..65ad7af346 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginServerUrlFormFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginServerUrlFormFragment.kt @@ -25,15 +25,18 @@ import android.view.inputmethod.EditorInfo import android.widget.ArrayAdapter import androidx.core.view.isInvisible import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope import com.google.android.material.textfield.TextInputLayout -import com.jakewharton.rxbinding3.widget.textChanges import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.utils.ensureProtocol import im.vector.app.core.utils.openUrlInChromeCustomTab import im.vector.app.databinding.FragmentLoginServerUrlFormBinding +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.failure.Failure +import reactivecircus.flowbinding.android.widget.textChanges import java.net.UnknownHostException import javax.inject.Inject @@ -61,11 +64,11 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment if (actionId == EditorInfo.IME_ACTION_DONE) { diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginActivity2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginActivity2.kt index 40dd1d2872..8f1b20aa7f 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginActivity2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginActivity2.kt @@ -92,10 +92,9 @@ open class LoginActivity2 : VectorBaseActivity(), ToolbarC addFirstFragment() } - loginViewModel - .subscribe(this) { - updateWithState(it) - } + loginViewModel.onEach { + updateWithState(it) + } loginViewModel.observeViewEvents { handleLoginViewEvents(it) } @@ -201,19 +200,19 @@ open class LoginActivity2 : VectorBaseActivity(), ToolbarC // Go back to the login fragment supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) } - is LoginViewEvents2.OnSendEmailSuccess -> + is LoginViewEvents2.OnSendEmailSuccess -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginWaitForEmailFragment2::class.java, LoginWaitForEmailFragmentArgument(event.email), tag = FRAGMENT_REGISTRATION_STAGE_TAG, option = commonOption) - is LoginViewEvents2.OpenSigninPasswordScreen -> { + is LoginViewEvents2.OpenSigninPasswordScreen -> { addFragmentToBackstack(R.id.loginFragmentContainer, LoginFragmentSigninPassword2::class.java, tag = FRAGMENT_LOGIN_TAG, option = commonOption) } - is LoginViewEvents2.OpenSignupPasswordScreen -> { + is LoginViewEvents2.OpenSignupPasswordScreen -> { addFragmentToBackstack(R.id.loginFragmentContainer, LoginFragmentSignupPassword2::class.java, tag = FRAGMENT_REGISTRATION_STAGE_TAG, diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSigninPassword2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSigninPassword2.kt index 71f1d10137..5c9cefd2db 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSigninPassword2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSigninPassword2.kt @@ -24,17 +24,20 @@ import android.view.ViewGroup import android.view.inputmethod.EditorInfo import androidx.autofill.HintConstants import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.Fail -import com.jakewharton.rxbinding3.widget.textChanges import im.vector.app.R import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.hidePassword import im.vector.app.databinding.FragmentLoginSigninPassword2Binding import im.vector.app.features.home.AvatarRenderer -import io.reactivex.rxkotlin.subscribeBy +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.auth.login.LoginProfileInfo import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.isInvalidPassword +import reactivecircus.flowbinding.android.widget.textChanges import javax.inject.Inject import javax.net.ssl.HttpsURLConnection @@ -121,11 +124,11 @@ class LoginFragmentSigninPassword2 @Inject constructor( views.passwordField .textChanges() .map { it.isNotEmpty() } - .subscribeBy { + .onEach { views.passwordFieldTil.error = null views.loginSubmit.isEnabled = it } - .disposeOnDestroyView() + .launchIn(viewLifecycleOwner.lifecycleScope) } private fun forgetPasswordClicked() { diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSigninUsername2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSigninUsername2.kt index 95862a4e90..b90887dba1 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSigninUsername2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSigninUsername2.kt @@ -22,13 +22,16 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.autofill.HintConstants -import com.jakewharton.rxbinding3.widget.textChanges +import androidx.lifecycle.lifecycleScope import im.vector.app.R import im.vector.app.core.extensions.hideKeyboard import im.vector.app.databinding.FragmentLoginSigninUsername2Binding -import io.reactivex.rxkotlin.subscribeBy +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.MatrixError +import reactivecircus.flowbinding.android.widget.textChanges import javax.inject.Inject /** @@ -83,11 +86,11 @@ class LoginFragmentSigninUsername2 @Inject constructor() : AbstractLoginFragment views.loginSubmit.setOnClickListener { submit() } views.loginField.textChanges() .map { it.trim().isNotEmpty() } - .subscribeBy { + .onEach { views.loginFieldTil.error = null views.loginSubmit.isEnabled = it } - .disposeOnDestroyView() + .launchIn(viewLifecycleOwner.lifecycleScope) } override fun resetViewModel() { diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSignupPassword2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSignupPassword2.kt index 28a87a0e8b..806ff0524b 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSignupPassword2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSignupPassword2.kt @@ -23,12 +23,14 @@ import android.view.View import android.view.ViewGroup import android.view.inputmethod.EditorInfo import androidx.autofill.HintConstants -import com.jakewharton.rxbinding3.widget.textChanges +import androidx.lifecycle.lifecycleScope import im.vector.app.R import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.hidePassword import im.vector.app.databinding.FragmentLoginSignupPassword2Binding -import io.reactivex.rxkotlin.subscribeBy +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import reactivecircus.flowbinding.android.widget.textChanges import javax.inject.Inject /** @@ -87,11 +89,11 @@ class LoginFragmentSignupPassword2 @Inject constructor() : AbstractLoginFragment private fun setupSubmitButton() { views.loginSubmit.setOnClickListener { submit() } views.passwordField.textChanges() - .subscribeBy { password -> + .onEach { password -> views.passwordFieldTil.error = null views.loginSubmit.isEnabled = password.isNotEmpty() } - .disposeOnDestroyView() + .launchIn(viewLifecycleOwner.lifecycleScope) } override fun resetViewModel() { diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSignupUsername2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSignupUsername2.kt index ff6a218796..51044ac153 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSignupUsername2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSignupUsername2.kt @@ -24,14 +24,17 @@ import android.view.View import android.view.ViewGroup import androidx.autofill.HintConstants import androidx.core.view.isVisible -import com.jakewharton.rxbinding3.widget.textChanges +import androidx.lifecycle.lifecycleScope import im.vector.app.R import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.toReducedUrl import im.vector.app.databinding.FragmentLoginSignupUsername2Binding import im.vector.app.features.login.LoginMode import im.vector.app.features.login.SocialLoginButtonsView -import io.reactivex.rxkotlin.subscribeBy +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import reactivecircus.flowbinding.android.widget.textChanges import javax.inject.Inject /** @@ -111,12 +114,12 @@ class LoginFragmentSignupUsername2 @Inject constructor() : AbstractSSOLoginFragm views.loginSubmit.setOnClickListener { submit() } views.loginField.textChanges() .map { it.trim() } - .subscribeBy { text -> + .onEach { text -> val isNotEmpty = text.isNotEmpty() views.loginFieldTil.error = null views.loginSubmit.isEnabled = isNotEmpty } - .disposeOnDestroyView() + .launchIn(viewLifecycleOwner.lifecycleScope) } override fun resetViewModel() { diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentToAny2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentToAny2.kt index a889c870a0..48792da007 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentToAny2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentToAny2.kt @@ -24,7 +24,7 @@ import android.view.ViewGroup import android.view.inputmethod.EditorInfo import androidx.autofill.HintConstants import androidx.core.view.isVisible -import com.jakewharton.rxbinding3.widget.textChanges +import androidx.lifecycle.lifecycleScope import im.vector.app.R import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.hidePassword @@ -32,11 +32,14 @@ import im.vector.app.core.extensions.toReducedUrl import im.vector.app.databinding.FragmentLoginSigninToAny2Binding import im.vector.app.features.login.LoginMode import im.vector.app.features.login.SocialLoginButtonsView -import io.reactivex.Observable -import io.reactivex.rxkotlin.subscribeBy +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.failure.isInvalidPassword +import reactivecircus.flowbinding.android.widget.textChanges import javax.inject.Inject /** @@ -136,20 +139,18 @@ class LoginFragmentToAny2 @Inject constructor() : AbstractSSOLoginFragment2 - isLoginNotEmpty && isPasswordNotEmpty - } - ) - .subscribeBy { + combine( + views.loginField.textChanges().map { it.trim().isNotEmpty() }, + views.passwordField.textChanges().map { it.isNotEmpty() } + ) { isLoginNotEmpty, isPasswordNotEmpty -> + isLoginNotEmpty && isPasswordNotEmpty + } + .onEach { views.loginFieldTil.error = null views.passwordFieldTil.error = null views.loginSubmit.isEnabled = it } - .disposeOnDestroyView() + .launchIn(viewLifecycleOwner.lifecycleScope) } private fun forgetPasswordClicked() { diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginGenericTextInputFormFragment2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginGenericTextInputFormFragment2.kt index a87dd6ae40..76af86fda8 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginGenericTextInputFormFragment2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginGenericTextInputFormFragment2.kt @@ -24,10 +24,10 @@ import android.view.View import android.view.ViewGroup import androidx.autofill.HintConstants import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.args import com.google.i18n.phonenumbers.NumberParseException import com.google.i18n.phonenumbers.PhoneNumberUtil -import com.jakewharton.rxbinding3.widget.textChanges import im.vector.app.R import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.isEmail @@ -36,9 +36,12 @@ import im.vector.app.core.extensions.toReducedUrl import im.vector.app.databinding.FragmentLoginGenericTextInputForm2Binding import im.vector.app.features.login.LoginGenericTextInputFormFragmentArgument import im.vector.app.features.login.TextInputFormFragmentMode +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.auth.registration.RegisterThreePid import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.is401 +import reactivecircus.flowbinding.android.widget.textChanges import javax.inject.Inject /** @@ -82,10 +85,10 @@ class LoginGenericTextInputFormFragment2 @Inject constructor() : AbstractLoginFr private fun setupTil() { views.loginGenericTextInputFormTextInput.textChanges() - .subscribe { + .onEach { views.loginGenericTextInputFormTil.error = null } - .disposeOnDestroyView() + .launchIn(viewLifecycleOwner.lifecycleScope) } private fun setupUi() { @@ -189,11 +192,11 @@ class LoginGenericTextInputFormFragment2 @Inject constructor() : AbstractLoginFr private fun setupSubmitButton() { views.loginGenericTextInputFormSubmit.isEnabled = false views.loginGenericTextInputFormTextInput.textChanges() - .subscribe { text -> + .onEach { text -> views.loginGenericTextInputFormSubmit.isEnabled = isInputValid(text) - text?.let { updateSubmitButtons(it) } + updateSubmitButtons(text) } - .disposeOnDestroyView() + .launchIn(viewLifecycleOwner.lifecycleScope) } private fun updateSubmitButtons(text: CharSequence) { diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginResetPasswordFragment2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginResetPasswordFragment2.kt index 6d6ddbc470..c00ed7275c 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginResetPasswordFragment2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginResetPasswordFragment2.kt @@ -23,8 +23,8 @@ import android.view.View import android.view.ViewGroup import android.view.inputmethod.EditorInfo import androidx.autofill.HintConstants +import androidx.lifecycle.lifecycleScope import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.jakewharton.rxbinding3.widget.textChanges import im.vector.app.R import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.hidePassword @@ -32,8 +32,11 @@ import im.vector.app.core.extensions.isEmail import im.vector.app.core.extensions.toReducedUrl import im.vector.app.core.utils.autoResetTextInputLayoutErrors import im.vector.app.databinding.FragmentLoginResetPassword2Binding -import io.reactivex.Observable -import io.reactivex.rxkotlin.subscribeBy +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import reactivecircus.flowbinding.android.widget.textChanges import javax.inject.Inject /** @@ -78,19 +81,16 @@ class LoginResetPasswordFragment2 @Inject constructor() : AbstractLoginFragment2 private fun setupSubmitButton() { views.resetPasswordSubmit.setOnClickListener { submit() } - - Observable - .combineLatest( - views.resetPasswordEmail.textChanges().map { it.isEmail() }, - views.passwordField.textChanges().map { it.isNotEmpty() }, - { isEmail, isPasswordNotEmpty -> - isEmail && isPasswordNotEmpty - } - ) - .subscribeBy { + combine( + views.resetPasswordEmail.textChanges().map { it.isEmail() }, + views.passwordField.textChanges().map { it.isNotEmpty() } + ) { isEmail, isPasswordNotEmpty -> + isEmail && isPasswordNotEmpty + } + .onEach { views.resetPasswordSubmit.isEnabled = it } - .disposeOnDestroyView() + .launchIn(viewLifecycleOwner.lifecycleScope) } private fun submit() { diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginServerUrlFormFragment2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginServerUrlFormFragment2.kt index 6a67f0513c..0732d176ac 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginServerUrlFormFragment2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginServerUrlFormFragment2.kt @@ -24,15 +24,18 @@ import android.view.ViewGroup import android.view.inputmethod.EditorInfo import android.widget.ArrayAdapter import androidx.core.view.isInvisible +import androidx.lifecycle.lifecycleScope import com.google.android.material.textfield.TextInputLayout -import com.jakewharton.rxbinding3.widget.textChanges import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.utils.ensureProtocol import im.vector.app.databinding.FragmentLoginServerUrlForm2Binding +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.MatrixError +import reactivecircus.flowbinding.android.widget.textChanges import java.net.UnknownHostException import javax.inject.Inject import javax.net.ssl.HttpsURLConnection @@ -60,11 +63,11 @@ class LoginServerUrlFormFragment2 @Inject constructor() : AbstractLoginFragment2 private fun setupHomeServerField() { views.loginServerUrlFormHomeServerUrl.textChanges() - .subscribe { + .onEach { views.loginServerUrlFormHomeServerUrlTil.error = null views.loginServerUrlFormSubmit.isEnabled = it.isNotBlank() } - .disposeOnDestroyView() + .launchIn(viewLifecycleOwner.lifecycleScope) views.loginServerUrlFormHomeServerUrl.setOnEditorActionListener { _, actionId, _ -> if (actionId == EditorInfo.IME_ACTION_DONE) { diff --git a/vector/src/main/java/im/vector/app/features/powerlevel/PowerLevelsFlowFactory.kt b/vector/src/main/java/im/vector/app/features/powerlevel/PowerLevelsFlowFactory.kt index 767d6f1ba7..d8857b3be3 100644 --- a/vector/src/main/java/im/vector/app/features/powerlevel/PowerLevelsFlowFactory.kt +++ b/vector/src/main/java/im/vector/app/features/powerlevel/PowerLevelsFlowFactory.kt @@ -33,8 +33,8 @@ class PowerLevelsFlowFactory(private val room: Room) { fun createFlow(): Flow { return room.flow() .liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition) - .flowOn(Dispatchers.Default) .mapOptional { it.content.toModel() } + .flowOn(Dispatchers.Default) .unwrap() } } diff --git a/vector/src/main/java/im/vector/app/features/reactions/EmojiReactionPickerActivity.kt b/vector/src/main/java/im/vector/app/features/reactions/EmojiReactionPickerActivity.kt index 5675af6960..d377c74ad7 100644 --- a/vector/src/main/java/im/vector/app/features/reactions/EmojiReactionPickerActivity.kt +++ b/vector/src/main/java/im/vector/app/features/reactions/EmojiReactionPickerActivity.kt @@ -28,18 +28,18 @@ import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.viewModel import com.google.android.material.tabs.TabLayout -import com.jakewharton.rxbinding3.widget.queryTextChanges import dagger.hilt.android.AndroidEntryPoint import im.vector.app.EmojiCompatFontProvider import im.vector.app.R import im.vector.app.core.extensions.observeEvent +import im.vector.app.core.flow.throttleFirst import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivityEmojiReactionPickerBinding import im.vector.app.features.reactions.data.EmojiDataSource -import io.reactivex.android.schedulers.AndroidSchedulers +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import timber.log.Timber -import java.util.concurrent.TimeUnit +import reactivecircus.flowbinding.android.widget.queryTextChanges import javax.inject.Inject /** @@ -167,13 +167,11 @@ class EmojiReactionPickerActivity : VectorBaseActivity Timber.e(err) } - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { query -> + .throttleFirst(600) + .onEach { query -> onQueryText(query.toString()) } - .disposeOnDestroy() + .launchIn(lifecycleScope) } searchItem.expandActionView() return true diff --git a/vector/src/main/java/im/vector/app/features/room/RequireActiveMembershipViewModel.kt b/vector/src/main/java/im/vector/app/features/room/RequireActiveMembershipViewModel.kt index 6ad93abe0c..d2ee3a56ec 100644 --- a/vector/src/main/java/im/vector/app/features/room/RequireActiveMembershipViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/room/RequireActiveMembershipViewModel.kt @@ -77,8 +77,8 @@ class RequireActiveMembershipViewModel @AssistedInject constructor( room.flow() .liveRoomSummary() .unwrap() - .flowOn(Dispatchers.Default) .map { mapToLeftViewEvent(room, it) } + .flowOn(Dispatchers.Default) } .unwrap() .onEach { event -> diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsFragment.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsFragment.kt index b61583df55..be1523f4ab 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsFragment.kt @@ -25,7 +25,6 @@ import android.view.ViewGroup import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.withState -import com.jakewharton.rxbinding3.appcompat.queryTextChanges import im.vector.app.R import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith @@ -37,12 +36,14 @@ import im.vector.app.core.utils.toast import im.vector.app.databinding.FragmentPublicRoomsBinding import im.vector.app.features.permalink.NavigationInterceptor import im.vector.app.features.permalink.PermalinkHandler -import io.reactivex.rxkotlin.subscribeBy +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoom +import reactivecircus.flowbinding.appcompat.queryTextChanges import timber.log.Timber -import java.util.concurrent.TimeUnit import javax.inject.Inject /** @@ -79,11 +80,11 @@ class PublicRoomsFragment @Inject constructor( setupRecyclerView() views.publicRoomsFilter.queryTextChanges() - .debounce(500, TimeUnit.MILLISECONDS) - .subscribeBy { + .debounce(500) + .onEach { viewModel.handle(RoomDirectoryAction.FilterWith(it.toString())) } - .disposeOnDestroyView() + .launchIn(viewLifecycleOwner.lifecycleScope) views.publicRoomsCreateNewRoom.debouncedClicks { sharedActionViewModel.post(RoomDirectorySharedAction.CreateRoom) diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryActivity.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryActivity.kt index 7ad8d0ce49..82dc31c4d6 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryActivity.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryActivity.kt @@ -19,6 +19,7 @@ package im.vector.app.features.roomdirectory import android.content.Context import android.content.Intent import android.os.Bundle +import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.viewModel import com.airbnb.mvrx.withState import dagger.hilt.android.AndroidEntryPoint @@ -33,6 +34,8 @@ import im.vector.app.features.navigation.Navigator import im.vector.app.features.roomdirectory.createroom.CreateRoomArgs import im.vector.app.features.roomdirectory.createroom.CreateRoomFragment import im.vector.app.features.roomdirectory.picker.RoomDirectoryPickerFragment +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import javax.inject.Inject @AndroidEntryPoint @@ -55,8 +58,8 @@ class RoomDirectoryActivity : VectorBaseActivity(), Matri } sharedActionViewModel - .observe() - .subscribe { sharedAction -> + .stream() + .onEach { sharedAction -> when (sharedAction) { is RoomDirectorySharedAction.Back -> popBackstack() is RoomDirectorySharedAction.CreateRoom -> { @@ -74,7 +77,7 @@ class RoomDirectoryActivity : VectorBaseActivity(), Matri is RoomDirectorySharedAction.Close -> finish() } } - .disposeOnDestroy() + .launchIn(lifecycleScope) } override fun initUiAndData() { diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomActivity.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomActivity.kt index eeb7d217c0..b3a21dadb9 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomActivity.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomActivity.kt @@ -19,6 +19,7 @@ package im.vector.app.features.roomdirectory.createroom import android.content.Context import android.content.Intent import android.os.Bundle +import androidx.lifecycle.lifecycleScope import com.google.android.material.appbar.MaterialToolbar import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R @@ -28,6 +29,8 @@ import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivitySimpleBinding import im.vector.app.features.roomdirectory.RoomDirectorySharedAction import im.vector.app.features.roomdirectory.RoomDirectorySharedActionViewModel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach /** * Simple container for [CreateRoomFragment] @@ -62,14 +65,14 @@ class CreateRoomActivity : VectorBaseActivity(), ToolbarC super.onCreate(savedInstanceState) sharedActionViewModel = viewModelProvider.get(RoomDirectorySharedActionViewModel::class.java) sharedActionViewModel - .observe() - .subscribe { sharedAction -> + .stream() + .onEach { sharedAction -> when (sharedAction) { is RoomDirectorySharedAction.Back, is RoomDirectorySharedAction.Close -> finish() } } - .disposeOnDestroy() + .launchIn(lifecycleScope) } companion object { diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt index c61da211a4..1244a0f64e 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt @@ -23,6 +23,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success import com.airbnb.mvrx.args @@ -44,6 +45,8 @@ import im.vector.app.features.roomdirectory.RoomDirectorySharedActionViewModel import im.vector.app.features.roomprofile.settings.joinrule.RoomJoinRuleBottomSheet import im.vector.app.features.roomprofile.settings.joinrule.RoomJoinRuleSharedActionViewModel import im.vector.app.features.roomprofile.settings.joinrule.toOption +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure import org.matrix.android.sdk.api.session.room.model.RoomJoinRules @@ -103,11 +106,11 @@ class CreateRoomFragment @Inject constructor( private fun setupRoomJoinRuleSharedActionViewModel() { roomJoinRuleSharedActionViewModel = activityViewModelProvider.get(RoomJoinRuleSharedActionViewModel::class.java) roomJoinRuleSharedActionViewModel - .observe() - .subscribe { action -> + .stream() + .onEach { action -> viewModel.handle(CreateRoomAction.SetVisibility(action.roomJoinRule)) } - .disposeOnDestroyView() + .launchIn(viewLifecycleOwner.lifecycleScope) } override fun showFailure(throwable: Throwable) { diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt index fdb639e7d6..c06a2927c5 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt @@ -20,6 +20,7 @@ package im.vector.app.features.roomprofile import android.content.Context import android.content.Intent import android.widget.Toast +import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.viewModel import com.google.android.material.appbar.MaterialToolbar @@ -41,6 +42,8 @@ import im.vector.app.features.roomprofile.notifications.RoomNotificationSettings import im.vector.app.features.roomprofile.permissions.RoomPermissionsFragment import im.vector.app.features.roomprofile.settings.RoomSettingsFragment import im.vector.app.features.roomprofile.uploads.RoomUploadsFragment +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import javax.inject.Inject @AndroidEntryPoint @@ -93,8 +96,8 @@ class RoomProfileActivity : } } sharedActionViewModel - .observe() - .subscribe { sharedAction -> + .stream() + .onEach { sharedAction -> when (sharedAction) { RoomProfileSharedAction.OpenRoomMembers -> openRoomMembers() RoomProfileSharedAction.OpenRoomSettings -> openRoomSettings() @@ -105,7 +108,7 @@ class RoomProfileActivity : RoomProfileSharedAction.OpenRoomNotificationSettings -> openRoomNotificationSettings() }.exhaustive } - .disposeOnDestroy() + .launchIn(lifecycleScope) requireActiveMembershipViewModel.observeViewEvents { when (it) { diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt index 23234f8bbd..e1a5cae907 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt @@ -26,6 +26,7 @@ import android.view.ViewGroup import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.view.isVisible import androidx.fragment.app.setFragmentResultListener +import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState @@ -52,6 +53,8 @@ import im.vector.app.features.home.room.list.actions.RoomListActionsArgs import im.vector.app.features.home.room.list.actions.RoomListQuickActionsBottomSheet import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedAction import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState import org.matrix.android.sdk.api.util.toMatrixItem @@ -124,9 +127,9 @@ class RoomProfileFragment @Inject constructor( }.exhaustive } roomListQuickActionsSharedActionViewModel - .observe() - .subscribe { handleQuickActions(it) } - .disposeOnDestroyView() + .stream() + .onEach { handleQuickActions(it) } + .launchIn(viewLifecycleOwner.lifecycleScope) setupClicks() setupLongClicks() } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasFragment.kt index e281c0f84d..15686a6848 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasFragment.kt @@ -21,6 +21,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState @@ -38,6 +39,8 @@ import im.vector.app.features.roomprofile.RoomProfileArgs import im.vector.app.features.roomprofile.alias.detail.RoomAliasBottomSheet import im.vector.app.features.roomprofile.alias.detail.RoomAliasBottomSheetSharedAction import im.vector.app.features.roomprofile.alias.detail.RoomAliasBottomSheetSharedActionViewModel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.session.room.alias.RoomAliasError import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility import org.matrix.android.sdk.api.util.toMatrixItem @@ -77,9 +80,9 @@ class RoomAliasFragment @Inject constructor( } sharedActionViewModel - .observe() - .subscribe { handleAliasAction(it) } - .disposeOnDestroyView() + .stream() + .onEach { handleAliasAction(it) } + .launchIn(viewLifecycleOwner.lifecycleScope) } private fun handleAliasAction(action: RoomAliasBottomSheetSharedAction?) { diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt index adf5a31f2a..0bbdd87f3e 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt @@ -27,11 +27,9 @@ import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.powerlevel.PowerLevelsFlowFactory -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @@ -95,7 +93,6 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState if (room.isEncrypted()) { room.flow().liveRoomMembers(roomMemberQueryParams) - .flowOn(Dispatchers.Main) .flatMapLatest { membersSummary -> session.cryptoService().getLiveCryptoDeviceInfo(membersSummary.map { it.userId }) .asFlow() diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberSummaryFilter.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberSummaryFilter.kt index 449b8663d6..eef8396c1a 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberSummaryFilter.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberSummaryFilter.kt @@ -16,7 +16,7 @@ package im.vector.app.features.roomprofile.members -import io.reactivex.functions.Predicate +import androidx.core.util.Predicate import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt index ce059881b8..0a5f8f4d9a 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt @@ -24,6 +24,7 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState @@ -46,6 +47,8 @@ import im.vector.app.features.roomprofile.settings.historyvisibility.RoomHistory import im.vector.app.features.roomprofile.settings.historyvisibility.RoomHistoryVisibilitySharedActionViewModel import im.vector.app.features.roomprofile.settings.joinrule.RoomJoinRuleActivity import im.vector.app.features.roomprofile.settings.joinrule.RoomJoinRuleSharedActionViewModel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.session.room.model.GuestAccess import org.matrix.android.sdk.api.util.toMatrixItem import java.util.UUID @@ -101,21 +104,21 @@ class RoomSettingsFragment @Inject constructor( private fun setupRoomJoinRuleSharedActionViewModel() { roomJoinRuleSharedActionViewModel = activityViewModelProvider.get(RoomJoinRuleSharedActionViewModel::class.java) roomJoinRuleSharedActionViewModel - .observe() - .subscribe { action -> + .stream() + .onEach { action -> viewModel.handle(RoomSettingsAction.SetRoomJoinRule(action.roomJoinRule)) } - .disposeOnDestroyView() + .launchIn(viewLifecycleOwner.lifecycleScope) } private fun setupRoomHistoryVisibilitySharedActionViewModel() { roomHistoryVisibilitySharedActionViewModel = activityViewModelProvider.get(RoomHistoryVisibilitySharedActionViewModel::class.java) roomHistoryVisibilitySharedActionViewModel - .observe() - .subscribe { action -> + .stream() + .onEach { action -> viewModel.handle(RoomSettingsAction.SetRoomHistoryVisibility(action.roomHistoryVisibility)) } - .disposeOnDestroyView() + .launchIn(viewLifecycleOwner.lifecycleScope) } private fun showSuccess() { diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleActivity.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleActivity.kt index bb8db019c3..c826fe5e64 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleActivity.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleActivity.kt @@ -40,6 +40,7 @@ import im.vector.app.features.home.room.detail.upgrade.MigrateRoomBottomSheet import im.vector.app.features.roomprofile.RoomProfileArgs import im.vector.app.features.roomprofile.settings.joinrule.advanced.RoomJoinRuleChooseRestrictedActions import im.vector.app.features.roomprofile.settings.joinrule.advanced.RoomJoinRuleChooseRestrictedEvents +import im.vector.app.features.roomprofile.settings.joinrule.advanced.RoomJoinRuleChooseRestrictedFragment import im.vector.app.features.roomprofile.settings.joinrule.advanced.RoomJoinRuleChooseRestrictedState import im.vector.app.features.roomprofile.settings.joinrule.advanced.RoomJoinRuleChooseRestrictedViewModel import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/advanced/RoomJoinRuleChooseRestrictedFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/advanced/RoomJoinRuleChooseRestrictedFragment.kt index 6d5d9c1f30..b65e90aeed 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/advanced/RoomJoinRuleChooseRestrictedFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/advanced/RoomJoinRuleChooseRestrictedFragment.kt @@ -14,27 +14,26 @@ * limitations under the License. */ -package im.vector.app.features.roomprofile.settings.joinrule +package im.vector.app.features.roomprofile.settings.joinrule.advanced import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.withState -import com.jakewharton.rxbinding3.appcompat.queryTextChanges import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith import im.vector.app.core.platform.OnBackPressed import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentSpaceRestrictedSelectBinding import im.vector.app.features.home.AvatarRenderer -import im.vector.app.features.roomprofile.settings.joinrule.advanced.ChooseRestrictedController -import im.vector.app.features.roomprofile.settings.joinrule.advanced.RoomJoinRuleChooseRestrictedActions -import im.vector.app.features.roomprofile.settings.joinrule.advanced.RoomJoinRuleChooseRestrictedViewModel -import io.reactivex.rxkotlin.subscribeBy +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.util.MatrixItem -import java.util.concurrent.TimeUnit +import reactivecircus.flowbinding.appcompat.queryTextChanges import javax.inject.Inject class RoomJoinRuleChooseRestrictedFragment @Inject constructor( @@ -54,11 +53,11 @@ class RoomJoinRuleChooseRestrictedFragment @Inject constructor( controller.listener = this views.recyclerView.configureWith(controller) views.roomsFilter.queryTextChanges() - .debounce(500, TimeUnit.MILLISECONDS) - .subscribeBy { + .debounce(500) + .onEach { viewModel.handle(RoomJoinRuleChooseRestrictedActions.FilterWith(it.toString())) } - .disposeOnDestroyView() + .launchIn(viewLifecycleOwner.lifecycleScope) views.okButton.debouncedClicks { parentFragmentManager.popBackStack() diff --git a/vector/src/main/java/im/vector/app/features/settings/SecretsSynchronisationInfo.kt b/vector/src/main/java/im/vector/app/features/settings/SecretsSynchronisationInfo.kt index 5afcb77587..e21366db02 100644 --- a/vector/src/main/java/im/vector/app/features/settings/SecretsSynchronisationInfo.kt +++ b/vector/src/main/java/im/vector/app/features/settings/SecretsSynchronisationInfo.kt @@ -26,7 +26,6 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NA import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME import org.matrix.android.sdk.flow.flow -import org.matrix.android.sdk.rx.SecretsSynchronisationInfo data class SecretsSynchronisationInfo( val isBackupSetup: Boolean, diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt index bffabf2e93..0a12a86ff0 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt @@ -27,8 +27,6 @@ import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.extensions.singletonEntryPoint import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.utils.toast -import io.reactivex.disposables.CompositeDisposable -import io.reactivex.disposables.Disposable import org.matrix.android.sdk.api.session.Session import timber.log.Timber @@ -67,31 +65,10 @@ abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat() { mLoadingView = vectorActivity.findViewById(R.id.vector_settings_spinner_views) } - @CallSuper - override fun onDestroyView() { - uiDisposables.clear() - super.onDestroyView() - } - - override fun onDestroy() { - uiDisposables.dispose() - super.onDestroy() - } - abstract fun bindPref() abstract var titleRes: Int - /* ========================================================================================== - * Disposable - * ========================================================================================== */ - - private val uiDisposables = CompositeDisposable() - - protected fun Disposable.disposeOnDestroyView() { - uiDisposables.add(this) - } - /* ========================================================================================== * Protected * ========================================================================================== */ diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt index b622d8aab4..438382ab3c 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt @@ -58,9 +58,6 @@ import im.vector.app.features.pin.PinMode import im.vector.app.features.raw.wellknown.getElementWellknown import im.vector.app.features.raw.wellknown.isE2EByDefault import im.vector.app.features.themes.ThemeUtils -import io.reactivex.disposables.Disposable -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -71,7 +68,6 @@ import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.internal.crypto.crosssigning.isVerified import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo import org.matrix.android.sdk.internal.crypto.model.rest.DevicesListResponse -import org.matrix.android.sdk.rx.SecretsSynchronisationInfo import javax.inject.Inject class VectorSettingsSecurityPrivacyFragment @Inject constructor( @@ -86,7 +82,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( override var titleRes = R.string.settings_security_and_privacy override val preferenceXmlRes = R.xml.vector_settings_security_privacy - private var disposables = mutableListOf() // cryptography private val mCryptographyCategory by lazy { @@ -149,11 +144,11 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( refreshMyDevice() refreshXSigningStatus() session.liveSecretSynchronisationInfo() - .flowOn(Dispatchers.Main) .onEach { refresh4SSection(it) refreshXSigningStatus() - }.launchIn(viewLifecycleOwner.lifecycleScope) + } + .launchIn(viewLifecycleOwner.lifecycleScope) lifecycleScope.launchWhenResumed { findPreference(VectorPreferences.SETTINGS_CRYPTOGRAPHY_HS_ADMIN_DISABLED_E2E_DEFAULT)?.isVisible = @@ -173,14 +168,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( // findPreference(VectorPreferences.SETTINGS_SECURE_BACKUP_RESET_PREFERENCE_KEY) // } - override fun onPause() { - super.onPause() - disposables.forEach { - it.dispose() - } - disposables.clear() - } - private fun refresh4SSection(info: SecretsSynchronisationInfo) { // it's a lot of if / else if / else // But it's not yet clear how to manage all cases diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt index e8300a1097..67ed2e18f2 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt @@ -29,11 +29,12 @@ import dagger.assisted.AssistedInject import im.vector.app.R import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory +import im.vector.app.core.flow.throttleFirst import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider +import im.vector.app.core.utils.PublishDataSource import im.vector.app.features.auth.ReAuthActivity import im.vector.app.features.login.ReAuthHelper -import io.reactivex.subjects.PublishSubject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged @@ -64,7 +65,6 @@ import org.matrix.android.sdk.internal.crypto.model.rest.DefaultBaseAuth import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo import org.matrix.android.sdk.internal.util.awaitCallback import timber.log.Timber -import java.util.concurrent.TimeUnit import javax.net.ssl.HttpsURLConnection import kotlin.coroutines.Continuation import kotlin.coroutines.resume @@ -103,7 +103,7 @@ class DevicesViewModel @AssistedInject constructor( companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() - private val refreshPublisher: PublishSubject = PublishSubject.create() + private val refreshSource = PublishDataSource() init { @@ -166,12 +166,12 @@ class DevicesViewModel @AssistedInject constructor( // ) // } - refreshPublisher.throttleFirst(4_000, TimeUnit.MILLISECONDS) - .subscribe { + refreshSource.stream().throttleFirst(4_000) + .onEach { session.cryptoService().fetchDevicesList(NoOpMatrixCallback()) session.cryptoService().downloadKeys(listOf(session.myUserId), true, NoOpMatrixCallback()) } - .disposeOnClear() + .launchIn(viewModelScope) // then force download queryRefreshDevicesList() } @@ -193,7 +193,7 @@ class DevicesViewModel @AssistedInject constructor( * It can be any mobile devices, and any browsers. */ private fun queryRefreshDevicesList() { - refreshPublisher.onNext(Unit) + refreshSource.post(Unit) } override fun handle(action: DevicesAction) { diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutActivity.kt b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutActivity.kt index 6e70b34002..4fba8fc3de 100644 --- a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutActivity.kt +++ b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutActivity.kt @@ -50,7 +50,7 @@ class SoftLogoutActivity : LoginActivity() { override fun initUiAndData() { super.initUiAndData() - softLogoutViewModel.subscribe(this) { + softLogoutViewModel.onEach { updateWithState(it) } diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutActivity2.kt b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutActivity2.kt index ed45069e92..26500b60f0 100644 --- a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutActivity2.kt +++ b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutActivity2.kt @@ -52,7 +52,7 @@ class SoftLogoutActivity2 : LoginActivity2() { override fun initUiAndData() { super.initUiAndData() - softLogoutViewModel.subscribe(this) { + softLogoutViewModel.onEach { updateWithState(it) } diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutFragment.kt b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutFragment.kt index 2aa7f15172..016d340f80 100644 --- a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutFragment.kt +++ b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutFragment.kt @@ -55,7 +55,7 @@ class SoftLogoutFragment @Inject constructor( setupRecyclerView() - softLogoutViewModel.subscribe(this) { softLogoutViewState -> + softLogoutViewModel.onEach { softLogoutViewState -> softLogoutController.update(softLogoutViewState) when (val mode = softLogoutViewState.asyncHomeServerLoginFlowRequest.invoke()) { is LoginMode.SsoAndPassword -> { diff --git a/vector/src/main/java/im/vector/app/features/spaces/LeaveSpaceBottomSheet.kt b/vector/src/main/java/im/vector/app/features/spaces/LeaveSpaceBottomSheet.kt index fb3fceaa02..a292b64ddd 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/LeaveSpaceBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/LeaveSpaceBottomSheet.kt @@ -26,12 +26,12 @@ import android.view.ViewGroup import androidx.core.text.toSpannable import androidx.core.view.isInvisible import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading import com.airbnb.mvrx.args import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.withState -import com.jakewharton.rxbinding3.widget.checkedChanges import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.error.ErrorFormatter @@ -43,10 +43,12 @@ import im.vector.app.core.utils.styleMatchingText import im.vector.app.databinding.BottomSheetLeaveSpaceBinding import im.vector.app.features.displayname.getBestName import im.vector.app.features.spaces.leave.SpaceLeaveAdvancedActivity -import io.reactivex.android.schedulers.AndroidSchedulers +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.parcelize.Parcelize import me.gujun.android.span.span import org.matrix.android.sdk.api.util.toMatrixItem +import reactivecircus.flowbinding.android.widget.checkedChanges import javax.inject.Inject @AndroidEntryPoint @@ -82,8 +84,7 @@ class LeaveSpaceBottomSheet : VectorBaseBottomSheetDialogFragment { settingsViewModel.handle(SpaceLeaveViewAction.SetAutoLeaveAll) @@ -100,7 +101,7 @@ class LeaveSpaceBottomSheet : VectorBaseBottomSheetDialogFragment() { @@ -39,8 +42,8 @@ class SpacePreviewActivity : VectorBaseActivity() { super.onCreate(savedInstanceState) sharedActionViewModel = viewModelProvider.get(SpacePreviewSharedActionViewModel::class.java) sharedActionViewModel - .observe() - .subscribe { action -> + .stream() + .onEach { action -> when (action) { SpacePreviewSharedAction.DismissAction -> finish() SpacePreviewSharedAction.ShowModalLoading -> showWaitingView() @@ -48,7 +51,7 @@ class SpacePreviewActivity : VectorBaseActivity() { is SpacePreviewSharedAction.ShowErrorMessage -> action.error?.let { showSnackbar(it) } } } - .disposeOnDestroy() + .launchIn(lifecycleScope) if (isFirstCreation()) { val simpleName = SpacePreviewFragment::class.java.simpleName diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceAdd3pidInvitesFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceAdd3pidInvitesFragment.kt index 6dc3ad8c21..4328c46188 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceAdd3pidInvitesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceAdd3pidInvitesFragment.kt @@ -50,7 +50,7 @@ class CreateSpaceAdd3pidInvitesFragment @Inject constructor( views.recyclerView.configureWith(epoxyController) epoxyController.listener = this - sharedViewModel.subscribe(this) { + sharedViewModel.onEach { invalidateState(it) } diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceDefaultRoomsFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceDefaultRoomsFragment.kt index 53a4ee689b..4ed7e91417 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceDefaultRoomsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceDefaultRoomsFragment.kt @@ -46,7 +46,7 @@ class CreateSpaceDefaultRoomsFragment @Inject constructor( views.recyclerView.configureWith(epoxyController) epoxyController.listener = this - sharedViewModel.subscribe(this) { + sharedViewModel.onEach { epoxyController.setData(it) } diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceDetailsFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceDetailsFragment.kt index 544c33948b..920ceed33c 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceDetailsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceDetailsFragment.kt @@ -50,7 +50,7 @@ class CreateSpaceDetailsFragment @Inject constructor( views.recyclerView.configureWith(epoxyController) epoxyController.listener = this - sharedViewModel.subscribe(this) { + sharedViewModel.onEach { epoxyController.setData(it) } diff --git a/vector/src/main/java/im/vector/app/features/spaces/leave/SelectChildrenController.kt b/vector/src/main/java/im/vector/app/features/spaces/leave/SelectChildrenController.kt index 39d1d72675..6260c136d9 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/leave/SelectChildrenController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/leave/SelectChildrenController.kt @@ -16,6 +16,7 @@ package im.vector.app.features.spaces.leave +import androidx.core.util.Predicate import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading @@ -27,7 +28,6 @@ import im.vector.app.core.epoxy.noResultItem import im.vector.app.core.resources.StringProvider import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.spaces.manage.roomSelectionItem -import io.reactivex.functions.Predicate import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.util.toMatrixItem diff --git a/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedActivity.kt b/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedActivity.kt index 541d883405..69de39e436 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedActivity.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedActivity.kt @@ -86,7 +86,7 @@ class SpaceLeaveAdvancedActivity : VectorBaseActivity + leaveViewModel.onEach { state -> when (state.leaveState) { is Loading -> { showWaitingView() diff --git a/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedFragment.kt index e78d90c6d9..b84f870f34 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedFragment.kt @@ -20,16 +20,18 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.withState -import com.jakewharton.rxbinding3.appcompat.queryTextChanges import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentSpaceLeaveAdvancedBinding -import io.reactivex.rxkotlin.subscribeBy +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.session.room.model.RoomSummary -import java.util.concurrent.TimeUnit +import reactivecircus.flowbinding.appcompat.queryTextChanges import javax.inject.Inject class SpaceLeaveAdvancedFragment @Inject constructor( @@ -54,11 +56,11 @@ class SpaceLeaveAdvancedFragment @Inject constructor( } views.publicRoomsFilter.queryTextChanges() - .debounce(100, TimeUnit.MILLISECONDS) - .subscribeBy { + .debounce(100) + .onEach { viewModel.handle(SpaceLeaveAdvanceViewAction.UpdateFilter(it.toString())) } - .disposeOnDestroyView() + .launchIn(viewLifecycleOwner.lifecycleScope) } override fun onDestroyView() { diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomFragment.kt index 5dbd35fc20..9bf304fa1c 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomFragment.kt @@ -22,6 +22,7 @@ import android.view.Menu import android.view.MenuItem import android.view.View import android.view.ViewGroup +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.LinearLayoutManager import com.airbnb.mvrx.Loading @@ -29,15 +30,16 @@ import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.jakewharton.rxbinding3.appcompat.queryTextChanges import im.vector.app.R import im.vector.app.core.extensions.cleanup import im.vector.app.core.platform.OnBackPressed import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentSpaceAddRoomsBinding -import io.reactivex.rxkotlin.subscribeBy +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.session.room.model.RoomSummary -import java.util.concurrent.TimeUnit +import reactivecircus.flowbinding.appcompat.queryTextChanges import javax.inject.Inject class SpaceAddRoomFragment @Inject constructor( @@ -72,11 +74,11 @@ class SpaceAddRoomFragment @Inject constructor( setupRecyclerView() views.publicRoomsFilter.queryTextChanges() - .debounce(100, TimeUnit.MILLISECONDS) - .subscribeBy { + .debounce(100) + .onEach { viewModel.handle(SpaceAddRoomActions.UpdateFilter(it.toString())) } - .disposeOnDestroyView() + .launchIn(viewLifecycleOwner.lifecycleScope) spaceEpoxyController.subHeaderText = getString(R.string.spaces_feeling_experimental_subspace) viewModel.selectionListLiveData.observe(viewLifecycleOwner) { diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceChildInfoMatchFilter.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceChildInfoMatchFilter.kt index 66878a4011..45fdbca598 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceChildInfoMatchFilter.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceChildInfoMatchFilter.kt @@ -16,7 +16,7 @@ package im.vector.app.features.spaces.manage -import io.reactivex.functions.Predicate +import androidx.core.util.Predicate import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageActivity.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageActivity.kt index 2dae088c2e..932110d0e3 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageActivity.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageActivity.kt @@ -22,6 +22,7 @@ import android.os.Bundle import android.os.Parcelable import androidx.core.view.isGone import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.viewModel import com.airbnb.mvrx.withState @@ -41,6 +42,8 @@ import im.vector.app.features.roomdirectory.createroom.CreateRoomFragment import im.vector.app.features.roomprofile.RoomProfileArgs import im.vector.app.features.roomprofile.alias.RoomAliasFragment import im.vector.app.features.roomprofile.permissions.RoomPermissionsFragment +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.parcelize.Parcelize @Parcelize @@ -80,14 +83,14 @@ class SpaceManageActivity : VectorBaseActivity(), sharedDirectoryActionViewModel = viewModelProvider.get(RoomDirectorySharedActionViewModel::class.java) sharedDirectoryActionViewModel - .observe() - .subscribe { sharedAction -> + .stream() + .onEach { sharedAction -> when (sharedAction) { is RoomDirectorySharedAction.Back, is RoomDirectorySharedAction.Close -> finish() } } - .disposeOnDestroy() + .launchIn(lifecycleScope) val args = intent?.getParcelableExtra(Mavericks.KEY_ARG) if (isFirstCreation()) { diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsFragment.kt index 5fbac3bb6a..125686d200 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsFragment.kt @@ -25,13 +25,13 @@ import android.view.ViewGroup import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode.Callback import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope import androidx.transition.TransitionManager import com.airbnb.epoxy.EpoxyVisibilityTracker import com.airbnb.mvrx.Loading import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState -import com.jakewharton.rxbinding3.appcompat.queryTextChanges import im.vector.app.R import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith @@ -39,9 +39,11 @@ import im.vector.app.core.platform.OnBackPressed import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.utils.toast import im.vector.app.databinding.FragmentSpaceAddRoomsBinding -import io.reactivex.rxkotlin.subscribeBy +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo -import java.util.concurrent.TimeUnit +import reactivecircus.flowbinding.appcompat.queryTextChanges import javax.inject.Inject class SpaceManageRoomsFragment @Inject constructor( @@ -72,11 +74,11 @@ class SpaceManageRoomsFragment @Inject constructor( epoxyVisibilityTracker.attach(views.roomList) views.publicRoomsFilter.queryTextChanges() - .debounce(200, TimeUnit.MILLISECONDS) - .subscribeBy { + .debounce(200) + .onEach { viewModel.handle(SpaceManageRoomViewAction.UpdateFilter(it.toString())) } - .disposeOnDestroyView() + .launchIn(viewLifecycleOwner.lifecycleScope) viewModel.onEach(SpaceManageRoomViewState::actionState) { actionState -> when (actionState) { diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt index c2ab015858..a0ab055311 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt @@ -24,6 +24,7 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel @@ -49,6 +50,8 @@ import im.vector.app.features.roomprofile.settings.RoomSettingsViewModel import im.vector.app.features.roomprofile.settings.RoomSettingsViewState import im.vector.app.features.roomprofile.settings.joinrule.RoomJoinRuleActivity import im.vector.app.features.roomprofile.settings.joinrule.RoomJoinRuleSharedActionViewModel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.session.room.model.GuestAccess import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility import org.matrix.android.sdk.api.session.room.model.RoomJoinRules @@ -142,11 +145,11 @@ class SpaceSettingsFragment @Inject constructor( private fun setupRoomJoinRuleSharedActionViewModel() { roomJoinRuleSharedActionViewModel = activityViewModelProvider.get(RoomJoinRuleSharedActionViewModel::class.java) roomJoinRuleSharedActionViewModel - .observe() - .subscribe { action -> + .stream() + .onEach { action -> viewModel.handle(RoomSettingsAction.SetRoomJoinRule(action.roomJoinRule)) } - .disposeOnDestroyView() + .launchIn(viewLifecycleOwner.lifecycleScope) } private var ignoreChanges = false diff --git a/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleActivity.kt b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleActivity.kt index 1f08802137..a269273cd5 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleActivity.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleActivity.kt @@ -21,6 +21,7 @@ import android.content.Intent import android.os.Bundle import androidx.core.view.isGone import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.Mavericks import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R @@ -30,6 +31,8 @@ import im.vector.app.core.platform.GenericIdArgs import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivitySimpleLoadingBinding import im.vector.app.features.spaces.share.ShareSpaceBottomSheet +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach @AndroidEntryPoint class SpacePeopleActivity : VectorBaseActivity() { @@ -75,8 +78,8 @@ class SpacePeopleActivity : VectorBaseActivity() { sharedActionViewModel = viewModelProvider.get(SpacePeopleSharedActionViewModel::class.java) sharedActionViewModel - .observe() - .subscribe { sharedAction -> + .stream() + .onEach { sharedAction -> when (sharedAction) { SpacePeopleSharedAction.Dismiss -> finish() is SpacePeopleSharedAction.NavigateToRoom -> navigateToRooms(sharedAction) @@ -88,7 +91,7 @@ class SpacePeopleActivity : VectorBaseActivity() { ShareSpaceBottomSheet.show(supportFragmentManager, sharedAction.spaceId) } } - }.disposeOnDestroy() + }.launchIn(lifecycleScope) } private fun navigateToRooms(action: SpacePeopleSharedAction.NavigateToRoom) { diff --git a/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleFragment.kt index 6e14893f77..c5cfed6974 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleFragment.kt @@ -20,13 +20,13 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState -import com.jakewharton.rxbinding3.appcompat.queryTextChanges import im.vector.app.R import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith @@ -37,9 +37,11 @@ import im.vector.app.core.resources.DrawableProvider import im.vector.app.databinding.FragmentRecyclerviewWithSearchBinding import im.vector.app.features.roomprofile.members.RoomMemberListAction import im.vector.app.features.roomprofile.members.RoomMemberListViewModel -import io.reactivex.rxkotlin.subscribeBy +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary -import java.util.concurrent.TimeUnit +import reactivecircus.flowbinding.appcompat.queryTextChanges import javax.inject.Inject class SpacePeopleFragment @Inject constructor( @@ -91,7 +93,7 @@ class SpacePeopleFragment @Inject constructor( handleViewEvents(it) } - viewModel.subscribe(this) { + viewModel.onEach { when (it.createAndInviteState) { is Loading -> sharedActionViewModel.post(SpacePeopleSharedAction.ShowModalLoading) Uninitialized, @@ -117,11 +119,11 @@ class SpacePeopleFragment @Inject constructor( private fun setupSearchView() { views.memberNameFilter.queryHint = getString(R.string.search_members_hint) views.memberNameFilter.queryTextChanges() - .debounce(100, TimeUnit.MILLISECONDS) - .subscribeBy { + .debounce(100) + .onEach { membersViewModel.handle(RoomMemberListAction.FilterMemberList(it.toString())) } - .disposeOnDestroyView() + .launchIn(viewLifecycleOwner.lifecycleScope) } private fun handleViewEvents(events: SpacePeopleViewEvents) { diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewFragment.kt index 7e08d7c924..4d0d301721 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewFragment.kt @@ -22,25 +22,27 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState -import com.jakewharton.rxbinding3.appcompat.navigationClicks import im.vector.app.R import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith +import im.vector.app.core.flow.throttleFirst import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentSpacePreviewBinding import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.spaces.SpacePreviewSharedAction import im.vector.app.features.spaces.SpacePreviewSharedActionViewModel -import io.reactivex.android.schedulers.AndroidSchedulers +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.util.MatrixItem -import java.util.concurrent.TimeUnit +import reactivecircus.flowbinding.appcompat.navigationClicks import javax.inject.Inject @Parcelize @@ -72,11 +74,11 @@ class SpacePreviewFragment @Inject constructor( handleViewEvents(it) } - views.roomPreviewNoPreviewToolbar.navigationClicks() - .throttleFirst(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { sharedActionViewModel.post(SpacePreviewSharedAction.DismissAction) } - .disposeOnDestroyView() + views.roomPreviewNoPreviewToolbar + .navigationClicks() + .throttleFirst(300) + .onEach { sharedActionViewModel.post(SpacePreviewSharedAction.DismissAction) } + .launchIn(viewLifecycleOwner.lifecycleScope) views.spacePreviewRecyclerView.configureWith(epoxyController) diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragment.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragment.kt index aed134816a..bb3caebafa 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragment.kt @@ -31,7 +31,6 @@ import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import com.google.android.material.chip.Chip -import com.jakewharton.rxbinding3.widget.textChanges import im.vector.app.R import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith @@ -45,8 +44,12 @@ import im.vector.app.databinding.FragmentUserListBinding import im.vector.app.features.homeserver.HomeServerCapabilitiesViewModel import im.vector.app.features.navigation.SettingsActivityPayload import im.vector.app.features.settings.VectorSettingsActivity +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.user.model.User +import reactivecircus.flowbinding.android.widget.textChanges import javax.inject.Inject class UserListFragment @Inject constructor( @@ -133,8 +136,8 @@ class UserListFragment @Inject constructor( private fun setupSearchView() { views.userListSearch .textChanges() - .startWith(views.userListSearch.text) - .subscribe { text -> + .onStart { emit(views.userListSearch.text) } + .onEach { text -> val searchValue = text.trim() val action = if (searchValue.isBlank()) { UserListAction.ClearSearchUsers @@ -143,7 +146,7 @@ class UserListFragment @Inject constructor( } viewModel.handle(action) } - .disposeOnDestroyView() + .launchIn(viewLifecycleOwner.lifecycleScope) views.userListSearch.setupAsSearch() views.userListSearch.requestFocus() diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt index fde69ce9ba..1187ace82e 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt @@ -28,12 +28,10 @@ import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.isEmail import im.vector.app.core.extensions.toggle import im.vector.app.core.platform.VectorViewModel -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.sample @@ -160,10 +158,10 @@ class UserListViewModel @AssistedInject constructor(@Assisted initialState: User knownUsersSearch .sample(300) - .flowOn(Dispatchers.Main) .flatMapLatest { search -> session.getPagedUsersLive(search, state.excludedUserIds).asFlow() - }.execute { + } + .execute { copy(knownUsers = it) } diff --git a/vector/src/test/java/im/vector/app/features/crypto/keys/KeysExporterTest.kt b/vector/src/test/java/im/vector/app/features/crypto/keys/KeysExporterTest.kt index c75abf5db4..57ad2a52ab 100644 --- a/vector/src/test/java/im/vector/app/features/crypto/keys/KeysExporterTest.kt +++ b/vector/src/test/java/im/vector/app/features/crypto/keys/KeysExporterTest.kt @@ -42,7 +42,7 @@ class KeysExporterTest { private val keysExporter = KeysExporter( session = FakeSession(fakeCryptoService = cryptoService), context = context.instance, - dispatchers = CoroutineDispatchers(Dispatchers.Unconfined) + dispatchers = CoroutineDispatchers(Dispatchers.Unconfined, Dispatchers.Unconfined) ) @Before diff --git a/vector/src/test/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModelTest.kt b/vector/src/test/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModelTest.kt index 506ac9c7d0..ea9335aaff 100644 --- a/vector/src/test/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModelTest.kt @@ -18,10 +18,10 @@ package im.vector.app.features.crypto.quads import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.test.MvRxTestRule -import im.vector.app.test.InstantRxRule import im.vector.app.test.fakes.FakeSession import im.vector.app.test.fakes.FakeStringProvider import im.vector.app.test.test +import kotlinx.coroutines.test.runBlockingTest import org.junit.Rule import org.junit.Test import org.matrix.android.sdk.api.session.securestorage.IntegrityResult @@ -39,8 +39,6 @@ private val KEY_INFO_WITHOUT_PASSPHRASE = KeyInfo(id = "id", content = SecretSto class SharedSecureStorageViewModelTest { - @get:Rule - val instantRx = InstantRxRule() @get:Rule val mvrxTestRule = MvRxTestRule() @@ -50,78 +48,100 @@ class SharedSecureStorageViewModelTest { @Test fun `given a key info with passphrase when initialising then step is EnterPassphrase`() { - givenKey(KEY_INFO_WITH_PASSPHRASE) - - val viewModel = createViewModel() - - viewModel.test().assertState(aViewState( - hasPassphrase = true, - step = SharedSecureStorageViewState.Step.EnterPassphrase - )) + runBlockingTest { + givenKey(KEY_INFO_WITH_PASSPHRASE) + val viewModel = createViewModel() + viewModel + .test(this) + .assertState(aViewState( + hasPassphrase = true, + step = SharedSecureStorageViewState.Step.EnterPassphrase + )) + .finish() + } } @Test fun `given a key info without passphrase when initialising then step is EnterKey`() { - givenKey(KEY_INFO_WITHOUT_PASSPHRASE) + runBlockingTest { + givenKey(KEY_INFO_WITHOUT_PASSPHRASE) - val viewModel = createViewModel() + val viewModel = createViewModel() - viewModel.test().assertState(aViewState( - hasPassphrase = false, - step = SharedSecureStorageViewState.Step.EnterKey - )) + viewModel + .test(this) + .assertState(aViewState( + hasPassphrase = false, + step = SharedSecureStorageViewState.Step.EnterKey + )) + .finish() + } } @Test fun `given on EnterKey step when going back then dismisses`() { - givenKey(KEY_INFO_WITHOUT_PASSPHRASE) + runBlockingTest { + givenKey(KEY_INFO_WITHOUT_PASSPHRASE) - val viewModel = createViewModel() - val test = viewModel.test() - - viewModel.handle(SharedSecureStorageAction.Back) - - test.assertEvents(SharedSecureStorageViewEvent.Dismiss) + val viewModel = createViewModel() + val test = viewModel.test(this) + viewModel.handle(SharedSecureStorageAction.Back) + test + .assertEvents(SharedSecureStorageViewEvent.Dismiss) + .finish() + } } @Test fun `given on passphrase step when using key then step is EnterKey`() { - givenKey(KEY_INFO_WITH_PASSPHRASE) - val viewModel = createViewModel() - val test = viewModel.test() + runBlockingTest { + givenKey(KEY_INFO_WITH_PASSPHRASE) + val viewModel = createViewModel() + val test = viewModel.test(this) - viewModel.handle(SharedSecureStorageAction.UseKey) + viewModel.handle(SharedSecureStorageAction.UseKey) - test.assertState(aViewState( - hasPassphrase = true, - step = SharedSecureStorageViewState.Step.EnterKey - )) + test + .assertState(aViewState( + hasPassphrase = true, + step = SharedSecureStorageViewState.Step.EnterKey + )) + .finish() + } } @Test fun `given a key info with passphrase and on EnterKey step when going back then step is EnterPassphrase`() { - givenKey(KEY_INFO_WITH_PASSPHRASE) - val viewModel = createViewModel() - val test = viewModel.test() + runBlockingTest { + givenKey(KEY_INFO_WITH_PASSPHRASE) + val viewModel = createViewModel() + val test = viewModel.test(this) - viewModel.handle(SharedSecureStorageAction.UseKey) - viewModel.handle(SharedSecureStorageAction.Back) + viewModel.handle(SharedSecureStorageAction.UseKey) + viewModel.handle(SharedSecureStorageAction.Back) - test.assertState(aViewState( - hasPassphrase = true, - step = SharedSecureStorageViewState.Step.EnterPassphrase - )) + test + .assertState(aViewState( + hasPassphrase = true, + step = SharedSecureStorageViewState.Step.EnterPassphrase + )) + .finish() + } } @Test fun `given on passphrase step when going back then dismisses`() { - givenKey(KEY_INFO_WITH_PASSPHRASE) - val viewModel = createViewModel() - val test = viewModel.test() + runBlockingTest { + givenKey(KEY_INFO_WITH_PASSPHRASE) + val viewModel = createViewModel() + val test = viewModel.test(this) - viewModel.handle(SharedSecureStorageAction.Back) + viewModel.handle(SharedSecureStorageAction.Back) - test.assertEvents(SharedSecureStorageViewEvent.Dismiss) + test + .assertEvents(SharedSecureStorageViewEvent.Dismiss) + .finish() + } } private fun createViewModel(): SharedSecureStorageViewModel { diff --git a/vector/src/test/java/im/vector/app/test/Extensions.kt b/vector/src/test/java/im/vector/app/test/Extensions.kt index f2a087fd52..e883df9906 100644 --- a/vector/src/test/java/im/vector/app/test/Extensions.kt +++ b/vector/src/test/java/im/vector/app/test/Extensions.kt @@ -20,27 +20,33 @@ import com.airbnb.mvrx.MavericksState import im.vector.app.core.platform.VectorViewEvents import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModelAction -import io.reactivex.observers.TestObserver +import kotlinx.coroutines.CoroutineScope import org.amshove.kluent.shouldBeEqualTo fun String.trimIndentOneLine() = trimIndent().replace("\n", "") -fun VectorViewModel.test(): ViewModelTest { +fun VectorViewModel.test(coroutineScope: CoroutineScope): ViewModelTest { val state = { com.airbnb.mvrx.withState(this) { it } } - val viewEvents = viewEvents.observe().test() + val viewEvents = viewEvents.stream().test(coroutineScope) return ViewModelTest(state, viewEvents) } class ViewModelTest( val state: () -> S, - val viewEvents: TestObserver + val viewEvents: FlowTestObserver ) { - fun assertEvents(vararg expected: VE) { + fun assertEvents(vararg expected: VE): ViewModelTest { viewEvents.assertValues(*expected) + return this } - fun assertState(expected: S) { + fun assertState(expected: S): ViewModelTest { state() shouldBeEqualTo expected + return this + } + + fun finish() { + viewEvents.finish() } } diff --git a/vector/src/test/java/im/vector/app/test/FlowTestObserver.kt b/vector/src/test/java/im/vector/app/test/FlowTestObserver.kt new file mode 100644 index 0000000000..955922d0c4 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/FlowTestObserver.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.test + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import org.junit.Assert.assertEquals + +fun Flow.test(scope: CoroutineScope): FlowTestObserver { + return FlowTestObserver(scope, this) +} + +class FlowTestObserver( + scope: CoroutineScope, + flow: Flow +) { + private val values = mutableListOf() + private val job: Job = flow + .onEach { + values.add(it) + }.launchIn(scope) + + fun assertNoValues(): FlowTestObserver { + assertEquals(emptyList(), this.values) + return this + } + + fun assertValues(vararg values: T): FlowTestObserver { + assertEquals(values.toList(), this.values) + return this + } + + fun finish() { + job.cancel() + } +} diff --git a/vector/src/test/java/im/vector/app/test/InstantRxRule.kt b/vector/src/test/java/im/vector/app/test/InstantRxRule.kt deleted file mode 100644 index 1145cb7dd1..0000000000 --- a/vector/src/test/java/im/vector/app/test/InstantRxRule.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (c) 2021 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.app.test - -import io.reactivex.android.plugins.RxAndroidPlugins -import io.reactivex.plugins.RxJavaPlugins -import io.reactivex.schedulers.Schedulers -import org.junit.rules.TestRule -import org.junit.runner.Description -import org.junit.runners.model.Statement - -class InstantRxRule : TestRule { - override fun apply(base: Statement, description: Description?): Statement { - RxJavaPlugins.setInitNewThreadSchedulerHandler { Schedulers.trampoline() } - RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() } - return base - } -}