Merge pull request #7369 from hayaksi1/fix/6130-space-left-room-joinable

Hide space children you have left that can only be joined by invitation
This commit is contained in:
Benoit Marty
2026-08-11 17:17:55 +02:00
committed by GitHub
2 changed files with 84 additions and 3 deletions
@@ -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.BaseRoom
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.room.join.JoinRoom 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.room.powerlevels.permissionsAsState
import io.element.android.libraries.matrix.api.spaces.SpaceRoom import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
@@ -117,12 +118,14 @@ class SpacePresenter(
val filteredChildren by remember { val filteredChildren by remember {
derivedStateOf { derivedStateOf {
val notRemoved = children.filterNot { it.roomId in removedRoomIds } val visibleChildren = children
.filterNot { it.roomId in removedRoomIds }
.filterNot { it.cannotBeRejoined() }
if (isManageMode) { if (isManageMode) {
// In manage mode, only show rooms (not spaces) // In manage mode, only show rooms (not spaces)
notRemoved.filter { !it.isSpace }.toImmutableList() visibleChildren.filter { !it.isSpace }.toImmutableList()
} else { } else {
notRemoved.toImmutableList() visibleChildren.toImmutableList()
} }
} }
} }
@@ -254,3 +257,7 @@ class SpacePresenter(
} }
} }
} }
private fun SpaceRoom.cannotBeRejoined(): Boolean {
return state == CurrentUserMembership.LEFT && joinRule == JoinRule.Invite
}
@@ -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.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomType 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.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.api.spaces.SpaceRoomList
import io.element.android.libraries.matrix.test.AN_EXCEPTION 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
import io.element.android.libraries.matrix.test.A_ROOM_ID_2 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_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.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeBaseRoom import io.element.android.libraries.matrix.test.room.FakeBaseRoom
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom 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 @Test
fun `present - removed rooms persist after flow update on partial failure`() = runTest { 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 // On partial failure, successfully removed rooms should stay filtered even after flow updates