Improve LeaveSpacePresenter and add a retry mechanism if loading the rooms fails.

This commit is contained in:
Benoit Marty 2025-10-03 14:43:47 +02:00
parent 2c3e4a45e4
commit b8e046cf4d
4 changed files with 57 additions and 35 deletions

View file

@ -10,6 +10,7 @@ package io.element.android.features.space.impl.leave
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
sealed interface LeaveSpaceEvents { sealed interface LeaveSpaceEvents {
data object Retry : LeaveSpaceEvents
data object SelectAllRooms : LeaveSpaceEvents data object SelectAllRooms : LeaveSpaceEvents
data object DeselectAllRooms : LeaveSpaceEvents data object DeselectAllRooms : LeaveSpaceEvents
data class ToggleRoomSelection(val roomId: RoomId) : LeaveSpaceEvents data class ToggleRoomSelection(val roomId: RoomId) : LeaveSpaceEvents

View file

@ -8,10 +8,11 @@
package io.element.android.features.space.impl.leave package io.element.android.features.space.impl.leave
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
@ -26,10 +27,9 @@ import io.element.android.libraries.architecture.runUpdatingState
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.LeaveSpaceHandle import io.element.android.libraries.matrix.api.spaces.LeaveSpaceHandle
import io.element.android.libraries.matrix.api.spaces.LeaveSpaceRoom import io.element.android.libraries.matrix.api.spaces.LeaveSpaceRoom
import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toPersistentSet
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -42,43 +42,55 @@ class LeaveSpacePresenter(
fun create(leaveSpaceHandle: LeaveSpaceHandle): LeaveSpacePresenter fun create(leaveSpaceHandle: LeaveSpaceHandle): LeaveSpacePresenter
} }
data class LeaveSpaceRooms(
val current: LeaveSpaceRoom?,
val others: List<LeaveSpaceRoom>,
)
@Composable @Composable
override fun present(): LeaveSpaceState { override fun present(): LeaveSpaceState {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
var currentSpace: LeaveSpaceRoom? by remember { mutableStateOf(null) } var retryCount by remember { mutableIntStateOf(0) }
val leaveSpaceAction = remember { val leaveSpaceAction = remember {
mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized)
} }
val selectedRoomIds = remember { var selectedRoomIds by remember {
mutableStateOf<ImmutableSet<RoomId>>(persistentSetOf()) mutableStateOf<Collection<RoomId>>(setOf())
} }
val leaveSpaceRooms by produceState(AsyncData.Loading()) { var leaveSpaceRooms by remember {
mutableStateOf<AsyncData<LeaveSpaceRooms>>(AsyncData.Loading())
}
LaunchedEffect(retryCount) {
val rooms = leaveSpaceHandle.rooms() val rooms = leaveSpaceHandle.rooms()
val (currentRoom, otherRooms) = rooms.getOrNull() val (currentRoom, otherRooms) = rooms.getOrNull()
.orEmpty() .orEmpty()
.partition { it.spaceRoom.roomId == leaveSpaceHandle.id } .partition { it.spaceRoom.roomId == leaveSpaceHandle.id }
currentSpace = currentRoom.firstOrNull()
// By default select all rooms that can be left // By default select all rooms that can be left
selectedRoomIds.value = otherRooms selectedRoomIds = otherRooms
.filter { it.isLastAdmin.not() } .filter { it.isLastAdmin.not() }
.map { it.spaceRoom.roomId } .map { it.spaceRoom.roomId }
.toPersistentSet() leaveSpaceRooms = rooms.fold(
value = rooms.fold( onSuccess = {
onSuccess = { AsyncData.Success(otherRooms) }, AsyncData.Success(
LeaveSpaceRooms(
current = currentRoom.firstOrNull(),
others = otherRooms.toImmutableList(),
)
)
},
onFailure = { AsyncData.Failure(it) } onFailure = { AsyncData.Failure(it) }
) )
} }
val selectableSpaceRooms by produceState( var selectableSpaceRooms by remember {
initialValue = AsyncData.Loading(), mutableStateOf<AsyncData<ImmutableList<SelectableSpaceRoom>>>(AsyncData.Loading())
key1 = leaveSpaceRooms, }
key2 = selectedRoomIds.value, LaunchedEffect(selectedRoomIds, leaveSpaceRooms) {
) { selectableSpaceRooms = leaveSpaceRooms.map {
value = leaveSpaceRooms.map { list -> it?.others.orEmpty().map { room ->
list.orEmpty().map { room ->
SelectableSpaceRoom( SelectableSpaceRoom(
spaceRoom = room.spaceRoom, spaceRoom = room.spaceRoom,
isLastAdmin = room.isLastAdmin, isLastAdmin = room.isLastAdmin,
isSelected = selectedRoomIds.value.contains(room.spaceRoom.roomId), isSelected = selectedRoomIds.contains(room.spaceRoom.roomId),
) )
}.toImmutableList() }.toImmutableList()
} }
@ -86,28 +98,29 @@ class LeaveSpacePresenter(
fun handleEvents(event: LeaveSpaceEvents) { fun handleEvents(event: LeaveSpaceEvents) {
when (event) { when (event) {
LeaveSpaceEvents.Retry -> {
leaveSpaceRooms = AsyncData.Loading()
retryCount += 1
}
LeaveSpaceEvents.DeselectAllRooms -> { LeaveSpaceEvents.DeselectAllRooms -> {
selectedRoomIds.value = persistentSetOf() selectedRoomIds = persistentSetOf()
} }
LeaveSpaceEvents.SelectAllRooms -> { LeaveSpaceEvents.SelectAllRooms -> {
selectedRoomIds.value = selectableSpaceRooms.dataOrNull() selectedRoomIds = selectableSpaceRooms.dataOrNull()
.orEmpty() .orEmpty()
.filter { it.isLastAdmin.not() } .filter { it.isLastAdmin.not() }
.map { it.spaceRoom.roomId } .map { it.spaceRoom.roomId }
.toPersistentSet()
} }
is LeaveSpaceEvents.ToggleRoomSelection -> { is LeaveSpaceEvents.ToggleRoomSelection -> {
val currentSet = selectedRoomIds.value selectedRoomIds = if (selectedRoomIds.contains(event.roomId)) {
selectedRoomIds.value = if (currentSet.contains(event.roomId)) { selectedRoomIds - event.roomId
currentSet - event.roomId
} else { } else {
currentSet + event.roomId selectedRoomIds + event.roomId
} }
.toPersistentSet()
} }
LeaveSpaceEvents.LeaveSpace -> coroutineScope.leaveSpace( LeaveSpaceEvents.LeaveSpace -> coroutineScope.leaveSpace(
leaveSpaceAction = leaveSpaceAction, leaveSpaceAction = leaveSpaceAction,
selectedRoomIds = selectedRoomIds.value, selectedRoomIds = selectedRoomIds,
) )
LeaveSpaceEvents.CloseError -> { LeaveSpaceEvents.CloseError -> {
leaveSpaceAction.value = AsyncAction.Uninitialized leaveSpaceAction.value = AsyncAction.Uninitialized
@ -116,8 +129,8 @@ class LeaveSpacePresenter(
} }
return LeaveSpaceState( return LeaveSpaceState(
spaceName = currentSpace?.spaceRoom?.name, spaceName = leaveSpaceRooms.dataOrNull()?.current?.spaceRoom?.name,
isLastAdmin = currentSpace?.isLastAdmin == true, isLastAdmin = leaveSpaceRooms.dataOrNull()?.current?.isLastAdmin == true,
selectableSpaceRooms = selectableSpaceRooms, selectableSpaceRooms = selectableSpaceRooms,
leaveSpaceAction = leaveSpaceAction.value, leaveSpaceAction = leaveSpaceAction.value,
eventSink = ::handleEvents, eventSink = ::handleEvents,
@ -126,7 +139,7 @@ class LeaveSpacePresenter(
private fun CoroutineScope.leaveSpace( private fun CoroutineScope.leaveSpace(
leaveSpaceAction: MutableState<AsyncAction<Unit>>, leaveSpaceAction: MutableState<AsyncAction<Unit>>,
selectedRoomIds: Set<RoomId>, selectedRoomIds: Collection<RoomId>,
) = launch { ) = launch {
runUpdatingState(leaveSpaceAction) { runUpdatingState(leaveSpaceAction) {
leaveSpaceHandle.leave(selectedRoomIds.toList()) leaveSpaceHandle.leave(selectedRoomIds.toList())

View file

@ -111,7 +111,9 @@ fun LeaveSpaceView(
is AsyncData.Failure -> item { is AsyncData.Failure -> item {
AsyncFailure( AsyncFailure(
throwable = state.selectableSpaceRooms.error, throwable = state.selectableSpaceRooms.error,
onRetry = null, onRetry = {
state.eventSink(LeaveSpaceEvents.Retry)
},
) )
} }
is AsyncData.Loading, is AsyncData.Loading,

View file

@ -60,9 +60,15 @@ class LeaveSpacePresenterTest {
val state = awaitItem() val state = awaitItem()
assertThat(state.selectableSpaceRooms.isLoading()).isTrue() assertThat(state.selectableSpaceRooms.isLoading()).isTrue()
assertThat(state.leaveSpaceAction).isEqualTo(AsyncAction.Uninitialized) assertThat(state.leaveSpaceAction).isEqualTo(AsyncAction.Uninitialized)
skipItems(2) skipItems(3)
val stateError = awaitItem() val stateError = awaitItem()
assertThat(stateError.selectableSpaceRooms.isFailure()).isTrue() assertThat(stateError.selectableSpaceRooms.isFailure()).isTrue()
// Retry
stateError.eventSink(LeaveSpaceEvents.Retry)
skipItems(1)
val stateLoadingAgain = awaitItem()
assertThat(stateLoadingAgain.selectableSpaceRooms.isLoading()).isTrue()
cancelAndIgnoreRemainingEvents()
} }
} }
@ -166,7 +172,7 @@ class LeaveSpacePresenterTest {
) )
) )
presenter.test { presenter.test {
skipItems(3) skipItems(4)
val state = awaitItem() val state = awaitItem()
state.eventSink(LeaveSpaceEvents.LeaveSpace) state.eventSink(LeaveSpaceEvents.LeaveSpace)
val stateLeaving = awaitItem() val stateLeaving = awaitItem()