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 {
data object LoadMore : 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 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<RoomId>()) }
val joinActions = remember { mutableStateOf(emptyMap<RoomId, AsyncAction<Unit>>()) }
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<Set<RoomId>>
spaceRoom: SpaceRoom, joiningRooms: MutableState<Map<RoomId, AsyncAction<Unit>>>
) = 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))
}
}

View File

@@ -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<RoomId>,
val hideInvitesAvatar: Boolean,
val hasMoreToLoad: Boolean,
val joiningRooms: ImmutableSet<RoomId>,
val joinActions: ImmutableMap<RoomId, AsyncAction<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.
*/
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<SpaceState> {
@@ -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<SpaceRoom> {

View File

@@ -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 = {},
)
}