Call spaceRoomList.reset when exiting add/remove room flow

This commit is contained in:
ganfra
2026-01-27 14:26:57 +01:00
parent 1e25938ef7
commit b60cfaccb6
8 changed files with 151 additions and 12 deletions

View File

@@ -14,4 +14,5 @@ sealed interface AddRoomToSpaceEvent {
data class OnSearchActiveChanged(val active: Boolean) : AddRoomToSpaceEvent
data object Save : AddRoomToSpaceEvent
data object ResetSaveAction : AddRoomToSpaceEvent
data object Dismiss : AddRoomToSpaceEvent
}

View File

@@ -40,7 +40,7 @@ class AddRoomToSpaceNode(
val state by stateFlow.collectAsState()
AddRoomToSpaceView(
state = state,
onBackClick = ::navigateUp,
onBackClick = callback::onFinish,
onRoomsAdded = callback::onFinish,
modifier = modifier
)

View File

@@ -27,7 +27,6 @@ import io.element.android.libraries.designsystem.theme.components.SearchBarResul
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
import io.element.android.libraries.matrix.api.spaces.SpaceService
import io.element.android.libraries.matrix.api.spaces.resetAndWaitForFullReload
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@@ -50,6 +49,8 @@ class AddRoomToSpacePresenter(
val searchQuery = rememberTextFieldState()
var isSearchActive by remember { mutableStateOf(false) }
val saveAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
// Track whether any rooms were added (for conditional reset on Dismiss)
var hasAddedRooms by remember { mutableStateOf(false) }
val coroutineScope = rememberCoroutineScope()
val dataSource = remember { dataSourceFactory.create(coroutineScope) }
@@ -96,6 +97,9 @@ class AddRoomToSpacePresenter(
dataSource = dataSource,
saveAction = saveAction,
onPartialSuccess = { successfullyAdded ->
if (successfullyAdded.isNotEmpty()) {
hasAddedRooms = true
}
selectedRooms = selectedRooms.filterNot { it.roomId in successfullyAdded }.toImmutableList()
},
)
@@ -103,6 +107,11 @@ class AddRoomToSpacePresenter(
AddRoomToSpaceEvent.ResetSaveAction -> {
saveAction.value = AsyncAction.Uninitialized
}
AddRoomToSpaceEvent.Dismiss -> {
if (hasAddedRooms) {
coroutineScope.launch { spaceRoomList.reset() }
}
}
}
}

View File

@@ -66,6 +66,7 @@ fun AddRoomToSpaceView(
if (state.isSearchActive) {
state.eventSink(AddRoomToSpaceEvent.OnSearchActiveChanged(false))
} else {
state.eventSink(AddRoomToSpaceEvent.Dismiss)
onBackClick()
}
}

View File

@@ -49,7 +49,6 @@ import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
@@ -136,6 +135,16 @@ class SpacePresenter(
val acceptDeclineInviteState = acceptDeclineInvitePresenter.present()
suspend fun exitManageMode(shouldReset: Boolean) {
isManageMode = false
selectedRoomIds = emptySet()
removedRoomIds = emptySet()
if (shouldReset) {
// Reset the space room list to see the updates.
spaceRoomList.reset()
}
}
fun handleEvent(event: SpaceEvents) {
when (event) {
// SpaceRoomList is loaded automatically as backend is really slow. Event is kept for future.
@@ -168,8 +177,7 @@ class SpacePresenter(
selectedRoomIds = emptySet()
}
SpaceEvents.ExitManageMode -> {
isManageMode = false
selectedRoomIds = emptySet()
localCoroutineScope.launch { exitManageMode(shouldReset = removedRoomIds.isNotEmpty()) }
}
is SpaceEvents.ToggleRoomSelection -> {
selectedRoomIds = if (event.roomId in selectedRoomIds) {
@@ -202,10 +210,7 @@ class SpacePresenter(
removeRoomsAction = AsyncAction.Failure(Exception("Failed to remove some rooms"))
} else {
removeRoomsAction = AsyncAction.Success(Unit)
isManageMode = false
selectedRoomIds = emptySet()
// Reset the space room list to see the updates.
spaceRoomList.reset()
exitManageMode(shouldReset = true)
}
}
}

View File

@@ -27,6 +27,7 @@ import io.element.android.libraries.matrix.ui.components.aSelectRoomInfo
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.test
import io.element.android.tests.testutils.testCoroutineDispatchers
@@ -278,6 +279,67 @@ class AddRoomToSpacePresenterTest {
}
}
@Test
fun `present - Dismiss without additions does not call reset`() = runTest {
val resetResult = lambdaRecorder<Result<Unit>>(ensureNeverCalled = true) { Result.success(Unit) }
val spaceRoomList = FakeSpaceRoomList(
paginateResult = { Result.success(Unit) },
resetResult = resetResult,
)
val presenter = createAddRoomToSpacePresenter(spaceRoomList = spaceRoomList)
presenter.test {
val state = awaitItem()
state.eventSink(AddRoomToSpaceEvent.Dismiss)
advanceUntilIdle()
// reset should NOT be called since no rooms were added
assert(resetResult).isNeverCalled()
}
}
@Test
fun `present - Dismiss after partial success calls reset`() = runTest {
val resetResult = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
val spaceRoomList = FakeSpaceRoomList(
paginateResult = { Result.success(Unit) },
resetResult = resetResult,
)
// Room 1 succeeds, Room 2 fails
val addChildToSpaceResult = lambdaRecorder<RoomId, RoomId, Result<Unit>> { _, childId ->
if (childId == A_ROOM_ID_2) {
Result.failure(AN_EXCEPTION)
} else {
Result.success(Unit)
}
}
val spaceService = FakeSpaceService(
addChildToSpaceResult = addChildToSpaceResult,
)
val presenter = createAddRoomToSpacePresenter(
spaceRoomList = spaceRoomList,
spaceService = spaceService,
)
presenter.test {
val state = awaitItem()
// Select two rooms
val room1 = aSelectRoomInfoList()[0]
val room2 = aSelectRoomInfoList()[1]
state.eventSink(AddRoomToSpaceEvent.ToggleRoom(room1))
awaitItem()
state.eventSink(AddRoomToSpaceEvent.ToggleRoom(room2))
awaitItem()
// Save - partial success (one room added, one failed)
state.eventSink(AddRoomToSpaceEvent.Save)
skipItems(1) // Loading
advanceUntilIdle()
val failureState = expectMostRecentItem()
assertThat(failureState.saveAction).isInstanceOf(AsyncAction.Failure::class.java)
// Dismiss after partial success - reset should be called
failureState.eventSink(AddRoomToSpaceEvent.Dismiss)
advanceUntilIdle()
assert(resetResult).isCalledOnce()
}
}
private fun TestScope.createAddRoomToSpacePresenter(
spaceRoomList: FakeSpaceRoomList = FakeSpaceRoomList(
paginateResult = { Result.success(Unit) },

View File

@@ -32,16 +32,19 @@ class AddRoomToSpaceViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test
fun `clicking back when search inactive invokes onBackClick`() {
fun `clicking back when search inactive emits Dismiss and invokes onBackClick`() {
val eventsRecorder = EventsRecorder<AddRoomToSpaceEvent>()
ensureCalledOnce {
rule.setAddRoomToSpaceView(
anAddRoomToSpaceState(
isSearchActive = false,
eventSink = eventsRecorder,
),
onBackClick = it,
)
rule.pressBack()
}
eventsRecorder.assertSingle(AddRoomToSpaceEvent.Dismiss)
}
@Test

View File

@@ -350,16 +350,24 @@ class SpacePresenterTest {
}
@Test
fun `present - exit manage mode clears selection`() = runTest {
val presenter = createSpacePresenter()
fun `present - exit manage mode without removals does not call reset`() = runTest {
val resetResult = lambdaRecorder<Result<Unit>>(ensureNeverCalled = true) { Result.success(Unit) }
val fakeSpaceRoomList = FakeSpaceRoomList(
paginateResult = { Result.success(Unit) },
resetResult = resetResult,
)
val presenter = createSpacePresenter(spaceRoomList = fakeSpaceRoomList)
presenter.test {
val initialState = awaitItem()
initialState.eventSink(SpaceEvents.EnterManageMode)
initialState.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID))
initialState.eventSink(SpaceEvents.ExitManageMode)
advanceUntilIdle()
val finalState = expectMostRecentItem()
assertThat(finalState.isManageMode).isFalse()
assertThat(finalState.selectedRoomIds).isEmpty()
// reset should NOT be called since no rooms were actually removed
assert(resetResult).isNeverCalled()
}
}
@@ -467,6 +475,56 @@ class SpacePresenterTest {
}
}
@Test
fun `present - exit manage mode after partial failure calls reset`() = runTest {
val aRoom1 = aSpaceRoom(
roomId = A_ROOM_ID,
roomType = RoomType.Room,
)
val aRoom2 = aSpaceRoom(
roomId = A_ROOM_ID_2,
roomType = RoomType.Room,
)
// Room 1 succeeds, Room 2 fails
val removeChildFromSpaceResult = lambdaRecorder<RoomId, RoomId, Result<Unit>> { _, childId ->
if (childId == A_ROOM_ID_2) {
Result.failure(AN_EXCEPTION)
} else {
Result.success(Unit)
}
}
val resetResult = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
val fakeSpaceRoomList = FakeSpaceRoomList(
initialSpaceRoomsValue = listOf(aRoom1, aRoom2),
paginateResult = { Result.success(Unit) },
resetResult = resetResult,
)
val presenter = createSpacePresenter(
spaceRoomList = fakeSpaceRoomList,
spaceService = FakeSpaceService(
removeChildFromSpaceResult = removeChildFromSpaceResult,
),
)
presenter.test {
awaitItem() // Initial empty state
advanceUntilIdle()
val stateWithChildren = awaitItem()
stateWithChildren.eventSink(SpaceEvents.EnterManageMode)
stateWithChildren.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID))
stateWithChildren.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID_2))
stateWithChildren.eventSink(SpaceEvents.RemoveSelectedRooms)
stateWithChildren.eventSink(SpaceEvents.ConfirmRoomRemoval)
advanceUntilIdle()
val failureState = expectMostRecentItem()
assertThat(failureState.removeRoomsAction.isFailure()).isTrue()
// Exit manage mode after partial failure - reset should be called
failureState.eventSink(SpaceEvents.ExitManageMode)
advanceUntilIdle()
expectMostRecentItem()
assert(resetResult).isCalledOnce()
}
}
@Test
fun `present - children filtered in manage mode shows only rooms`() = runTest {
val aRoom = aSpaceRoom(