Merge branch 'develop' into feature/fga/space_manage_rooms

This commit is contained in:
ganfra 2026-01-14 17:54:43 +01:00
commit bb082191e4
495 changed files with 5979 additions and 3199 deletions

View file

@ -203,6 +203,7 @@ class RustMatrixClient(
roomMembershipObserver = roomMembershipObserver,
sessionCoroutineScope = sessionCoroutineScope,
sessionDispatcher = sessionDispatcher,
analyticsService = analyticsService,
)
override val sessionVerificationService = RustSessionVerificationService(
@ -393,6 +394,7 @@ class RustMatrixClient(
joinRuleOverride = createRoomParams.joinRuleOverride?.map(),
historyVisibilityOverride = createRoomParams.historyVisibilityOverride?.map(),
canonicalAlias = createRoomParams.roomAliasName.getOrNull(),
isSpace = createRoomParams.isSpace,
)
val roomId = RoomId(innerClient.createRoom(rustParams))
// Wait to receive the room back from the sync but do not returns failure if it fails.

View file

@ -56,7 +56,7 @@ internal class RoomListFactory(
val loadingStateFlow: MutableStateFlow<RoomList.LoadingState> = MutableStateFlow(RoomList.LoadingState.NotLoaded)
val filteredSummariesFlow = MutableSharedFlow<List<RoomSummary>>(replay = 1, extraBufferCapacity = 1)
val summariesFlow = MutableSharedFlow<List<RoomSummary>>(replay = 1, extraBufferCapacity = 1)
val processor = RoomSummaryListProcessor(summariesFlow, innerRoomListService, coroutineContext, roomSummaryFactory)
val processor = RoomSummaryListProcessor(summariesFlow, innerRoomListService, coroutineContext, roomSummaryFactory, analyticsService)
// Makes sure we don't miss any events
val dynamicEvents = MutableSharedFlow<RoomListDynamicEvents>(replay = 100)
val currentFilter = MutableStateFlow(initialFilter)

View file

@ -9,6 +9,7 @@
package io.element.android.libraries.matrix.impl.roomlist
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@ -18,6 +19,7 @@ import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
import org.matrix.rustcomponents.sdk.RoomListServiceInterface
import org.matrix.rustcomponents.sdk.use
import timber.log.Timber
import kotlin.collections.groupingBy
import kotlin.coroutines.CoroutineContext
class RoomSummaryListProcessor(
@ -25,26 +27,21 @@ class RoomSummaryListProcessor(
private val roomListService: RoomListServiceInterface,
private val coroutineContext: CoroutineContext,
private val roomSummaryFactory: RoomSummaryFactory,
private val analyticsService: AnalyticsService,
) {
private val mutex = Mutex()
suspend fun postUpdate(updates: List<RoomListEntriesUpdate>) {
updateRoomSummaries {
updateRoomSummaries(updates) {
Timber.v("Update rooms from postUpdates (with ${updates.size} items) on ${Thread.currentThread()}")
updates.forEach { update ->
applyUpdate(update)
}
// TODO remove once https://github.com/element-hq/element-x-android/issues/5031 has been confirmed as fixed
val duplicates = groupingBy { it.roomId }.eachCount().filter { it.value > 1 }
if (duplicates.isNotEmpty()) {
Timber.e("Found duplicates in room summaries after a list update from the SDK: $duplicates. Updates: $updates")
}
}
}
suspend fun rebuildRoomSummaries() {
updateRoomSummaries {
updateRoomSummaries(emptyList()) {
forEachIndexed { i, summary ->
val result = buildRoomSummaryForIdentifier(summary.roomId.value)
if (result != null) {
@ -112,12 +109,32 @@ class RoomSummaryListProcessor(
}
}
private suspend fun updateRoomSummaries(block: suspend MutableList<RoomSummary>.() -> Unit) = withContext(coroutineContext) {
private suspend fun updateRoomSummaries(updates: List<RoomListEntriesUpdate>, block: suspend MutableList<RoomSummary>.() -> Unit) = withContext(
coroutineContext
) {
mutex.withLock {
val current = roomSummaries.replayCache.lastOrNull()
val mutableRoomSummaries = current.orEmpty().toMutableList()
block(mutableRoomSummaries)
roomSummaries.emit(mutableRoomSummaries)
// TODO remove once https://github.com/element-hq/element-x-android/issues/5031 has been confirmed as fixed
val uniqueRooms = mutableRoomSummaries.distinctBy { it.roomId }
if (uniqueRooms.size != mutableRoomSummaries.size) {
val duplicates = mutableRoomSummaries.groupingBy { it.roomId }.eachCount().filter { it.value > 1 }
if (duplicates.isNotEmpty()) {
analyticsService.trackError(
IllegalStateException(
"Found duplicates in room summaries after a list update from the SDK: $duplicates. " +
"Updates: ${updates.description()}"
)
)
}
}
roomSummaries.emit(uniqueRooms)
}
}
}
private fun List<RoomListEntriesUpdate>.description(): String = joinToString { it.describe() }

View file

@ -12,6 +12,7 @@ import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -32,6 +33,7 @@ class RustSpaceRoomList(
private val innerProvider: suspend () -> InnerSpaceRoomList,
private val coroutineScope: CoroutineScope,
spaceRoomMapper: SpaceRoomMapper,
private val analyticsService: AnalyticsService,
) : SpaceRoomList {
private val innerCompletable = CompletableDeferred<InnerSpaceRoomList>()
@ -43,7 +45,8 @@ class RustSpaceRoomList(
MutableStateFlow(SpaceRoomList.PaginationStatus.Idle(hasMoreToLoad = false))
private val spaceListUpdateProcessor = SpaceListUpdateProcessor(
spaceRoomsFlow = spaceRoomsFlow,
mapper = spaceRoomMapper
mapper = spaceRoomMapper,
analyticsService = analyticsService,
)
init {

View file

@ -17,6 +17,7 @@ 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.SpaceService
import io.element.android.libraries.matrix.impl.util.cancelAndDestroy
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
@ -41,23 +42,40 @@ class RustSpaceService(
private val sessionCoroutineScope: CoroutineScope,
private val sessionDispatcher: CoroutineDispatcher,
private val roomMembershipObserver: RoomMembershipObserver,
private val analyticsService: AnalyticsService,
) : SpaceService {
private val spaceRoomMapper = SpaceRoomMapper()
override val spaceRoomsFlow = MutableSharedFlow<List<SpaceRoom>>(replay = 1, extraBufferCapacity = 1)
private val spaceListUpdateProcessor = SpaceListUpdateProcessor(
spaceRoomsFlow = spaceRoomsFlow,
mapper = spaceRoomMapper
mapper = spaceRoomMapper,
analyticsService = analyticsService,
)
override suspend fun joinedSpaces(): Result<List<SpaceRoom>> = withContext(sessionDispatcher) {
runCatchingExceptions {
innerSpaceService.topLevelJoinedSpaces()
.map {
it.let(spaceRoomMapper::map)
}
innerSpaceService
.topLevelJoinedSpaces()
.map(spaceRoomMapper::map)
}
}
override suspend fun joinedParents(spaceId: RoomId): Result<List<SpaceRoom>> = withContext(sessionDispatcher) {
runCatchingExceptions {
innerSpaceService
.joinedParentsOfChild(spaceId.value)
.map(spaceRoomMapper::map)
}
}
override suspend fun getSpaceRoom(spaceId: RoomId): SpaceRoom? = withContext(sessionDispatcher) {
runCatchingExceptions {
innerSpaceService.getSpaceRoom(spaceId.value)?.let { spaceRoom ->
spaceRoomMapper.map(spaceRoom)
}
}.getOrNull()
}
override fun spaceRoomList(id: RoomId): SpaceRoomList {
val childCoroutineScope = sessionCoroutineScope.childScope(sessionDispatcher, "SpaceRoomListScope-$this")
return RustSpaceRoomList(
@ -65,6 +83,7 @@ class RustSpaceService(
innerProvider = { innerSpaceService.spaceRoomList(id.value) },
coroutineScope = childCoroutineScope,
spaceRoomMapper = spaceRoomMapper,
analyticsService = analyticsService,
)
}

View file

@ -9,6 +9,7 @@
package io.element.android.libraries.matrix.impl.spaces
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.sync.Mutex
@ -19,17 +20,18 @@ import timber.log.Timber
internal class SpaceListUpdateProcessor(
private val spaceRoomsFlow: MutableSharedFlow<List<SpaceRoom>>,
private val mapper: SpaceRoomMapper,
private val analyticsService: AnalyticsService,
) {
private val mutex = Mutex()
suspend fun postUpdates(updates: List<SpaceListUpdate>) {
Timber.v("Update space rooms from postUpdates (with ${updates.size} items) on ${Thread.currentThread()}")
updateSpaceRooms {
updateSpaceRooms(updates) {
updates.forEach { update -> applyUpdate(update) }
}
}
private suspend fun updateSpaceRooms(block: MutableList<SpaceRoom>.() -> Unit) =
private suspend fun updateSpaceRooms(updates: List<SpaceListUpdate>, block: MutableList<SpaceRoom>.() -> Unit) =
mutex.withLock {
val spaceRooms = if (spaceRoomsFlow.replayCache.isNotEmpty()) {
spaceRoomsFlow.first().toMutableList()
@ -37,7 +39,17 @@ internal class SpaceListUpdateProcessor(
mutableListOf()
}
block(spaceRooms)
spaceRoomsFlow.emit(spaceRooms)
val uniqueRooms = spaceRooms.distinctBy { it.roomId }
// TODO remove once https://github.com/element-hq/element-x-android/issues/5031 has been confirmed as fixed
if (spaceRooms.size != uniqueRooms.size) {
val duplicateKeys = spaceRooms.groupBy { it.roomId }.filter { it.value.size > 1 }.keys
analyticsService.trackError(
IllegalStateException("Found duplicate keys in space rooms list ($duplicateKeys) after SDK updates: ${updates.description()}")
)
}
spaceRoomsFlow.emit(uniqueRooms)
}
private fun MutableList<SpaceRoom>.applyUpdate(update: SpaceListUpdate) {
@ -83,3 +95,19 @@ internal class SpaceListUpdateProcessor(
}
}
}
private fun List<SpaceListUpdate>.description(): String = joinToString { it.description() }
private fun SpaceListUpdate.description(): String = when (this) {
is SpaceListUpdate.Append -> "Append(${values.map { it.roomId }})"
SpaceListUpdate.Clear -> "Clear"
is SpaceListUpdate.Insert -> "Insert($index, ${value.roomId})"
SpaceListUpdate.PopBack -> "PopBack"
SpaceListUpdate.PopFront -> "PopFront"
is SpaceListUpdate.PushBack -> "PushBack(${value.roomId})"
is SpaceListUpdate.PushFront -> "PushFront(${value.roomId})"
is SpaceListUpdate.Remove -> "Remove($index)"
is SpaceListUpdate.Reset -> "Reset(${values.map { it.roomId }})"
is SpaceListUpdate.Set -> "Set($index, ${value.roomId})"
is SpaceListUpdate.Truncate -> "Truncate($length)"
}

View file

@ -16,7 +16,9 @@ import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiRoomListSe
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.room.aRoomSummary
import io.element.android.services.analytics.test.FakeAnalyticsService
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
@ -33,11 +35,10 @@ class RoomSummaryListProcessorTest {
summaries.value = listOf(aRoomSummary())
val processor = createProcessor()
val newEntry = aRustRoom(A_ROOM_ID_2)
processor.postUpdate(listOf(RoomListEntriesUpdate.Append(listOf(newEntry, newEntry, newEntry))))
processor.postUpdate(listOf(RoomListEntriesUpdate.Append(listOf(aRustRoom(A_ROOM_ID_2), aRustRoom(A_ROOM_ID_3), aRustRoom(A_ROOM_ID_4)))))
assertThat(summaries.value.count()).isEqualTo(4)
assertThat(summaries.value.subList(1, 4).all { it.roomId == A_ROOM_ID_2 }).isTrue()
assertThat(summaries.value.subList(1, 4).map { it.roomId }).isEqualTo(listOf(A_ROOM_ID_2, A_ROOM_ID_3, A_ROOM_ID_4))
}
@Test
@ -182,5 +183,6 @@ class RoomSummaryListProcessorTest {
FakeFfiRoomListService(),
coroutineContext = StandardTestDispatcher(testScheduler),
roomSummaryFactory = RoomSummaryFactory(),
analyticsService = FakeAnalyticsService(),
)
}

View file

@ -14,7 +14,9 @@ import io.element.android.libraries.matrix.impl.fixtures.factories.aRustSpaceRoo
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.previewutils.room.aSpaceRoom
import io.element.android.services.analytics.test.FakeAnalyticsService
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
@ -29,11 +31,14 @@ class RoomSummaryListProcessorTest {
spaceRoomsFlow.value = listOf(aSpaceRoom())
val processor = createProcessor()
val newEntry = aRustSpaceRoom(roomId = A_ROOM_ID_2)
processor.postUpdates(listOf(SpaceListUpdate.Append(listOf(newEntry, newEntry, newEntry))))
processor.postUpdates(
listOf(
SpaceListUpdate.Append(listOf(aRustSpaceRoom(roomId = A_ROOM_ID_2), aRustSpaceRoom(roomId = A_ROOM_ID_3), aRustSpaceRoom(roomId = A_ROOM_ID_4)))
)
)
assertThat(spaceRoomsFlow.value.count()).isEqualTo(4)
assertThat(spaceRoomsFlow.value.subList(1, 4).all { it.roomId == A_ROOM_ID_2 }).isTrue()
assertThat(spaceRoomsFlow.value.subList(1, 4).map { it.roomId }).isEqualTo(listOf(A_ROOM_ID_2, A_ROOM_ID_3, A_ROOM_ID_4))
}
@Test
@ -186,5 +191,6 @@ class RoomSummaryListProcessorTest {
) = SpaceListUpdateProcessor(
spaceRoomsFlow = spaceRoomsFlow,
mapper = SpaceRoomMapper(),
analyticsService = FakeAnalyticsService(),
)
}

View file

@ -18,6 +18,7 @@ import io.element.android.libraries.matrix.impl.fixtures.factories.aRustSpaceRoo
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiSpaceRoomList
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.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.lambda.lambdaRecorder
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
@ -97,6 +98,7 @@ class RustSpaceRoomListTest {
innerProvider = innerProvider,
coroutineScope = backgroundScope,
spaceRoomMapper = spaceRoomMapper,
analyticsService = FakeAnalyticsService(),
)
}
}