Create SyncOrchestrator (#4176)

* Create `SyncOrchestrator` to centralise the sync start/stop flow through the whole app: the decision is based on several inputs: sync state, network available, app in foreground, app in call, app needing to sync an event for a notification.

* Make network monitor return network connectivity status, not internet connectivity

* Don't stop the `SyncService` when network connection is lost, let it fail instead. This prevents an issue when using the offline mode of the SDK, which made the wrong UI states to be shown when the `SyncState` is `Idle` (that is, after the service being manually stopped).

* Rename `NetworkStatus.Online/Offline` to `Connected/Disconnected` so they're not easily mistaken with internet connectivity instead
This commit is contained in:
Jorge Martin Espinosa 2025-02-06 16:36:57 +01:00 committed by GitHub
parent ce1c01e626
commit 3c87fb05b2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
44 changed files with 851 additions and 344 deletions

View file

@ -8,9 +8,9 @@
package io.element.android.features.analytics.impl
import android.app.Activity
import androidx.activity.compose.LocalActivity
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
@ -34,7 +34,7 @@ class AnalyticsOptInNode @AssistedInject constructor(
@Composable
override fun View(modifier: Modifier) {
val activity = LocalContext.current as Activity
val activity = requireNotNull(LocalActivity.current)
val isDark = ElementTheme.isLightTheme.not()
val state = presenter.present()
AnalyticsOptInView(

View file

@ -40,6 +40,7 @@ dependencies {
implementation(projects.libraries.push.api)
implementation(projects.libraries.uiStrings)
implementation(projects.services.analytics.api)
implementation(projects.services.appnavstate.api)
implementation(projects.services.toolbox.api)
implementation(libs.androidx.webkit)
implementation(libs.coil.compose)
@ -59,6 +60,7 @@ dependencies {
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.push.test)
testImplementation(projects.services.analytics.test)
testImplementation(projects.services.appnavstate.test)
testImplementation(projects.tests.testutils)
testImplementation(libs.androidx.compose.ui.test.junit)
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)

View file

@ -39,6 +39,7 @@ import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
import io.element.android.libraries.network.useragent.UserAgentProvider
import io.element.android.services.analytics.api.ScreenTracker
import io.element.android.services.appnavstate.api.AppForegroundStateService
import io.element.android.services.toolbox.api.systemclock.SystemClock
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn
@ -58,9 +59,9 @@ class CallScreenPresenter @AssistedInject constructor(
private val dispatchers: CoroutineDispatchers,
private val matrixClientsProvider: MatrixClientProvider,
private val screenTracker: ScreenTracker,
private val appCoroutineScope: CoroutineScope,
private val activeCallManager: ActiveCallManager,
private val languageTagProvider: LanguageTagProvider,
private val appForegroundStateService: AppForegroundStateService,
) : Presenter<CallScreenState> {
@AssistedFactory
interface Factory {
@ -226,19 +227,13 @@ class CallScreenPresenter @AssistedInject constructor(
if (state == SyncState.Running) {
client.notifyCallStartIfNeeded(callType.roomId)
} else {
client.syncService().startSync()
appForegroundStateService.updateIsInCallState(true)
}
}
}
onDispose {
// We can't use the local coroutine scope here because it will be disposed before this effect
appCoroutineScope.launch {
client.syncService().run {
if (syncState.value == SyncState.Running) {
stopSync()
}
}
}
// Make sure we mark the call as ended in the app state
appForegroundStateService.updateIsInCallState(false)
}
}
}

View file

@ -32,10 +32,9 @@ import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
import io.element.android.libraries.network.useragent.UserAgentProvider
import io.element.android.services.analytics.api.ScreenTracker
import io.element.android.services.analytics.test.FakeScreenTracker
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
import io.element.android.services.toolbox.api.systemclock.SystemClock
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.consumeItemsUntilTimeout
import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.testCoroutineDispatchers
@ -243,7 +242,7 @@ class CallScreenPresenterTest {
}
@Test
fun `present - automatically starts the Matrix client sync when on RoomCall`() = runTest {
fun `present - automatically sets the isInCall state when starting the call and disposing the screen`() = runTest {
val navigator = FakeCallScreenNavigator()
val widgetDriver = FakeMatrixWidgetDriver()
val startSyncLambda = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
@ -251,6 +250,7 @@ class CallScreenPresenterTest {
this.startSyncLambda = startSyncLambda
}
val matrixClient = FakeMatrixClient(syncService = syncService)
val appForegroundStateService = FakeAppForegroundStateService()
val presenter = createCallScreenPresenter(
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
widgetDriver = widgetDriver,
@ -258,34 +258,7 @@ class CallScreenPresenterTest {
dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
matrixClientsProvider = FakeMatrixClientProvider(getClient = { Result.success(matrixClient) }),
screenTracker = FakeScreenTracker {},
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
consumeItemsUntilTimeout()
assert(startSyncLambda).isCalledOnce()
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - automatically stops the Matrix client sync on dispose`() = runTest {
val navigator = FakeCallScreenNavigator()
val widgetDriver = FakeMatrixWidgetDriver()
val stopSyncLambda = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
val syncService = FakeSyncService(SyncState.Running).apply {
this.stopSyncLambda = stopSyncLambda
}
val matrixClient = FakeMatrixClient(syncService = syncService)
val presenter = createCallScreenPresenter(
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
widgetDriver = widgetDriver,
navigator = navigator,
dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
matrixClientsProvider = FakeMatrixClientProvider(getClient = { Result.success(matrixClient) }),
screenTracker = FakeScreenTracker {},
appForegroundStateService = appForegroundStateService,
)
val hasRun = Mutex(true)
val job = launch {
@ -296,11 +269,25 @@ class CallScreenPresenterTest {
}
}
hasRun.lock()
appForegroundStateService.isInCall.test {
// The initial isInCall state will always be false
assertThat(awaitItem()).isFalse()
job.cancelAndJoin()
// Wait until the call starts
hasRun.lock()
assert(stopSyncLambda).isCalledOnce()
// Then it'll be true once the call is active
assertThat(awaitItem()).isTrue()
// If we dispose the screen
job.cancelAndJoin()
// The isInCall state is now false
assertThat(awaitItem()).isFalse()
// And there are no more events
ensureAllEventsConsumed()
}
}
@Test
@ -354,6 +341,7 @@ class CallScreenPresenterTest {
matrixClientsProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(),
activeCallManager: FakeActiveCallManager = FakeActiveCallManager(),
screenTracker: ScreenTracker = FakeScreenTracker(),
appForegroundStateService: FakeAppForegroundStateService = FakeAppForegroundStateService(),
): CallScreenPresenter {
val userAgentProvider = object : UserAgentProvider {
override fun provide(): String {
@ -369,10 +357,10 @@ class CallScreenPresenterTest {
clock = clock,
dispatchers = dispatchers,
matrixClientsProvider = matrixClientsProvider,
appCoroutineScope = this,
activeCallManager = activeCallManager,
screenTracker = screenTracker,
languageTagProvider = FakeLanguageTagProvider("en-US"),
appForegroundStateService = appForegroundStateService,
)
}
}

View file

@ -8,9 +8,9 @@
package io.element.android.features.createroom.impl.root
import android.app.Activity
import androidx.activity.compose.LocalActivity
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import com.bumble.appyx.core.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
@ -55,7 +55,7 @@ class CreateRoomRootNode @AssistedInject constructor(
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
val activity = LocalContext.current as Activity
val activity = requireNotNull(LocalActivity.current)
CreateRoomRootView(
state = state,
modifier = modifier,

View file

@ -94,7 +94,7 @@ class DefaultLockScreenService @Inject constructor(
*/
private fun observeAppForegroundState() {
coroutineScope.launch {
appForegroundStateService.start()
appForegroundStateService.startObservingForeground()
appForegroundStateService.isInForeground.collect { isInForeground ->
if (isInForeground) {
lockJob?.cancel()

View file

@ -7,11 +7,10 @@
package io.element.android.features.lockscreen.impl.unlock
import android.app.Activity
import androidx.activity.compose.LocalActivity
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
@ -42,7 +41,7 @@ class PinUnlockNode @AssistedInject constructor(
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
val activity = LocalContext.current as Activity
val activity = requireNotNull(LocalActivity.current)
val isDark = ElementTheme.isLightTheme.not()
LaunchedEffect(state.isUnlocked) {
if (state.isUnlocked) {

View file

@ -9,10 +9,10 @@ package io.element.android.features.login.impl
import android.app.Activity
import android.os.Parcelable
import androidx.activity.compose.LocalActivity
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.lifecycleScope
import com.bumble.appyx.core.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext
@ -199,7 +199,7 @@ class LoginFlowNode @AssistedInject constructor(
@Composable
override fun View(modifier: Modifier) {
activity = LocalContext.current as? Activity
activity = requireNotNull(LocalActivity.current)
darkTheme = !ElementTheme.isLightTheme
DisposableEffect(Unit) {
onDispose {

View file

@ -8,9 +8,9 @@
package io.element.android.features.login.impl.screens.createaccount
import android.app.Activity
import androidx.activity.compose.LocalActivity
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
@ -41,7 +41,7 @@ class CreateAccountNode @AssistedInject constructor(
@Composable
override fun View(modifier: Modifier) {
val activity = LocalContext.current as Activity
val activity = requireNotNull(LocalActivity.current)
val isDark = ElementTheme.isLightTheme.not()
val state = presenter.present()
CreateAccountView(

View file

@ -7,10 +7,9 @@
package io.element.android.features.logout.impl
import android.app.Activity
import androidx.activity.compose.LocalActivity
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
@ -42,7 +41,7 @@ class LogoutNode @AssistedInject constructor(
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
val activity = LocalContext.current as Activity
val activity = requireNotNull(LocalActivity.current)
val isDark = ElementTheme.isLightTheme.not()
LogoutView(
state = state,

View file

@ -9,6 +9,7 @@ package io.element.android.features.messages.impl
import android.app.Activity
import android.content.Context
import androidx.activity.compose.LocalActivity
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
@ -17,7 +18,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.Lifecycle
import com.bumble.appyx.core.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext
@ -223,7 +223,7 @@ class MessagesNode @AssistedInject constructor(
@Composable
override fun View(modifier: Modifier) {
val activity = LocalContext.current as Activity
val activity = requireNotNull(LocalActivity.current)
val isDark = ElementTheme.isLightTheme.not()
CompositionLocalProvider(
LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories,

View file

@ -8,6 +8,7 @@
package io.element.android.features.messages.impl.pinned
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.coroutine.mapState
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.di.SingleIn
@ -26,6 +27,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.withContext
import javax.inject.Inject
@SingleIn(RoomScope::class)
@ -33,6 +35,7 @@ class PinnedEventsTimelineProvider @Inject constructor(
private val room: MatrixRoom,
private val syncService: SyncService,
private val featureFlagService: FeatureFlagService,
private val dispatchers: CoroutineDispatchers,
) : TimelineProvider {
private val _timelineStateFlow: MutableStateFlow<AsyncData<Timeline>> =
MutableStateFlow(AsyncData.Uninitialized)
@ -100,7 +103,9 @@ class PinnedEventsTimelineProvider @Inject constructor(
when (timelineStateFlow.value) {
is AsyncData.Uninitialized, is AsyncData.Failure -> {
timelineStateFlow.emit(AsyncData.Loading())
room.pinnedEventsTimeline()
withContext(dispatchers.io) {
room.pinnedEventsTimeline()
}
.fold(
{ timelineStateFlow.emit(AsyncData.Success(it)) },
{ timelineStateFlow.emit(AsyncData.Failure(it)) }

View file

@ -194,7 +194,8 @@ class PinnedMessagesBannerPresenterTest {
syncService = syncService,
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.PinnedEvents.key to isFeatureEnabled)
)
),
dispatchers = testCoroutineDispatchers(),
)
timelineProvider.launchIn(backgroundScope)

View file

@ -38,6 +38,7 @@ import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.TestScope
@ -302,7 +303,8 @@ class PinnedMessagesListPresenterTest {
syncService = syncService,
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.PinnedEvents.key to isFeatureEnabled)
)
),
dispatchers = testCoroutineDispatchers(),
)
timelineProvider.launchIn(backgroundScope)
return PinnedMessagesListPresenter(

View file

@ -9,6 +9,14 @@ package io.element.android.features.networkmonitor.api
import kotlinx.coroutines.flow.StateFlow
/**
* Monitors the network status of the device, providing the current network connectivity status as a flow.
*
* **Note:** network connectivity does not imply internet connectivity. The device can be connected to a network that can't reach the homeserver.
*/
interface NetworkMonitor {
/**
* A flow containing the current network connectivity status.
*/
val connectivity: StateFlow<NetworkStatus>
}

View file

@ -7,7 +7,19 @@
package io.element.android.features.networkmonitor.api
/**
* Network connectivity status of the device.
*
* **Note:** this is *network* connectivity status, not *internet* connectivity status.
*/
enum class NetworkStatus {
Online,
Offline
/**
* The device is connected to a network.
*/
Connected,
/**
* The device is not connected to any networks.
*/
Disconnected
}

View file

@ -12,7 +12,6 @@ package io.element.android.features.networkmonitor.impl
import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.networkmonitor.api.NetworkMonitor
@ -55,20 +54,18 @@ class DefaultNetworkMonitor @Inject constructor(
override fun onLost(network: Network) {
if (activeNetworksCount.decrementAndGet() == 0) {
trySendBlocking(NetworkStatus.Offline)
trySendBlocking(NetworkStatus.Disconnected)
}
}
override fun onAvailable(network: Network) {
if (activeNetworksCount.incrementAndGet() > 0) {
trySendBlocking(NetworkStatus.Online)
trySendBlocking(NetworkStatus.Connected)
}
}
}
trySendBlocking(connectivityManager.activeNetworkStatus())
val request = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
.build()
val request = NetworkRequest.Builder().build()
connectivityManager.registerNetworkCallback(request, callback)
Timber.d("Subscribe")
@ -85,17 +82,6 @@ class DefaultNetworkMonitor @Inject constructor(
.stateIn(appCoroutineScope, SharingStarted.WhileSubscribed(), connectivityManager.activeNetworkStatus())
private fun ConnectivityManager.activeNetworkStatus(): NetworkStatus {
return activeNetwork?.let {
getNetworkCapabilities(it)?.getNetworkStatus()
} ?: NetworkStatus.Offline
}
private fun NetworkCapabilities.getNetworkStatus(): NetworkStatus {
val hasInternet = hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
return if (hasInternet) {
NetworkStatus.Online
} else {
NetworkStatus.Offline
}
return if (activeNetwork != null) NetworkStatus.Connected else NetworkStatus.Disconnected
}
}

View file

@ -11,6 +11,6 @@ import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus
import kotlinx.coroutines.flow.MutableStateFlow
class FakeNetworkMonitor(initialStatus: NetworkStatus = NetworkStatus.Online) : NetworkMonitor {
class FakeNetworkMonitor(initialStatus: NetworkStatus = NetworkStatus.Connected) : NetworkMonitor {
override val connectivity = MutableStateFlow(initialStatus)
}

View file

@ -8,9 +8,9 @@
package io.element.android.features.preferences.impl.about
import android.app.Activity
import androidx.activity.compose.LocalActivity
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
@ -41,7 +41,7 @@ class AboutNode @AssistedInject constructor(
@Composable
override fun View(modifier: Modifier) {
val activity = LocalContext.current as Activity
val activity = requireNotNull(LocalActivity.current)
val isDark = ElementTheme.isLightTheme.not()
val state = presenter.present()
AboutView(

View file

@ -7,10 +7,9 @@
package io.element.android.features.preferences.impl.developer
import android.app.Activity
import androidx.activity.compose.LocalActivity
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import com.airbnb.android.showkase.models.Showkase
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
@ -29,7 +28,7 @@ class DeveloperSettingsNode @AssistedInject constructor(
) : Node(buildContext, plugins = plugins) {
@Composable
override fun View(modifier: Modifier) {
val activity = LocalContext.current as Activity
val activity = requireNotNull(LocalActivity.current)
fun openShowkase() {
val intent = Showkase.getBrowserIntent(activity)
activity.startActivity(intent)

View file

@ -79,10 +79,10 @@ class DeveloperSettingsPresenter @Inject constructor(
.doesHideImagesAndVideosFlow()
.collectAsState(initial = false)
val tracingLogLevel by appPreferencesStore
.getTracingLogLevelFlow()
.map { AsyncData.Success(it.toLogLevelItem()) }
.collectAsState(initial = AsyncData.Uninitialized)
val tracingLogLevelFlow = remember {
appPreferencesStore.getTracingLogLevelFlow().map { AsyncData.Success(it.toLogLevelItem()) }
}
val tracingLogLevel by tracingLogLevelFlow.collectAsState(initial = AsyncData.Uninitialized)
LaunchedEffect(Unit) {
FeatureFlags.entries

View file

@ -8,9 +8,9 @@
package io.element.android.features.preferences.impl.root
import android.app.Activity
import androidx.activity.compose.LocalActivity
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
@ -113,7 +113,7 @@ class PreferencesRootNode @AssistedInject constructor(
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
val activity = LocalContext.current as Activity
val activity = requireNotNull(LocalActivity.current)
val isDark = ElementTheme.isLightTheme.not()
PreferencesRootView(
state = state,

View file

@ -7,10 +7,9 @@
package io.element.android.features.rageshake.impl.bugreport
import android.app.Activity
import androidx.activity.compose.LocalActivity
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
@ -38,7 +37,7 @@ class BugReportNode @AssistedInject constructor(
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
val activity = LocalContext.current as? Activity
val activity = LocalActivity.current
BugReportView(
state = state,
modifier = modifier,

View file

@ -8,9 +8,9 @@
package io.element.android.features.roomlist.impl
import android.app.Activity
import androidx.activity.compose.LocalActivity
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import com.bumble.appyx.core.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
@ -92,7 +92,7 @@ class RoomListNode @AssistedInject constructor(
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
val activity = LocalContext.current as Activity
val activity = requireNotNull(LocalActivity.current)
RoomListView(
state = state,

View file

@ -9,11 +9,11 @@ package io.element.android.features.securebackup.impl.reset
import android.app.Activity
import android.os.Parcelable
import androidx.activity.compose.LocalActivity
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
@ -160,7 +160,7 @@ class ResetIdentityFlowNode @AssistedInject constructor(
override fun View(modifier: Modifier) {
// Workaround to get the current activity
if (!this::activity.isInitialized) {
activity = LocalContext.current as Activity
activity = requireNotNull(LocalActivity.current)
}
val startResetState by resetIdentityFlowManager.currentHandleFlow.collectAsState()

View file

@ -8,9 +8,9 @@
package io.element.android.features.verifysession.impl.outgoing
import android.app.Activity
import androidx.activity.compose.LocalActivity
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
@ -45,7 +45,7 @@ class VerifySelfSessionNode @AssistedInject constructor(
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
val activity = LocalContext.current as Activity
val activity = requireNotNull(LocalActivity.current)
val isDark = ElementTheme.isLightTheme.not()
VerifySelfSessionView(
state = state,