feature (space) : manage failures to join in Space screen

This commit is contained in:
ganfra 2025-09-29 20:38:55 +02:00
parent 0390fde615
commit 526bc27a08
5 changed files with 70 additions and 28 deletions

View file

@ -12,4 +12,5 @@ import io.element.android.libraries.matrix.api.spaces.SpaceRoom
sealed interface SpaceEvents { sealed interface SpaceEvents {
data object LoadMore : SpaceEvents data object LoadMore : SpaceEvents
data class Join(val spaceRoom: SpaceRoom): SpaceEvents data class Join(val spaceRoom: SpaceRoom): SpaceEvents
data object ClearFailures : SpaceEvents
} }

View file

@ -17,10 +17,11 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory import dev.zacsweers.metro.AssistedFactory
import im.vector.app.features.analytics.plan.JoinedRoom
import dev.zacsweers.metro.AssistedInject 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.invite.api.SeenInvitesStore
import io.element.android.features.space.api.SpaceEntryPoint 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.architecture.Presenter
import io.element.android.libraries.core.coroutine.mapState import io.element.android.libraries.core.coroutine.mapState
import io.element.android.libraries.di.annotations.SessionCoroutineScope 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 io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar
import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toPersistentList import kotlinx.collections.immutable.toPersistentList
import kotlinx.collections.immutable.toPersistentMap
import kotlinx.collections.immutable.toPersistentSet import kotlinx.collections.immutable.toPersistentSet
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.jvm.optionals.getOrNull import kotlin.jvm.optionals.getOrNull
@AssistedInject @AssistedInject class SpacePresenter(
class SpacePresenter(
@Assisted private val inputs: SpaceEntryPoint.Inputs, @Assisted private val inputs: SpaceEntryPoint.Inputs,
private val client: MatrixClient, private val client: MatrixClient,
private val seenInvitesStore: SeenInvitesStore, private val seenInvitesStore: SeenInvitesStore,
@ -76,15 +77,12 @@ class SpacePresenter(
}.collectAsState() }.collectAsState()
val currentSpace by spaceRoomList.currentSpaceFlow.collectAsState() val currentSpace by spaceRoomList.currentSpaceFlow.collectAsState()
val joiningRooms = remember { mutableStateOf(emptySet<RoomId>()) } val joinActions = remember { mutableStateOf(emptyMap<RoomId, AsyncAction<Unit>>()) }
LaunchedEffect(children, joiningRooms.value) { LaunchedEffect(children) {
val joinedChildren = children val joinedChildren = children.filter { it.state == CurrentUserMembership.JOINED }.map { it.roomId }.toSet()
.filter { it.state == CurrentUserMembership.JOINED } joinActions.value.let { currentlyJoining ->
.map { it.roomId } joinActions.value = currentlyJoining - joinedChildren
.toSet()
joiningRooms.value.let { currentlyJoining ->
joiningRooms.value = currentlyJoining - joinedChildren
} }
} }
@ -92,7 +90,13 @@ class SpacePresenter(
when (event) { when (event) {
SpaceEvents.LoadMore -> localCoroutineScope.paginate() SpaceEvents.LoadMore -> localCoroutineScope.paginate()
is SpaceEvents.Join -> { 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, seenSpaceInvites = seenSpaceInvites,
hideInvitesAvatar = hideInvitesAvatar, hideInvitesAvatar = hideInvitesAvatar,
hasMoreToLoad = hasMoreToLoad, hasMoreToLoad = hasMoreToLoad,
joiningRooms = joiningRooms.value.toPersistentSet(), joinActions = joinActions.value.toPersistentMap(),
eventSink = ::handleEvents, eventSink = ::handleEvents,
) )
} }
private fun CoroutineScope.joinRoom( private fun CoroutineScope.joinRoom(
spaceRoom: SpaceRoom, joiningRooms: MutableState<Set<RoomId>> spaceRoom: SpaceRoom, joiningRooms: MutableState<Map<RoomId, AsyncAction<Unit>>>
) = launch { ) = launch {
joiningRooms.value = joiningRooms.value + spaceRoom.roomId joiningRooms.value = joiningRooms.value + mapOf(spaceRoom.roomId to AsyncAction.Loading)
joinRoom.invoke( joinRoom.invoke(
roomIdOrAlias = spaceRoom.roomId.toRoomIdOrAlias(), roomIdOrAlias = spaceRoom.roomId.toRoomIdOrAlias(),
serverNames = spaceRoom.via, serverNames = spaceRoom.via,
trigger = JoinedRoom.Trigger.SpaceHierarchy, trigger = JoinedRoom.Trigger.SpaceHierarchy,
).onFailure { ).onFailure {
joiningRooms.value = joiningRooms.value - spaceRoom.roomId joiningRooms.value = joiningRooms.value + mapOf(spaceRoom.roomId to AsyncAction.Failure(it))
} }
} }

View file

@ -7,9 +7,11 @@
package io.element.android.features.space.impl.root 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.core.RoomId
import io.element.android.libraries.matrix.api.spaces.SpaceRoom import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.ImmutableSet
data class SpaceState( data class SpaceState(
@ -18,6 +20,11 @@ data class SpaceState(
val seenSpaceInvites: ImmutableSet<RoomId>, val seenSpaceInvites: ImmutableSet<RoomId>,
val hideInvitesAvatar: Boolean, val hideInvitesAvatar: Boolean,
val hasMoreToLoad: Boolean, val hasMoreToLoad: Boolean,
val joiningRooms: ImmutableSet<RoomId>, val joinActions: ImmutableMap<RoomId, AsyncAction<Unit>>,
val eventSink: (SpaceEvents) -> Unit val eventSink: (SpaceEvents) -> Unit
) ) {
fun isJoining(spaceId: RoomId): Boolean = joinActions[spaceId] == AsyncAction.Loading
val hasAnyFailure: Boolean = joinActions.values.any {
it is AsyncAction.Failure
}
}

View file

@ -5,14 +5,16 @@
* Please see LICENSE files in the repository root for full details. * 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 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.core.RoomId
import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.spaces.SpaceRoom import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import io.element.android.libraries.previewutils.room.aSpaceRoom import io.element.android.libraries.previewutils.room.aSpaceRoom
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableMap
import kotlinx.collections.immutable.toImmutableSet import kotlinx.collections.immutable.toImmutableSet
open class SpaceStateProvider : PreviewParameterProvider<SpaceState> { open class SpaceStateProvider : PreviewParameterProvider<SpaceState> {
@ -56,7 +58,7 @@ fun aSpaceState(
seenSpaceInvites = seenSpaceInvites.toImmutableSet(), seenSpaceInvites = seenSpaceInvites.toImmutableSet(),
hideInvitesAvatar = hideInvitesAvatar, hideInvitesAvatar = hideInvitesAvatar,
hasMoreToLoad = hasMoreToLoad, hasMoreToLoad = hasMoreToLoad,
joiningRooms = joiningRooms.toImmutableSet(), joinActions = joiningRooms.associateWith { AsyncAction.Uninitialized }.toImmutableMap(),
eventSink = {}) eventSink = {})
private fun aListOfSpaceRooms(): List<SpaceRoom> { private fun aListOfSpaceRooms(): List<SpaceRoom> {

View file

@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
@ -34,6 +35,9 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons 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.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize 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.matrix.ui.model.getAvatarData
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay
@Composable @Composable
fun SpaceView( fun SpaceView(
state: SpaceState, state: SpaceState,
onBackClick: () -> Unit, onBackClick: () -> Unit,
onLeaveSpaceClick: () -> Unit,
onRoomClick: (spaceRoom: SpaceRoom) -> Unit, onRoomClick: (spaceRoom: SpaceRoom) -> Unit,
onShareSpace: () -> Unit, onShareSpace: () -> Unit,
onLeaveSpaceClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Scaffold( Scaffold(
modifier = modifier, modifier = modifier,
topBar = { topBar = {
SpaceViewTopBar( SpaceViewTopBar(
state = state, currentSpace = state.currentSpace, onBackClick = onBackClick,
onBackClick = onBackClick,
onLeaveSpaceClick = onLeaveSpaceClick, onLeaveSpaceClick = onLeaveSpaceClick,
onShareSpace = onShareSpace, 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 @Composable
@ -115,7 +143,7 @@ private fun SpaceViewContent(
state.children.forEach { spaceRoom -> state.children.forEach { spaceRoom ->
item { item {
val isInvitation = spaceRoom.state == CurrentUserMembership.INVITED val isInvitation = spaceRoom.state == CurrentUserMembership.INVITED
val isCurrentlyJoining = spaceRoom.roomId in state.joiningRooms val isCurrentlyJoining = state.isJoining(spaceRoom.roomId)
SpaceRoomItemView( SpaceRoomItemView(
spaceRoom = spaceRoom, spaceRoom = spaceRoom,
showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites, showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites,
@ -163,13 +191,12 @@ private fun LoadingMoreIndicator(
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun SpaceViewTopBar( private fun SpaceViewTopBar(
state: SpaceState, currentSpace: SpaceRoom?,
onBackClick: () -> Unit, onBackClick: () -> Unit,
@Suppress("unused") onLeaveSpaceClick: () -> Unit, @Suppress("unused") onLeaveSpaceClick: () -> Unit,
onShareSpace: () -> Unit, onShareSpace: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val currentSpace = state.currentSpace
TopAppBar( TopAppBar(
modifier = modifier, modifier = modifier,
navigationIcon = { navigationIcon = {
@ -229,6 +256,7 @@ private fun SpaceViewTopBar(
) )
*/ */
} }
}, },
) )
} }
@ -290,9 +318,9 @@ internal fun SpaceViewPreview(
) = ElementPreview { ) = ElementPreview {
SpaceView( SpaceView(
state = state, state = state,
onBackClick = {},
onLeaveSpaceClick = {},
onRoomClick = {}, onRoomClick = {},
onShareSpace = {}, onShareSpace = {},
onLeaveSpaceClick = {},
onBackClick = {},
) )
} }