Split MatrixRoom into BaseRoom and JoinedRoom (#4561)

`JoinedRoom` will now contain both a mandatory live timeline reference and all the functionality associated to it.

`BaseRoom` on the other hand will contain only functionality that's shared for both joined and not joined rooms.

`NotJoinedRoom` is a wrapper around `RoomPreviewInfo` data and a possible local `BaseRoom`, if it exists.

The `RustRoomFactory` cache is now gone since the persistent event cache should have the same effect.
This commit is contained in:
Jorge Martin Espinosa 2025-04-23 15:53:40 +02:00 committed by GitHub
parent 91cb84ce8d
commit 619aa6f2de
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
193 changed files with 2921 additions and 2567 deletions

View file

@ -9,9 +9,9 @@ package io.element.android.libraries.matrix.ui.model
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.RoomInfo
fun MatrixRoomInfo.getAvatarData(size: AvatarSize) = AvatarData(
fun RoomInfo.getAvatarData(size: AvatarSize) = AvatarData(
id = id.value,
name = name,
url = avatarUrl,

View file

@ -14,19 +14,19 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembersState
import io.element.android.libraries.matrix.api.room.roomMembers
@Composable
fun MatrixRoom.getRoomMemberAsState(userId: UserId): State<RoomMember?> {
fun BaseRoom.getRoomMemberAsState(userId: UserId): State<RoomMember?> {
val roomMembersState by membersStateFlow.collectAsState()
return getRoomMemberAsState(roomMembersState = roomMembersState, userId = userId)
}
@Composable
fun getRoomMemberAsState(roomMembersState: MatrixRoomMembersState, userId: UserId): State<RoomMember?> {
fun getRoomMemberAsState(roomMembersState: RoomMembersState, userId: UserId): State<RoomMember?> {
val roomMembers = roomMembersState.roomMembers()
return remember(roomMembers) {
derivedStateOf {
@ -38,7 +38,7 @@ fun getRoomMemberAsState(roomMembersState: MatrixRoomMembersState, userId: UserI
}
@Composable
fun MatrixRoom.getDirectRoomMember(roomMembersState: MatrixRoomMembersState): State<RoomMember?> {
fun BaseRoom.getDirectRoomMember(roomMembersState: RoomMembersState): State<RoomMember?> {
val roomMembers = roomMembersState.roomMembers()
val roomInfo by roomInfoFlow.collectAsState()
return remember(roomMembersState, roomInfo.isDirect) {
@ -52,6 +52,6 @@ fun MatrixRoom.getDirectRoomMember(roomMembersState: MatrixRoomMembersState): St
}
@Composable
fun MatrixRoom.getCurrentRoomMember(roomMembersState: MatrixRoomMembersState): State<RoomMember?> {
fun BaseRoom.getCurrentRoomMember(roomMembersState: RoomMembersState): State<RoomMember?> {
return getRoomMemberAsState(roomMembersState = roomMembersState, userId = sessionId)
}

View file

@ -12,7 +12,7 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.isDm
@ -25,77 +25,77 @@ import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn
import io.element.android.libraries.matrix.api.room.powerlevels.canSendMessage
@Composable
fun MatrixRoom.canSendMessageAsState(type: MessageEventType, updateKey: Long): State<Boolean> {
fun BaseRoom.canSendMessageAsState(type: MessageEventType, updateKey: Long): State<Boolean> {
return produceState(initialValue = true, key1 = updateKey) {
value = canSendMessage(type).getOrElse { true }
}
}
@Composable
fun MatrixRoom.canInviteAsState(updateKey: Long): State<Boolean> {
fun BaseRoom.canInviteAsState(updateKey: Long): State<Boolean> {
return produceState(initialValue = false, key1 = updateKey) {
value = canInvite().getOrElse { false }
}
}
@Composable
fun MatrixRoom.canRedactOwnAsState(updateKey: Long): State<Boolean> {
fun BaseRoom.canRedactOwnAsState(updateKey: Long): State<Boolean> {
return produceState(initialValue = false, key1 = updateKey) {
value = canRedactOwn().getOrElse { false }
}
}
@Composable
fun MatrixRoom.canRedactOtherAsState(updateKey: Long): State<Boolean> {
fun BaseRoom.canRedactOtherAsState(updateKey: Long): State<Boolean> {
return produceState(initialValue = false, key1 = updateKey) {
value = canRedactOther().getOrElse { false }
}
}
@Composable
fun MatrixRoom.canCall(updateKey: Long): State<Boolean> {
fun BaseRoom.canCall(updateKey: Long): State<Boolean> {
return produceState(initialValue = false, key1 = updateKey) {
value = canUserJoinCall(sessionId).getOrElse { false }
}
}
@Composable
fun MatrixRoom.canPinUnpin(updateKey: Long): State<Boolean> {
fun BaseRoom.canPinUnpin(updateKey: Long): State<Boolean> {
return produceState(initialValue = false, key1 = updateKey) {
value = canUserPinUnpin(sessionId).getOrElse { false }
}
}
@Composable
fun MatrixRoom.isDmAsState(): State<Boolean> {
fun BaseRoom.isDmAsState(): State<Boolean> {
return produceState(initialValue = false) {
roomInfoFlow.collect { value = it.isDm }
}
}
@Composable
fun MatrixRoom.canKickAsState(updateKey: Long): State<Boolean> {
fun BaseRoom.canKickAsState(updateKey: Long): State<Boolean> {
return produceState(initialValue = false, key1 = updateKey) {
value = canKick().getOrElse { false }
}
}
@Composable
fun MatrixRoom.canBanAsState(updateKey: Long): State<Boolean> {
fun BaseRoom.canBanAsState(updateKey: Long): State<Boolean> {
return produceState(initialValue = false, key1 = updateKey) {
value = canBan().getOrElse { false }
}
}
@Composable
fun MatrixRoom.canHandleKnockRequestsAsState(updateKey: Long): State<Boolean> {
fun BaseRoom.canHandleKnockRequestsAsState(updateKey: Long): State<Boolean> {
return produceState(initialValue = false, key1 = updateKey) {
value = canHandleKnockRequests().getOrElse { false }
}
}
@Composable
fun MatrixRoom.userPowerLevelAsState(updateKey: Long): State<Long> {
fun BaseRoom.userPowerLevelAsState(updateKey: Long): State<Long> {
return produceState(initialValue = 0, key1 = updateKey) {
value = userRole(sessionId)
.getOrDefault(RoomMember.Role.USER)
@ -104,26 +104,26 @@ fun MatrixRoom.userPowerLevelAsState(updateKey: Long): State<Long> {
}
@Composable
fun MatrixRoom.isOwnUserAdmin(): Boolean {
fun BaseRoom.isOwnUserAdmin(): Boolean {
val roomInfo by roomInfoFlow.collectAsState()
val powerLevel = roomInfo.userPowerLevels[sessionId] ?: 0L
return RoomMember.Role.forPowerLevel(powerLevel) == RoomMember.Role.ADMIN
}
@Composable
fun MatrixRoom.rawName(): String? {
fun BaseRoom.rawName(): String? {
val roomInfo by roomInfoFlow.collectAsState()
return roomInfo.rawName
}
@Composable
fun MatrixRoom.topic(): String? {
fun BaseRoom.topic(): String? {
val roomInfo by roomInfoFlow.collectAsState()
return roomInfo.topic
}
@Composable
fun MatrixRoom.avatarUrl(): String? {
fun BaseRoom.avatarUrl(): String? {
val roomInfo by roomInfoFlow.collectAsState()
return roomInfo.avatarUrl
}

View file

@ -12,7 +12,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.roomMembers
import io.element.android.libraries.matrix.ui.model.getAvatarData
@ -29,7 +29,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@OptIn(ExperimentalCoroutinesApi::class)
fun MatrixRoom.roomMemberIdentityStateChange(): Flow<ImmutableList<RoomMemberIdentityStateChange>> {
fun JoinedRoom.roomMemberIdentityStateChange(): Flow<ImmutableList<RoomMemberIdentityStateChange>> {
return roomInfoFlow
.filter {
// Room cannot become unencrypted, so we can just apply a filter here.
@ -52,7 +52,7 @@ fun MatrixRoom.roomMemberIdentityStateChange(): Flow<ImmutableList<RoomMemberIde
}
}
fun ProduceStateScope<PersistentList<RoomMemberIdentityStateChange>>.observeRoomMemberIdentityStateChange(room: MatrixRoom) {
fun ProduceStateScope<PersistentList<RoomMemberIdentityStateChange>>.observeRoomMemberIdentityStateChange(room: JoinedRoom) {
room.roomMemberIdentityStateChange()
.onEach { roomMemberIdentityStateChanges ->
value = roomMemberIdentityStateChanges.toPersistentList()

View file

@ -11,26 +11,26 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.RoomMembersState
import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.A_USER_ID_3
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.libraries.matrix.test.room.aRoomMember
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.test.runTest
import org.junit.Test
class MatrixRoomMembersTest {
class RoomMembersTest {
private val roomMember1 = aRoomMember(A_USER_ID)
private val roomMember2 = aRoomMember(A_USER_ID_2)
private val roomMember3 = aRoomMember(A_USER_ID_3)
@Test
fun `getDirectRoomMember emits other member for encrypted DM with 2 joined members`() = runTest {
val matrixRoom = FakeMatrixRoom(
val joinedRoom = FakeBaseRoom(
sessionId = A_USER_ID,
initialRoomInfo = aRoomInfo(
isDirect = true,
@ -38,8 +38,8 @@ class MatrixRoomMembersTest {
)
)
moleculeFlow(RecompositionMode.Immediate) {
matrixRoom.getDirectRoomMember(
MatrixRoomMembersState.Ready(persistentListOf(roomMember1, roomMember2))
joinedRoom.getDirectRoomMember(
RoomMembersState.Ready(persistentListOf(roomMember1, roomMember2))
)
}.test {
assertThat(awaitItem().value).isEqualTo(roomMember2)
@ -48,13 +48,13 @@ class MatrixRoomMembersTest {
@Test
fun `getDirectRoomMember emit null if the room is not a dm`() = runTest {
val matrixRoom = FakeMatrixRoom(
val joinedRoom = FakeBaseRoom(
sessionId = A_USER_ID,
initialRoomInfo = aRoomInfo(isDirect = false)
)
moleculeFlow(RecompositionMode.Immediate) {
matrixRoom.getDirectRoomMember(
MatrixRoomMembersState.Ready(persistentListOf(roomMember1, roomMember2))
joinedRoom.getDirectRoomMember(
RoomMembersState.Ready(persistentListOf(roomMember1, roomMember2))
)
}.test {
assertThat(awaitItem().value).isNull()
@ -63,7 +63,7 @@ class MatrixRoomMembersTest {
@Test
fun `getDirectRoomMember emits other member even if the room is not encrypted`() = runTest {
val matrixRoom = FakeMatrixRoom(
val joinedRoom = FakeBaseRoom(
sessionId = A_USER_ID,
initialRoomInfo = aRoomInfo(
isDirect = true,
@ -71,8 +71,8 @@ class MatrixRoomMembersTest {
)
)
moleculeFlow(RecompositionMode.Immediate) {
matrixRoom.getDirectRoomMember(
MatrixRoomMembersState.Ready(persistentListOf(roomMember1, roomMember2))
joinedRoom.getDirectRoomMember(
RoomMembersState.Ready(persistentListOf(roomMember1, roomMember2))
)
}.test {
assertThat(awaitItem().value).isEqualTo(roomMember2)
@ -81,13 +81,13 @@ class MatrixRoomMembersTest {
@Test
fun `getDirectRoomMember emit null if the room has only 1 member`() = runTest {
val matrixRoom = FakeMatrixRoom(
val joinedRoom = FakeBaseRoom(
sessionId = A_USER_ID,
initialRoomInfo = aRoomInfo(isDirect = true)
)
moleculeFlow(RecompositionMode.Immediate) {
matrixRoom.getDirectRoomMember(
MatrixRoomMembersState.Ready(persistentListOf(roomMember1))
joinedRoom.getDirectRoomMember(
RoomMembersState.Ready(persistentListOf(roomMember1))
)
}.test {
assertThat(awaitItem().value).isNull()
@ -96,14 +96,14 @@ class MatrixRoomMembersTest {
@Test
fun `getDirectRoomMember emit null if the room has only 3 members`() = runTest {
val matrixRoom = FakeMatrixRoom(
val joinedRoom = FakeBaseRoom(
sessionId = A_USER_ID,
).apply {
givenRoomInfo(aRoomInfo(isDirect = true))
}
moleculeFlow(RecompositionMode.Immediate) {
matrixRoom.getDirectRoomMember(
MatrixRoomMembersState.Ready(persistentListOf(roomMember1, roomMember2, roomMember3))
joinedRoom.getDirectRoomMember(
RoomMembersState.Ready(persistentListOf(roomMember1, roomMember2, roomMember3))
)
}.test {
assertThat(awaitItem().value).isNull()
@ -112,13 +112,13 @@ class MatrixRoomMembersTest {
@Test
fun `getDirectRoomMember emit null if the other member is not active`() = runTest {
val matrixRoom = FakeMatrixRoom(
val joinedRoom = FakeBaseRoom(
sessionId = A_USER_ID,
initialRoomInfo = aRoomInfo(isDirect = true),
)
moleculeFlow(RecompositionMode.Immediate) {
matrixRoom.getDirectRoomMember(
MatrixRoomMembersState.Ready(
joinedRoom.getDirectRoomMember(
RoomMembersState.Ready(
persistentListOf(
roomMember1,
roomMember2.copy(membership = RoomMembershipState.BAN),
@ -132,7 +132,7 @@ class MatrixRoomMembersTest {
@Test
fun `getDirectRoomMember emit the other member if there are 2 active members`() = runTest {
val matrixRoom = FakeMatrixRoom(
val joinedRoom = FakeBaseRoom(
sessionId = A_USER_ID,
initialRoomInfo = aRoomInfo(
isDirect = true,
@ -140,8 +140,8 @@ class MatrixRoomMembersTest {
)
)
moleculeFlow(RecompositionMode.Immediate) {
matrixRoom.getDirectRoomMember(
MatrixRoomMembersState.Ready(
joinedRoom.getDirectRoomMember(
RoomMembersState.Ready(
persistentListOf(
roomMember1,
roomMember2,
@ -156,10 +156,10 @@ class MatrixRoomMembersTest {
@Test
fun `getCurrentRoomMember returns the current user`() = runTest {
val matrixRoom = FakeMatrixRoom(sessionId = A_USER_ID)
val joinedRoom = FakeBaseRoom(sessionId = A_USER_ID)
moleculeFlow(RecompositionMode.Immediate) {
matrixRoom.getCurrentRoomMember(
MatrixRoomMembersState.Ready(
joinedRoom.getCurrentRoomMember(
RoomMembersState.Ready(
persistentListOf(
roomMember1,
roomMember2,
@ -174,10 +174,10 @@ class MatrixRoomMembersTest {
@Test
fun `getCurrentRoomMember returns null if the member is not found`() = runTest {
val matrixRoom = FakeMatrixRoom(sessionId = A_USER_ID)
val joinedRoom = FakeBaseRoom(sessionId = A_USER_ID)
moleculeFlow(RecompositionMode.Immediate) {
matrixRoom.getCurrentRoomMember(
MatrixRoomMembersState.Ready(
joinedRoom.getCurrentRoomMember(
RoomMembersState.Ready(
persistentListOf(
roomMember2,
roomMember3,