Merge pull request #3631 from element-hq/feature/fga/rework_room_summary

Rework room summary
This commit is contained in:
ganfra 2024-10-09 11:44:45 +02:00 committed by GitHub
commit a3db4b2043
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
70 changed files with 734 additions and 563 deletions

View file

@ -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<RoomSummary>) -> Boolean = when (roomIdOrAlias) {
is RoomIdOrAlias.Alias -> { roomSummaries: List<RoomSummary> ->
val found = roomSummaries.find { it.aliases.contains(roomIdOrAlias.roomAlias) }
found != null && found.currentUserMembership == CurrentUserMembership.JOINED
}
is RoomIdOrAlias.Id -> { roomSummaries: List<RoomSummary> ->
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<Optional<MatrixRoomInfo>> {
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<Optional<RoomSummary>> {
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) {

View file

@ -53,6 +53,10 @@ class MatrixRoomInfoMapper {
activeRoomCallParticipants = it.activeRoomCallParticipants.map(::UserId).toImmutableList(),
heroes = it.elementHeroes().toImmutableList(),
pinnedEventIds = it.pinnedEventIds.map(::EventId).toImmutableList(),
isMarkedUnread = it.isMarkedUnread,
numUnreadMessages = it.numUnreadMessages.toLong(),
numUnreadMentions = it.numUnreadMentions.toLong(),
numUnreadNotifications = it.numUnreadNotifications.toLong(),
)
}
}

View file

@ -31,7 +31,7 @@ internal class RoomListFactory(
private val innerRoomListService: RoomListService,
private val sessionCoroutineScope: CoroutineScope,
) {
private val roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory()
private val roomSummaryDetailsFactory: RoomSummaryFactory = RoomSummaryFactory()
/**
* Creates a room list that can be used to load more rooms and filter them dynamically.

View file

@ -8,6 +8,7 @@
package io.element.android.libraries.matrix.impl.roomlist
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
@ -17,19 +18,19 @@ val RoomListFilter.predicate
is RoomListFilter.Any -> { _: RoomSummary -> true }
RoomListFilter.None -> { _: RoomSummary -> false }
RoomListFilter.Category.Group -> { roomSummary: RoomSummary ->
!roomSummary.isDm && !roomSummary.isInvited()
!roomSummary.info.isDm && !roomSummary.isInvited()
}
RoomListFilter.Category.People -> { roomSummary: RoomSummary ->
roomSummary.isDm && !roomSummary.isInvited()
roomSummary.info.isDm && !roomSummary.isInvited()
}
RoomListFilter.Favorite -> { roomSummary: RoomSummary ->
roomSummary.isFavorite && !roomSummary.isInvited()
roomSummary.info.isFavorite && !roomSummary.isInvited()
}
RoomListFilter.Unread -> { roomSummary: RoomSummary ->
!roomSummary.isInvited() && (roomSummary.numUnreadNotifications > 0 || roomSummary.isMarkedUnread)
!roomSummary.isInvited() && (roomSummary.info.numUnreadNotifications > 0 || roomSummary.info.isMarkedUnread)
}
is RoomListFilter.NormalizedMatchRoomName -> { roomSummary: RoomSummary ->
roomSummary.name.orEmpty().contains(pattern, ignoreCase = true)
roomSummary.info.name.orEmpty().contains(pattern, ignoreCase = true)
}
RoomListFilter.Invite -> { roomSummary: RoomSummary ->
roomSummary.isInvited()
@ -50,4 +51,4 @@ fun List<RoomSummary>.filter(filter: RoomListFilter): List<RoomSummary> {
}
}
private fun RoomSummary.isInvited() = currentUserMembership == CurrentUserMembership.INVITED
private fun RoomSummary.isInvited() = info.currentUserMembership == CurrentUserMembership.INVITED

View file

@ -1,51 +0,0 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.roomlist
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.room.isDm
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.impl.notificationsettings.RoomNotificationSettingsMapper
import io.element.android.libraries.matrix.impl.room.elementHeroes
import io.element.android.libraries.matrix.impl.room.map
import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper
import io.element.android.libraries.matrix.impl.room.message.RoomMessageFactory
import org.matrix.rustcomponents.sdk.RoomListItem
import org.matrix.rustcomponents.sdk.use
class RoomSummaryDetailsFactory(
private val roomMessageFactory: RoomMessageFactory = RoomMessageFactory(),
) {
suspend fun create(roomListItem: RoomListItem): RoomSummary {
val roomInfo = roomListItem.roomInfo()
val latestRoomMessage = roomListItem.latestEvent().use { event ->
roomMessageFactory.create(event)
}
return RoomSummary(
roomId = RoomId(roomInfo.id),
name = roomInfo.displayName,
canonicalAlias = roomInfo.canonicalAlias?.let(::RoomAlias),
alternativeAliases = roomInfo.alternativeAliases.map(::RoomAlias),
isDirect = roomInfo.isDirect,
avatarUrl = roomInfo.avatarUrl,
numUnreadMentions = roomInfo.numUnreadMentions.toInt(),
numUnreadMessages = roomInfo.numUnreadMessages.toInt(),
numUnreadNotifications = roomInfo.numUnreadNotifications.toInt(),
isMarkedUnread = roomInfo.isMarkedUnread,
lastMessage = latestRoomMessage,
inviter = roomInfo.inviter?.let(RoomMemberMapper::map),
userDefinedNotificationMode = roomInfo.cachedUserDefinedNotificationMode?.let(RoomNotificationSettingsMapper::mapMode),
hasRoomCall = roomInfo.hasRoomCall,
isDm = isDm(isDirect = roomInfo.isDirect, activeMembersCount = roomInfo.activeMembersCount.toInt()),
isFavorite = roomInfo.isFavourite,
currentUserMembership = roomInfo.membership.map(),
heroes = roomInfo.elementHeroes(),
)
}
}

View file

@ -0,0 +1,30 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.roomlist
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.impl.room.MatrixRoomInfoMapper
import io.element.android.libraries.matrix.impl.room.message.RoomMessageFactory
import org.matrix.rustcomponents.sdk.RoomListItem
import org.matrix.rustcomponents.sdk.use
class RoomSummaryFactory(
private val roomMessageFactory: RoomMessageFactory = RoomMessageFactory(),
private val roomInfoMapper: MatrixRoomInfoMapper = MatrixRoomInfoMapper(),
) {
suspend fun create(roomListItem: RoomListItem): RoomSummary {
val roomInfo = roomListItem.roomInfo().let(roomInfoMapper::map)
val latestRoomMessage = roomListItem.latestEvent().use { event ->
roomMessageFactory.create(event)
}
return RoomSummary(
info = roomInfo,
lastMessage = latestRoomMessage,
)
}
}

View file

@ -23,7 +23,7 @@ class RoomSummaryListProcessor(
private val roomSummaries: MutableSharedFlow<List<RoomSummary>>,
private val roomListService: RoomListServiceInterface,
private val coroutineContext: CoroutineContext,
private val roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(),
private val roomSummaryDetailsFactory: RoomSummaryFactory = RoomSummaryFactory(),
) {
private val roomSummariesByIdentifier = HashMap<String, RoomSummary>()
private val mutex = Mutex()

View file

@ -105,6 +105,10 @@ class MatrixRoomInfoMapperTest {
).toImmutableList(),
pinnedEventIds = listOf(AN_EVENT_ID).toPersistentList(),
creator = A_USER_ID,
isMarkedUnread = false,
numUnreadMessages = 12L,
numUnreadNotifications = 13L,
numUnreadMentions = 14L,
)
)
}
@ -174,6 +178,10 @@ class MatrixRoomInfoMapperTest {
heroes = emptyList<MatrixUser>().toImmutableList(),
pinnedEventIds = emptyList<EventId>().toPersistentList(),
creator = null,
isMarkedUnread = true,
numUnreadMessages = 12L,
numUnreadNotifications = 13L,
numUnreadMentions = 14L,
)
)
}

View file

@ -19,7 +19,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
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.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled
import io.element.android.libraries.matrix.test.room.aRoomSummary
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
@ -29,7 +29,7 @@ import org.junit.Test
class DefaultJoinRoomTest {
@Test
fun `when using roomId and there is no server names, the classic join room API is used`() = runTest {
val roomSummary = aRoomSummaryFilled()
val roomSummary = aRoomSummary()
val joinRoomLambda = lambdaRecorder { _: RoomId -> Result.success(roomSummary) }
val joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomIdOrAlias, _: List<String> -> Result.success(roomSummary) }
val roomResult = FakeMatrixRoom()
@ -64,7 +64,7 @@ class DefaultJoinRoomTest {
@Test
fun `when using roomId and server names are available, joinRoomByIdOrAlias API is used`() = runTest {
val roomSummary = aRoomSummaryFilled()
val roomSummary = aRoomSummary()
val joinRoomLambda = lambdaRecorder { _: RoomId -> Result.success(roomSummary) }
val joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomIdOrAlias, _: List<String> -> Result.success(roomSummary) }
val roomResult = FakeMatrixRoom()
@ -100,7 +100,7 @@ class DefaultJoinRoomTest {
@Test
fun `when using roomAlias, joinRoomByIdOrAlias API is used`() = runTest {
val roomSummary = aRoomSummaryFilled()
val roomSummary = aRoomSummary()
val joinRoomLambda = lambdaRecorder { _: RoomId -> Result.success(roomSummary) }
val joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomIdOrAlias, _: List<String> -> Result.success(roomSummary) }
val roomResult = FakeMatrixRoom()

View file

@ -16,10 +16,11 @@ import org.junit.Test
class RoomListFilterTest {
private val regularRoom = aRoomSummary(
isDm = false
isDirect = false,
)
private val dmRoom = aRoomSummary(
isDm = true
isDirect = true,
activeMembersCount = 2
)
private val favoriteRoom = aRoomSummary(
isFavorite = true

View file

@ -15,7 +15,6 @@ 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.room.aRoomSummary
import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
@ -40,7 +39,7 @@ class RoomSummaryListProcessorTest {
@Test
fun `PushBack adds a new entry at the end of the list`() = runTest {
summaries.value = listOf(aRoomSummaryFilled())
summaries.value = listOf(aRoomSummary())
val processor = createProcessor()
processor.postUpdate(listOf(RoomListEntriesUpdate.PushBack(FakeRustRoomListItem(A_ROOM_ID_2))))
@ -50,7 +49,7 @@ class RoomSummaryListProcessorTest {
@Test
fun `PushFront inserts a new entry at the start of the list`() = runTest {
summaries.value = listOf(aRoomSummaryFilled())
summaries.value = listOf(aRoomSummary())
val processor = createProcessor()
processor.postUpdate(listOf(RoomListEntriesUpdate.PushFront(FakeRustRoomListItem(A_ROOM_ID_2))))
@ -60,7 +59,7 @@ class RoomSummaryListProcessorTest {
@Test
fun `Set replaces an entry at some index`() = runTest {
summaries.value = listOf(aRoomSummaryFilled())
summaries.value = listOf(aRoomSummary())
val processor = createProcessor()
val index = 0
@ -72,7 +71,7 @@ class RoomSummaryListProcessorTest {
@Test
fun `Insert inserts a new entry at the provided index`() = runTest {
summaries.value = listOf(aRoomSummaryFilled())
summaries.value = listOf(aRoomSummary())
val processor = createProcessor()
val index = 0
@ -84,7 +83,10 @@ class RoomSummaryListProcessorTest {
@Test
fun `Remove removes an entry at some index`() = runTest {
summaries.value = listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(A_ROOM_ID_2))
summaries.value = listOf(
aRoomSummary(roomId = A_ROOM_ID),
aRoomSummary(A_ROOM_ID_2)
)
val processor = createProcessor()
val index = 0
@ -96,7 +98,10 @@ class RoomSummaryListProcessorTest {
@Test
fun `PopBack removes an entry at the end of the list`() = runTest {
summaries.value = listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(A_ROOM_ID_2))
summaries.value = listOf(
aRoomSummary(roomId = A_ROOM_ID),
aRoomSummary(A_ROOM_ID_2)
)
val processor = createProcessor()
val index = 0
@ -108,7 +113,10 @@ class RoomSummaryListProcessorTest {
@Test
fun `PopFront removes an entry at the start of the list`() = runTest {
summaries.value = listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(A_ROOM_ID_2))
summaries.value = listOf(
aRoomSummary(roomId = A_ROOM_ID),
aRoomSummary(A_ROOM_ID_2)
)
val processor = createProcessor()
val index = 0
@ -120,7 +128,10 @@ class RoomSummaryListProcessorTest {
@Test
fun `Clear removes all the entries`() = runTest {
summaries.value = listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(A_ROOM_ID_2))
summaries.value = listOf(
aRoomSummary(roomId = A_ROOM_ID),
aRoomSummary(A_ROOM_ID_2)
)
val processor = createProcessor()
processor.postUpdate(listOf(RoomListEntriesUpdate.Clear))
@ -130,7 +141,10 @@ class RoomSummaryListProcessorTest {
@Test
fun `Truncate removes all entries after the provided length`() = runTest {
summaries.value = listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(A_ROOM_ID_2))
summaries.value = listOf(
aRoomSummary(roomId = A_ROOM_ID),
aRoomSummary(A_ROOM_ID_2)
)
val processor = createProcessor()
val index = 0
@ -142,7 +156,10 @@ class RoomSummaryListProcessorTest {
@Test
fun `Reset removes all entries and add the provided ones`() = runTest {
summaries.value = listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(A_ROOM_ID_2))
summaries.value = listOf(
aRoomSummary(roomId = A_ROOM_ID),
aRoomSummary(A_ROOM_ID_2)
)
val processor = createProcessor()
val index = 0
@ -156,6 +173,6 @@ class RoomSummaryListProcessorTest {
summaries,
FakeRustRoomListService(),
coroutineContext = StandardTestDispatcher(testScheduler),
roomSummaryDetailsFactory = RoomSummaryDetailsFactory(),
roomSummaryDetailsFactory = RoomSummaryFactory(),
)
}