mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 12:07:28 +00:00
Keep the incoming share displayed when the current account is signed out
The incoming share was displayed in the logged in flow of the current session, so it was destroyed when the current session was signed out, for instance from another device. When another session was signed out, the room select screen was kept and displayed an error if this session was selected. To make the behavior consistent: - The incoming share is now displayed by the RootFlowNode, on top of the logged in flow, and does not depend on a session anymore. The latest used session is passed from the RootFlowNode to the room select screen, through the share flow, and is used as its initial session. - When the root of the RootFlowNode backstack changes while an incoming share is displayed, the new root is set below the incoming share, using the new replaceRoot backstack operation, instead of removing all the elements. - The room select screen handles the invalidation of the current session like for any other session. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+28
-22
@@ -65,17 +65,19 @@ class RoomSelectPresenter(
|
||||
val queryState = rememberTextFieldState()
|
||||
var isSearchActive by remember { mutableStateOf(false) }
|
||||
var isAccountListExpanded by remember { mutableStateOf(false) }
|
||||
// Null until the sessions are loaded
|
||||
val sessions by remember { sessionStore.sessionsFlow() }.collectAsState(initial = null)
|
||||
var selectedSessionId by remember { mutableStateOf(initialSessionId) }
|
||||
val sessions by remember { sessionStore.sessionsFlow() }.collectAsState(initial = emptyList())
|
||||
// All the accounts are proposed, even the ones with an invalid token. Selecting them displays an error.
|
||||
val accounts by remember {
|
||||
derivedStateOf { sessions.map { it.toMatrixUser() } }
|
||||
derivedStateOf { sessions.orEmpty().map { it.toMatrixUser() } }
|
||||
}
|
||||
// The selected session can have an invalid token, or be invalidated while this screen is displayed, for instance if it is
|
||||
// signed out from another device. The invalidation of the initial session, which is the current one, is handled by the application.
|
||||
// signed out from another device.
|
||||
val isSelectedSessionInvalid by remember {
|
||||
derivedStateOf {
|
||||
selectedSessionId != initialSessionId && sessions.none { it.userId == selectedSessionId.value && it.isTokenValid }
|
||||
val loadedSessions = sessions ?: return@derivedStateOf false
|
||||
loadedSessions.none { it.userId == selectedSessionId.value && it.isTokenValid }
|
||||
}
|
||||
}
|
||||
LaunchedEffect(isSelectedSessionInvalid) {
|
||||
@@ -99,27 +101,31 @@ class RoomSelectPresenter(
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
// Create one data source per session. When the selected session changes, the previous
|
||||
// data source is removed from the composition and its coroutine scope is cancelled.
|
||||
val (selectedSessionDataSource, hasClientError) = key(selectedSessionId) {
|
||||
val dataSourceCoroutineScope = rememberCoroutineScope()
|
||||
// The sessions are restored at startup, so the client should already be in memory.
|
||||
// A null value means that the client is being restored, or that the session is invalid: its client cannot be restored,
|
||||
// and an in-memory client would have been destroyed, so do not try to get it.
|
||||
val initialRoomListService = if (isSelectedSessionInvalid) null else matrixClientProvider.getOrNull(selectedSessionId)?.roomListService
|
||||
val roomListServiceResult by produceState(initialRoomListService?.let { Result.success(it) }) {
|
||||
if (value == null && !isSelectedSessionInvalid) {
|
||||
value = matrixClientProvider.getOrRestore(selectedSessionId)
|
||||
.onFailure { Timber.e(it, "Failed to get the client for $selectedSessionId") }
|
||||
.map { it.roomListService }
|
||||
// Also use the validity of the session as a key, so that the data source is removed if the session is invalidated, and recreated
|
||||
// if the session becomes valid again.
|
||||
val (dataSource, hasClientError) = key(selectedSessionId, isSelectedSessionInvalid) {
|
||||
val sessionId = selectedSessionId
|
||||
if (isSelectedSessionInvalid) {
|
||||
// Do not try to get the client of an invalid session: it cannot be restored, and an in-memory client would have been destroyed.
|
||||
null to false
|
||||
} else {
|
||||
val dataSourceCoroutineScope = rememberCoroutineScope()
|
||||
// The sessions are restored at startup, so the client should already be in memory.
|
||||
// A null value means that the client is being restored.
|
||||
val roomListServiceResult by produceState(matrixClientProvider.getOrNull(sessionId)?.roomListService?.let { Result.success(it) }) {
|
||||
if (value == null) {
|
||||
value = matrixClientProvider.getOrRestore(sessionId)
|
||||
.onFailure { Timber.e(it, "Failed to get the client for $sessionId") }
|
||||
.map { it.roomListService }
|
||||
}
|
||||
}
|
||||
val roomListService = roomListServiceResult?.getOrNull()
|
||||
val dataSource = remember(roomListService) {
|
||||
roomListService?.let { dataSourceFactory.create(dataSourceCoroutineScope, it) }
|
||||
}
|
||||
dataSource to (roomListServiceResult?.isFailure == true)
|
||||
}
|
||||
val roomListService = roomListServiceResult?.getOrNull()
|
||||
val dataSource = remember(roomListService) {
|
||||
roomListService?.let { dataSourceFactory.create(dataSourceCoroutineScope, it) }
|
||||
}
|
||||
dataSource to (roomListServiceResult?.isFailure == true)
|
||||
}
|
||||
// Do not use the data source of an invalid session, its client has been destroyed
|
||||
val dataSource = selectedSessionDataSource.takeIf { !isSelectedSessionInvalid }
|
||||
val hasRoomListError = hasClientError || isSelectedSessionInvalid
|
||||
|
||||
val searchQuery = queryState.text.toString()
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ class DefaultRoomSelectEntryPointTest : RobolectricTest() {
|
||||
assertThat(initialSessionId).isEqualTo(A_SESSION_ID)
|
||||
assertThat(mode).isEqualTo(testMode)
|
||||
assertThat(maxNumberOfRooms).isEqualTo(testMaxNumberOfRooms)
|
||||
createRoomSelectPresenter(mode, maxNumberOfRooms)
|
||||
createRoomSelectPresenter(mode = mode, maxNumberOfRooms = maxNumberOfRooms)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
+32
-3
@@ -13,6 +13,7 @@ import app.cash.turbine.Event
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
@@ -113,7 +114,7 @@ class RoomSelectPresenterTest {
|
||||
val presenter = createRoomSelectPresenter(
|
||||
mode = RoomSelectMode.Share,
|
||||
sessionStore = InMemorySessionStore(
|
||||
initialList = listOf(aSessionData(sessionId = A_SESSION_ID.value)),
|
||||
initialList = listOf(aSessionData(sessionId = A_SESSION_ID.value, isTokenValid = true)),
|
||||
),
|
||||
)
|
||||
presenter.test {
|
||||
@@ -208,6 +209,30 @@ class RoomSelectPresenterTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - the current account is invalidated while selected, an error is displayed`() = runTest {
|
||||
val roomSummary = aRoomSummary()
|
||||
val sessionStore = aSessionStoreWithTwoAccounts()
|
||||
val presenter = createRoomSelectPresenter(
|
||||
mode = RoomSelectMode.Share,
|
||||
sessionStore = sessionStore,
|
||||
roomListService = FakeRoomListService(createRoomListLambda = { FakeDynamicRoomList(summaries = MutableStateFlow(listOf(roomSummary))) }),
|
||||
)
|
||||
presenter.test {
|
||||
val state = consumeItemsUntilPredicate { it.resultState is SearchBarResultState.Results }.last()
|
||||
state.eventSink(RoomSelectEvent.ToggleSelectedRoom(roomSummary.toSelectRoomInfo()))
|
||||
assertThat(consumeItemsUntilPredicate { it.selectedRooms.isNotEmpty() }.last().selectedRooms).hasSize(1)
|
||||
// The current session is signed out from another device while the screen is displayed
|
||||
sessionStore.updateData(sessionStore.getSession(A_SESSION_ID.value)!!.copy(isTokenValid = false))
|
||||
val errorState = consumeItemsUntilPredicate { it.hasRoomListError && it.selectedRooms.isEmpty() }.last()
|
||||
assertThat(errorState.selectedAccount.userId).isEqualTo(A_SESSION_ID)
|
||||
assertThat(errorState.resultState).isInstanceOf(SearchBarResultState.Initial::class.java)
|
||||
// The user can switch to the other account
|
||||
assertThat(errorState.otherAccounts.map { it.userId }).containsExactly(A_SESSION_ID_2)
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - another account is invalidated, it is still proposed and there is no error`() = runTest {
|
||||
val sessionStore = aSessionStoreWithTwoAccounts()
|
||||
@@ -389,15 +414,19 @@ class RoomSelectPresenterTest {
|
||||
}
|
||||
|
||||
internal fun TestScope.createRoomSelectPresenter(
|
||||
initialSessionId: SessionId = A_SESSION_ID,
|
||||
mode: RoomSelectMode = RoomSelectMode.Forward,
|
||||
maxNumberOfRooms: Int = RoomSelectEntryPoint.DEFAULT_MAX_NUMBER_OF_ROOMS,
|
||||
roomListService: RoomListService = FakeRoomListService(),
|
||||
sessionStore: SessionStore = InMemorySessionStore(),
|
||||
// The current session is always in the store
|
||||
sessionStore: SessionStore = InMemorySessionStore(
|
||||
initialList = listOf(aSessionData(sessionId = A_SESSION_ID.value, isTokenValid = true)),
|
||||
),
|
||||
matrixClientProvider: MatrixClientProvider = FakeMatrixClientProvider { sessionId ->
|
||||
Result.success(FakeMatrixClient(sessionId = sessionId, roomListService = roomListService))
|
||||
},
|
||||
) = RoomSelectPresenter(
|
||||
initialSessionId = A_SESSION_ID,
|
||||
initialSessionId = initialSessionId,
|
||||
mode = mode,
|
||||
maxNumberOfRooms = maxNumberOfRooms,
|
||||
dataSourceFactory = object : RoomSelectSearchDataSource.Factory {
|
||||
|
||||
Reference in New Issue
Block a user