diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt index 3e2a805b5c..0562c5e2d6 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt @@ -45,6 +45,8 @@ import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomIdOrAlias +import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias +import io.element.android.libraries.matrix.api.getRoomInfoFlow import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias import kotlinx.coroutines.flow.combine @@ -120,12 +122,9 @@ class RoomFlowNode @AssistedInject constructor( } private fun subscribeToRoomInfoFlow(roomId: RoomId, serverNames: List) { - val roomInfoFlow = client.getRoomInfoFlow( - roomId = roomId - ).map { it.getOrNull() } - - val isSpaceFlow = roomInfoFlow.map { it?.isSpace.orFalse() }.distinctUntilChanged() - val currentMembershipFlow = roomInfoFlow.map { it?.currentUserMembership }.distinctUntilChanged() + val roomInfoFlow = client.getRoomInfoFlow(roomIdOrAlias = roomId.toRoomIdOrAlias()) + val isSpaceFlow = roomInfoFlow.map { it.getOrNull()?.isSpace.orFalse() }.distinctUntilChanged() + val currentMembershipFlow = roomInfoFlow.map { it.getOrNull()?.currentUserMembership }.distinctUntilChanged() combine(currentMembershipFlow, isSpaceFlow) { membership, isSpace -> Timber.d("Room membership: $membership") when (membership) { diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt index eceece9660..8e0fa9193e 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt @@ -33,6 +33,8 @@ import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomIdOrAlias +import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias +import io.element.android.libraries.matrix.api.getRoomInfoFlow import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.RoomType @@ -70,7 +72,7 @@ class JoinRoomPresenter @AssistedInject constructor( override fun present(): JoinRoomState { val coroutineScope = rememberCoroutineScope() var retryCount by remember { mutableIntStateOf(0) } - val roomInfo by matrixClient.getRoomInfoFlow(roomId).collectAsState(initial = Optional.empty()) + val roomInfo by matrixClient.getRoomInfoFlow(roomId.toRoomIdOrAlias()).collectAsState(initial = Optional.empty()) val joinAction: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) } val knockAction: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) } val contentState by produceState( @@ -204,7 +206,7 @@ internal fun MatrixRoomInfo.toContentState(): ContentState { name = name, topic = topic, alias = canonicalAlias, - numberOfMembers = activeMembersCount, + numberOfMembers = activeMembersCount.toLong(), isDm = isDm, roomType = if (isSpace) RoomType.Space else RoomType.Room, roomAvatarUrl = avatarUrl, diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt index 730bedea98..a560b5fea8 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt @@ -19,7 +19,6 @@ import io.element.android.libraries.matrix.api.core.RoomIdOrAlias import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias import io.element.android.libraries.matrix.api.room.RoomType -import io.element.android.libraries.matrix.api.room.isDm import io.element.android.libraries.matrix.ui.model.InviteSender open class JoinRoomStateProvider : PreviewParameterProvider { diff --git a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt index 835640bdea..0b2206d6df 100644 --- a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt +++ b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt @@ -32,8 +32,8 @@ import io.element.android.libraries.matrix.test.A_ROOM_NAME import io.element.android.libraries.matrix.test.A_SERVER_LIST import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.core.aBuildMeta -import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.aRoomMember +import io.element.android.libraries.matrix.test.room.aRoomSummary import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom import io.element.android.libraries.matrix.ui.model.toInviteSender import io.element.android.tests.testutils.WarmUpRule @@ -67,10 +67,10 @@ class JoinRoomPresenterTest { @Test fun `present - when room is joined then content state is filled with his data`() = runTest { - val roomInfo = aRoomInfo() + val roomSummary = aRoomSummary() val matrixClient = FakeMatrixClient().apply { - getRoomInfoFlowLambda = { _ -> - flowOf(Optional.of(roomInfo)) + getRoomSummaryFlowLambda = { _ -> + flowOf(Optional.of(roomSummary)) } } val presenter = createJoinRoomPresenter( @@ -81,22 +81,22 @@ class JoinRoomPresenterTest { awaitItem().also { state -> val contentState = state.contentState as ContentState.Loaded assertThat(contentState.roomId).isEqualTo(A_ROOM_ID) - assertThat(contentState.name).isEqualTo(roomInfo.name) - assertThat(contentState.topic).isEqualTo(roomInfo.topic) - assertThat(contentState.alias).isEqualTo(roomInfo.canonicalAlias) - assertThat(contentState.numberOfMembers).isEqualTo(roomInfo.activeMembersCount) - assertThat(contentState.isDm).isEqualTo(roomInfo.isDirect) - assertThat(contentState.roomAvatarUrl).isEqualTo(roomInfo.avatarUrl) + assertThat(contentState.name).isEqualTo(roomSummary.info.name) + assertThat(contentState.topic).isEqualTo(roomSummary.info.topic) + assertThat(contentState.alias).isEqualTo(roomSummary.info.canonicalAlias) + assertThat(contentState.numberOfMembers).isEqualTo(roomSummary.info.activeMembersCount) + assertThat(contentState.isDm).isEqualTo(roomSummary.info.isDirect) + assertThat(contentState.roomAvatarUrl).isEqualTo(roomSummary.info.avatarUrl) } } } @Test fun `present - when room is invited then join authorization is equal to invited`() = runTest { - val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.INVITED) + val roomSummary = aRoomSummary(currentUserMembership = CurrentUserMembership.INVITED) val matrixClient = FakeMatrixClient().apply { - getRoomInfoFlowLambda = { _ -> - flowOf(Optional.of(roomInfo)) + getRoomSummaryFlowLambda = { _ -> + flowOf(Optional.of(roomSummary)) } } val presenter = createJoinRoomPresenter( @@ -114,13 +114,13 @@ class JoinRoomPresenterTest { fun `present - when room is invited then join authorization is equal to invited, an inviter is provided`() = runTest { val inviter = aRoomMember(userId = UserId("@bob:example.com"), displayName = "Bob") val expectedInviteSender = inviter.toInviteSender() - val roomInfo = aRoomInfo( + val roomSummary = aRoomSummary( currentUserMembership = CurrentUserMembership.INVITED, inviter = inviter, ) val matrixClient = FakeMatrixClient().apply { - getRoomInfoFlowLambda = { _ -> - flowOf(Optional.of(roomInfo)) + getRoomSummaryFlowLambda = { _ -> + flowOf(Optional.of(roomSummary)) } } val presenter = createJoinRoomPresenter( @@ -140,10 +140,10 @@ class JoinRoomPresenterTest { val acceptDeclinePresenter = Presenter { anAcceptDeclineInviteState(eventSink = eventSinkRecorder) } - val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.INVITED) + val roomSummary = aRoomSummary(currentUserMembership = CurrentUserMembership.INVITED) val matrixClient = FakeMatrixClient().apply { - getRoomInfoFlowLambda = { _ -> - flowOf(Optional.of(roomInfo)) + getRoomSummaryFlowLambda = { _ -> + flowOf(Optional.of(roomSummary)) } } val presenter = createJoinRoomPresenter( @@ -224,10 +224,10 @@ class JoinRoomPresenterTest { @Test fun `present - when room is left and public then join authorization is equal to canJoin`() = runTest { - val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.LEFT, isPublic = true) + val roomSummary = aRoomSummary(currentUserMembership = CurrentUserMembership.LEFT, isPublic = true) val matrixClient = FakeMatrixClient().apply { - getRoomInfoFlowLambda = { _ -> - flowOf(Optional.of(roomInfo)) + getRoomSummaryFlowLambda = { _ -> + flowOf(Optional.of(roomSummary)) } } val presenter = createJoinRoomPresenter( @@ -243,10 +243,10 @@ class JoinRoomPresenterTest { @Test fun `present - when room is left and not public then join authorization is equal to unknown`() = runTest { - val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.LEFT, isPublic = false) + val roomSummary = aRoomSummary(currentUserMembership = CurrentUserMembership.LEFT, isPublic = false) val matrixClient = FakeMatrixClient().apply { - getRoomInfoFlowLambda = { _ -> - flowOf(Optional.of(roomInfo)) + getRoomSummaryFlowLambda = { _ -> + flowOf(Optional.of(roomSummary)) } } val presenter = createJoinRoomPresenter( diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt index 6dacb03dfb..ea26e29719 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt @@ -38,6 +38,8 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map import java.io.Closeable import java.util.Optional @@ -94,7 +96,13 @@ interface MatrixClient : Closeable { suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result fun roomMembershipObserver(): RoomMembershipObserver - fun getRoomInfoFlow(roomId: RoomId): Flow> + + /** + * Get a room summary flow for a given room ID or alias. + * The flow will emit a new value whenever the room summary is updated. + * The flow will emit Optional.empty item if the room is not found. + */ + fun getRoomSummaryFlow(roomIdOrAlias: RoomIdOrAlias): Flow> fun isMe(userId: UserId?) = userId == sessionId @@ -142,3 +150,14 @@ interface MatrixClient : Closeable { fun canDeactivateAccount(): Boolean suspend fun deactivateAccount(password: String, eraseData: Boolean): Result } + +/** + * Get a room info flow for a given room ID or alias. + * The flow will emit a new value whenever the room info is updated. + * The flow will emit Optional.empty item if the room is not found. + */ +fun MatrixClient.getRoomInfoFlow(roomIdOrAlias: RoomIdOrAlias): Flow> { + return getRoomSummaryFlow(roomIdOrAlias) + .map { roomSummary -> roomSummary.map { it.info } } + .distinctUntilChanged() +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index b47ac6fdc7..1abe47a362 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -32,7 +32,6 @@ import io.element.android.libraries.matrix.api.pusher.PushersService import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.InvitedRoom import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.api.room.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias import io.element.android.libraries.matrix.api.room.preview.RoomPreview @@ -84,8 +83,8 @@ import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -105,8 +104,8 @@ import org.matrix.rustcomponents.sdk.use import timber.log.Timber import java.io.File import java.util.Optional +import kotlin.jvm.optionals.getOrNull import kotlin.time.Duration -import kotlin.time.Duration.Companion.INFINITE import kotlin.time.Duration.Companion.seconds import org.matrix.rustcomponents.sdk.CreateRoomParameters as RustCreateRoomParameters import org.matrix.rustcomponents.sdk.RoomPreset as RustRoomPreset @@ -262,21 +261,14 @@ class RustMatrixClient( * @param timeout the timeout to wait for the room to be available * @throws TimeoutCancellationException if the room is not available after the timeout */ - private suspend fun awaitJoinedRoom(roomIdOrAlias: RoomIdOrAlias, timeout: Duration): RoomSummary { - val predicate: (List) -> Boolean = when (roomIdOrAlias) { - is RoomIdOrAlias.Alias -> { roomSummaries: List -> - val found = roomSummaries.find { it.aliases.contains(roomIdOrAlias.roomAlias) } - found != null && found.currentUserMembership == CurrentUserMembership.JOINED - } - is RoomIdOrAlias.Id -> { roomSummaries: List -> - val found = roomSummaries.find { it.roomId == roomIdOrAlias.roomId } - found != null && found.currentUserMembership == CurrentUserMembership.JOINED - } - } + private suspend fun awaitJoinedRoom( + roomIdOrAlias: RoomIdOrAlias, + timeout: Duration + ): RoomSummary { return withTimeout(timeout) { - roomListService.allRooms.summaries - .filter(predicate) - .first() + getRoomSummaryFlow(roomIdOrAlias) + .mapNotNull { optionalRoomSummary -> optionalRoomSummary.getOrNull() } + .filter { roomSummary -> roomSummary.info.currentUserMembership == CurrentUserMembership.JOINED } .first() // Ensure that the room is ready .also { client.awaitRoomRemoteEcho(it.roomId.value) } @@ -568,20 +560,21 @@ class RustMatrixClient( override fun roomMembershipObserver(): RoomMembershipObserver = roomMembershipObserver - override fun getRoomInfoFlow(roomId: RoomId): Flow> { - return flow { - var room = getRoom(roomId) - if (room == null) { - emit(Optional.empty()) - awaitJoinedRoom(roomId.toRoomIdOrAlias(), INFINITE) - room = getRoom(roomId) + override fun getRoomSummaryFlow(roomIdOrAlias: RoomIdOrAlias): Flow> { + val predicate: (RoomSummary) -> Boolean = when (roomIdOrAlias) { + is RoomIdOrAlias.Alias -> { roomSummary -> + roomSummary.info.aliases.contains(roomIdOrAlias.roomAlias) } - room?.use { - room.roomInfoFlow - .map { roomInfo -> Optional.of(roomInfo) } - .collect(this) + is RoomIdOrAlias.Id -> { roomSummary -> + roomSummary.roomId == roomIdOrAlias.roomId } - }.distinctUntilChanged() + } + return roomListService.allRooms.summaries + .map { roomSummaries -> + val roomSummary = roomSummaries.firstOrNull(predicate) + Optional.ofNullable(roomSummary) + } + .distinctUntilChanged() } override suspend fun setAllSendQueuesEnabled(enabled: Boolean) = withContext(sessionDispatcher) { diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index 2d5e29f064..d784bba502 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -24,7 +24,6 @@ import io.element.android.libraries.matrix.api.oidc.AccountManagementAction import io.element.android.libraries.matrix.api.pusher.PushersService import io.element.android.libraries.matrix.api.room.InvitedRoom import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.api.room.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias import io.element.android.libraries.matrix.api.room.preview.RoomPreview @@ -118,8 +117,8 @@ class FakeMatrixClient( var knockRoomLambda: (RoomId) -> Result = { Result.success(Unit) } - var getRoomInfoFlowLambda = { _: RoomId -> - flowOf>(Optional.empty()) + var getRoomSummaryFlowLambda = { _: RoomIdOrAlias -> + flowOf>(Optional.empty()) } var logoutLambda: (Boolean, Boolean) -> String? = { _, _ -> null @@ -316,7 +315,7 @@ class FakeMatrixClient( return Result.success(visitedRoomsId) } - override fun getRoomInfoFlow(roomId: RoomId) = getRoomInfoFlowLambda(roomId) + override fun getRoomSummaryFlow(roomIdOrAlias: RoomIdOrAlias) = getRoomSummaryFlowLambda(roomIdOrAlias) var setAllSendQueuesEnabledLambda = lambdaRecorder(ensureNeverCalled = true) { _: Boolean -> // no-op