Merge pull request #4363 from vector-im/feature/fga/rx_flow_migration

Feature/fga/rx flow migration
This commit is contained in:
Benoit Marty 2021-11-04 18:44:48 +01:00 committed by GitHub
commit f3655d4664
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
117 changed files with 1101 additions and 1003 deletions

1
changelog.d/4219.misc Normal file
View file

@ -0,0 +1 @@
Finish migration from RxJava to Flow

View file

@ -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",

41
docs/rx_flow_migration.md Normal file
View file

@ -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.

View file

@ -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<List<RoomSummary>> {
return session.getRoomSummariesLive(queryParams).asFlow()
fun liveRoomSummaries(queryParams: RoomSummaryQueryParams, sortOrder: RoomSortOrder = RoomSortOrder.NONE): Flow<List<RoomSummary>> {
return session.getRoomSummariesLive(queryParams, sortOrder).asFlow()
.startWith(session.coroutineDispatchers.io) {
session.getRoomSummaries(queryParams)
session.getRoomSummaries(queryParams, sortOrder)
}
}

View file

@ -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"

View file

@ -279,25 +279,9 @@ SOFTWARE.
Copyright 2012 The Dagger Authors
</li>
<li>
<b>rxkotlin</b>
<b>FlowBinding</b>
<br/>
Copyright io.reactivex.
</li>
<li>
<b>rxandroid</b>
<br/>
Copyright io.reactivex.
</li>
<li>
<b>rxrelay</b>
<br/>
Copyright 2014 Netflix, Inc.
Copyright 2015 Jake Wharton
</li>
<li>
<b>rxbinding</b>
<br/>
Copyright (C) 2015 Jake Wharton
Copyright 2019 Yang Chen
</li>
<li>
<b>Epoxy</b>

View file

@ -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<RoomGroupingMethod>>(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 -> {

View file

@ -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()

View file

@ -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

View file

@ -137,6 +137,6 @@ object VectorStaticModule {
@Provides
@JvmStatic
fun providesCoroutineDispatchers(): CoroutineDispatchers {
return CoroutineDispatchers(io = Dispatchers.IO)
return CoroutineDispatchers(io = Dispatchers.IO, computation = Dispatchers.Default)
}
}

View file

@ -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)

View file

@ -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 <T> Flow<T>.chunk(durationInMillis: Long): Flow<List<T>> {
require(durationInMillis > 0) { "Duration should be greater than 0" }
return flow {
coroutineScope {
val events = ArrayList<T>()
val ticker = fixedPeriodTicker(durationInMillis)
try {
val upstreamValues = produce(capacity = Channel.CONFLATED) {
collect { value -> send(value) }
}
while (isActive) {
var hasTimedOut = false
select<Unit> {
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 <T> Flow<T>.throttleFirst(windowDuration: Long): Flow<T> = 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<Unit> {
return scope.fixedPeriodTicker(delayMillis, initialDelayMillis).consumeAsFlow()
}
private fun CoroutineScope.fixedPeriodTicker(delayMillis: Long, initialDelayMillis: Long = delayMillis): ReceiveChannel<Unit> {
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)
}
}
}

View file

@ -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<VB : ViewBinding> : AppCompatActivity(), MavericksView {
@ -104,13 +104,12 @@ abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), Maver
protected fun <T : VectorViewEvents> 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<VB : ViewBinding> : 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<VB : ViewBinding> : 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<VB : ViewBinding> : AppCompatActivity(), Maver
// For debug only
private var debugReceiver: DebugReceiver? = null
private val uiDisposables = CompositeDisposable()
private val restorables = ArrayList<Restorable>()
override fun attachBaseContext(base: Context) {
@ -175,10 +173,6 @@ abstract class VectorBaseActivity<VB : ViewBinding> : 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<VB : ViewBinding> : AppCompatActivity(), Maver
override fun onDestroy() {
super.onDestroy()
Timber.i("onDestroy Activity ${javaClass.simpleName}")
uiDisposables.dispose()
}
private val pinStartForActivityResult = registerStartForActivityResult { activityResult ->

View file

@ -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<VB : ViewBinding> : 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<VB : ViewBinding> : 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<VB : ViewBinding> : BottomShe
protected fun <T : VectorViewEvents> VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) {
viewEvents
.observe()
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
.stream()
.onEach {
observer(it)
}
.disposeOnDestroyView()
.launchIn(viewLifecycleOwner.lifecycleScope)
}
}

View file

@ -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<VB : ViewBinding> : Fragment(), MavericksView {
@ -148,7 +148,6 @@ abstract class VectorBaseFragment<VB : ViewBinding> : 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<VB : ViewBinding> : Fragment(), MavericksView
@CallSuper
override fun onDestroy() {
Timber.i("onDestroy Fragment ${javaClass.simpleName}")
uiDisposables.dispose()
super.onDestroy()
}
@ -221,29 +219,18 @@ abstract class VectorBaseFragment<VB : ViewBinding> : Fragment(), MavericksView
}
}
/* ==========================================================================================
* Disposable
* ========================================================================================== */
private val uiDisposables = CompositeDisposable()
protected fun Disposable.disposeOnDestroyView() {
uiDisposables.add(this)
}
/* ==========================================================================================
* ViewEvents
* ========================================================================================== */
protected fun <T : VectorViewEvents> 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<VB : ViewBinding> : 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)
}
/* ==========================================================================================

View file

@ -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<S : MavericksState, VA : VectorViewModelAction, VE : VectorViewEvents>(initialState: S) :
BaseMvRxViewModel<S>(initialState) {
interface Factory<S : MavericksState> {
fun create(state: S): BaseMvRxViewModel<S>
}
MavericksViewModel<S>(initialState) {
// Used to post transient events to the View
protected val _viewEvents = PublishDataSource<VE>()
val viewEvents: DataSource<VE> = _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 <T> Single<T>.toAsync(stateReducer: S.(Async<T>) -> S): Single<Async<T>> {
setState { stateReducer(Loading()) }
return map { Success(it) as Async<T> }
.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 <T> Observable<T>.toAsync(stateReducer: S.(Async<T>) -> S): Observable<Async<T>> {
setState { stateReducer(Loading()) }
return map { Success(it) as Async<T> }
.onErrorReturn { Fail(it) }
.doOnNext { setState { stateReducer(it) } }
}
abstract fun handle(action: VA)
}

View file

@ -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
}
}
}
}
}

View file

@ -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 {

View file

@ -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<T> {
fun observe(): Observable<T>
fun stream(): Flow<T>
}
interface MutableDataSource<T> : DataSource<T> {
@ -34,25 +33,17 @@ interface MutableDataSource<T> : DataSource<T> {
*/
open class BehaviorDataSource<T>(private val defaultValue: T? = null) : MutableDataSource<T> {
private val behaviorRelay = createRelay()
private val mutableFlow = MutableSharedFlow<T>(replay = 1)
val currentValue: T?
get() = behaviorRelay.value
get() = mutableFlow.replayCache.firstOrNull()
override fun observe(): Observable<T> {
return behaviorRelay.hide().observeOn(AndroidSchedulers.mainThread())
override fun stream(): Flow<T> {
return mutableFlow
}
override fun post(value: T) {
behaviorRelay.accept(value!!)
}
private fun createRelay(): BehaviorRelay<T> {
return if (defaultValue == null) {
BehaviorRelay.create()
} else {
BehaviorRelay.createDefault(defaultValue)
}
mutableFlow.tryEmit(value)
}
}
@ -61,13 +52,13 @@ open class BehaviorDataSource<T>(private val defaultValue: T? = null) : MutableD
*/
open class PublishDataSource<T> : MutableDataSource<T> {
private val publishRelay = PublishRelay.create<T>()
private val mutableFlow = MutableSharedFlow<T>(replay = 0, extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
override fun observe(): Observable<T> {
return publishRelay.hide().observeOn(AndroidSchedulers.mainThread())
override fun stream(): Flow<T> {
return mutableFlow
}
override fun post(value: T) {
publishRelay.accept(value!!)
mutableFlow.tryEmit(value)
}
}

View file

@ -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 <T> Single<T>.subscribeLogError(): Disposable {
return subscribe(Functions.emptyConsumer(), { Timber.e(it) })
}
fun Completable.subscribeLogError(): Disposable {
return subscribe({}, { Timber.e(it) })
}

View file

@ -39,7 +39,7 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetC
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
callViewModel.subscribe(this) {
callViewModel.onEach {
renderState(it)
}

View file

@ -35,6 +35,7 @@ import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Mavericks
import com.airbnb.mvrx.viewModel
@ -61,7 +62,8 @@ import im.vector.app.features.home.room.detail.RoomDetailActivity
import im.vector.app.features.home.room.detail.RoomDetailArgs
import io.github.hyuwah.draggableviewlib.DraggableView
import io.github.hyuwah.draggableviewlib.setupDraggable
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.extensions.orFalse
import org.matrix.android.sdk.api.logger.LoggerTag
@ -130,7 +132,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
setSupportActionBar(views.callToolbar)
configureCallViews()
callViewModel.subscribe(this) {
callViewModel.onEach {
renderState(it)
}
@ -141,12 +143,11 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
}
callViewModel.viewEvents
.observe()
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
.stream()
.onEach {
handleViewEvents(it)
}
.disposeOnDestroy()
.launchIn(lifecycleScope)
callViewModel.onEach(VectorCallViewState::callId, VectorCallViewState::isVideoCall) { _, isVideoCall ->
if (isVideoCall) {

View file

@ -68,7 +68,7 @@ class VectorJitsiActivity : VectorBaseActivity<ActivityJitsiBinding>(), JitsiMee
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
jitsiViewModel.subscribe(this) {
jitsiViewModel.onEach {
renderState(it)
}

View file

@ -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<WeakReference<SurfaceViewRenderer>> = ArrayList()
private var remoteSurfaceRenderers: MutableList<WeakReference<SurfaceViewRenderer>> = ArrayList()
private val iceCandidateSource: PublishSubject<IceCandidate> = 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<IceCandidate>()
private var localIceCandidateJob: Job? = null
private val remoteCandidateSource: ReplaySubject<IceCandidate> = ReplaySubject.create()
private var remoteIceCandidateDisposable: Disposable? = null
private val remoteCandidateSource: MutableSharedFlow<IceCandidate> = 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)
}
}
}

View file

@ -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() {

View file

@ -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,

View file

@ -63,7 +63,7 @@ class SharedSecureStorageActivity :
viewModel.observeViewEvents { observeViewEvents(it) }
viewModel.subscribe(this) { renderState(it) }
viewModel.onEach { renderState(it) }
}
override fun onDestroy() {

View file

@ -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<FragmentSsssAccessFromKeyBinding>() {
@ -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) }

View file

@ -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)

View file

@ -55,7 +55,7 @@ class SharedSecuredStorageResetAllFragment @Inject constructor() :
}
}
sharedViewModel.subscribe(this) { state ->
sharedViewModel.onEach { state ->
views.ssssResetOtherDevices.setTextOrHide(
state.activeDeviceCount
.takeIf { it > 0 }

View file

@ -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) {

View file

@ -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) {

View file

@ -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() }

View file

@ -66,7 +66,7 @@ class RoomDevToolActivity : SimpleFragmentActivity(), FragmentManager.OnBackStac
override fun initUiAndData() {
super.initUiAndData()
viewModel.subscribe(this) {
viewModel.onEach {
renderState(it)
}

View file

@ -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() {

View file

@ -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)

View file

@ -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<HomeActivityArgs>(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

View file

@ -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)

View file

@ -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<HomeDetailViewState, HomeDetailAction, HomeDetailViewEvents>(initialState),
VectorViewModel<HomeDetailViewState, HomeDetailAction, HomeDetailViewEvents>(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)
}
}

View file

@ -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

View file

@ -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<String>) {
@ -89,7 +97,7 @@ class ShortcutsHandler @Inject constructor(
}
private fun createShortcuts(rooms: List<RoomSummary>) {
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)

View file

@ -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<UnreadMessagesState, EmptyAction, EmptyViewEvents>(initialState) {
VectorViewModel<UnreadMessagesState, EmptyAction, EmptyViewEvents>(initialState) {
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<UnreadMessagesSharedViewModel, UnreadMessagesState> {
@ -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)
)
}
}
}

View file

@ -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) {

View file

@ -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 {

View file

@ -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<RoomDetailAction.TimelineEventTurnsInvisible>()
private val visibleEventsObservable = BehaviorRelay.create<RoomDetailAction.TimelineEventTurnsVisible>()
private val invisibleEventsSource = BehaviorDataSource<RoomDetailAction.TimelineEventTurnsInvisible>()
private val visibleEventsSource = BehaviorDataSource<RoomDetailAction.TimelineEventTurnsVisible>()
private var timelineEvents = MutableSharedFlow<List<TimelineEvent>>(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() {

View file

@ -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)
}
}

View file

@ -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<List<ReactionInfo>> = 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<ReactionAggregatedSummary>.toReactionInfoList(): Single<List<ReactionInfo>> {
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
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -20,6 +20,4 @@ import im.vector.app.features.home.RoomListDisplayMode
interface RoomListSectionBuilder {
fun buildSections(mode: RoomListDisplayMode): List<RoomsSection>
fun dispose()
}

View file

@ -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<RoomsSection> {
val activeGroupAwareQueries = mutableListOf<UpdatableLivePageResult>()
val sections = mutableListOf<RoomsSection>()
@ -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()
}
}

View file

@ -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<List<SpaceChildInfo>>()
val suggestedRoomsFlow = // MutableLiveData<List<SpaceChildInfo>>()
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<SuggestedRoomInfo>()
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()
}
}

