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 5ce6575493..de30ddb6c1 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 @@ -35,6 +35,7 @@ import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias import io.element.android.libraries.matrix.api.room.BaseRoom import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.join.JoinRoom +import io.element.android.libraries.matrix.api.room.join.JoinRule import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState import io.element.android.libraries.matrix.api.spaces.SpaceRoom import io.element.android.libraries.matrix.api.spaces.SpaceRoomList @@ -117,12 +118,14 @@ class SpacePresenter( val filteredChildren by remember { derivedStateOf { - val notRemoved = children.filterNot { it.roomId in removedRoomIds } + val visibleChildren = children + .filterNot { it.roomId in removedRoomIds } + .filterNot { it.cannotBeRejoined() } if (isManageMode) { // In manage mode, only show rooms (not spaces) - notRemoved.filter { !it.isSpace }.toImmutableList() + visibleChildren.filter { !it.isSpace }.toImmutableList() } else { - notRemoved.toImmutableList() + visibleChildren.toImmutableList() } } } @@ -254,3 +257,7 @@ class SpacePresenter( } } } + +private fun SpaceRoom.cannotBeRejoined(): Boolean { + return state == CurrentUserMembership.LEFT && joinRule == JoinRule.Invite +} diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt index ba4e10a447..2de52a600b 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt @@ -30,11 +30,13 @@ import io.element.android.libraries.matrix.api.room.BaseRoom import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.RoomType import io.element.android.libraries.matrix.api.room.join.JoinRoom +import io.element.android.libraries.matrix.api.room.join.JoinRule import io.element.android.libraries.matrix.api.spaces.SpaceRoomList import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID_2 import io.element.android.libraries.matrix.test.A_ROOM_ID_3 +import io.element.android.libraries.matrix.test.A_ROOM_ID_4 import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.room.FakeBaseRoom import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom @@ -537,6 +539,78 @@ class SpacePresenterTest { } } + @Test + fun `present - left children which can only be joined by invitation are filtered out`() = runTest { + val leftInviteOnlyRoom = aSpaceRoom( + roomId = A_ROOM_ID, + roomType = RoomType.Room, + state = CurrentUserMembership.LEFT, + joinRule = JoinRule.Invite, + ) + val leftPublicRoom = aSpaceRoom( + roomId = A_ROOM_ID_2, + roomType = RoomType.Room, + state = CurrentUserMembership.LEFT, + joinRule = JoinRule.Public, + ) + val leftKnockRoom = aSpaceRoom( + roomId = A_ROOM_ID_3, + roomType = RoomType.Room, + state = CurrentUserMembership.LEFT, + joinRule = JoinRule.Knock, + ) + val leftRoomWithUnknownJoinRule = aSpaceRoom( + roomId = A_ROOM_ID_4, + roomType = RoomType.Room, + state = CurrentUserMembership.LEFT, + joinRule = null, + ) + val fakeSpaceRoomList = FakeSpaceRoomList( + initialSpaceRoomsValue = listOf(leftInviteOnlyRoom, leftPublicRoom, leftKnockRoom, leftRoomWithUnknownJoinRule), + paginateResult = { Result.success(Unit) }, + ) + val presenter = createSpacePresenter(spaceRoomList = fakeSpaceRoomList) + presenter.test { + awaitItem() + advanceUntilIdle() + val stateWithChildren = expectMostRecentItem() + assertThat(stateWithChildren.children).containsExactly(leftPublicRoom, leftKnockRoom, leftRoomWithUnknownJoinRule) + } + } + + @Test + fun `present - children which can only be joined by invitation are kept when not left`() = runTest { + val joinedRoom = aSpaceRoom( + roomId = A_ROOM_ID, + roomType = RoomType.Room, + state = CurrentUserMembership.JOINED, + joinRule = JoinRule.Invite, + ) + val invitedRoom = aSpaceRoom( + roomId = A_ROOM_ID_2, + roomType = RoomType.Room, + state = CurrentUserMembership.INVITED, + joinRule = JoinRule.Invite, + ) + val roomWithUnknownMembership = aSpaceRoom( + roomId = A_ROOM_ID_3, + roomType = RoomType.Room, + state = null, + joinRule = JoinRule.Invite, + ) + val fakeSpaceRoomList = FakeSpaceRoomList( + initialSpaceRoomsValue = listOf(joinedRoom, invitedRoom, roomWithUnknownMembership), + paginateResult = { Result.success(Unit) }, + ) + val presenter = createSpacePresenter(spaceRoomList = fakeSpaceRoomList) + presenter.test { + awaitItem() + advanceUntilIdle() + val stateWithChildren = expectMostRecentItem() + assertThat(stateWithChildren.children).containsExactly(joinedRoom, invitedRoom, roomWithUnknownMembership) + } + } + @Test fun `present - removed rooms persist after flow update on partial failure`() = runTest { // On partial failure, successfully removed rooms should stay filtered even after flow updates