diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceService.kt index 3ea8261cf7..299209e188 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceService.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.SharedFlow interface SpaceService { val topLevelSpacesFlow: SharedFlow> + val spaceFiltersFlow: SharedFlow> suspend fun joinedParents(spaceId: RoomId): Result> suspend fun getSpaceRoom(spaceId: RoomId): SpaceRoom? diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceServiceFilter.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceServiceFilter.kt new file mode 100644 index 0000000000..e599353876 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceServiceFilter.kt @@ -0,0 +1,24 @@ +/* + * 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.api.spaces + +import io.element.android.libraries.matrix.api.core.RoomId + +/** + * Represents a space filter for filtering rooms by space membership. + * + * @property spaceRoom The space room associated with this filter. + * @property level The nesting level of the space (0 = top level, 1 = first level child, etc.). + * @property descendants The list of room IDs that are descendants of this space. + */ +data class SpaceServiceFilter( + val spaceRoom: SpaceRoom, + val level: Int, + val descendants: List, +) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceService.kt index ab5d05e576..0f2c92dae2 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceService.kt @@ -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,6 +48,8 @@ class RustSpaceService( private val analyticsService: AnalyticsService, ) : SpaceService { private val spaceRoomMapper = SpaceRoomMapper() + private val spaceFilterMapper = SpaceServiceFilterMapper(spaceRoomMapper) + override val topLevelSpacesFlow = MutableSharedFlow>(replay = 1, extraBufferCapacity = 1) private val spaceListUpdateProcessor = SpaceListUpdateProcessor( spaceRoomsFlow = topLevelSpacesFlow, @@ -52,6 +57,12 @@ class RustSpaceService( analyticsService = analyticsService, ) + override val spaceFiltersFlow = MutableSharedFlow>(replay = 1, extraBufferCapacity = 1) + private val spaceFilterUpdateProcessor = SpaceServiceFilterUpdateProcessor( + spaceFiltersFlow = spaceFiltersFlow, + mapper = spaceFilterMapper, + ) + override suspend fun joinedParents(spaceId: RoomId): Result> = withContext(sessionDispatcher) { runCatchingExceptions { innerSpaceService @@ -115,6 +126,13 @@ class RustSpaceService( spaceListUpdateProcessor.postUpdates(updates) } .launchIn(sessionCoroutineScope) + + innerSpaceService + .spaceFilterListUpdate() + .onEach { updates -> + spaceFilterUpdateProcessor.postUpdates(updates) + } + .launchIn(sessionCoroutineScope) } } @@ -134,3 +152,20 @@ internal fun SpaceServiceInterface.spaceListUpdate(): Flow }.catch { Timber.d(it, "spaceDiffFlow() failed") }.buffer(Channel.UNLIMITED) + +internal fun SpaceServiceInterface.spaceFilterListUpdate(): Flow> = + callbackFlow { + val listener = object : SpaceServiceSpaceFiltersListener { + override fun onUpdate(filterUpdates: List) { + 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) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceServiceFilterMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceServiceFilterMapper.kt new file mode 100644 index 0000000000..50c06ac002 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceServiceFilterMapper.kt @@ -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) }, + ) + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceServiceFilterUpdateProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceServiceFilterUpdateProcessor.kt new file mode 100644 index 0000000000..6d037b725c --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceServiceFilterUpdateProcessor.kt @@ -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>, + private val mapper: SpaceServiceFilterMapper, +) { + private val mutex = Mutex() + + suspend fun postUpdates(updates: List) { + 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.() -> Unit) = + mutex.withLock { + val spaceFilters = if (spaceFiltersFlow.replayCache.isNotEmpty()) { + spaceFiltersFlow.first().toMutableList() + } else { + mutableListOf() + } + block(spaceFilters) + spaceFiltersFlow.emit(spaceFilters) + } + + private fun MutableList.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() + } + } + } +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/spaces/FakeSpaceService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/spaces/FakeSpaceService.kt index 19df31500b..e4c1c9475d 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/spaces/FakeSpaceService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/spaces/FakeSpaceService.kt @@ -13,6 +13,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.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.simulateLongTask import kotlinx.coroutines.flow.MutableSharedFlow @@ -36,6 +37,14 @@ class FakeSpaceService( _topLevelSpacesFlow.emit(value) } + private val _spaceServiceFiltersFlow = MutableSharedFlow>() + override val spaceFiltersFlow: SharedFlow> + get() = _spaceServiceFiltersFlow.asSharedFlow() + + suspend fun emitSpaceFilters(value: List) { + _spaceServiceFiltersFlow.emit(value) + } + override suspend fun joinedParents(spaceId: RoomId): Result> { return joinedParentsResult(spaceId) }