View file

@ -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()
}
}

View file

@ -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,

View file

@ -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<String>()
private val activeSessionIds = mutableSetOf<String>()
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) {

View file

@ -85,10 +85,9 @@ open class LoginActivity : VectorBaseActivity<ActivityLoginBinding>(), ToolbarCo
addFirstFragment()
}
loginViewModel
.subscribe(this) {
updateWithState(it)
}
loginViewModel.onEach {
updateWithState(it)
}
loginViewModel.observeViewEvents { handleLoginViewEvents(it) }

View file

@ -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<FragmentLog
private fun setupSubmitButton() {
views.loginSubmit.setOnClickListener { submit() }
Observable
.combineLatest(
views.loginField.textChanges().map { it.trim().isNotEmpty() },
views.passwordField.textChanges().map { it.isNotEmpty() },
{ isLoginNotEmpty, isPasswordNotEmpty ->
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() {

View file

@ -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 {

View file

@ -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<F
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.resetPasswordEmailTil.error = null
views.passwordFieldTil.error = null
views.resetPasswordSubmit.isEnabled = it
}
.disposeOnDestroyView()
.launchIn(viewLifecycleOwner.lifecycleScope)
}
private fun submit() {

View file

@ -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<F
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) {

View file

@ -92,10 +92,9 @@ open class LoginActivity2 : VectorBaseActivity<ActivityLoginBinding>(), ToolbarC
addFirstFragment()
}
loginViewModel
.subscribe(this) {
updateWithState(it)
}
loginViewModel.onEach {
updateWithState(it)
}
loginViewModel.observeViewEvents { handleLoginViewEvents(it) }
@ -201,19 +200,19 @@ open class LoginActivity2 : VectorBaseActivity<ActivityLoginBinding>(), 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,

View file

@ -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() {

View file

@ -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() {

View file

@ -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() {

View file

@ -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() {

View file

@ -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<Frag
private fun setupSubmitButton() {
views.loginSubmit.setOnClickListener { submit() }
Observable
.combineLatest(
views.loginField.textChanges().map { it.trim().isNotEmpty() },
views.passwordField.textChanges().map { it.isNotEmpty() },
{ isLoginNotEmpty, isPasswordNotEmpty ->
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() {

View file

@ -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) {

View file

@ -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() {

View file

@ -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) {

View file

@ -33,8 +33,8 @@ class PowerLevelsFlowFactory(private val room: Room) {
fun createFlow(): Flow<PowerLevelsContent> {
return room.flow()
.liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition)
.flowOn(Dispatchers.Default)
.mapOptional { it.content.toModel<PowerLevelsContent>() }
.flowOn(Dispatchers.Default)
.unwrap()
}
}

View file

@ -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<ActivityEmojiReactionPick
}
searchView.queryTextChanges()
.throttleWithTimeout(600, TimeUnit.MILLISECONDS)
.doOnError { err -> Timber.e(err) }
.observeOn(AndroidSchedulers.mainThread())
.subscribe { query ->
.throttleFirst(600)
.onEach { query ->
onQueryText(query.toString())
}
.disposeOnDestroy()
.launchIn(lifecycleScope)
}
searchItem.expandActionView()
return true

View file

@ -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 ->

View file

@ -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)

View file

@ -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<ActivitySimpleBinding>(), Matri
}
sharedActionViewModel
.observe()
.subscribe { sharedAction ->
.stream()
.onEach { sharedAction ->
when (sharedAction) {
is RoomDirectorySharedAction.Back -> popBackstack()
is RoomDirectorySharedAction.CreateRoom -> {
@ -74,7 +77,7 @@ class RoomDirectoryActivity : VectorBaseActivity<ActivitySimpleBinding>(), Matri
is RoomDirectorySharedAction.Close -> finish()
}
}
.disposeOnDestroy()
.launchIn(lifecycleScope)
}
override fun initUiAndData() {

View file

@ -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<ActivitySimpleBinding>(), 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 {

View file

@ -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) {

View file

@ -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) {

View file

@ -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()
}

View file

@ -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?) {

View file

@ -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()

View file

@ -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

View file

@ -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() {

View file

@ -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

View file

@ -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()

View file

@ -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,

View file

@ -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
* ========================================================================================== */

View file

@ -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<Disposable>()
// 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<VectorPreference>(VectorPreferences.SETTINGS_CRYPTOGRAPHY_HS_ADMIN_DISABLED_E2E_DEFAULT)?.isVisible =
@ -173,14 +168,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
// findPreference<VectorPreference>(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

View file

@ -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<DevicesViewModel, DevicesViewState> by hiltMavericksViewModelFactory()
private val refreshPublisher: PublishSubject<Unit> = PublishSubject.create()
private val refreshSource = PublishDataSource<Unit>()
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) {

View file

@ -50,7 +50,7 @@ class SoftLogoutActivity : LoginActivity() {
override fun initUiAndData() {
super.initUiAndData()
softLogoutViewModel.subscribe(this) {
softLogoutViewModel.onEach {
updateWithState(it)
}

View file

@ -52,7 +52,7 @@ class SoftLogoutActivity2 : LoginActivity2() {
override fun initUiAndData() {
super.initUiAndData()
softLogoutViewModel.subscribe(this) {
softLogoutViewModel.onEach {
updateWithState(it)
}

View file

@ -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 -> {

View file

@ -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<BottomSheetLea
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
views.autoLeaveRadioGroup.checkedChanges()
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
.onEach {
when (it) {
views.leaveAll.id -> {
settingsViewModel.handle(SpaceLeaveViewAction.SetAutoLeaveAll)
@ -100,7 +101,7 @@ class LeaveSpaceBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetLea
}
}
}
.disposeOnDestroyView()
.launchIn(viewLifecycleOwner.lifecycleScope)
views.leaveButton.debouncedClicks {
settingsViewModel.handle(SpaceLeaveViewAction.LeaveSpace)

View file

@ -71,7 +71,7 @@ class SpaceCreationActivity : SimpleFragmentActivity() {
override fun initUiAndData() {
super.initUiAndData()
viewModel.subscribe(this) {
viewModel.onEach {
renderState(it)
}

View file

@ -35,6 +35,7 @@ import im.vector.app.group
import im.vector.app.space
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
@ -89,14 +90,11 @@ class SpaceListViewModel @AssistedInject constructor(@Assisted initialState: Spa
// observeSelectionState()
appStateHandler.selectedRoomGroupingObservable
.distinctUntilChanged()
.subscribe {
setState {
copy(
selectedGroupingMethod = it.orNull() ?: RoomGroupingMethod.BySpace(null)
)
}
.setOnEach {
copy(
selectedGroupingMethod = it.orNull() ?: RoomGroupingMethod.BySpace(null)
)
}
.disposeOnClear()
session.getGroupSummariesLive(groupSummaryQueryParams {})
.asFlow()
@ -114,7 +112,6 @@ class SpaceListViewModel @AssistedInject constructor(@Assisted initialState: Spa
}, sortOrder = RoomSortOrder.NONE
).asFlow()
.sample(300)
.flowOn(Dispatchers.Default)
.onEach {
val inviteCount = if (autoAcceptInvites.hideInvites) {
0
@ -140,7 +137,9 @@ class SpaceListViewModel @AssistedInject constructor(@Assisted initialState: Spa
homeAggregateCount = counts
)
}
}.launchIn(viewModelScope)
}
.flowOn(Dispatchers.Default)
.launchIn(viewModelScope)
}
override fun handle(action: SpaceListAction) {

View file

@ -19,6 +19,7 @@ package im.vector.app.features.spaces
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.lifecycle.lifecycleScope
import com.airbnb.mvrx.Mavericks
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R
@ -27,6 +28,8 @@ import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.databinding.ActivitySimpleBinding
import im.vector.app.features.spaces.preview.SpacePreviewArgs
import im.vector.app.features.spaces.preview.SpacePreviewFragment
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@AndroidEntryPoint
class SpacePreviewActivity : VectorBaseActivity<ActivitySimpleBinding>() {
@ -39,8 +42,8 @@ class SpacePreviewActivity : VectorBaseActivity<ActivitySimpleBinding>() {
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<ActivitySimpleBinding>() {
is SpacePreviewSharedAction.ShowErrorMessage -> action.error?.let { showSnackbar(it) }
}
}
.disposeOnDestroy()
.launchIn(lifecycleScope)
if (isFirstCreation()) {
val simpleName = SpacePreviewFragment::class.java.simpleName

View file

@ -50,7 +50,7 @@ class CreateSpaceAdd3pidInvitesFragment @Inject constructor(
views.recyclerView.configureWith(epoxyController)
epoxyController.listener = this
sharedViewModel.subscribe(this) {
sharedViewModel.onEach {
invalidateState(it)
}

View file

@ -46,7 +46,7 @@ class CreateSpaceDefaultRoomsFragment @Inject constructor(
views.recyclerView.configureWith(epoxyController)
epoxyController.listener = this
sharedViewModel.subscribe(this) {
sharedViewModel.onEach {
epoxyController.setData(it)
}

View file

@ -50,7 +50,7 @@ class CreateSpaceDetailsFragment @Inject constructor(
views.recyclerView.configureWith(epoxyController)
epoxyController.listener = this
sharedViewModel.subscribe(this) {
sharedViewModel.onEach {
epoxyController.setData(it)
}

View file

@ -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

Some files were not shown because too many files have changed in this diff Show more