Merge pull request #6136 from element-hq/feature/fga/space_room_list_filter

Add Space Filters feature for Room List
This commit is contained in:
ganfra 2026-02-05 16:14:16 +01:00 committed by GitHub
commit 6327641469
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
75 changed files with 1329 additions and 182 deletions

View file

@ -14,6 +14,7 @@ import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.Any
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.Category
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.DeduplicateVersions
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.Favourite
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.Identifiers
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.Invite
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.NonLeft
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.NonSpace
@ -60,6 +61,7 @@ internal object RoomListFilterMapper {
return when (filter) {
is RoomListFilter.All -> All(filters = filter.filters.map { mapFilter(it) })
is RoomListFilter.Any -> Any(filters = filter.filters.map { mapFilter(it) })
is RoomListFilter.Identifiers -> Identifiers(identifiers = filter.values.map { it.value })
RoomListFilter.None -> None
RoomListFilter.Category.Group -> Category(RoomListFilterCategory.GROUP)
RoomListFilter.Category.People -> Category(RoomListFilterCategory.PEOPLE)

View file

@ -16,6 +16,7 @@ import io.element.android.libraries.matrix.api.spaces.LeaveSpaceHandle
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.api.spaces.SpaceServiceFilter
import io.element.android.libraries.matrix.impl.util.cancelAndDestroy
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.CoroutineDispatcher
@ -31,9 +32,11 @@ import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.SpaceFilterUpdate
import org.matrix.rustcomponents.sdk.SpaceListUpdate
import org.matrix.rustcomponents.sdk.SpaceServiceInterface
import org.matrix.rustcomponents.sdk.SpaceServiceJoinedSpacesListener
import org.matrix.rustcomponents.sdk.SpaceServiceSpaceFiltersListener
import timber.log.Timber
import org.matrix.rustcomponents.sdk.SpaceService as ClientSpaceService
@ -45,20 +48,20 @@ class RustSpaceService(
private val analyticsService: AnalyticsService,
) : SpaceService {
private val spaceRoomMapper = SpaceRoomMapper()
override val spaceRoomsFlow = MutableSharedFlow<List<SpaceRoom>>(replay = 1, extraBufferCapacity = 1)
private val spaceFilterMapper = SpaceServiceFilterMapper(spaceRoomMapper)
override val topLevelSpacesFlow = MutableSharedFlow<List<SpaceRoom>>(replay = 1, extraBufferCapacity = 1)
private val spaceListUpdateProcessor = SpaceListUpdateProcessor(
spaceRoomsFlow = spaceRoomsFlow,
spaceRoomsFlow = topLevelSpacesFlow,
mapper = spaceRoomMapper,
analyticsService = analyticsService,
)
override suspend fun joinedSpaces(): Result<List<SpaceRoom>> = withContext(sessionDispatcher) {
runCatchingExceptions {
innerSpaceService
.topLevelJoinedSpaces()
.map(spaceRoomMapper::map)
}
}
override val spaceFiltersFlow = MutableSharedFlow<List<SpaceServiceFilter>>(replay = 1, extraBufferCapacity = 1)
private val spaceFilterUpdateProcessor = SpaceServiceFilterUpdateProcessor(
spaceFiltersFlow = spaceFiltersFlow,
mapper = spaceFilterMapper,
)
override suspend fun joinedParents(spaceId: RoomId): Result<List<SpaceRoom>> = withContext(sessionDispatcher) {
runCatchingExceptions {
@ -123,6 +126,13 @@ class RustSpaceService(
spaceListUpdateProcessor.postUpdates(updates)
}
.launchIn(sessionCoroutineScope)
innerSpaceService
.spaceFilterListUpdate()
.onEach { updates ->
spaceFilterUpdateProcessor.postUpdates(updates)
}
.launchIn(sessionCoroutineScope)
}
}
@ -142,3 +152,20 @@ internal fun SpaceServiceInterface.spaceListUpdate(): Flow<List<SpaceListUpdate>
}.catch {
Timber.d(it, "spaceDiffFlow() failed")
}.buffer(Channel.UNLIMITED)
internal fun SpaceServiceInterface.spaceFilterListUpdate(): Flow<List<SpaceFilterUpdate>> =
callbackFlow {
val listener = object : SpaceServiceSpaceFiltersListener {
override fun onUpdate(filterUpdates: List<SpaceFilterUpdate>) {
trySendBlocking(filterUpdates)
}
}
Timber.d("Open spaceFilterDiffFlow for SpaceServiceInterface ${this@spaceFilterListUpdate}")
val taskHandle = subscribeToSpaceFilters(listener)
awaitClose {
Timber.d("Close spaceFilterDiffFlow for SpaceServiceInterface ${this@spaceFilterListUpdate}")
taskHandle.cancelAndDestroy()
}
}.catch {
Timber.d(it, "spaceFilterListUpdate() failed")
}.buffer(Channel.UNLIMITED)

View file

@ -0,0 +1,25 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.spaces
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter
import org.matrix.rustcomponents.sdk.SpaceFilter as RustSpaceFilter
class SpaceServiceFilterMapper(
private val spaceRoomMapper: SpaceRoomMapper,
) {
fun map(spaceFilter: RustSpaceFilter): SpaceServiceFilter {
return SpaceServiceFilter(
spaceRoom = spaceRoomMapper.map(spaceFilter.spaceRoom),
level = spaceFilter.level.toInt(),
descendants = spaceFilter.descendants.map { RoomId(it) },
)
}
}

View file

@ -0,0 +1,85 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.spaces
import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.matrix.rustcomponents.sdk.SpaceFilterUpdate
import timber.log.Timber
internal class SpaceServiceFilterUpdateProcessor(
private val spaceFiltersFlow: MutableSharedFlow<List<SpaceServiceFilter>>,
private val mapper: SpaceServiceFilterMapper,
) {
private val mutex = Mutex()
suspend fun postUpdates(updates: List<SpaceFilterUpdate>) {
Timber.v("Update space filters from postUpdates (with ${updates.size} items) on ${Thread.currentThread()}")
updateSpaceFilters {
updates.forEach { update -> applyUpdate(update) }
}
}
private suspend fun updateSpaceFilters(block: MutableList<SpaceServiceFilter>.() -> Unit) =
mutex.withLock {
val spaceFilters = if (spaceFiltersFlow.replayCache.isNotEmpty()) {
spaceFiltersFlow.first().toMutableList()
} else {
mutableListOf()
}
block(spaceFilters)
spaceFiltersFlow.emit(spaceFilters)
}
private fun MutableList<SpaceServiceFilter>.applyUpdate(update: SpaceFilterUpdate) {
when (update) {
is SpaceFilterUpdate.Append -> {
val newFilters = update.values.map(mapper::map)
addAll(newFilters)
}
SpaceFilterUpdate.Clear -> clear()
is SpaceFilterUpdate.Insert -> {
val newFilter = mapper.map(update.value)
add(update.index.toInt(), newFilter)
}
SpaceFilterUpdate.PopBack -> {
removeAt(lastIndex)
}
SpaceFilterUpdate.PopFront -> {
removeAt(0)
}
is SpaceFilterUpdate.PushBack -> {
val newFilter = mapper.map(update.value)
add(newFilter)
}
is SpaceFilterUpdate.PushFront -> {
val newFilter = mapper.map(update.value)
add(0, newFilter)
}
is SpaceFilterUpdate.Remove -> {
removeAt(update.index.toInt())
}
is SpaceFilterUpdate.Reset -> {
clear()
val newFilters = update.values.map(mapper::map)
addAll(newFilters)
}
is SpaceFilterUpdate.Set -> {
val newFilter = mapper.map(update.value)
this[update.index.toInt()] = newFilter
}
is SpaceFilterUpdate.Truncate -> {
subList(update.length.toInt(), size).clear()
}
}
}
}