From 526bc27a08781dccbec28dff3f83d2c0b289205c Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 29 Sep 2025 20:38:55 +0200 Subject: [PATCH] feature (space) : manage failures to join in Space screen --- .../features/space/impl/root/SpaceEvents.kt | 1 + .../space/impl/root/SpacePresenter.kt | 36 ++++++++------- .../features/space/impl/root/SpaceState.kt | 11 ++++- .../space/impl/root/SpaceStateProvider.kt | 6 ++- .../features/space/impl/root/SpaceView.kt | 44 +++++++++++++++---- 5 files changed, 70 insertions(+), 28 deletions(-) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceEvents.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceEvents.kt index 97bffcb7d2..ece17889a0 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceEvents.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceEvents.kt @@ -12,4 +12,5 @@ import io.element.android.libraries.matrix.api.spaces.SpaceRoom sealed interface SpaceEvents { data object LoadMore : SpaceEvents data class Join(val spaceRoom: SpaceRoom): SpaceEvents + data object ClearFailures : SpaceEvents } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt index b322501c25..c709e9e5f3 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt @@ -17,10 +17,11 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedFactory -import im.vector.app.features.analytics.plan.JoinedRoom import dev.zacsweers.metro.AssistedInject +import im.vector.app.features.analytics.plan.JoinedRoom import io.element.android.features.invite.api.SeenInvitesStore import io.element.android.features.space.api.SpaceEntryPoint +import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.mapState import io.element.android.libraries.di.annotations.SessionCoroutineScope @@ -34,14 +35,14 @@ import io.element.android.libraries.matrix.api.spaces.SpaceRoomList import io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.toPersistentList +import kotlinx.collections.immutable.toPersistentMap import kotlinx.collections.immutable.toPersistentSet import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlin.jvm.optionals.getOrNull -@AssistedInject -class SpacePresenter( +@AssistedInject class SpacePresenter( @Assisted private val inputs: SpaceEntryPoint.Inputs, private val client: MatrixClient, private val seenInvitesStore: SeenInvitesStore, @@ -76,15 +77,12 @@ class SpacePresenter( }.collectAsState() val currentSpace by spaceRoomList.currentSpaceFlow.collectAsState() - val joiningRooms = remember { mutableStateOf(emptySet()) } + val joinActions = remember { mutableStateOf(emptyMap>()) } - LaunchedEffect(children, joiningRooms.value) { - val joinedChildren = children - .filter { it.state == CurrentUserMembership.JOINED } - .map { it.roomId } - .toSet() - joiningRooms.value.let { currentlyJoining -> - joiningRooms.value = currentlyJoining - joinedChildren + LaunchedEffect(children) { + val joinedChildren = children.filter { it.state == CurrentUserMembership.JOINED }.map { it.roomId }.toSet() + joinActions.value.let { currentlyJoining -> + joinActions.value = currentlyJoining - joinedChildren } } @@ -92,7 +90,13 @@ class SpacePresenter( when (event) { SpaceEvents.LoadMore -> localCoroutineScope.paginate() is SpaceEvents.Join -> { - sessionCoroutineScope.joinRoom(event.spaceRoom, joiningRooms) + sessionCoroutineScope.joinRoom(event.spaceRoom, joinActions) + } + SpaceEvents.ClearFailures -> { + val failedActions = joinActions.value + .filterValues { it is AsyncAction.Failure } + .mapValues { AsyncAction.Uninitialized } + joinActions.value = joinActions.value + failedActions } } } @@ -102,21 +106,21 @@ class SpacePresenter( seenSpaceInvites = seenSpaceInvites, hideInvitesAvatar = hideInvitesAvatar, hasMoreToLoad = hasMoreToLoad, - joiningRooms = joiningRooms.value.toPersistentSet(), + joinActions = joinActions.value.toPersistentMap(), eventSink = ::handleEvents, ) } private fun CoroutineScope.joinRoom( - spaceRoom: SpaceRoom, joiningRooms: MutableState> + spaceRoom: SpaceRoom, joiningRooms: MutableState>> ) = launch { - joiningRooms.value = joiningRooms.value + spaceRoom.roomId + joiningRooms.value = joiningRooms.value + mapOf(spaceRoom.roomId to AsyncAction.Loading) joinRoom.invoke( roomIdOrAlias = spaceRoom.roomId.toRoomIdOrAlias(), serverNames = spaceRoom.via, trigger = JoinedRoom.Trigger.SpaceHierarchy, ).onFailure { - joiningRooms.value = joiningRooms.value - spaceRoom.roomId + joiningRooms.value = joiningRooms.value + mapOf(spaceRoom.roomId to AsyncAction.Failure(it)) } } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt index acba9ab03f..5a629f94dd 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt @@ -7,9 +7,11 @@ package io.element.android.features.space.impl.root +import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.spaces.SpaceRoom import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.ImmutableSet data class SpaceState( @@ -18,6 +20,11 @@ data class SpaceState( val seenSpaceInvites: ImmutableSet, val hideInvitesAvatar: Boolean, val hasMoreToLoad: Boolean, - val joiningRooms: ImmutableSet, + val joinActions: ImmutableMap>, val eventSink: (SpaceEvents) -> Unit -) +) { + fun isJoining(spaceId: RoomId): Boolean = joinActions[spaceId] == AsyncAction.Loading + val hasAnyFailure: Boolean = joinActions.values.any { + it is AsyncAction.Failure + } +} diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt index 41a1e80f94..ac2327c8a7 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt @@ -5,14 +5,16 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.space.impl +package io.element.android.features.space.impl.root import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.spaces.SpaceRoom import io.element.android.libraries.previewutils.room.aSpaceRoom import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toImmutableMap import kotlinx.collections.immutable.toImmutableSet open class SpaceStateProvider : PreviewParameterProvider { @@ -56,7 +58,7 @@ fun aSpaceState( seenSpaceInvites = seenSpaceInvites.toImmutableSet(), hideInvitesAvatar = hideInvitesAvatar, hasMoreToLoad = hasMoreToLoad, - joiningRooms = joiningRooms.toImmutableSet(), + joinActions = joiningRooms.associateWith { AsyncAction.Uninitialized }.toImmutableMap(), eventSink = {}) private fun aListOfSpaceRooms(): List { diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt index 860f73f318..c8000d84d6 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.LocalContentColor @@ -34,6 +35,9 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.libraries.designsystem.components.async.AsyncIndicator +import io.element.android.libraries.designsystem.components.async.AsyncIndicatorHost +import io.element.android.libraries.designsystem.components.async.rememberAsyncIndicatorState import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize @@ -58,22 +62,22 @@ import io.element.android.libraries.matrix.ui.components.SpaceRoomItemView import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.delay @Composable fun SpaceView( state: SpaceState, onBackClick: () -> Unit, - onLeaveSpaceClick: () -> Unit, onRoomClick: (spaceRoom: SpaceRoom) -> Unit, onShareSpace: () -> Unit, + onLeaveSpaceClick: () -> Unit, modifier: Modifier = Modifier, ) { Scaffold( modifier = modifier, topBar = { SpaceViewTopBar( - state = state, - onBackClick = onBackClick, + currentSpace = state.currentSpace, onBackClick = onBackClick, onLeaveSpaceClick = onLeaveSpaceClick, onShareSpace = onShareSpace, ) @@ -89,6 +93,30 @@ fun SpaceView( } }, ) + JoinRoomFailureEffect( + hasAnyFailure = state.hasAnyFailure, + eventSink = state.eventSink + ) +} + +@Composable +private fun JoinRoomFailureEffect( + hasAnyFailure: Boolean, + eventSink: (SpaceEvents) -> Unit, +) { + val asyncIndicatorState = rememberAsyncIndicatorState() + AsyncIndicatorHost(modifier = Modifier.statusBarsPadding(), asyncIndicatorState) + LaunchedEffect(hasAnyFailure) { + if (hasAnyFailure) { + asyncIndicatorState.enqueue { + AsyncIndicator.Failure(text = stringResource(CommonStrings.common_something_went_wrong)) + } + delay(AsyncIndicator.DURATION_SHORT) + eventSink(SpaceEvents.ClearFailures) + } else { + asyncIndicatorState.clear() + } + } } @Composable @@ -115,7 +143,7 @@ private fun SpaceViewContent( state.children.forEach { spaceRoom -> item { val isInvitation = spaceRoom.state == CurrentUserMembership.INVITED - val isCurrentlyJoining = spaceRoom.roomId in state.joiningRooms + val isCurrentlyJoining = state.isJoining(spaceRoom.roomId) SpaceRoomItemView( spaceRoom = spaceRoom, showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites, @@ -163,13 +191,12 @@ private fun LoadingMoreIndicator( @OptIn(ExperimentalMaterial3Api::class) @Composable private fun SpaceViewTopBar( - state: SpaceState, + currentSpace: SpaceRoom?, onBackClick: () -> Unit, @Suppress("unused") onLeaveSpaceClick: () -> Unit, onShareSpace: () -> Unit, modifier: Modifier = Modifier, ) { - val currentSpace = state.currentSpace TopAppBar( modifier = modifier, navigationIcon = { @@ -229,6 +256,7 @@ private fun SpaceViewTopBar( ) */ } + }, ) } @@ -290,9 +318,9 @@ internal fun SpaceViewPreview( ) = ElementPreview { SpaceView( state = state, - onBackClick = {}, - onLeaveSpaceClick = {}, onRoomClick = {}, onShareSpace = {}, + onLeaveSpaceClick = {}, + onBackClick = {}, ) }