mirror of
https://github.com/element-hq/element-android
synced 2024-11-28 13:38:49 +03:00
Create a ViewState for HomeActivity
And disable the popup - WIP
This commit is contained in:
parent
1365240f69
commit
48a30a7b82
6 changed files with 164 additions and 54 deletions
|
@ -20,10 +20,13 @@ import androidx.lifecycle.LiveData
|
||||||
|
|
||||||
interface InitialSyncProgressService {
|
interface InitialSyncProgressService {
|
||||||
|
|
||||||
fun getInitialSyncProgressStatus() : LiveData<Status?>
|
fun getInitialSyncProgressStatus(): LiveData<Status>
|
||||||
|
|
||||||
data class Status(
|
sealed class Status {
|
||||||
|
object Idle : Status()
|
||||||
|
data class Progressing(
|
||||||
@StringRes val statusText: Int,
|
@StringRes val statusText: Int,
|
||||||
val percentProgress: Int = 0
|
val percentProgress: Int = 0
|
||||||
)
|
) : Status()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,11 +25,11 @@ import javax.inject.Inject
|
||||||
@SessionScope
|
@SessionScope
|
||||||
class DefaultInitialSyncProgressService @Inject constructor() : InitialSyncProgressService {
|
class DefaultInitialSyncProgressService @Inject constructor() : InitialSyncProgressService {
|
||||||
|
|
||||||
private var status = MutableLiveData<InitialSyncProgressService.Status>()
|
private val status = MutableLiveData<InitialSyncProgressService.Status>()
|
||||||
|
|
||||||
private var rootTask: TaskInfo? = null
|
private var rootTask: TaskInfo? = null
|
||||||
|
|
||||||
override fun getInitialSyncProgressStatus(): LiveData<InitialSyncProgressService.Status?> {
|
override fun getInitialSyncProgressStatus(): LiveData<InitialSyncProgressService.Status> {
|
||||||
return status
|
return status
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,13 +63,13 @@ class DefaultInitialSyncProgressService @Inject constructor() : InitialSyncProgr
|
||||||
parent?.setProgress(endedTask.offset + (endedTask.totalProgress * endedTask.parentWeight).toInt())
|
parent?.setProgress(endedTask.offset + (endedTask.totalProgress * endedTask.parentWeight).toInt())
|
||||||
}
|
}
|
||||||
if (endedTask?.parent == null) {
|
if (endedTask?.parent == null) {
|
||||||
status.postValue(null)
|
status.postValue(InitialSyncProgressService.Status.Idle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun endAll() {
|
fun endAll() {
|
||||||
rootTask = null
|
rootTask = null
|
||||||
status.postValue(null)
|
status.postValue(InitialSyncProgressService.Status.Idle)
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class TaskInfo(@StringRes var nameRes: Int,
|
private inner class TaskInfo(@StringRes var nameRes: Int,
|
||||||
|
@ -102,9 +102,7 @@ class DefaultInitialSyncProgressService @Inject constructor() : InitialSyncProgr
|
||||||
it.setProgress(offset + parentProgress)
|
it.setProgress(offset + parentProgress)
|
||||||
} ?: run {
|
} ?: run {
|
||||||
Timber.v("--- ${leaf().nameRes}: $currentProgress")
|
Timber.v("--- ${leaf().nameRes}: $currentProgress")
|
||||||
status.postValue(
|
status.postValue(InitialSyncProgressService.Status.Progressing(leaf().nameRes, currentProgress))
|
||||||
InitialSyncProgressService.Status(leaf().nameRes, currentProgress)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,20 +19,16 @@ package im.vector.riotx.features.home
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.view.GravityCompat
|
import androidx.core.view.GravityCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.drawerlayout.widget.DrawerLayout
|
import androidx.drawerlayout.widget.DrawerLayout
|
||||||
import androidx.lifecycle.Observer
|
import com.airbnb.mvrx.MvRx
|
||||||
import com.airbnb.mvrx.viewModel
|
import com.airbnb.mvrx.viewModel
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.session.InitialSyncProgressService
|
||||||
import im.vector.matrix.android.api.session.Session
|
|
||||||
import im.vector.matrix.android.api.util.toMatrixItem
|
|
||||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
|
||||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.di.ActiveSessionHolder
|
import im.vector.riotx.core.di.ActiveSessionHolder
|
||||||
import im.vector.riotx.core.di.ScreenComponent
|
import im.vector.riotx.core.di.ScreenComponent
|
||||||
|
@ -46,21 +42,28 @@ import im.vector.riotx.features.crypto.recover.BootstrapBottomSheet
|
||||||
import im.vector.riotx.features.disclaimer.showDisclaimerDialog
|
import im.vector.riotx.features.disclaimer.showDisclaimerDialog
|
||||||
import im.vector.riotx.features.notifications.NotificationDrawerManager
|
import im.vector.riotx.features.notifications.NotificationDrawerManager
|
||||||
import im.vector.riotx.features.popup.PopupAlertManager
|
import im.vector.riotx.features.popup.PopupAlertManager
|
||||||
import im.vector.riotx.features.popup.VerificationVectorAlert
|
|
||||||
import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
|
import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
|
||||||
import im.vector.riotx.features.settings.VectorPreferences
|
import im.vector.riotx.features.settings.VectorPreferences
|
||||||
import im.vector.riotx.features.workers.signout.SignOutViewModel
|
import im.vector.riotx.features.workers.signout.SignOutViewModel
|
||||||
import im.vector.riotx.push.fcm.FcmHelper
|
import im.vector.riotx.push.fcm.FcmHelper
|
||||||
|
import kotlinx.android.parcel.Parcelize
|
||||||
import kotlinx.android.synthetic.main.activity_home.*
|
import kotlinx.android.synthetic.main.activity_home.*
|
||||||
import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
|
import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class HomeActivityArgs(
|
||||||
|
val clearNotification: Boolean,
|
||||||
|
val accountCreation: Boolean
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDetectorSharedViewModel.Factory {
|
class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDetectorSharedViewModel.Factory {
|
||||||
|
|
||||||
private lateinit var sharedActionViewModel: HomeSharedActionViewModel
|
private lateinit var sharedActionViewModel: HomeSharedActionViewModel
|
||||||
|
|
||||||
private val homeActivityViewModel: HomeActivityViewModel by viewModel()
|
private val homeActivityViewModel: HomeActivityViewModel by viewModel()
|
||||||
|
@Inject lateinit var viewModelFactory: HomeActivityViewModel.Factory
|
||||||
|
|
||||||
@Inject lateinit var activeSessionHolder: ActiveSessionHolder
|
@Inject lateinit var activeSessionHolder: ActiveSessionHolder
|
||||||
@Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler
|
@Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler
|
||||||
|
@ -114,21 +117,33 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
|
||||||
}
|
}
|
||||||
.disposeOnDestroy()
|
.disposeOnDestroy()
|
||||||
|
|
||||||
if (intent.getBooleanExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION, false)) {
|
val args = intent.getParcelableExtra<HomeActivityArgs>(MvRx.KEY_ARG)
|
||||||
|
|
||||||
|
if (args?.clearNotification == true) {
|
||||||
notificationDrawerManager.clearAllEvents()
|
notificationDrawerManager.clearAllEvents()
|
||||||
intent.removeExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION)
|
|
||||||
}
|
|
||||||
if (intent.getBooleanExtra(EXTRA_ACCOUNT_CREATION, false)) {
|
|
||||||
sharedActionViewModel.post(HomeActivitySharedAction.PromptForSecurityBootstrap)
|
|
||||||
homeActivityViewModel.isAccountCreation = true
|
|
||||||
intent.removeExtra(EXTRA_ACCOUNT_CREATION)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
activeSessionHolder.getSafeActiveSession()?.getInitialSyncProgressStatus()?.observe(this, Observer { status ->
|
homeActivityViewModel.subscribe(this) { renderState(it) }
|
||||||
if (status == null) {
|
|
||||||
waiting_view.isVisible = false
|
/*
|
||||||
|
// TODO Remove
|
||||||
|
// Ask again if the app is relaunched
|
||||||
|
if (!homeActivityViewModel.hasDisplayedCompleteSecurityPrompt
|
||||||
|
&& activeSessionHolder.getSafeActiveSession()?.hasAlreadySynced() == true) {
|
||||||
promptCompleteSecurityIfNeeded()
|
promptCompleteSecurityIfNeeded()
|
||||||
} else {
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
shortcutsHandler.observeRoomsAndBuildShortcuts()
|
||||||
|
.disposeOnDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderState(state: HomeActivityViewState) {
|
||||||
|
when (val status = state.initialSyncProgressServiceStatus) {
|
||||||
|
is InitialSyncProgressService.Status.Idle -> {
|
||||||
|
waiting_view.isVisible = false
|
||||||
|
}
|
||||||
|
is InitialSyncProgressService.Status.Progressing -> {
|
||||||
homeActivityViewModel.hasDisplayedCompleteSecurityPrompt = false
|
homeActivityViewModel.hasDisplayedCompleteSecurityPrompt = false
|
||||||
Timber.v("${getString(status.statusText)} ${status.percentProgress}")
|
Timber.v("${getString(status.statusText)} ${status.percentProgress}")
|
||||||
waiting_view.setOnClickListener {
|
waiting_view.setOnClickListener {
|
||||||
|
@ -146,18 +161,11 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
|
||||||
}
|
}
|
||||||
waiting_view.isVisible = true
|
waiting_view.isVisible = true
|
||||||
}
|
}
|
||||||
})
|
}.exhaustive
|
||||||
|
|
||||||
// Ask again if the app is relaunched
|
|
||||||
if (!homeActivityViewModel.hasDisplayedCompleteSecurityPrompt
|
|
||||||
&& activeSessionHolder.getSafeActiveSession()?.hasAlreadySynced() == true) {
|
|
||||||
promptCompleteSecurityIfNeeded()
|
|
||||||
}
|
|
||||||
|
|
||||||
shortcutsHandler.observeRoomsAndBuildShortcuts()
|
|
||||||
.disposeOnDestroy()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// TODO Remove
|
||||||
private fun promptCompleteSecurityIfNeeded() {
|
private fun promptCompleteSecurityIfNeeded() {
|
||||||
val session = activeSessionHolder.getSafeActiveSession() ?: return
|
val session = activeSessionHolder.getSafeActiveSession() ?: return
|
||||||
if (!session.hasAlreadySynced()) return
|
if (!session.hasAlreadySynced()) return
|
||||||
|
@ -172,7 +180,10 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// TODO Remove
|
||||||
|
/*
|
||||||
private fun alertCompleteSecurity(session: Session) {
|
private fun alertCompleteSecurity(session: Session) {
|
||||||
val myCrossSigningKeys = session.cryptoService().crossSigningService()
|
val myCrossSigningKeys = session.cryptoService().crossSigningService()
|
||||||
.getMyCrossSigningKeys()
|
.getMyCrossSigningKeys()
|
||||||
|
@ -204,7 +215,10 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
// TODO Remove
|
||||||
private fun promptSecurityEvent(session: Session, titleRes: Int, descRes: Int, action: ((VectorBaseActivity) -> Unit)) {
|
private fun promptSecurityEvent(session: Session, titleRes: Int, descRes: Int, action: ((VectorBaseActivity) -> Unit)) {
|
||||||
homeActivityViewModel.hasDisplayedCompleteSecurityPrompt = true
|
homeActivityViewModel.hasDisplayedCompleteSecurityPrompt = true
|
||||||
popupAlertManager.postVectorAlert(
|
popupAlertManager.postVectorAlert(
|
||||||
|
@ -225,12 +239,12 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent?) {
|
override fun onNewIntent(intent: Intent?) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
if (intent?.hasExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION) == true) {
|
if (intent?.getParcelableExtra<HomeActivityArgs>(MvRx.KEY_ARG)?.clearNotification == true) {
|
||||||
notificationDrawerManager.clearAllEvents()
|
notificationDrawerManager.clearAllEvents()
|
||||||
intent.removeExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -293,14 +307,15 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val EXTRA_CLEAR_EXISTING_NOTIFICATION = "EXTRA_CLEAR_EXISTING_NOTIFICATION"
|
|
||||||
private const val EXTRA_ACCOUNT_CREATION = "EXTRA_ACCOUNT_CREATION"
|
|
||||||
|
|
||||||
fun newIntent(context: Context, clearNotification: Boolean = false, accountCreation: Boolean = false): Intent {
|
fun newIntent(context: Context, clearNotification: Boolean = false, accountCreation: Boolean = false): Intent {
|
||||||
|
val args = HomeActivityArgs(
|
||||||
|
clearNotification = clearNotification,
|
||||||
|
accountCreation = accountCreation
|
||||||
|
)
|
||||||
|
|
||||||
return Intent(context, HomeActivity::class.java)
|
return Intent(context, HomeActivity::class.java)
|
||||||
.apply {
|
.apply {
|
||||||
putExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION, clearNotification)
|
putExtra(MvRx.KEY_ARG, args)
|
||||||
putExtra(EXTRA_ACCOUNT_CREATION, accountCreation)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,5 +25,6 @@ sealed class HomeActivitySharedAction : VectorSharedAction {
|
||||||
object OpenDrawer : HomeActivitySharedAction()
|
object OpenDrawer : HomeActivitySharedAction()
|
||||||
object CloseDrawer : HomeActivitySharedAction()
|
object CloseDrawer : HomeActivitySharedAction()
|
||||||
object OpenGroup : HomeActivitySharedAction()
|
object OpenGroup : HomeActivitySharedAction()
|
||||||
|
// TODO Remove?
|
||||||
object PromptForSecurityBootstrap : HomeActivitySharedAction()
|
object PromptForSecurityBootstrap : HomeActivitySharedAction()
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,18 +16,87 @@
|
||||||
|
|
||||||
package im.vector.riotx.features.home
|
package im.vector.riotx.features.home
|
||||||
|
|
||||||
import com.airbnb.mvrx.MvRxState
|
import com.airbnb.mvrx.MvRx
|
||||||
|
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||||
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
|
import com.squareup.inject.assisted.Assisted
|
||||||
|
import com.squareup.inject.assisted.AssistedInject
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||||
|
import im.vector.matrix.rx.asObservable
|
||||||
|
import im.vector.riotx.core.di.ActiveSessionHolder
|
||||||
import im.vector.riotx.core.platform.EmptyAction
|
import im.vector.riotx.core.platform.EmptyAction
|
||||||
import im.vector.riotx.core.platform.EmptyViewEvents
|
import im.vector.riotx.core.platform.EmptyViewEvents
|
||||||
import im.vector.riotx.core.platform.VectorViewModel
|
import im.vector.riotx.core.platform.VectorViewModel
|
||||||
|
import im.vector.riotx.features.login.ReAuthHelper
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
data class EmptyState(
|
class HomeActivityViewModel @AssistedInject constructor(
|
||||||
val dummy: Boolean = false
|
@Assisted initialState: HomeActivityViewState,
|
||||||
) : MvRxState
|
@Assisted private val args: HomeActivityArgs,
|
||||||
|
private val activeSessionHolder: ActiveSessionHolder,
|
||||||
|
private val reAuthHelper: ReAuthHelper
|
||||||
|
) : VectorViewModel<HomeActivityViewState, EmptyAction, EmptyViewEvents>(initialState) {
|
||||||
|
|
||||||
class HomeActivityViewModel : VectorViewModel<EmptyState, EmptyAction, EmptyViewEvents>(EmptyState()) {
|
@AssistedInject.Factory
|
||||||
|
interface Factory {
|
||||||
|
fun create(initialState: HomeActivityViewState, args: HomeActivityArgs): HomeActivityViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object : MvRxViewModelFactory<HomeActivityViewModel, HomeActivityViewState> {
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
override fun create(viewModelContext: ViewModelContext, state: HomeActivityViewState): HomeActivityViewModel? {
|
||||||
|
val activity: HomeActivity = viewModelContext.activity()
|
||||||
|
val args: HomeActivityArgs? = activity.intent.getParcelableExtra(MvRx.KEY_ARG)
|
||||||
|
return args?.let { activity.viewModelFactory.create(state, it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Remove?
|
||||||
var hasDisplayedCompleteSecurityPrompt: Boolean = false
|
var hasDisplayedCompleteSecurityPrompt: Boolean = false
|
||||||
var isAccountCreation: Boolean = false
|
|
||||||
|
init {
|
||||||
|
observeInitialSync()
|
||||||
|
mayBeInitializeCrossSigning()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun observeInitialSync() {
|
||||||
|
val session = activeSessionHolder.getSafeActiveSession() ?: return
|
||||||
|
|
||||||
|
session.getInitialSyncProgressStatus()
|
||||||
|
.asObservable()
|
||||||
|
.subscribe { status ->
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
initialSyncProgressServiceStatus = status
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.disposeOnClear()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mayBeInitializeCrossSigning() {
|
||||||
|
if (args.accountCreation) {
|
||||||
|
val password = reAuthHelper.data ?: return Unit.also {
|
||||||
|
Timber.w("No password to init cross signing")
|
||||||
|
}
|
||||||
|
|
||||||
|
val session = activeSessionHolder.getSafeActiveSession() ?: return Unit.also {
|
||||||
|
Timber.w("No session to init cross signing")
|
||||||
|
}
|
||||||
|
|
||||||
|
// We do not use the viewModel context because we do not want to cancel this action
|
||||||
|
Timber.d("Initialize cross signing")
|
||||||
|
session.cryptoService().crossSigningService().initializeCrossSigning(
|
||||||
|
authParams = UserPasswordAuth(
|
||||||
|
session = null,
|
||||||
|
user = session.myUserId,
|
||||||
|
password = password
|
||||||
|
)
|
||||||
|
)
|
||||||
|
// TODO Download keys?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun handle(action: EmptyAction) {
|
override fun handle(action: EmptyAction) {
|
||||||
// NA
|
// NA
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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.riotx.features.home
|
||||||
|
|
||||||
|
import com.airbnb.mvrx.MvRxState
|
||||||
|
import im.vector.matrix.android.api.session.InitialSyncProgressService
|
||||||
|
|
||||||
|
data class HomeActivityViewState(
|
||||||
|
val initialSyncProgressServiceStatus: InitialSyncProgressService.Status = InitialSyncProgressService.Status.Idle
|
||||||
|
) : MvRxState
|
Loading…
Reference in a new issue