Refresh Element Classic state each time ClassicFlowNode is resumed.
This ensure that Element X is always up to date regarding Element Classic state.
This commit is contained in:
parent
76de9db94e
commit
fd3c4c2b2b
14 changed files with 30 additions and 119 deletions
|
|
@ -43,7 +43,6 @@ interface ElementClassicConnection {
|
|||
fun start()
|
||||
fun stop()
|
||||
fun requestSession()
|
||||
fun requestAvatar(userId: UserId)
|
||||
val stateFlow: StateFlow<ElementClassicConnectionState>
|
||||
}
|
||||
|
||||
|
|
@ -174,7 +173,7 @@ class DefaultElementClassicConnection(
|
|||
}
|
||||
}
|
||||
|
||||
override fun requestAvatar(userId: UserId) {
|
||||
private fun requestAvatar(userId: UserId) {
|
||||
Timber.tag(loggerTag.value).d("requestAvatar()")
|
||||
coroutineScope.launch {
|
||||
val finalMessenger = messenger
|
||||
|
|
@ -225,6 +224,11 @@ class DefaultElementClassicConnection(
|
|||
coroutineScope.launch {
|
||||
val updatedState = ensureHomeserverIsSupported(state)
|
||||
emitState(updatedState)
|
||||
val userId = (updatedState as? ElementClassicConnectionState.ElementClassicReady)?.elementClassicSession?.userId
|
||||
if (userId != null) {
|
||||
// Step 2, request the avatar
|
||||
requestAvatar(userId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -241,6 +245,9 @@ class DefaultElementClassicConnection(
|
|||
)
|
||||
} else {
|
||||
val avatar = BundleCompat.getParcelable(data, KEY_USER_AVATAR_PARCELABLE, Bitmap::class.java)
|
||||
// If the avatar is identical to the current one, do not emit a new state to avoid unnecessary recompositions
|
||||
// and blink on the avatar image
|
||||
if (avatar == null || !avatar.sameAs(currentState.avatar)) {
|
||||
val updatedState = currentState.copy(
|
||||
avatar = avatar,
|
||||
)
|
||||
|
|
@ -248,6 +255,7 @@ class DefaultElementClassicConnection(
|
|||
emitState(updatedState)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Timber.tag(loggerTag.value).w("Received profile data but current state is not ElementClassicReady: %s", currentState)
|
||||
}
|
||||
|
|
@ -343,6 +351,10 @@ class DefaultElementClassicConnection(
|
|||
append(doesContainBackupKey)
|
||||
}
|
||||
)
|
||||
// Ensure avatar is not lost when refreshing the data
|
||||
val currentAvatar = (stateFlow.value as? ElementClassicConnectionState.ElementClassicReady)
|
||||
?.takeIf { it.elementClassicSession.userId == userId }
|
||||
?.avatar
|
||||
ElementClassicConnectionState.ElementClassicReady(
|
||||
elementClassicSession = ElementClassicSession(
|
||||
userId = userId,
|
||||
|
|
@ -352,7 +364,7 @@ class DefaultElementClassicConnection(
|
|||
doesContainBackupKey = doesContainBackupKey,
|
||||
),
|
||||
displayName = displayName,
|
||||
avatar = null,
|
||||
avatar = currentAvatar,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import android.os.Parcelable
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.bumble.appyx.core.lifecycle.subscribe
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
|
|
@ -75,6 +76,11 @@ class ClassicFlowNode(
|
|||
override fun onBuilt() {
|
||||
super.onBuilt()
|
||||
observeElementClassicConnection()
|
||||
lifecycle.subscribe(
|
||||
onResume = {
|
||||
classicFlowNodeHelper.onResume()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun observeElementClassicConnection() {
|
||||
|
|
|
|||
|
|
@ -26,6 +26,10 @@ class ClassicFlowNodeHelper(
|
|||
private val elementClassicConnection: ElementClassicConnection,
|
||||
private val sessionStore: SessionStore,
|
||||
) {
|
||||
fun onResume() {
|
||||
elementClassicConnection.requestSession()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun navigationEventFlow(): Flow<NavigationEvent> {
|
||||
return elementClassicConnection.stateFlow
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
package io.element.android.features.login.impl.screens.classic.loginwithclassic
|
||||
|
||||
sealed interface LoginWithClassicEvent {
|
||||
data object RefreshData : LoginWithClassicEvent
|
||||
data object Submit : LoginWithClassicEvent
|
||||
data object ClearError : LoginWithClassicEvent
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,13 +56,6 @@ class LoginWithClassicPresenter(
|
|||
|
||||
fun handleEvent(event: LoginWithClassicEvent) {
|
||||
when (event) {
|
||||
LoginWithClassicEvent.RefreshData -> {
|
||||
// Request the avatar if not known yet
|
||||
val currentState = elementClassicConnection.stateFlow.value
|
||||
if ((currentState as? ElementClassicConnectionState.ElementClassicReady)?.avatar == null) {
|
||||
elementClassicConnection.requestAvatar(userId)
|
||||
}
|
||||
}
|
||||
LoginWithClassicEvent.Submit -> {
|
||||
val currentState = elementClassicConnection.stateFlow.value
|
||||
if (currentState is ElementClassicConnectionState.ElementClassicReady) {
|
||||
|
|
|
|||
|
|
@ -33,8 +33,6 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.compose.LifecycleEventEffect
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.login.impl.R
|
||||
import io.element.android.features.login.impl.login.LoginModeView
|
||||
|
|
@ -67,10 +65,6 @@ fun LoginWithClassicView(
|
|||
onCreateAccountContinue: (url: String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LifecycleEventEffect(Lifecycle.Event.ON_RESUME) {
|
||||
state.eventSink(LoginWithClassicEvent.RefreshData)
|
||||
}
|
||||
|
||||
val isLoading by remember(state.loginMode) {
|
||||
derivedStateOf {
|
||||
state.loginMode is AsyncData.Loading
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.screens.classic.missingkeybackup
|
||||
|
||||
sealed interface MissingKeyBackupEvent {
|
||||
data object OnResume : MissingKeyBackupEvent
|
||||
}
|
||||
|
|
@ -8,38 +8,18 @@
|
|||
package io.element.android.features.login.impl.screens.classic.missingkeybackup
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.features.login.impl.classic.ElementClassicConnection
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
|
||||
@Inject
|
||||
class MissingKeyBackupPresenter(
|
||||
private val buildMeta: BuildMeta,
|
||||
private val elementClassicConnection: ElementClassicConnection,
|
||||
) : Presenter<MissingKeyBackupState> {
|
||||
@Composable
|
||||
override fun present(): MissingKeyBackupState {
|
||||
var resumeCounter by remember { mutableIntStateOf(0) }
|
||||
fun handleEvent(event: MissingKeyBackupEvent) {
|
||||
when (event) {
|
||||
MissingKeyBackupEvent.OnResume -> {
|
||||
resumeCounter++
|
||||
if (resumeCounter > 1) {
|
||||
// The user has returned to this screen, we can assume they have gone to the backup flow and are now back here
|
||||
elementClassicConnection.requestSession()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return MissingKeyBackupState(
|
||||
appName = buildMeta.applicationName,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,5 +9,4 @@ package io.element.android.features.login.impl.screens.classic.missingkeybackup
|
|||
|
||||
data class MissingKeyBackupState(
|
||||
val appName: String,
|
||||
val eventSink: (MissingKeyBackupEvent) -> Unit
|
||||
)
|
||||
|
|
|
|||
|
|
@ -19,8 +19,6 @@ open class MissingKeyBackupStateProvider : PreviewParameterProvider<MissingKeyBa
|
|||
|
||||
fun aMissingKeyBackupState(
|
||||
appName: String = "AppName",
|
||||
eventSink: (MissingKeyBackupEvent) -> Unit = {},
|
||||
) = MissingKeyBackupState(
|
||||
appName = appName,
|
||||
eventSink = eventSink
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.login.impl.R
|
||||
import io.element.android.libraries.designsystem.atomic.organisms.NumberedListOrganism
|
||||
|
|
@ -25,7 +24,6 @@ import io.element.android.libraries.designsystem.components.BigIcon
|
|||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Button
|
||||
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Composable
|
||||
|
|
@ -35,11 +33,6 @@ fun MissingKeyBackupView(
|
|||
onOpenClassicClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
OnLifecycleEvent { _, event ->
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
state.eventSink.invoke(MissingKeyBackupEvent.OnResume)
|
||||
}
|
||||
}
|
||||
FlowStepPage(
|
||||
modifier = modifier,
|
||||
onBackClick = onBackClick,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
package io.element.android.features.login.impl.classic
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
|
@ -17,13 +16,11 @@ class FakeElementClassicConnection(
|
|||
private val startResult: () -> Unit = { lambdaError() },
|
||||
private val stopResult: () -> Unit = { lambdaError() },
|
||||
private val requestSessionResult: () -> Unit = { lambdaError() },
|
||||
private val requestAvatarResult: (UserId) -> Unit = { lambdaError() },
|
||||
initialState: ElementClassicConnectionState = ElementClassicConnectionState.Idle
|
||||
) : ElementClassicConnection {
|
||||
override fun start() = startResult()
|
||||
override fun stop() = stopResult()
|
||||
override fun requestSession() = requestSessionResult()
|
||||
override fun requestAvatar(userId: UserId) = requestAvatarResult(userId)
|
||||
private val mutableStateFlow = MutableStateFlow(initialState)
|
||||
override val stateFlow: StateFlow<ElementClassicConnectionState> = mutableStateFlow.asStateFlow()
|
||||
suspend fun emitState(state: ElementClassicConnectionState) {
|
||||
|
|
|
|||
|
|
@ -57,36 +57,6 @@ class LoginWithClassicPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - refresh data invokes the expected methods`() = runTest {
|
||||
val requestAvatarResult = lambdaRecorder<UserId, Unit> { }
|
||||
val elementClassicConnection = FakeElementClassicConnection(
|
||||
startResult = {},
|
||||
requestAvatarResult = requestAvatarResult,
|
||||
)
|
||||
val presenter = createPresenter(
|
||||
elementClassicConnection = elementClassicConnection,
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
elementClassicConnection.emitState(
|
||||
anElementClassicReady(
|
||||
elementClassicSession = anElementClassicSession(
|
||||
userId = A_USER_ID,
|
||||
secrets = A_SECRET,
|
||||
roomKeysVersion = ROOM_KEYS_VERSION,
|
||||
),
|
||||
displayName = A_USER_NAME,
|
||||
)
|
||||
)
|
||||
val readyState = awaitItem()
|
||||
assertThat(readyState.userId).isEqualTo(A_USER_ID)
|
||||
assertThat(readyState.displayName).isEqualTo(A_USER_NAME)
|
||||
readyState.eventSink(LoginWithClassicEvent.RefreshData)
|
||||
requestAvatarResult.assertions().isCalledOnce()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - start login with correct state - user can login`() = runTest {
|
||||
val authenticationService = FakeMatrixAuthenticationService(
|
||||
|
|
|
|||
|
|
@ -8,12 +8,9 @@
|
|||
package io.element.android.features.login.impl.screens.classic.missingkeybackup
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.login.impl.classic.ElementClassicConnection
|
||||
import io.element.android.features.login.impl.classic.FakeElementClassicConnection
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.matrix.test.AN_APPLICATION_NAME
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.test
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
|
@ -27,29 +24,10 @@ class MissingKeyBackupPresenterTest {
|
|||
assertThat(initialState.appName).isEqualTo(AN_APPLICATION_NAME)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - when the screen is resumed twice, the start over method is called`() = runTest {
|
||||
val requestSessionResult = lambdaRecorder<Unit> { }
|
||||
val presenter = createPresenter(
|
||||
elementClassicConnection = FakeElementClassicConnection(
|
||||
requestSessionResult = requestSessionResult,
|
||||
),
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(MissingKeyBackupEvent.OnResume)
|
||||
expectNoEvents()
|
||||
initialState.eventSink(MissingKeyBackupEvent.OnResume)
|
||||
requestSessionResult.assertions().isCalledOnce()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createPresenter(
|
||||
buildMeta: BuildMeta = aBuildMeta(applicationName = AN_APPLICATION_NAME),
|
||||
elementClassicConnection: ElementClassicConnection = FakeElementClassicConnection(),
|
||||
) = MissingKeyBackupPresenter(
|
||||
buildMeta = buildMeta,
|
||||
elementClassicConnection = elementClassicConnection,
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue