From 2132b65bf67729e1485e48afed8af5a6f2c344ba Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 30 Jan 2026 16:56:29 +0100 Subject: [PATCH 01/22] Spaces : some cleanup on SpaceService --- .../home/impl/spaces/HomeSpacesPresenter.kt | 2 +- .../libraries/matrix/api/spaces/SpaceService.kt | 4 +--- .../matrix/impl/spaces/RustSpaceService.kt | 12 ++---------- .../matrix/test/spaces/FakeSpaceService.kt | 15 +++++---------- 4 files changed, 9 insertions(+), 24 deletions(-) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenter.kt index d9e6aaa4d3..707ac73261 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenter.kt @@ -36,7 +36,7 @@ class HomeSpacesPresenter( val canCreateSpaces by featureFlagsService.isFeatureEnabledFlow(FeatureFlags.CreateSpaces).collectAsState(false) val hideInvitesAvatar by client.rememberHideInvitesAvatar() val spaceRooms by remember { - client.spaceService.spaceRoomsFlow.map { it.toImmutableList() } + client.spaceService.topLevelSpacesFlow.map { it.toImmutableList() } }.collectAsState(persistentListOf()) val seenSpaceInvites by remember { 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 1122415d58..3ea8261cf7 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 @@ -12,9 +12,7 @@ import io.element.android.libraries.matrix.api.core.RoomId import kotlinx.coroutines.flow.SharedFlow interface SpaceService { - val spaceRoomsFlow: SharedFlow> - suspend fun joinedSpaces(): Result> - + val topLevelSpacesFlow: SharedFlow> suspend fun joinedParents(spaceId: RoomId): Result> suspend fun getSpaceRoom(spaceId: RoomId): SpaceRoom? 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 0bf677e09d..ab5d05e576 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 @@ -45,21 +45,13 @@ class RustSpaceService( private val analyticsService: AnalyticsService, ) : SpaceService { private val spaceRoomMapper = SpaceRoomMapper() - override val spaceRoomsFlow = MutableSharedFlow>(replay = 1, extraBufferCapacity = 1) + override val topLevelSpacesFlow = MutableSharedFlow>(replay = 1, extraBufferCapacity = 1) private val spaceListUpdateProcessor = SpaceListUpdateProcessor( - spaceRoomsFlow = spaceRoomsFlow, + spaceRoomsFlow = topLevelSpacesFlow, mapper = spaceRoomMapper, analyticsService = analyticsService, ) - override suspend fun joinedSpaces(): Result> = withContext(sessionDispatcher) { - runCatchingExceptions { - innerSpaceService - .topLevelJoinedSpaces() - .map(spaceRoomMapper::map) - } - } - override suspend fun joinedParents(spaceId: RoomId): Result> = withContext(sessionDispatcher) { runCatchingExceptions { innerSpaceService 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 b7a40fdeef..19df31500b 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 @@ -20,7 +20,6 @@ import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow class FakeSpaceService( - private val joinedSpacesResult: () -> Result> = { lambdaError() }, private val spaceRoomListResult: (RoomId) -> SpaceRoomList = { lambdaError() }, private val leaveSpaceHandleResult: (RoomId) -> LeaveSpaceHandle = { lambdaError() }, private val removeChildFromSpaceResult: (RoomId, RoomId) -> Result = { _, _ -> lambdaError() }, @@ -29,16 +28,12 @@ class FakeSpaceService( private val editableSpacesResult: () -> Result> = { lambdaError() }, private val addChildToSpaceResult: (RoomId, RoomId) -> Result = { _, _ -> lambdaError() }, ) : SpaceService { - private val _spaceRoomsFlow = MutableSharedFlow>() - override val spaceRoomsFlow: SharedFlow> - get() = _spaceRoomsFlow.asSharedFlow() + private val _topLevelSpacesFlow = MutableSharedFlow>() + override val topLevelSpacesFlow: SharedFlow> + get() = _topLevelSpacesFlow.asSharedFlow() - suspend fun emitSpaceRoomList(value: List) { - _spaceRoomsFlow.emit(value) - } - - override suspend fun joinedSpaces(): Result> = simulateLongTask { - return joinedSpacesResult() + suspend fun emitTopLevelSpaces(value: List) { + _topLevelSpacesFlow.emit(value) } override suspend fun joinedParents(spaceId: RoomId): Result> { From 9d576394ac522f07e8c4319b4b473ff0ca18f0c4 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 30 Jan 2026 17:37:54 +0100 Subject: [PATCH 02/22] Spaces : expose new SpaceServiceFilter --- .../matrix/api/spaces/SpaceService.kt | 1 + .../matrix/api/spaces/SpaceServiceFilter.kt | 24 ++++++ .../matrix/impl/spaces/RustSpaceService.kt | 35 ++++++++ .../impl/spaces/SpaceServiceFilterMapper.kt | 25 ++++++ .../SpaceServiceFilterUpdateProcessor.kt | 85 +++++++++++++++++++ .../matrix/test/spaces/FakeSpaceService.kt | 9 ++ 6 files changed, 179 insertions(+) create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceServiceFilter.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceServiceFilterMapper.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceServiceFilterUpdateProcessor.kt 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) } From a9a0c252b963d4e2fe9a85ed6ce46227fc5a98e4 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 2 Feb 2026 11:22:11 +0100 Subject: [PATCH 03/22] Create base classes for SpaceFilters feature --- .../impl/spacefilters/SpaceFiltersEvent.kt | 10 +++++++ .../spacefilters/SpaceFiltersPresenter.kt | 22 +++++++++++++++ .../impl/spacefilters/SpaceFiltersState.kt | 12 ++++++++ .../spacefilters/SpaceFiltersStateProvider.kt | 21 ++++++++++++++ .../impl/spacefilters/SpaceFiltersView.kt | 28 +++++++++++++++++++ 5 files changed, 93 insertions(+) create mode 100644 features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersEvent.kt create mode 100644 features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenter.kt create mode 100644 features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersState.kt create mode 100644 features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersStateProvider.kt create mode 100644 features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersEvent.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersEvent.kt new file mode 100644 index 0000000000..2db514b68d --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersEvent.kt @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2026 Element Creations 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.features.home.impl.spacefilters + +sealed interface SpaceFiltersEvent diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenter.kt new file mode 100644 index 0000000000..21ed0c1070 --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenter.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2026 Element Creations 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.features.home.impl.spacefilters + +import androidx.compose.runtime.Composable +import dev.zacsweers.metro.Inject +import io.element.android.libraries.architecture.Presenter + +@Inject +class SpaceFiltersPresenter : Presenter { + @Composable + override fun present(): SpaceFiltersState { + return SpaceFiltersState( + eventSink = {}, + ) + } +} diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersState.kt new file mode 100644 index 0000000000..af56cfdba1 --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersState.kt @@ -0,0 +1,12 @@ +/* + * Copyright (c) 2026 Element Creations 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.features.home.impl.spacefilters + +data class SpaceFiltersState( + val eventSink: (SpaceFiltersEvent) -> Unit, +) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersStateProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersStateProvider.kt new file mode 100644 index 0000000000..b999228969 --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersStateProvider.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2026 Element Creations 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.features.home.impl.spacefilters + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider + +class SpaceFiltersStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf(aSpaceFiltersState()) +} + +fun aSpaceFiltersState( + eventSink: (SpaceFiltersEvent) -> Unit = {}, +) = SpaceFiltersState( + eventSink = eventSink, +) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt new file mode 100644 index 0000000000..a9250a74e2 --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2026 Element Creations 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.features.home.impl.spacefilters + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.PreviewParameter +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight + +@Composable +fun SpaceFiltersView( + state: SpaceFiltersState, + modifier: Modifier = Modifier +) { + // TODO +} + +@PreviewsDayNight +@Composable +internal fun SpaceFiltersViewPreview(@PreviewParameter(SpaceFiltersStateProvider::class) state: SpaceFiltersState) = ElementPreview { + SpaceFiltersView(state = state) +} From 908616e8e86ee9a3a356152d94a73c598365de7e Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 2 Feb 2026 16:15:35 +0100 Subject: [PATCH 04/22] Add RoomListSpaceFilters FF --- .../android/libraries/featureflag/api/FeatureFlags.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt index 4422330924..46f39b2e99 100644 --- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt +++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt @@ -84,6 +84,13 @@ enum class FeatureFlags( defaultValue = { false }, isFinished = false, ), + RoomListSpaceFilters( + key = "feature.roomListSpaceFilters", + title = "Room list space filters", + description = "Allow filtering the room list by space.", + defaultValue = { false }, + isFinished = false, + ), PrintLogsToLogcat( key = "feature.print_logs_to_logcat", title = "Print logs to logcat", From e2e49ba3841005e67e8aa0c6d1c0d901a85a7817 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 2 Feb 2026 16:15:54 +0100 Subject: [PATCH 05/22] Model SpaceFilters ui states --- .../impl/spacefilters/SpaceFiltersEvent.kt | 20 ++++- .../spacefilters/SpaceFiltersPresenter.kt | 80 ++++++++++++++++++- .../impl/spacefilters/SpaceFiltersState.kt | 23 +++++- .../spacefilters/SpaceFiltersStateProvider.kt | 50 +++++++++++- 4 files changed, 161 insertions(+), 12 deletions(-) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersEvent.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersEvent.kt index 2db514b68d..b57b274cd5 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersEvent.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersEvent.kt @@ -7,4 +7,22 @@ package io.element.android.features.home.impl.spacefilters -sealed interface SpaceFiltersEvent +import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter + +sealed interface SpaceFiltersEvent { + // Only valid in Unselected state + sealed interface Unselected : SpaceFiltersEvent { + data object ShowFilters : Unselected + } + + // Only valid in Selecting state + sealed interface Selecting : SpaceFiltersEvent { + data object Cancel : Selecting + data class SelectFilter(val spaceFilter: SpaceServiceFilter) : Selecting + } + + // Only valid in Selected state + sealed interface Selected : SpaceFiltersEvent { + data object ClearSelection : Selected + } +} diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenter.kt index 21ed0c1070..dbccfa0a1b 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenter.kt @@ -8,15 +8,87 @@ package io.element.android.features.home.impl.spacefilters import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import dev.zacsweers.metro.Inject import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.map @Inject -class SpaceFiltersPresenter : Presenter { +class SpaceFiltersPresenter( + private val featureFlagService: FeatureFlagService, + private val matrixClient: MatrixClient, +) : Presenter { @Composable override fun present(): SpaceFiltersState { - return SpaceFiltersState( - eventSink = {}, - ) + val isFeatureEnabled by featureFlagService + .isFeatureEnabledFlow(FeatureFlags.RoomListSpaceFilters) + .collectAsState(initial = false) + + if (!isFeatureEnabled) { + return SpaceFiltersState.Disabled + } + + val availableFilters by remember { + matrixClient.spaceService.spaceFiltersFlow.map { it.toImmutableList() } + }.collectAsState(initial = persistentListOf()) + + var selectionMode by remember { mutableStateOf(SelectionMode.Unselected) } + + fun handleUnselectedEvent(event: SpaceFiltersEvent.Unselected) { + when (event) { + SpaceFiltersEvent.Unselected.ShowFilters -> { + selectionMode = SelectionMode.Selecting + } + } + } + + fun handleSelectingEvent(event: SpaceFiltersEvent.Selecting) { + when (event) { + SpaceFiltersEvent.Selecting.Cancel -> { + selectionMode = SelectionMode.Unselected + } + is SpaceFiltersEvent.Selecting.SelectFilter -> { + selectionMode = SelectionMode.Selected(event.spaceFilter) + } + } + } + + fun handleSelectedEvent(event: SpaceFiltersEvent.Selected) { + when (event) { + SpaceFiltersEvent.Selected.ClearSelection -> { + selectionMode = SelectionMode.Unselected + } + } + } + + return when (val mode = selectionMode) { + SelectionMode.Unselected -> SpaceFiltersState.Unselected( + eventSink = ::handleUnselectedEvent, + ) + SelectionMode.Selecting -> SpaceFiltersState.Selecting( + availableFilters = availableFilters, + eventSink = ::handleSelectingEvent, + ) + is SelectionMode.Selected -> SpaceFiltersState.Selected( + selectedFilter = mode.filter, + eventSink = ::handleSelectedEvent, + ) + } } } + +private sealed interface SelectionMode { + data object Unselected : SelectionMode + data object Selecting : SelectionMode + data class Selected(val filter: SpaceServiceFilter) : SelectionMode +} diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersState.kt index af56cfdba1..1e54a51bcb 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersState.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersState.kt @@ -7,6 +7,23 @@ package io.element.android.features.home.impl.spacefilters -data class SpaceFiltersState( - val eventSink: (SpaceFiltersEvent) -> Unit, -) +import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter +import kotlinx.collections.immutable.ImmutableList + +sealed interface SpaceFiltersState { + data object Disabled : SpaceFiltersState + + data class Unselected( + val eventSink: (SpaceFiltersEvent.Unselected) -> Unit, + ) : SpaceFiltersState + + data class Selecting( + val availableFilters: ImmutableList, + val eventSink: (SpaceFiltersEvent.Selecting) -> Unit, + ) : SpaceFiltersState + + data class Selected( + val selectedFilter: SpaceServiceFilter, + val eventSink: (SpaceFiltersEvent.Selected) -> Unit, + ) : SpaceFiltersState +} diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersStateProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersStateProvider.kt index b999228969..f6c5cccef4 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersStateProvider.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersStateProvider.kt @@ -8,14 +8,56 @@ package io.element.android.features.home.impl.spacefilters import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter +import io.element.android.libraries.previewutils.room.aSpaceRoom +import kotlinx.collections.immutable.persistentListOf class SpaceFiltersStateProvider : PreviewParameterProvider { override val values: Sequence - get() = sequenceOf(aSpaceFiltersState()) + get() = sequenceOf( + aDisabledSpaceFiltersState(), + anUnselectedSpaceFiltersState(), + aSelectingSpaceFiltersState(), + aSelectedSpaceFiltersState(), + ) } -fun aSpaceFiltersState( - eventSink: (SpaceFiltersEvent) -> Unit = {}, -) = SpaceFiltersState( +fun aDisabledSpaceFiltersState() = SpaceFiltersState.Disabled + +fun anUnselectedSpaceFiltersState( + eventSink: (SpaceFiltersEvent.Unselected) -> Unit = {}, +) = SpaceFiltersState.Unselected( eventSink = eventSink, ) + +fun aSelectingSpaceFiltersState( + availableFilters: List = listOf( + aSpaceServiceFilter(displayName = "Work"), + aSpaceServiceFilter(displayName = "Personal", roomId = RoomId("!personal:example.com")), + aSpaceServiceFilter(displayName = "Gaming", roomId = RoomId("!gaming:example.com")), + ), + eventSink: (SpaceFiltersEvent.Selecting) -> Unit = {}, +) = SpaceFiltersState.Selecting( + availableFilters = persistentListOf(*availableFilters.toTypedArray()), + eventSink = eventSink, +) + +fun aSelectedSpaceFiltersState( + selectedFilter: SpaceServiceFilter = aSpaceServiceFilter(displayName = "Work"), + eventSink: (SpaceFiltersEvent.Selected) -> Unit = {}, +) = SpaceFiltersState.Selected( + selectedFilter = selectedFilter, + eventSink = eventSink, +) + +fun aSpaceServiceFilter( + displayName: String = "Space", + roomId: RoomId = RoomId("!space:example.com"), + level: Int = 0, + descendants: List = emptyList(), +) = SpaceServiceFilter( + spaceRoom = aSpaceRoom(displayName = displayName, roomId = roomId), + level = level, + descendants = descendants, +) From bb3e955ef8812330e617e43e3883e62ad7a27f15 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 2 Feb 2026 19:38:32 +0100 Subject: [PATCH 06/22] Complete SpaceFiltersView ui --- .../spacefilters/SpaceFiltersStateProvider.kt | 25 +++- .../impl/spacefilters/SpaceFiltersView.kt | 131 +++++++++++++++++- 2 files changed, 151 insertions(+), 5 deletions(-) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersStateProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersStateProvider.kt index f6c5cccef4..3c64b93e40 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersStateProvider.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersStateProvider.kt @@ -8,6 +8,7 @@ package io.element.android.features.home.impl.spacefilters import androidx.compose.ui.tooling.preview.PreviewParameterProvider +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.spaces.SpaceServiceFilter import io.element.android.libraries.previewutils.room.aSpaceRoom @@ -33,9 +34,24 @@ fun anUnselectedSpaceFiltersState( fun aSelectingSpaceFiltersState( availableFilters: List = listOf( - aSpaceServiceFilter(displayName = "Work"), - aSpaceServiceFilter(displayName = "Personal", roomId = RoomId("!personal:example.com")), - aSpaceServiceFilter(displayName = "Gaming", roomId = RoomId("!gaming:example.com")), + aSpaceServiceFilter( + displayName = "Work", + canonicalAlias = RoomAlias("#work:example.com"), + ), + aSpaceServiceFilter( + displayName = "Personal", + roomId = RoomId("!personal:example.com"), + ), + aSpaceServiceFilter( + displayName = "Projects", + roomId = RoomId("!projects:example.com"), + canonicalAlias = RoomAlias("#projects:example.com"), + level = 1, + ), + aSpaceServiceFilter( + displayName = "Gaming", + roomId = RoomId("!gaming:example.com"), + ), ), eventSink: (SpaceFiltersEvent.Selecting) -> Unit = {}, ) = SpaceFiltersState.Selecting( @@ -54,10 +70,11 @@ fun aSelectedSpaceFiltersState( fun aSpaceServiceFilter( displayName: String = "Space", roomId: RoomId = RoomId("!space:example.com"), + canonicalAlias: RoomAlias? = null, level: Int = 0, descendants: List = emptyList(), ) = SpaceServiceFilter( - spaceRoom = aSpaceRoom(displayName = displayName, roomId = roomId), + spaceRoom = aSpaceRoom(displayName = displayName, roomId = roomId, canonicalAlias = canonicalAlias), level = level, descendants = descendants, ) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt index a9250a74e2..a8a6e0a23f 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt @@ -7,18 +7,147 @@ package io.element.android.features.home.impl.spacefilters +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.libraries.designsystem.components.avatar.Avatar +import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.designsystem.components.avatar.AvatarType import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter +import io.element.android.libraries.matrix.ui.model.getAvatarData +@OptIn(ExperimentalMaterial3Api::class) @Composable fun SpaceFiltersView( state: SpaceFiltersState, modifier: Modifier = Modifier ) { - // TODO + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + var showSheet by remember { mutableStateOf(false) } + + LaunchedEffect(state) { + when (state) { + is SpaceFiltersState.Selecting -> showSheet = true + else -> { + sheetState.hide() + showSheet = false + } + } + } + + Box(modifier = modifier) { + if (showSheet && state is SpaceFiltersState.Selecting) { + ModalBottomSheet( + modifier = Modifier + .systemBarsPadding() + .navigationBarsPadding(), + sheetState = sheetState, + onDismissRequest = { state.eventSink(SpaceFiltersEvent.Selecting.Cancel) }, + ) { + SpaceFiltersBottomSheetContent( + filters = state.availableFilters, + onFilterSelected = { filter -> + state.eventSink(SpaceFiltersEvent.Selecting.SelectFilter(filter)) + } + ) + } + } + } +} + +@Composable +private fun SpaceFiltersBottomSheetContent( + filters: List, + onFilterSelected: (SpaceServiceFilter) -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .fillMaxWidth() + .padding(bottom = 16.dp) + ) { + Text( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + text = "Your spaces", + style = ElementTheme.typography.fontBodyLgMedium, + ) + Spacer(modifier = Modifier.height(8.dp)) + LazyColumn { + items(filters) { filter -> + SpaceFilterItem( + filter = filter, + onClick = { onFilterSelected(filter) } + ) + } + } + } +} + +@Composable +private fun SpaceFilterItem( + filter: SpaceServiceFilter, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + val spaceRoom = filter.spaceRoom + val supportingText = spaceRoom.canonicalAlias?.value + + Row( + modifier = modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + // Level-based indentation + Spacer(modifier = Modifier.width((16 * filter.level).dp)) + Avatar( + avatarData = spaceRoom.getAvatarData(AvatarSize.RoomSelectRoomListItem), + avatarType = AvatarType.Space(), + ) + Spacer(modifier = Modifier.width(16.dp)) + Column { + Text( + text = spaceRoom.displayName, + style = ElementTheme.typography.fontBodyLgMedium, + color = ElementTheme.colors.textPrimary, + ) + if (supportingText != null) { + Text( + text = supportingText, + style = ElementTheme.typography.fontBodyMdRegular, + color = ElementTheme.colors.textSecondary, + ) + } + } + } } @PreviewsDayNight From f67c7b2b1c03bdc62545e987609a1aa65ffa2c29 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 2 Feb 2026 19:51:22 +0100 Subject: [PATCH 07/22] Add SearchField to SpaceFiltersView --- .../spacefilters/SpaceFiltersPresenter.kt | 14 +++++++++---- .../impl/spacefilters/SpaceFiltersState.kt | 15 ++++++++++++- .../spacefilters/SpaceFiltersStateProvider.kt | 3 +++ .../impl/spacefilters/SpaceFiltersView.kt | 21 ++++++++++++++----- 4 files changed, 43 insertions(+), 10 deletions(-) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenter.kt index dbccfa0a1b..bcc6071723 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenter.kt @@ -7,6 +7,7 @@ package io.element.android.features.home.impl.spacefilters +import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -42,6 +43,7 @@ class SpaceFiltersPresenter( matrixClient.spaceService.spaceFiltersFlow.map { it.toImmutableList() } }.collectAsState(initial = persistentListOf()) + var selectionMode by remember { mutableStateOf(SelectionMode.Unselected) } fun handleUnselectedEvent(event: SpaceFiltersEvent.Unselected) { @@ -75,10 +77,14 @@ class SpaceFiltersPresenter( SelectionMode.Unselected -> SpaceFiltersState.Unselected( eventSink = ::handleUnselectedEvent, ) - SelectionMode.Selecting -> SpaceFiltersState.Selecting( - availableFilters = availableFilters, - eventSink = ::handleSelectingEvent, - ) + SelectionMode.Selecting -> { + val searchQuery = rememberTextFieldState() + SpaceFiltersState.Selecting( + availableFilters = availableFilters, + searchQuery = searchQuery, + eventSink = ::handleSelectingEvent, + ) + } is SelectionMode.Selected -> SpaceFiltersState.Selected( selectedFilter = mode.filter, eventSink = ::handleSelectedEvent, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersState.kt index 1e54a51bcb..cecda8b381 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersState.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersState.kt @@ -7,8 +7,10 @@ package io.element.android.features.home.impl.spacefilters +import androidx.compose.foundation.text.input.TextFieldState import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList sealed interface SpaceFiltersState { data object Disabled : SpaceFiltersState @@ -19,8 +21,19 @@ sealed interface SpaceFiltersState { data class Selecting( val availableFilters: ImmutableList, + val searchQuery: TextFieldState, val eventSink: (SpaceFiltersEvent.Selecting) -> Unit, - ) : SpaceFiltersState + ) : SpaceFiltersState { + val visibleFilters: ImmutableList + get() { + val query = searchQuery.text.toString() + if (query.isBlank()) return availableFilters + return availableFilters.filter { filter -> + filter.spaceRoom.displayName.contains(query, ignoreCase = true) || + (filter.spaceRoom.canonicalAlias?.value ?: "").contains(query, ignoreCase = true) + }.toImmutableList() + } + } data class Selected( val selectedFilter: SpaceServiceFilter, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersStateProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersStateProvider.kt index 3c64b93e40..03159a9db4 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersStateProvider.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersStateProvider.kt @@ -7,6 +7,7 @@ package io.element.android.features.home.impl.spacefilters +import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomId @@ -53,9 +54,11 @@ fun aSelectingSpaceFiltersState( roomId = RoomId("!gaming:example.com"), ), ), + searchQuery: TextFieldState = TextFieldState(), eventSink: (SpaceFiltersEvent.Selecting) -> Unit = {}, ) = SpaceFiltersState.Selecting( availableFilters = persistentListOf(*availableFilters.toTypedArray()), + searchQuery = searchQuery, eventSink = eventSink, ) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt index a8a6e0a23f..29313cb924 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt @@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable @@ -30,6 +31,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme @@ -39,9 +41,11 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarType import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet +import io.element.android.libraries.designsystem.theme.components.SearchField import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter import io.element.android.libraries.matrix.ui.model.getAvatarData +import io.element.android.libraries.ui.strings.CommonStrings @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -72,7 +76,8 @@ fun SpaceFiltersView( onDismissRequest = { state.eventSink(SpaceFiltersEvent.Selecting.Cancel) }, ) { SpaceFiltersBottomSheetContent( - filters = state.availableFilters, + filters = state.visibleFilters, + searchQuery = state.searchQuery, onFilterSelected = { filter -> state.eventSink(SpaceFiltersEvent.Selecting.SelectFilter(filter)) } @@ -85,20 +90,26 @@ fun SpaceFiltersView( @Composable private fun SpaceFiltersBottomSheetContent( filters: List, + searchQuery: TextFieldState, onFilterSelected: (SpaceServiceFilter) -> Unit, modifier: Modifier = Modifier ) { Column( modifier = modifier .fillMaxWidth() - .padding(bottom = 16.dp) + .padding(vertical = 16.dp, horizontal = 16.dp) ) { Text( - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), text = "Your spaces", - style = ElementTheme.typography.fontBodyLgMedium, + style = ElementTheme.typography.fontHeadingSmMedium, ) - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(12.dp)) + SearchField( + state = searchQuery, + modifier = Modifier.fillMaxWidth(), + placeholder = stringResource(CommonStrings.action_search), + ) + Spacer(modifier = Modifier.height(16.dp)) LazyColumn { items(filters) { filter -> SpaceFilterItem( From c993a6b38788dc1e2dd31acaf784a1cc0aeadae5 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 2 Feb 2026 20:09:06 +0100 Subject: [PATCH 08/22] Expose SpaceFilterState to RoomListState --- .../android/features/home/impl/roomlist/RoomListPresenter.kt | 4 ++++ .../android/features/home/impl/roomlist/RoomListState.kt | 2 ++ .../features/home/impl/roomlist/RoomListStateProvider.kt | 4 ++++ .../features/home/impl/roomlist/RoomListPresenterTest.kt | 4 ++++ 4 files changed, 14 insertions(+) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt index 830a84ee4d..86999b4103 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt @@ -31,6 +31,7 @@ import io.element.android.features.home.impl.datasource.RoomListDataSource import io.element.android.features.home.impl.filters.RoomListFiltersState import io.element.android.features.home.impl.search.RoomListSearchEvent import io.element.android.features.home.impl.search.RoomListSearchState +import io.element.android.features.home.impl.spacefilters.SpaceFiltersState import io.element.android.features.invite.api.SeenInvitesStore import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents.AcceptInvite import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents.DeclineInvite @@ -83,6 +84,7 @@ class RoomListPresenter( private val seenInvitesStore: SeenInvitesStore, private val announcementService: AnnouncementService, private val coldStartWatcher: AnalyticsColdStartWatcher, + private val spaceFiltersPresenter: Presenter, ) : Presenter { private val encryptionService = client.encryptionService @@ -92,6 +94,7 @@ class RoomListPresenter( val leaveRoomState = leaveRoomPresenter.present() val filtersState = filtersPresenter.present() val searchState = searchPresenter.present() + val spaceFiltersState = spaceFiltersPresenter.present() val acceptDeclineInviteState = acceptDeclineInvitePresenter.present() LaunchedEffect(Unit) { @@ -163,6 +166,7 @@ class RoomListPresenter( leaveRoomState = leaveRoomState, filtersState = filtersState, searchState = searchState, + spaceFiltersState = spaceFiltersState, contentState = contentState, acceptDeclineInviteState = acceptDeclineInviteState, hideInvitesAvatars = hideInvitesAvatar, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt index b19344ba1d..e0f4943621 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.Immutable import io.element.android.features.home.impl.filters.RoomListFiltersState import io.element.android.features.home.impl.model.RoomListRoomSummary import io.element.android.features.home.impl.search.RoomListSearchState +import io.element.android.features.home.impl.spacefilters.SpaceFiltersState import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState @@ -26,6 +27,7 @@ data class RoomListState( val leaveRoomState: LeaveRoomState, val filtersState: RoomListFiltersState, val searchState: RoomListSearchState, + val spaceFiltersState: SpaceFiltersState, val contentState: RoomListContentState, val acceptDeclineInviteState: AcceptDeclineInviteState, val hideInvitesAvatars: Boolean, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateProvider.kt index 1b61ba20c7..7f83eb7eb0 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateProvider.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateProvider.kt @@ -18,6 +18,8 @@ import io.element.android.features.home.impl.model.aRoomListRoomSummary import io.element.android.features.home.impl.model.anInviteSender import io.element.android.features.home.impl.search.RoomListSearchState import io.element.android.features.home.impl.search.aRoomListSearchState +import io.element.android.features.home.impl.spacefilters.SpaceFiltersState +import io.element.android.features.home.impl.spacefilters.aDisabledSpaceFiltersState import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState import io.element.android.features.invite.api.acceptdecline.anAcceptDeclineInviteState import io.element.android.features.leaveroom.api.LeaveRoomEvent @@ -52,6 +54,7 @@ internal fun aRoomListState( leaveRoomState: LeaveRoomState = aLeaveRoomState(), searchState: RoomListSearchState = aRoomListSearchState(), filtersState: RoomListFiltersState = aRoomListFiltersState(), + spaceFiltersState: SpaceFiltersState = aDisabledSpaceFiltersState(), contentState: RoomListContentState = aRoomsContentState(), acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(), hideInvitesAvatars: Boolean = false, @@ -63,6 +66,7 @@ internal fun aRoomListState( leaveRoomState = leaveRoomState, filtersState = filtersState, searchState = searchState, + spaceFiltersState = spaceFiltersState, contentState = contentState, acceptDeclineInviteState = acceptDeclineInviteState, hideInvitesAvatars = hideInvitesAvatars, diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt index 9ee95cc811..99a5550a08 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt @@ -21,6 +21,8 @@ import io.element.android.features.home.impl.model.createRoomListRoomSummary import io.element.android.features.home.impl.search.RoomListSearchEvent import io.element.android.features.home.impl.search.RoomListSearchState import io.element.android.features.home.impl.search.aRoomListSearchState +import io.element.android.features.home.impl.spacefilters.SpaceFiltersState +import io.element.android.features.home.impl.spacefilters.aDisabledSpaceFiltersState import io.element.android.features.invite.api.SeenInvitesStore import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState @@ -660,6 +662,7 @@ class RoomListPresenterTest { analyticsService: AnalyticsService = FakeAnalyticsService(), filtersPresenter: Presenter = Presenter { aRoomListFiltersState() }, searchPresenter: Presenter = Presenter { aRoomListSearchState() }, + spaceFiltersPresenter: Presenter = Presenter { aDisabledSpaceFiltersState() }, acceptDeclineInvitePresenter: Presenter = Presenter { anAcceptDeclineInviteState() }, notificationCleaner: NotificationCleaner = FakeNotificationCleaner(), appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(), @@ -683,6 +686,7 @@ class RoomListPresenterTest { searchPresenter = searchPresenter, sessionPreferencesStore = sessionPreferencesStore, filtersPresenter = filtersPresenter, + spaceFiltersPresenter = spaceFiltersPresenter, analyticsService = analyticsService, acceptDeclineInvitePresenter = acceptDeclineInvitePresenter, fullScreenIntentPermissionsPresenter = { aFullScreenIntentPermissionsState() }, From eb94015996047d849495d00e30b147f88a2e5531 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 2 Feb 2026 21:02:43 +0100 Subject: [PATCH 09/22] Add space filter button to HomeTopBar and integrate SpaceFiltersView --- .../android/features/home/impl/HomeView.kt | 4 +- .../home/impl/components/HomeTopBar.kt | 97 +++++++++++++++++-- .../spacefilters/SpaceFiltersPresenter.kt | 4 +- .../spacefilters/SpaceFiltersStateProvider.kt | 8 +- .../impl/spacefilters/SpaceFiltersView.kt | 17 +++- 5 files changed, 112 insertions(+), 18 deletions(-) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt index 7d210c3447..e6056e6636 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt @@ -50,6 +50,7 @@ import io.element.android.features.home.impl.roomlist.RoomListDeclineInviteMenu import io.element.android.features.home.impl.roomlist.RoomListEvent import io.element.android.features.home.impl.roomlist.RoomListState import io.element.android.features.home.impl.search.RoomListSearchView +import io.element.android.features.home.impl.spacefilters.SpaceFiltersView import io.element.android.features.home.impl.spaces.HomeSpacesView import io.element.android.libraries.androidutils.throttler.FirstThrottler import io.element.android.libraries.designsystem.preview.ElementPreview @@ -168,7 +169,6 @@ private fun HomeScaffold( topBar = { HomeTopBar( selectedNavigationItem = state.currentHomeNavigationBarItem, - title = stringResource(state.currentHomeNavigationBarItem.labelRes), currentUserAndNeighbors = state.currentUserAndNeighbors, showAvatarIndicator = state.showAvatarIndicator, areSearchResultsDisplayed = roomListState.searchState.isSearchActive, @@ -182,6 +182,7 @@ private fun HomeScaffold( scrollBehavior = scrollBehavior, displayFilters = state.displayRoomListFilters, filtersState = roomListState.filtersState, + spaceFiltersState = roomListState.spaceFiltersState, canCreateSpaces = state.homeSpacesState.canCreateSpaces, canReportBug = state.canReportBug, modifier = Modifier.hazeEffect( @@ -256,6 +257,7 @@ private fun HomeScaffold( .consumeWindowInsets(padding) .hazeSource(state = hazeState) ) + SpaceFiltersView(roomListState.spaceFiltersState) } HomeNavigationBarItem.Spaces -> { HomeSpacesView( diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/HomeTopBar.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/HomeTopBar.kt index c92a5b9fb8..1f0e580d3e 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/HomeTopBar.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/HomeTopBar.kt @@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.pager.VerticalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.rememberTopAppBarState @@ -40,10 +41,14 @@ import io.element.android.appconfig.RoomListConfig import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.home.impl.HomeNavigationBarItem -import io.element.android.features.home.impl.R import io.element.android.features.home.impl.filters.RoomListFiltersState import io.element.android.features.home.impl.filters.RoomListFiltersView import io.element.android.features.home.impl.filters.aRoomListFiltersState +import io.element.android.features.home.impl.spacefilters.SpaceFiltersEvent +import io.element.android.features.home.impl.spacefilters.SpaceFiltersState +import io.element.android.features.home.impl.spacefilters.aDisabledSpaceFiltersState +import io.element.android.features.home.impl.spacefilters.aSelectedSpaceFiltersState +import io.element.android.features.home.impl.spacefilters.anUnselectedSpaceFiltersState import io.element.android.libraries.designsystem.atomic.atoms.RedIndicatorAtom import io.element.android.libraries.designsystem.components.TopAppBarScrollBehaviorLayout import io.element.android.libraries.designsystem.components.avatar.Avatar @@ -75,7 +80,6 @@ import kotlinx.collections.immutable.toImmutableList @Composable fun HomeTopBar( selectedNavigationItem: HomeNavigationBarItem, - title: String, currentUserAndNeighbors: ImmutableList, showAvatarIndicator: Boolean, areSearchResultsDisplayed: Boolean, @@ -89,6 +93,7 @@ fun HomeTopBar( canReportBug: Boolean, displayFilters: Boolean, filtersState: RoomListFiltersState, + spaceFiltersState: SpaceFiltersState, modifier: Modifier = Modifier, ) { Column(modifier) { @@ -103,12 +108,21 @@ fun HomeTopBar( scrolledContainerColor = Color.Transparent, ), title = { + val displayTitle = when (selectedNavigationItem) { + HomeNavigationBarItem.Chats -> { + when (spaceFiltersState) { + is SpaceFiltersState.Selected -> spaceFiltersState.selectedFilter.spaceRoom.displayName + else -> stringResource(selectedNavigationItem.labelRes) + } + } + HomeNavigationBarItem.Spaces -> stringResource(selectedNavigationItem.labelRes) + } Text( modifier = Modifier.semantics { heading() }, style = ElementTheme.typography.aliasScreenTitle, - text = title, + text = displayTitle, ) }, navigationIcon = { @@ -124,7 +138,8 @@ fun HomeTopBar( HomeNavigationBarItem.Chats -> RoomListMenuItems( onToggleSearch = onToggleSearch, onMenuActionClick = onMenuActionClick, - canReportBug = canReportBug + canReportBug = canReportBug, + spaceFiltersState = spaceFiltersState, ) HomeNavigationBarItem.Spaces -> SpacesMenuItems( canCreateSpaces = canCreateSpaces, @@ -154,6 +169,7 @@ private fun RoomListMenuItems( onToggleSearch: () -> Unit, onMenuActionClick: (RoomListMenuAction) -> Unit, canReportBug: Boolean, + spaceFiltersState: SpaceFiltersState, ) { IconButton( onClick = onToggleSearch, @@ -163,6 +179,7 @@ private fun RoomListMenuItems( contentDescription = stringResource(CommonStrings.action_search), ) } + SpaceFilterButton(spaceFiltersState = spaceFiltersState) if (RoomListConfig.HAS_DROP_DOWN_MENU) { var showMenu by remember { mutableStateOf(false) } IconButton( @@ -228,6 +245,47 @@ private fun SpacesMenuItems( } } +@Composable +private fun SpaceFilterButton( + spaceFiltersState: SpaceFiltersState, +) { + when (spaceFiltersState) { + SpaceFiltersState.Disabled -> Unit + is SpaceFiltersState.Unselected -> { + IconButton( + onClick = { spaceFiltersState.eventSink(SpaceFiltersEvent.Unselected.ShowFilters) } + ) { + Icon( + imageVector = CompoundIcons.Filter(), + contentDescription = null, + ) + } + } + is SpaceFiltersState.Selecting -> { + IconButton(onClick = {}) { + Icon( + imageVector = CompoundIcons.Filter(), + contentDescription = null, + ) + } + } + is SpaceFiltersState.Selected -> { + IconButton( + colors = IconButtonDefaults.iconButtonColors( + containerColor = ElementTheme.colors.bgAccentRest, + contentColor = ElementTheme.colors.iconOnSolidPrimary, + ), + onClick = { spaceFiltersState.eventSink(SpaceFiltersEvent.Selected.ClearSelection) }, + ) { + Icon( + imageVector = CompoundIcons.Filter(), + contentDescription = null, + ) + } + } + } +} + @Composable private fun NavigationIcon( currentUserAndNeighbors: ImmutableList, @@ -309,7 +367,6 @@ private fun AccountIcon( internal fun HomeTopBarPreview() = ElementPreview { HomeTopBar( selectedNavigationItem = HomeNavigationBarItem.Chats, - title = stringResource(R.string.screen_roomlist_main_space_title), currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")), showAvatarIndicator = false, areSearchResultsDisplayed = false, @@ -322,6 +379,7 @@ internal fun HomeTopBarPreview() = ElementPreview { canReportBug = true, displayFilters = true, filtersState = aRoomListFiltersState(), + spaceFiltersState = anUnselectedSpaceFiltersState(), onMenuActionClick = {}, ) } @@ -332,7 +390,6 @@ internal fun HomeTopBarPreview() = ElementPreview { internal fun HomeTopBarSpacesPreview() = ElementPreview { HomeTopBar( selectedNavigationItem = HomeNavigationBarItem.Spaces, - title = stringResource(R.string.screen_home_tab_spaces), currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")), showAvatarIndicator = false, areSearchResultsDisplayed = false, @@ -345,6 +402,7 @@ internal fun HomeTopBarSpacesPreview() = ElementPreview { canReportBug = true, displayFilters = false, filtersState = aRoomListFiltersState(), + spaceFiltersState = anUnselectedSpaceFiltersState(), onMenuActionClick = {}, ) } @@ -355,7 +413,6 @@ internal fun HomeTopBarSpacesPreview() = ElementPreview { internal fun HomeTopBarWithIndicatorPreview() = ElementPreview { HomeTopBar( selectedNavigationItem = HomeNavigationBarItem.Chats, - title = stringResource(R.string.screen_roomlist_main_space_title), currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")), showAvatarIndicator = true, areSearchResultsDisplayed = false, @@ -368,6 +425,7 @@ internal fun HomeTopBarWithIndicatorPreview() = ElementPreview { canReportBug = true, displayFilters = true, filtersState = aRoomListFiltersState(), + spaceFiltersState = anUnselectedSpaceFiltersState(), onMenuActionClick = {}, ) } @@ -378,7 +436,6 @@ internal fun HomeTopBarWithIndicatorPreview() = ElementPreview { internal fun HomeTopBarMultiAccountPreview() = ElementPreview { HomeTopBar( selectedNavigationItem = HomeNavigationBarItem.Chats, - title = stringResource(R.string.screen_roomlist_main_space_title), currentUserAndNeighbors = aMatrixUserList().take(3).toImmutableList(), showAvatarIndicator = false, areSearchResultsDisplayed = false, @@ -391,6 +448,30 @@ internal fun HomeTopBarMultiAccountPreview() = ElementPreview { canReportBug = true, displayFilters = true, filtersState = aRoomListFiltersState(), + spaceFiltersState = anUnselectedSpaceFiltersState(), + onMenuActionClick = {}, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@PreviewsDayNight +@Composable +internal fun HomeTopSpaceFiltersSelectedPreview() = ElementPreview { + HomeTopBar( + selectedNavigationItem = HomeNavigationBarItem.Chats, + currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")), + showAvatarIndicator = false, + areSearchResultsDisplayed = false, + scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()), + onOpenSettings = {}, + onAccountSwitch = {}, + onToggleSearch = {}, + onCreateSpace = {}, + canCreateSpaces = true, + canReportBug = true, + displayFilters = true, + filtersState = aRoomListFiltersState(), + spaceFiltersState = aSelectedSpaceFiltersState(), onMenuActionClick = {}, ) } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenter.kt index bcc6071723..3e3e789de6 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenter.kt @@ -14,8 +14,10 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.Inject import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.di.SessionScope import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.MatrixClient @@ -24,7 +26,7 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.map -@Inject +@ContributesBinding(SessionScope::class) class SpaceFiltersPresenter( private val featureFlagService: FeatureFlagService, private val matrixClient: MatrixClient, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersStateProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersStateProvider.kt index 03159a9db4..931d39aef4 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersStateProvider.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersStateProvider.kt @@ -18,10 +18,8 @@ import kotlinx.collections.immutable.persistentListOf class SpaceFiltersStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( - aDisabledSpaceFiltersState(), - anUnselectedSpaceFiltersState(), aSelectingSpaceFiltersState(), - aSelectedSpaceFiltersState(), + aSelectingSpaceFiltersState(searchQuery = "Pr") ) } @@ -54,11 +52,11 @@ fun aSelectingSpaceFiltersState( roomId = RoomId("!gaming:example.com"), ), ), - searchQuery: TextFieldState = TextFieldState(), + searchQuery: String = "", eventSink: (SpaceFiltersEvent.Selecting) -> Unit = {}, ) = SpaceFiltersState.Selecting( availableFilters = persistentListOf(*availableFilters.toTypedArray()), - searchQuery = searchQuery, + searchQuery = TextFieldState(searchQuery), eventSink = eventSink, ) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt index 29313cb924..5a8c583e57 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding @@ -32,9 +33,11 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme +import io.element.android.features.home.impl.R import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarType @@ -97,16 +100,20 @@ private fun SpaceFiltersBottomSheetContent( Column( modifier = modifier .fillMaxWidth() - .padding(vertical = 16.dp, horizontal = 16.dp) + .fillMaxHeight(0.9f) + .padding(vertical = 16.dp) ) { Text( - text = "Your spaces", + text = stringResource(R.string.screen_roomlist_your_spaces), style = ElementTheme.typography.fontHeadingSmMedium, + modifier = Modifier.padding(horizontal = 16.dp), + maxLines = 1, + overflow = TextOverflow.Ellipsis, ) Spacer(modifier = Modifier.height(12.dp)) SearchField( state = searchQuery, - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), placeholder = stringResource(CommonStrings.action_search), ) Spacer(modifier = Modifier.height(16.dp)) @@ -149,12 +156,16 @@ private fun SpaceFilterItem( text = spaceRoom.displayName, style = ElementTheme.typography.fontBodyLgMedium, color = ElementTheme.colors.textPrimary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, ) if (supportingText != null) { Text( text = supportingText, style = ElementTheme.typography.fontBodyMdRegular, color = ElementTheme.colors.textSecondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, ) } } From 915aca9c1dd1634d3230715a845a967ffeae9d76 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 2 Feb 2026 21:46:33 +0100 Subject: [PATCH 10/22] Add room list filter combining based on space selection --- .../home/impl/filters/RoomListFilter.kt | 11 ++++++ .../impl/filters/RoomListFiltersPresenter.kt | 35 ++----------------- .../home/impl/roomlist/RoomListPresenter.kt | 18 ++++++++++ .../impl/spacefilters/SpaceFiltersState.kt | 8 +++++ .../filters/RoomListFiltersPresenterTest.kt | 17 --------- .../matrix/api/roomlist/RoomListFilter.kt | 6 ++++ .../impl/roomlist/RoomListFilterMapper.kt | 2 ++ 7 files changed, 48 insertions(+), 49 deletions(-) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFilter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFilter.kt index 1f627eca4e..3e07c565db 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFilter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFilter.kt @@ -9,6 +9,7 @@ package io.element.android.features.home.impl.filters import io.element.android.features.home.impl.R +import io.element.android.libraries.matrix.api.roomlist.RoomListFilter as MatrixRoomListFilter /** * Enum class representing the different filters that can be applied to the room list. @@ -30,3 +31,13 @@ enum class RoomListFilter(val stringResource: Int) { Invites -> setOf(Rooms, People, Unread, Favourites) } } + +fun RoomListFilter.into(): MatrixRoomListFilter { + return when (this) { + RoomListFilter.Rooms -> MatrixRoomListFilter.Category.Group + RoomListFilter.People -> MatrixRoomListFilter.Category.People + RoomListFilter.Unread -> MatrixRoomListFilter.Unread + RoomListFilter.Favourites -> MatrixRoomListFilter.Favorite + RoomListFilter.Invites -> MatrixRoomListFilter.Invite + } +} diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenter.kt index 3c808045fd..e73660219c 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenter.kt @@ -9,24 +9,17 @@ package io.element.android.features.home.impl.filters import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.produceState import dev.zacsweers.metro.Inject -import io.element.android.features.home.impl.datasource.RoomListDataSource import io.element.android.features.home.impl.filters.selection.FilterSelectionStrategy import io.element.android.libraries.architecture.Presenter import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.map -import io.element.android.libraries.matrix.api.roomlist.RoomListFilter as MatrixRoomListFilter @Inject class RoomListFiltersPresenter( - private val roomListDataSource: RoomListDataSource, private val filterSelectionStrategy: FilterSelectionStrategy, ) : Presenter { - private val initialFilters = filterSelectionStrategy.filterSelectionStates.value.toImmutableList() - @Composable override fun present(): RoomListFiltersState { fun handleEvent(event: RoomListFiltersEvent) { @@ -40,31 +33,9 @@ class RoomListFiltersPresenter( } } - val filters by produceState(initialValue = initialFilters) { - filterSelectionStrategy.filterSelectionStates - .map { filters -> - value = filters.toImmutableList() - filters.mapNotNull { filterState -> - if (!filterState.isSelected) { - return@mapNotNull null - } - when (filterState.filter) { - RoomListFilter.Rooms -> MatrixRoomListFilter.Category.Group - RoomListFilter.People -> MatrixRoomListFilter.Category.People - RoomListFilter.Unread -> MatrixRoomListFilter.Unread - RoomListFilter.Favourites -> MatrixRoomListFilter.Favorite - RoomListFilter.Invites -> MatrixRoomListFilter.Invite - } - } - } - .collectLatest { filters -> - val result = MatrixRoomListFilter.All(filters) - roomListDataSource.updateFilter(result) - } - } - + val filters by filterSelectionStrategy.filterSelectionStates.collectAsState() return RoomListFiltersState( - filterSelectionStates = filters, + filterSelectionStates = filters.toImmutableList(), eventSink = ::handleEvent, ) } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt index 86999b4103..f52e8dafc2 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt @@ -29,9 +29,11 @@ import io.element.android.features.announcement.api.Announcement import io.element.android.features.announcement.api.AnnouncementService import io.element.android.features.home.impl.datasource.RoomListDataSource import io.element.android.features.home.impl.filters.RoomListFiltersState +import io.element.android.features.home.impl.filters.into import io.element.android.features.home.impl.search.RoomListSearchEvent import io.element.android.features.home.impl.search.RoomListSearchState import io.element.android.features.home.impl.spacefilters.SpaceFiltersState +import io.element.android.features.home.impl.spacefilters.selectedFilter import io.element.android.features.invite.api.SeenInvitesStore import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents.AcceptInvite import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents.DeclineInvite @@ -45,6 +47,7 @@ import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.encryption.RecoveryState import io.element.android.libraries.matrix.api.roomlist.RoomList +import io.element.android.libraries.matrix.api.roomlist.RoomListFilter import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar import io.element.android.libraries.preferences.api.store.AppPreferencesStore @@ -153,6 +156,21 @@ class RoomListPresenter( } } + LaunchedEffect(filtersState.filterSelectionStates, spaceFiltersState.selectedFilter()) { + val selectedFilters = filtersState.filterSelectionStates.mapNotNull { filterState -> + if (!filterState.isSelected) { + return@mapNotNull null + } + filterState.filter.into() + } + val selectedSpaceFilter = when (spaceFiltersState) { + is SpaceFiltersState.Selected -> RoomListFilter.Identifiers(spaceFiltersState.selectedFilter.descendants) + else -> null + } + val allFilters = RoomListFilter.All(selectedFilters + listOfNotNull(selectedSpaceFilter)) + roomListDataSource.updateFilter(allFilters) + } + val contentState = roomListContentState( securityBannerDismissed, showNewNotificationSoundBanner, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersState.kt index cecda8b381..2ff5517455 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersState.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersState.kt @@ -8,6 +8,7 @@ package io.element.android.features.home.impl.spacefilters import androidx.compose.foundation.text.input.TextFieldState +import io.element.android.features.home.impl.filters.RoomListFilter import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -40,3 +41,10 @@ sealed interface SpaceFiltersState { val eventSink: (SpaceFiltersEvent.Selected) -> Unit, ) : SpaceFiltersState } + +fun SpaceFiltersState.selectedFilter(): SpaceServiceFilter? { + return when (this) { + is SpaceFiltersState.Selected -> this.selectedFilter + else -> null + } +} diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenterTest.kt index c30e279b2e..bb00a9edac 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenterTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenterTest.kt @@ -9,9 +9,6 @@ package io.element.android.features.home.impl.filters import com.google.common.truth.Truth.assertThat -import io.element.android.features.home.impl.FakeDateTimeObserver -import io.element.android.features.home.impl.datasource.RoomListDataSource -import io.element.android.features.home.impl.datasource.aRoomListRoomSummaryFactory import io.element.android.features.home.impl.filters.selection.DefaultFilterSelectionStrategy import io.element.android.features.home.impl.filters.selection.FilterSelectionState import io.element.android.libraries.dateformatter.api.DateFormatter @@ -22,10 +19,8 @@ import io.element.android.libraries.matrix.api.notificationsettings.Notification import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService -import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.awaitLastSequentialItem import io.element.android.tests.testutils.test -import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceUntilIdle @@ -117,18 +112,6 @@ private fun TestScope.createRoomListFiltersPresenter( roomLatestEventFormatter: RoomLatestEventFormatter = FakeRoomLatestEventFormatter(), ): RoomListFiltersPresenter { return RoomListFiltersPresenter( - roomListDataSource = RoomListDataSource( - roomListService = roomListService, - roomListRoomSummaryFactory = aRoomListRoomSummaryFactory( - dateFormatter = dateFormatter, - roomLatestEventFormatter = roomLatestEventFormatter, - ), - coroutineDispatchers = testCoroutineDispatchers(), - notificationSettingsService = notificationSettingsService, - sessionCoroutineScope = backgroundScope, - dateTimeObserver = FakeDateTimeObserver(), - analyticsService = FakeAnalyticsService(), - ), filterSelectionStrategy = DefaultFilterSelectionStrategy(), ) } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListFilter.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListFilter.kt index 3c6e35d339..b4abedd75d 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListFilter.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListFilter.kt @@ -8,6 +8,8 @@ package io.element.android.libraries.matrix.api.roomlist +import io.element.android.libraries.matrix.api.core.RoomId + sealed interface RoomListFilter { companion object { /** @@ -41,6 +43,10 @@ sealed interface RoomListFilter { val filters: List ) : RoomListFilter + data class Identifiers( + val values : List, + ): RoomListFilter + /** * A filter that matches rooms that are unread. */ diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFilterMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFilterMapper.kt index fdee790b19..648376a9cc 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFilterMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFilterMapper.kt @@ -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) From 49b58f249b39b4fb69be593212d9d252c9631e0e Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 3 Feb 2026 10:24:38 +0100 Subject: [PATCH 11/22] Simplify SpaceFilterButton and add content description --- .../home/impl/components/HomeTopBar.kt | 59 ++++++++----------- 1 file changed, 25 insertions(+), 34 deletions(-) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/HomeTopBar.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/HomeTopBar.kt index 1f0e580d3e..fb7388e9e8 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/HomeTopBar.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/HomeTopBar.kt @@ -41,12 +41,12 @@ import io.element.android.appconfig.RoomListConfig import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.home.impl.HomeNavigationBarItem +import io.element.android.features.home.impl.R import io.element.android.features.home.impl.filters.RoomListFiltersState import io.element.android.features.home.impl.filters.RoomListFiltersView import io.element.android.features.home.impl.filters.aRoomListFiltersState import io.element.android.features.home.impl.spacefilters.SpaceFiltersEvent import io.element.android.features.home.impl.spacefilters.SpaceFiltersState -import io.element.android.features.home.impl.spacefilters.aDisabledSpaceFiltersState import io.element.android.features.home.impl.spacefilters.aSelectedSpaceFiltersState import io.element.android.features.home.impl.spacefilters.anUnselectedSpaceFiltersState import io.element.android.libraries.designsystem.atomic.atoms.RedIndicatorAtom @@ -249,41 +249,32 @@ private fun SpacesMenuItems( private fun SpaceFilterButton( spaceFiltersState: SpaceFiltersState, ) { - when (spaceFiltersState) { - SpaceFiltersState.Disabled -> Unit - is SpaceFiltersState.Unselected -> { - IconButton( - onClick = { spaceFiltersState.eventSink(SpaceFiltersEvent.Unselected.ShowFilters) } - ) { - Icon( - imageVector = CompoundIcons.Filter(), - contentDescription = null, - ) - } - } - is SpaceFiltersState.Selecting -> { - IconButton(onClick = {}) { - Icon( - imageVector = CompoundIcons.Filter(), - contentDescription = null, - ) - } - } - is SpaceFiltersState.Selected -> { - IconButton( - colors = IconButtonDefaults.iconButtonColors( - containerColor = ElementTheme.colors.bgAccentRest, - contentColor = ElementTheme.colors.iconOnSolidPrimary, - ), - onClick = { spaceFiltersState.eventSink(SpaceFiltersEvent.Selected.ClearSelection) }, - ) { - Icon( - imageVector = CompoundIcons.Filter(), - contentDescription = null, - ) - } + if (spaceFiltersState == SpaceFiltersState.Disabled) return + + fun onClick() { + when (spaceFiltersState) { + is SpaceFiltersState.Unselected -> spaceFiltersState.eventSink(SpaceFiltersEvent.Unselected.ShowFilters) + is SpaceFiltersState.Selected -> spaceFiltersState.eventSink(SpaceFiltersEvent.Selected.ClearSelection) + else -> Unit } } + val isSelected = spaceFiltersState is SpaceFiltersState.Selected + IconButton( + onClick = ::onClick, + colors = if (isSelected) { + IconButtonDefaults.iconButtonColors( + containerColor = ElementTheme.colors.bgAccentRest, + contentColor = ElementTheme.colors.iconOnSolidPrimary, + ) + } else { + IconButtonDefaults.iconButtonColors() + }, + ) { + Icon( + imageVector = CompoundIcons.Filter(), + contentDescription = stringResource(R.string.screen_roomlist_your_spaces), + ) + } } @Composable From d51253eb74f19b2e00aad2fd2113016dbae31bbb Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 3 Feb 2026 21:22:56 +0100 Subject: [PATCH 12/22] Handle SpaceFilter interaction with other RoomListFilters --- .../android/features/home/impl/HomeView.kt | 1 + .../impl/components/RoomListContentView.kt | 14 ++++++++++-- .../RoomListFiltersEmptyStateResources.kt | 6 ++++- .../home/impl/filters/RoomListFiltersEvent.kt | 1 + .../impl/filters/RoomListFiltersPresenter.kt | 3 +++ .../DefaultFilterSelectionStrategy.kt | 22 ++++++++++++++----- .../selection/FilterSelectionStrategy.kt | 1 + .../home/impl/roomlist/RoomListPresenter.kt | 18 ++++++++++----- .../RoomListFiltersEmptyStateResourcesTest.kt | 22 +++++++++++++------ 9 files changed, 67 insertions(+), 21 deletions(-) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt index e6056e6636..9c0791230a 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt @@ -228,6 +228,7 @@ private fun HomeScaffold( RoomListContentView( contentState = roomListState.contentState, filtersState = roomListState.filtersState, + spaceFiltersState = roomListState.spaceFiltersState, lazyListState = roomsLazyListState, hideInvitesAvatars = roomListState.hideInvitesAvatars, eventSink = roomListState.eventSink, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt index f3628fce9d..a03399baf7 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt @@ -45,6 +45,8 @@ import io.element.android.features.home.impl.roomlist.RoomListContentState import io.element.android.features.home.impl.roomlist.RoomListContentStateProvider import io.element.android.features.home.impl.roomlist.RoomListEvent import io.element.android.features.home.impl.roomlist.SecurityBannerState +import io.element.android.features.home.impl.spacefilters.SpaceFiltersState +import io.element.android.features.home.impl.spacefilters.anUnselectedSpaceFiltersState import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Button @@ -59,6 +61,7 @@ import kotlinx.collections.immutable.ImmutableList fun RoomListContentView( contentState: RoomListContentState, filtersState: RoomListFiltersState, + spaceFiltersState: SpaceFiltersState, lazyListState: LazyListState, hideInvitesAvatars: Boolean, eventSink: (RoomListEvent) -> Unit, @@ -93,6 +96,7 @@ fun RoomListContentView( state = contentState, hideInvitesAvatars = hideInvitesAvatars, filtersState = filtersState, + spaceFiltersState = spaceFiltersState, eventSink = eventSink, onSetUpRecoveryClick = onSetUpRecoveryClick, onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick, @@ -172,6 +176,7 @@ private fun RoomsView( state: RoomListContentState.Rooms, hideInvitesAvatars: Boolean, filtersState: RoomListFiltersState, + spaceFiltersState: SpaceFiltersState, eventSink: (RoomListEvent) -> Unit, onSetUpRecoveryClick: () -> Unit, onConfirmRecoveryKeyClick: () -> Unit, @@ -180,9 +185,12 @@ private fun RoomsView( lazyListState: LazyListState, modifier: Modifier = Modifier, ) { - if (state.summaries.isEmpty() && filtersState.hasAnyFilterSelected) { + val isSpaceFilterSelected = spaceFiltersState is SpaceFiltersState.Selected + val hasAnyFilterSelected = filtersState.hasAnyFilterSelected || isSpaceFilterSelected + if (state.summaries.isEmpty() && hasAnyFilterSelected) { EmptyViewForFilterStates( selectedFilters = filtersState.selectedFilters(), + isSpaceFilterSelected = isSpaceFilterSelected, modifier = modifier.fillMaxSize() ) } else { @@ -278,9 +286,10 @@ private fun RoomsViewList( @Composable private fun EmptyViewForFilterStates( selectedFilters: ImmutableList, + isSpaceFilterSelected: Boolean, modifier: Modifier = Modifier, ) { - val emptyStateResources = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters) ?: return + val emptyStateResources = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters, isSpaceFilterSelected) ?: return EmptyScaffold( title = emptyStateResources.title, subtitle = emptyStateResources.subtitle, @@ -331,6 +340,7 @@ internal fun RoomListContentViewPreview(@PreviewParameter(RoomListContentStatePr ) } ), + spaceFiltersState = anUnselectedSpaceFiltersState(), hideInvitesAvatars = false, eventSink = {}, onSetUpRecoveryClick = {}, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEmptyStateResources.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEmptyStateResources.kt index 7381ac308e..084c3c9c0c 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEmptyStateResources.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEmptyStateResources.kt @@ -24,8 +24,12 @@ data class RoomListFiltersEmptyStateResources( /** * Create a [RoomListFiltersEmptyStateResources] from a list of selected filters. */ - fun fromSelectedFilters(selectedFilters: List): RoomListFiltersEmptyStateResources? { + fun fromSelectedFilters(selectedFilters: List, isSpaceFilterSelected: Boolean): RoomListFiltersEmptyStateResources? { return when { + isSpaceFilterSelected -> RoomListFiltersEmptyStateResources( + title = R.string.screen_roomlist_filter_mixed_empty_state_title, + subtitle = R.string.screen_roomlist_filter_mixed_empty_state_subtitle + ) selectedFilters.isEmpty() -> null selectedFilters.size == 1 -> { when (selectedFilters.first()) { diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEvent.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEvent.kt index a34e91e089..d4383f2091 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEvent.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEvent.kt @@ -11,4 +11,5 @@ package io.element.android.features.home.impl.filters sealed interface RoomListFiltersEvent { data class ToggleFilter(val filter: RoomListFilter) : RoomListFiltersEvent data object ClearSelectedFilters : RoomListFiltersEvent + data class SetHiddenFilter(val filters: Set): RoomListFiltersEvent } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenter.kt index e73660219c..08d6fe4ef0 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenter.kt @@ -30,6 +30,9 @@ class RoomListFiltersPresenter( is RoomListFiltersEvent.ToggleFilter -> { filterSelectionStrategy.toggle(event.filter) } + is RoomListFiltersEvent.SetHiddenFilter -> { + filterSelectionStrategy.setHiddenFilters(event.filters) + } } } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/DefaultFilterSelectionStrategy.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/DefaultFilterSelectionStrategy.kt index 877e934727..707a1c83e3 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/DefaultFilterSelectionStrategy.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/DefaultFilterSelectionStrategy.kt @@ -15,17 +15,29 @@ import kotlinx.coroutines.flow.MutableStateFlow @ContributesBinding(SessionScope::class) class DefaultFilterSelectionStrategy : FilterSelectionStrategy { - private val selectedFilters = LinkedHashSet() + private val _selectedFilters = LinkedHashSet() + private val hiddenFilters = LinkedHashSet() + private val selectedFilters + get() = _selectedFilters - hiddenFilters + + private val availableFilters + get() = RoomListFilter.entries.toSet() - hiddenFilters override val filterSelectionStates = MutableStateFlow(buildFilters()) + override fun setHiddenFilters(filters: Set) { + hiddenFilters.clear() + hiddenFilters.addAll(filters) + filterSelectionStates.value = buildFilters() + } + override fun select(filter: RoomListFilter) { - selectedFilters.add(filter) + _selectedFilters.add(filter) filterSelectionStates.value = buildFilters() } override fun deselect(filter: RoomListFilter) { - selectedFilters.remove(filter) + _selectedFilters.remove(filter) filterSelectionStates.value = buildFilters() } @@ -34,7 +46,7 @@ class DefaultFilterSelectionStrategy : FilterSelectionStrategy { } override fun clear() { - selectedFilters.clear() + _selectedFilters.clear() filterSelectionStates.value = buildFilters() } @@ -45,7 +57,7 @@ class DefaultFilterSelectionStrategy : FilterSelectionStrategy { isSelected = true ) } - val unselectedFilters = RoomListFilter.entries - selectedFilters - selectedFilters.flatMap { it.incompatibleFilters }.toSet() + val unselectedFilters = availableFilters - selectedFilters - selectedFilters.flatMap { it.incompatibleFilters }.toSet() val unselectedFilterStates = unselectedFilters.map { FilterSelectionState( filter = it, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/FilterSelectionStrategy.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/FilterSelectionStrategy.kt index f0877b5e0d..036c786f61 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/FilterSelectionStrategy.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/FilterSelectionStrategy.kt @@ -14,6 +14,7 @@ import kotlinx.coroutines.flow.StateFlow interface FilterSelectionStrategy { val filterSelectionStates: StateFlow> + fun setHiddenFilters(filters: Set) fun select(filter: RoomListFilter) fun deselect(filter: RoomListFilter) fun isSelected(filter: RoomListFilter): Boolean diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt index f52e8dafc2..1a16f2f865 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt @@ -28,6 +28,9 @@ import im.vector.app.features.analytics.plan.Interaction import io.element.android.features.announcement.api.Announcement import io.element.android.features.announcement.api.AnnouncementService import io.element.android.features.home.impl.datasource.RoomListDataSource +import io.element.android.features.home.impl.filters.RoomListFilter.People +import io.element.android.features.home.impl.filters.RoomListFilter.Rooms +import io.element.android.features.home.impl.filters.RoomListFiltersEvent import io.element.android.features.home.impl.filters.RoomListFiltersState import io.element.android.features.home.impl.filters.into import io.element.android.features.home.impl.search.RoomListSearchEvent @@ -156,13 +159,16 @@ class RoomListPresenter( } } - LaunchedEffect(filtersState.filterSelectionStates, spaceFiltersState.selectedFilter()) { - val selectedFilters = filtersState.filterSelectionStates.mapNotNull { filterState -> - if (!filterState.isSelected) { - return@mapNotNull null - } - filterState.filter.into() + LaunchedEffect(spaceFiltersState.selectedFilter()) { + val hiddenFilters = if (spaceFiltersState is SpaceFiltersState.Selected) { + setOf(People, Rooms) + } else { + emptySet() } + filtersState.eventSink(RoomListFiltersEvent.SetHiddenFilter(hiddenFilters)) + } + LaunchedEffect(filtersState.filterSelectionStates, spaceFiltersState.selectedFilter()) { + val selectedFilters = filtersState.selectedFilters().map { filter -> filter.into() } val selectedSpaceFilter = when (spaceFiltersState) { is SpaceFiltersState.Selected -> RoomListFilter.Identifiers(spaceFiltersState.selectedFilter.descendants) else -> null diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEmptyStateResourcesTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEmptyStateResourcesTest.kt index 250f43ee8f..1762d0d6bf 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEmptyStateResourcesTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEmptyStateResourcesTest.kt @@ -16,14 +16,14 @@ class RoomListFiltersEmptyStateResourcesTest { @Test fun `fromSelectedFilters should return null when selectedFilters is empty`() { val selectedFilters = emptyList() - val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters) + val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters, isSpaceFilterSelected = false) assertThat(result).isNull() } @Test fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has only unread filter`() { val selectedFilters = listOf(RoomListFilter.Unread) - val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters) + val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters, isSpaceFilterSelected = false) assertThat(result).isNotNull() assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_unreads_empty_state_title) assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_subtitle) @@ -32,7 +32,7 @@ class RoomListFiltersEmptyStateResourcesTest { @Test fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has only people filter`() { val selectedFilters = listOf(RoomListFilter.People) - val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters) + val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters, isSpaceFilterSelected = false) assertThat(result).isNotNull() assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_people_empty_state_title) assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_subtitle) @@ -41,7 +41,7 @@ class RoomListFiltersEmptyStateResourcesTest { @Test fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has only rooms filter`() { val selectedFilters = listOf(RoomListFilter.Rooms) - val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters) + val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters, isSpaceFilterSelected = false) assertThat(result).isNotNull() assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_rooms_empty_state_title) assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_subtitle) @@ -50,7 +50,7 @@ class RoomListFiltersEmptyStateResourcesTest { @Test fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has only favourites filter`() { val selectedFilters = listOf(RoomListFilter.Favourites) - val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters) + val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters, isSpaceFilterSelected = false) assertThat(result).isNotNull() assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_favourites_empty_state_title) assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_favourites_empty_state_subtitle) @@ -59,7 +59,7 @@ class RoomListFiltersEmptyStateResourcesTest { @Test fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has only invites filter`() { val selectedFilters = listOf(RoomListFilter.Invites) - val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters) + val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters, isSpaceFilterSelected = false) assertThat(result).isNotNull() assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_invites_empty_state_title) assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_subtitle) @@ -68,7 +68,15 @@ class RoomListFiltersEmptyStateResourcesTest { @Test fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has multiple filters`() { val selectedFilters = listOf(RoomListFilter.Unread, RoomListFilter.People, RoomListFilter.Rooms, RoomListFilter.Favourites) - val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters) + val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters, isSpaceFilterSelected = false) + assertThat(result).isNotNull() + assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_title) + assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_subtitle) + } + + @Test + fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when isSpaceFilterSelected is true`() { + val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(emptyList(), isSpaceFilterSelected = true) assertThat(result).isNotNull() assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_title) assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_subtitle) From 53e82c011604715e8b6c1bd6825623692be1aed4 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 4 Feb 2026 11:34:43 +0100 Subject: [PATCH 13/22] Revert changes on room list filters being hidden --- .../home/impl/filters/RoomListFiltersEvent.kt | 1 - .../impl/filters/RoomListFiltersPresenter.kt | 3 --- .../DefaultFilterSelectionStrategy.kt | 20 +++++-------------- .../selection/FilterSelectionStrategy.kt | 2 -- .../home/impl/roomlist/RoomListPresenter.kt | 14 ++----------- .../impl/spacefilters/SpaceFiltersState.kt | 7 ++++++- 6 files changed, 13 insertions(+), 34 deletions(-) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEvent.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEvent.kt index d4383f2091..a34e91e089 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEvent.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEvent.kt @@ -11,5 +11,4 @@ package io.element.android.features.home.impl.filters sealed interface RoomListFiltersEvent { data class ToggleFilter(val filter: RoomListFilter) : RoomListFiltersEvent data object ClearSelectedFilters : RoomListFiltersEvent - data class SetHiddenFilter(val filters: Set): RoomListFiltersEvent } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenter.kt index 08d6fe4ef0..e73660219c 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenter.kt @@ -30,9 +30,6 @@ class RoomListFiltersPresenter( is RoomListFiltersEvent.ToggleFilter -> { filterSelectionStrategy.toggle(event.filter) } - is RoomListFiltersEvent.SetHiddenFilter -> { - filterSelectionStrategy.setHiddenFilters(event.filters) - } } } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/DefaultFilterSelectionStrategy.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/DefaultFilterSelectionStrategy.kt index 707a1c83e3..847dbc2c39 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/DefaultFilterSelectionStrategy.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/DefaultFilterSelectionStrategy.kt @@ -15,29 +15,19 @@ import kotlinx.coroutines.flow.MutableStateFlow @ContributesBinding(SessionScope::class) class DefaultFilterSelectionStrategy : FilterSelectionStrategy { - private val _selectedFilters = LinkedHashSet() - private val hiddenFilters = LinkedHashSet() - private val selectedFilters - get() = _selectedFilters - hiddenFilters - + private val selectedFilters = LinkedHashSet() private val availableFilters - get() = RoomListFilter.entries.toSet() - hiddenFilters + get() = RoomListFilter.entries.toSet() override val filterSelectionStates = MutableStateFlow(buildFilters()) - override fun setHiddenFilters(filters: Set) { - hiddenFilters.clear() - hiddenFilters.addAll(filters) - filterSelectionStates.value = buildFilters() - } - override fun select(filter: RoomListFilter) { - _selectedFilters.add(filter) + selectedFilters.add(filter) filterSelectionStates.value = buildFilters() } override fun deselect(filter: RoomListFilter) { - _selectedFilters.remove(filter) + selectedFilters.remove(filter) filterSelectionStates.value = buildFilters() } @@ -46,7 +36,7 @@ class DefaultFilterSelectionStrategy : FilterSelectionStrategy { } override fun clear() { - _selectedFilters.clear() + selectedFilters.clear() filterSelectionStates.value = buildFilters() } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/FilterSelectionStrategy.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/FilterSelectionStrategy.kt index 036c786f61..ebdb58fa2b 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/FilterSelectionStrategy.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/FilterSelectionStrategy.kt @@ -13,8 +13,6 @@ import kotlinx.coroutines.flow.StateFlow interface FilterSelectionStrategy { val filterSelectionStates: StateFlow> - - fun setHiddenFilters(filters: Set) fun select(filter: RoomListFilter) fun deselect(filter: RoomListFilter) fun isSelected(filter: RoomListFilter): Boolean diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt index 1a16f2f865..ae016a31c1 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt @@ -36,6 +36,7 @@ import io.element.android.features.home.impl.filters.into import io.element.android.features.home.impl.search.RoomListSearchEvent import io.element.android.features.home.impl.search.RoomListSearchState import io.element.android.features.home.impl.spacefilters.SpaceFiltersState +import io.element.android.features.home.impl.spacefilters.into import io.element.android.features.home.impl.spacefilters.selectedFilter import io.element.android.features.invite.api.SeenInvitesStore import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents.AcceptInvite @@ -159,20 +160,9 @@ class RoomListPresenter( } } - LaunchedEffect(spaceFiltersState.selectedFilter()) { - val hiddenFilters = if (spaceFiltersState is SpaceFiltersState.Selected) { - setOf(People, Rooms) - } else { - emptySet() - } - filtersState.eventSink(RoomListFiltersEvent.SetHiddenFilter(hiddenFilters)) - } LaunchedEffect(filtersState.filterSelectionStates, spaceFiltersState.selectedFilter()) { val selectedFilters = filtersState.selectedFilters().map { filter -> filter.into() } - val selectedSpaceFilter = when (spaceFiltersState) { - is SpaceFiltersState.Selected -> RoomListFilter.Identifiers(spaceFiltersState.selectedFilter.descendants) - else -> null - } + val selectedSpaceFilter = spaceFiltersState.selectedFilter().into() val allFilters = RoomListFilter.All(selectedFilters + listOfNotNull(selectedSpaceFilter)) roomListDataSource.updateFilter(allFilters) } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersState.kt index 2ff5517455..bef781ae1e 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersState.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersState.kt @@ -8,7 +8,7 @@ package io.element.android.features.home.impl.spacefilters import androidx.compose.foundation.text.input.TextFieldState -import io.element.android.features.home.impl.filters.RoomListFilter +import io.element.android.libraries.matrix.api.roomlist.RoomListFilter import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -48,3 +48,8 @@ fun SpaceFiltersState.selectedFilter(): SpaceServiceFilter? { else -> null } } + +fun SpaceServiceFilter?.into(): RoomListFilter? { + return this?.let { RoomListFilter.Identifiers(descendants) } +} + From 4ea5337886eec76ab28d6ac86c55016732bae0d4 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 4 Feb 2026 11:35:18 +0100 Subject: [PATCH 14/22] Ensure selected space filter gets update --- .../spacefilters/SpaceFiltersPresenter.kt | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenter.kt index 3e3e789de6..3b1eaddd30 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenter.kt @@ -10,12 +10,12 @@ package io.element.android.features.home.impl.spacefilters import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import dev.zacsweers.metro.ContributesBinding -import dev.zacsweers.metro.Inject import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.di.SessionScope import io.element.android.libraries.featureflag.api.FeatureFlagService @@ -45,7 +45,6 @@ class SpaceFiltersPresenter( matrixClient.spaceService.spaceFiltersFlow.map { it.toImmutableList() } }.collectAsState(initial = persistentListOf()) - var selectionMode by remember { mutableStateOf(SelectionMode.Unselected) } fun handleUnselectedEvent(event: SpaceFiltersEvent.Unselected) { @@ -87,10 +86,20 @@ class SpaceFiltersPresenter( eventSink = ::handleSelectingEvent, ) } - is SelectionMode.Selected -> SpaceFiltersState.Selected( - selectedFilter = mode.filter, - eventSink = ::handleSelectedEvent, - ) + is SelectionMode.Selected -> { + // Keep in sync with the available filters if rooms are added/removed + val selectedFilter by remember { + derivedStateOf { + availableFilters + .firstOrNull { it.spaceRoom.roomId == mode.filter.spaceRoom.roomId } + ?: mode.filter + } + } + SpaceFiltersState.Selected( + selectedFilter = selectedFilter, + eventSink = ::handleSelectedEvent, + ) + } } } } From 354e126a96080d5b312e791525a3ed3bc23d27d2 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 4 Feb 2026 13:40:06 +0100 Subject: [PATCH 15/22] Add back navigation to clear space filter selection --- .../android/features/home/impl/HomeState.kt | 2 ++ .../android/features/home/impl/HomeView.kt | 15 +++++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeState.kt index 07b4be123c..e27ccb72ae 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeState.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeState.kt @@ -9,6 +9,7 @@ package io.element.android.features.home.impl import io.element.android.features.home.impl.roomlist.RoomListState +import io.element.android.features.home.impl.spacefilters.SpaceFiltersState import io.element.android.features.home.impl.spaces.HomeSpacesState import io.element.android.features.logout.api.direct.DirectLogoutState import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage @@ -31,6 +32,7 @@ data class HomeState( val directLogoutState: DirectLogoutState, val eventSink: (HomeEvent) -> Unit, ) { + val isBackHandlerEnabled = currentHomeNavigationBarItem != HomeNavigationBarItem.Chats || roomListState.spaceFiltersState is SpaceFiltersState.Selected val displayActions = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats val displayRoomListFilters = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats && roomListState.displayFilters val showNavigationBar = homeSpacesState.canCreateSpaces || homeSpacesState.spaceRooms.isNotEmpty() diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt index 9c0791230a..eee6f49db3 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt @@ -50,6 +50,8 @@ import io.element.android.features.home.impl.roomlist.RoomListDeclineInviteMenu import io.element.android.features.home.impl.roomlist.RoomListEvent import io.element.android.features.home.impl.roomlist.RoomListState import io.element.android.features.home.impl.search.RoomListSearchView +import io.element.android.features.home.impl.spacefilters.SpaceFiltersEvent +import io.element.android.features.home.impl.spacefilters.SpaceFiltersState import io.element.android.features.home.impl.spacefilters.SpaceFiltersView import io.element.android.features.home.impl.spaces.HomeSpacesView import io.element.android.libraries.androidutils.throttler.FirstThrottler @@ -154,10 +156,15 @@ private fun HomeScaffold( val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage) val roomListState: RoomListState = state.roomListState - BackHandler( - enabled = state.currentHomeNavigationBarItem != HomeNavigationBarItem.Chats, - ) { - state.eventSink(HomeEvent.SelectHomeNavigationBarItem(HomeNavigationBarItem.Chats)) + BackHandler(enabled = state.isBackHandlerEnabled) { + if (state.currentHomeNavigationBarItem != HomeNavigationBarItem.Chats) { + state.eventSink(HomeEvent.SelectHomeNavigationBarItem(HomeNavigationBarItem.Chats)) + } else { + val spaceFiltersState = state.roomListState.spaceFiltersState + if (spaceFiltersState is SpaceFiltersState.Selected) { + spaceFiltersState.eventSink(SpaceFiltersEvent.Selected.ClearSelection) + } + } } val hazeState = rememberHazeState() From 36fb3e251dab4840a879f7a6ebb2403a2ababce6 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 4 Feb 2026 14:42:57 +0100 Subject: [PATCH 16/22] Add tests for SpaceFiltersPresenter and SpaceFiltersView and fix quality --- .../android/features/home/impl/HomeState.kt | 2 +- .../home/impl/components/HomeTopBar.kt | 46 ++-- .../features/home/impl/di/RoomListModule.kt | 5 + .../home/impl/roomlist/RoomListPresenter.kt | 2 - .../spacefilters/SpaceFiltersPresenter.kt | 5 +- .../impl/spacefilters/SpaceFiltersState.kt | 3 +- .../filters/RoomListFiltersPresenterTest.kt | 21 +- .../spacefilters/SpaceFiltersPresenterTest.kt | 223 ++++++++++++++++++ .../impl/spacefilters/SpaceFiltersViewTest.kt | 80 +++++++ .../matrix/api/roomlist/RoomListFilter.kt | 4 +- .../matrix/test/spaces/FakeSpaceService.kt | 6 +- .../tests/konsist/KonsistPreviewTest.kt | 1 + 12 files changed, 345 insertions(+), 53 deletions(-) create mode 100644 features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenterTest.kt create mode 100644 features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersViewTest.kt diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeState.kt index e27ccb72ae..dbb994e23e 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeState.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeState.kt @@ -32,7 +32,7 @@ data class HomeState( val directLogoutState: DirectLogoutState, val eventSink: (HomeEvent) -> Unit, ) { - val isBackHandlerEnabled = currentHomeNavigationBarItem != HomeNavigationBarItem.Chats || roomListState.spaceFiltersState is SpaceFiltersState.Selected + val isBackHandlerEnabled = currentHomeNavigationBarItem != HomeNavigationBarItem.Chats || roomListState.spaceFiltersState is SpaceFiltersState.Selected val displayActions = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats val displayRoomListFilters = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats && roomListState.displayFilters val showNavigationBar = homeSpacesState.canCreateSpaces || homeSpacesState.spaceRooms.isNotEmpty() diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/HomeTopBar.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/HomeTopBar.kt index fb7388e9e8..f8786c544c 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/HomeTopBar.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/HomeTopBar.kt @@ -375,6 +375,29 @@ internal fun HomeTopBarPreview() = ElementPreview { ) } +@OptIn(ExperimentalMaterial3Api::class) +@PreviewsDayNight +@Composable +internal fun HomeTopBarSpaceFiltersSelectedPreview() = ElementPreview { + HomeTopBar( + selectedNavigationItem = HomeNavigationBarItem.Chats, + currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")), + showAvatarIndicator = false, + areSearchResultsDisplayed = false, + scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()), + onOpenSettings = {}, + onAccountSwitch = {}, + onToggleSearch = {}, + onCreateSpace = {}, + canCreateSpaces = true, + canReportBug = true, + displayFilters = true, + filtersState = aRoomListFiltersState(), + spaceFiltersState = aSelectedSpaceFiltersState(), + onMenuActionClick = {}, + ) +} + @OptIn(ExperimentalMaterial3Api::class) @PreviewsDayNight @Composable @@ -443,26 +466,3 @@ internal fun HomeTopBarMultiAccountPreview() = ElementPreview { onMenuActionClick = {}, ) } - -@OptIn(ExperimentalMaterial3Api::class) -@PreviewsDayNight -@Composable -internal fun HomeTopSpaceFiltersSelectedPreview() = ElementPreview { - HomeTopBar( - selectedNavigationItem = HomeNavigationBarItem.Chats, - currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")), - showAvatarIndicator = false, - areSearchResultsDisplayed = false, - scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()), - onOpenSettings = {}, - onAccountSwitch = {}, - onToggleSearch = {}, - onCreateSpace = {}, - canCreateSpaces = true, - canReportBug = true, - displayFilters = true, - filtersState = aRoomListFiltersState(), - spaceFiltersState = aSelectedSpaceFiltersState(), - onMenuActionClick = {}, - ) -} diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/di/RoomListModule.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/di/RoomListModule.kt index ea80c1b3cb..1eeba4fff7 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/di/RoomListModule.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/di/RoomListModule.kt @@ -17,6 +17,8 @@ import io.element.android.features.home.impl.roomlist.RoomListPresenter import io.element.android.features.home.impl.roomlist.RoomListState import io.element.android.features.home.impl.search.RoomListSearchPresenter import io.element.android.features.home.impl.search.RoomListSearchState +import io.element.android.features.home.impl.spacefilters.SpaceFiltersPresenter +import io.element.android.features.home.impl.spacefilters.SpaceFiltersState import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.di.SessionScope @@ -31,4 +33,7 @@ interface RoomListModule { @Binds fun bindFiltersPresenter(presenter: RoomListFiltersPresenter): Presenter + + @Binds + fun bindSpaceFiltersPresenter(presenter: SpaceFiltersPresenter): Presenter } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt index ae016a31c1..2010555cd7 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt @@ -28,9 +28,7 @@ import im.vector.app.features.analytics.plan.Interaction import io.element.android.features.announcement.api.Announcement import io.element.android.features.announcement.api.AnnouncementService import io.element.android.features.home.impl.datasource.RoomListDataSource -import io.element.android.features.home.impl.filters.RoomListFilter.People import io.element.android.features.home.impl.filters.RoomListFilter.Rooms -import io.element.android.features.home.impl.filters.RoomListFiltersEvent import io.element.android.features.home.impl.filters.RoomListFiltersState import io.element.android.features.home.impl.filters.into import io.element.android.features.home.impl.search.RoomListSearchEvent diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenter.kt index 3b1eaddd30..a3e283b341 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenter.kt @@ -15,9 +15,8 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.di.SessionScope import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.MatrixClient @@ -26,7 +25,7 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.map -@ContributesBinding(SessionScope::class) +@Inject class SpaceFiltersPresenter( private val featureFlagService: FeatureFlagService, private val matrixClient: MatrixClient, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersState.kt index bef781ae1e..347439e853 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersState.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersState.kt @@ -8,11 +8,13 @@ package io.element.android.features.home.impl.spacefilters import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.runtime.Immutable import io.element.android.libraries.matrix.api.roomlist.RoomListFilter import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList +@Immutable sealed interface SpaceFiltersState { data object Disabled : SpaceFiltersState @@ -52,4 +54,3 @@ fun SpaceFiltersState.selectedFilter(): SpaceServiceFilter? { fun SpaceServiceFilter?.into(): RoomListFilter? { return this?.let { RoomListFilter.Identifiers(descendants) } } - diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenterTest.kt index bb00a9edac..9fb87c0eec 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenterTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenterTest.kt @@ -11,14 +11,6 @@ package io.element.android.features.home.impl.filters import com.google.common.truth.Truth.assertThat import io.element.android.features.home.impl.filters.selection.DefaultFilterSelectionStrategy import io.element.android.features.home.impl.filters.selection.FilterSelectionState -import io.element.android.libraries.dateformatter.api.DateFormatter -import io.element.android.libraries.dateformatter.test.FakeDateFormatter -import io.element.android.libraries.eventformatter.api.RoomLatestEventFormatter -import io.element.android.libraries.eventformatter.test.FakeRoomLatestEventFormatter -import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService -import io.element.android.libraries.matrix.api.roomlist.RoomListService -import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService -import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService import io.element.android.tests.testutils.awaitLastSequentialItem import io.element.android.tests.testutils.test import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -49,8 +41,7 @@ class RoomListFiltersPresenterTest { @Test @OptIn(ExperimentalCoroutinesApi::class) fun `present - toggle rooms filter`() = runTest { - val roomListService = FakeRoomListService() - val presenter = createRoomListFiltersPresenter(roomListService) + val presenter = createRoomListFiltersPresenter() presenter.test { awaitItem().eventSink.invoke(RoomListFiltersEvent.ToggleFilter(RoomListFilter.Rooms)) awaitLastSequentialItem().let { state -> @@ -84,8 +75,7 @@ class RoomListFiltersPresenterTest { @Test @OptIn(ExperimentalCoroutinesApi::class) fun `present - clear filters event`() = runTest { - val roomListService = FakeRoomListService() - val presenter = createRoomListFiltersPresenter(roomListService) + val presenter = createRoomListFiltersPresenter() presenter.test { awaitItem().eventSink.invoke(RoomListFiltersEvent.ToggleFilter(RoomListFilter.Rooms)) awaitLastSequentialItem().let { state -> @@ -105,12 +95,7 @@ private fun filterSelectionState(filter: RoomListFilter, selected: Boolean) = Fi isSelected = selected, ) -private fun TestScope.createRoomListFiltersPresenter( - roomListService: RoomListService = FakeRoomListService(), - notificationSettingsService: NotificationSettingsService = FakeNotificationSettingsService(), - dateFormatter: DateFormatter = FakeDateFormatter(), - roomLatestEventFormatter: RoomLatestEventFormatter = FakeRoomLatestEventFormatter(), -): RoomListFiltersPresenter { +private fun TestScope.createRoomListFiltersPresenter(): RoomListFiltersPresenter { return RoomListFiltersPresenter( filterSelectionStrategy = DefaultFilterSelectionStrategy(), ) diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenterTest.kt new file mode 100644 index 0000000000..8621721e2a --- /dev/null +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenterTest.kt @@ -0,0 +1,223 @@ +/* + * Copyright (c) 2026 Element Creations 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.features.home.impl.spacefilters + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.spaces.FakeSpaceService +import io.element.android.tests.testutils.awaitLastSequentialItem +import io.element.android.tests.testutils.test +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class SpaceFiltersPresenterTest { + @Test + fun `present - when feature flag is disabled returns Disabled state`() = runTest { + val presenter = createSpaceFiltersPresenter( + featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to false) + ) + ) + presenter.test { + val state = awaitItem() + assertThat(state).isEqualTo(SpaceFiltersState.Disabled) + } + } + + @Test + fun `present - when feature flag is enabled returns Unselected state initially`() = runTest { + val presenter = createSpaceFiltersPresenter( + featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true) + ) + ) + presenter.test { + val state = awaitLastSequentialItem() + assertThat(state).isInstanceOf(SpaceFiltersState.Unselected::class.java) + } + } + + @Test + fun `present - ShowFilters event transitions from Unselected to Selecting`() = runTest { + val presenter = createSpaceFiltersPresenter( + featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true) + ) + ) + presenter.test { + val unselectedState = awaitLastSequentialItem() as SpaceFiltersState.Unselected + unselectedState.eventSink(SpaceFiltersEvent.Unselected.ShowFilters) + + val selectingState = awaitLastSequentialItem() + assertThat(selectingState).isInstanceOf(SpaceFiltersState.Selecting::class.java) + } + } + + @Test + fun `present - Cancel event in Selecting state transitions back to Unselected`() = runTest { + val presenter = createSpaceFiltersPresenter( + featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true) + ) + ) + presenter.test { + // Start in Unselected + val unselectedState = awaitLastSequentialItem() as SpaceFiltersState.Unselected + unselectedState.eventSink(SpaceFiltersEvent.Unselected.ShowFilters) + + // Now in Selecting + val selectingState = awaitLastSequentialItem() as SpaceFiltersState.Selecting + selectingState.eventSink(SpaceFiltersEvent.Selecting.Cancel) + + // Back to Unselected + val finalState = awaitLastSequentialItem() + assertThat(finalState).isInstanceOf(SpaceFiltersState.Unselected::class.java) + } + } + + @Test + fun `present - SelectFilter event in Selecting state transitions to Selected`() = runTest { + val spaceFilter = aSpaceServiceFilter(displayName = "Test Space") + val presenter = createSpaceFiltersPresenter( + featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true) + ) + ) + presenter.test { + // Start in Unselected + val unselectedState = awaitLastSequentialItem() as SpaceFiltersState.Unselected + unselectedState.eventSink(SpaceFiltersEvent.Unselected.ShowFilters) + + // Now in Selecting + val selectingState = awaitLastSequentialItem() as SpaceFiltersState.Selecting + selectingState.eventSink(SpaceFiltersEvent.Selecting.SelectFilter(spaceFilter)) + + // Now in Selected + val selectedState = awaitLastSequentialItem() as SpaceFiltersState.Selected + assertThat(selectedState.selectedFilter).isEqualTo(spaceFilter) + } + } + + @Test + fun `present - ClearSelection event in Selected state transitions back to Unselected`() = runTest { + val spaceFilter = aSpaceServiceFilter(displayName = "Test Space") + val presenter = createSpaceFiltersPresenter( + featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true) + ) + ) + presenter.test { + // Start in Unselected + val unselectedState = awaitLastSequentialItem() as SpaceFiltersState.Unselected + unselectedState.eventSink(SpaceFiltersEvent.Unselected.ShowFilters) + + // Now in Selecting + val selectingState = awaitLastSequentialItem() as SpaceFiltersState.Selecting + selectingState.eventSink(SpaceFiltersEvent.Selecting.SelectFilter(spaceFilter)) + + // Now in Selected + val selectedState = awaitLastSequentialItem() as SpaceFiltersState.Selected + selectedState.eventSink(SpaceFiltersEvent.Selected.ClearSelection) + + // Back to Unselected + val finalState = awaitLastSequentialItem() + assertThat(finalState).isInstanceOf(SpaceFiltersState.Unselected::class.java) + } + } + + @Test + fun `present - available filters are passed from SpaceService`() = runTest { + val spaceFilter1 = aSpaceServiceFilter(displayName = "Work", roomId = RoomId("!work:example.com")) + val spaceFilter2 = aSpaceServiceFilter(displayName = "Personal", roomId = RoomId("!personal:example.com")) + val spaceFilters = listOf(spaceFilter1, spaceFilter2) + + val spaceService = FakeSpaceService() + val matrixClient = FakeMatrixClient(spaceService = spaceService) + + val presenter = createSpaceFiltersPresenter( + featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true) + ), + matrixClient = matrixClient, + ) + presenter.test { + // Start in Unselected + val unselectedState = awaitLastSequentialItem() as SpaceFiltersState.Unselected + unselectedState.eventSink(SpaceFiltersEvent.Unselected.ShowFilters) + + // Emit space filters + spaceService.emitSpaceFilters(spaceFilters) + + // Now in Selecting with available filters + val selectingState = awaitLastSequentialItem() as SpaceFiltersState.Selecting + assertThat(selectingState.availableFilters).containsExactly(spaceFilter1, spaceFilter2).inOrder() + } + } + + @Test + fun `present - selected filter stays in sync when available filters update`() = runTest { + val originalFilter = aSpaceServiceFilter( + displayName = "Work", + roomId = RoomId("!work:example.com"), + descendants = listOf(RoomId("!room1:example.com")) + ) + val updatedFilter = aSpaceServiceFilter( + displayName = "Work", + roomId = RoomId("!work:example.com"), + descendants = listOf(RoomId("!room1:example.com"), RoomId("!room2:example.com")) + ) + + val spaceService = FakeSpaceService() + val matrixClient = FakeMatrixClient(spaceService = spaceService) + + val presenter = createSpaceFiltersPresenter( + featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true) + ), + matrixClient = matrixClient, + ) + presenter.test { + // Start in Unselected + val unselectedState = awaitLastSequentialItem() as SpaceFiltersState.Unselected + unselectedState.eventSink(SpaceFiltersEvent.Unselected.ShowFilters) + + // Emit initial space filters + spaceService.emitSpaceFilters(listOf(originalFilter)) + + // Now in Selecting + val selectingState = awaitLastSequentialItem() as SpaceFiltersState.Selecting + selectingState.eventSink(SpaceFiltersEvent.Selecting.SelectFilter(originalFilter)) + + // Now in Selected + val selectedState = awaitLastSequentialItem() as SpaceFiltersState.Selected + assertThat(selectedState.selectedFilter.descendants).hasSize(1) + + // Emit updated space filters + spaceService.emitSpaceFilters(listOf(updatedFilter)) + + // Selected filter should be updated + val updatedSelectedState = awaitLastSequentialItem() as SpaceFiltersState.Selected + assertThat(updatedSelectedState.selectedFilter.descendants).hasSize(2) + } + } + + private fun createSpaceFiltersPresenter( + featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(), + matrixClient: FakeMatrixClient = FakeMatrixClient(), + ): SpaceFiltersPresenter { + return SpaceFiltersPresenter( + featureFlagService = featureFlagService, + matrixClient = matrixClient, + ) + } +} diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersViewTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersViewTest.kt new file mode 100644 index 0000000000..5c1325b107 --- /dev/null +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersViewTest.kt @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2026 Element Creations 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.features.home.impl.spacefilters + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.libraries.matrix.test.A_ROOM_ALIAS +import io.element.android.tests.testutils.EventsRecorder +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class SpaceFiltersViewTest { + @get:Rule + val rule = createAndroidComposeRule() + + @Test + fun `clicking on a filter with alias shows display name and alias`() { + val filter = aSpaceServiceFilter( + displayName = "Test Space", + canonicalAlias = A_ROOM_ALIAS, + ) + val eventsRecorder = EventsRecorder() + rule.setSpaceFiltersView( + state = aSelectingSpaceFiltersState( + availableFilters = listOf(filter), + eventSink = eventsRecorder, + ) + ) + + // Both display name and alias should be visible + rule.onNodeWithText(filter.spaceRoom.displayName).assertExists() + rule.onNodeWithText(A_ROOM_ALIAS.value).assertExists() + + rule.onNodeWithText(filter.spaceRoom.displayName).performClick() + + eventsRecorder.assertSingle(SpaceFiltersEvent.Selecting.SelectFilter(filter)) + } + + @Test + fun `multiple filters are displayed and clickable`() { + val filter1 = aSpaceServiceFilter(displayName = "Space One") + val filter2 = aSpaceServiceFilter(displayName = "Space Two") + val eventsRecorder = EventsRecorder() + rule.setSpaceFiltersView( + state = aSelectingSpaceFiltersState( + availableFilters = listOf(filter1, filter2), + eventSink = eventsRecorder, + ) + ) + + // Both filters should be visible + rule.onNodeWithText(filter1.spaceRoom.displayName).assertExists() + rule.onNodeWithText(filter2.spaceRoom.displayName).assertExists() + + // Click on second filter + rule.onNodeWithText(filter2.spaceRoom.displayName).performClick() + + eventsRecorder.assertSingle(SpaceFiltersEvent.Selecting.SelectFilter(filter2)) + } +} + +private fun AndroidComposeTestRule.setSpaceFiltersView( + state: SpaceFiltersState, +) { + setContent { + SpaceFiltersView(state = state) + } +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListFilter.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListFilter.kt index b4abedd75d..11eed2128b 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListFilter.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListFilter.kt @@ -44,8 +44,8 @@ sealed interface RoomListFilter { ) : RoomListFilter data class Identifiers( - val values : List, - ): RoomListFilter + val values: List, + ) : RoomListFilter /** * A filter that matches rooms that are unread. 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 e4c1c9475d..f6deef5c9a 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 @@ -37,12 +37,12 @@ class FakeSpaceService( _topLevelSpacesFlow.emit(value) } - private val _spaceServiceFiltersFlow = MutableSharedFlow>() + private val _spaceFiltersFlow = MutableSharedFlow>() override val spaceFiltersFlow: SharedFlow> - get() = _spaceServiceFiltersFlow.asSharedFlow() + get() = _spaceFiltersFlow.asSharedFlow() suspend fun emitSpaceFilters(value: List) { - _spaceServiceFiltersFlow.emit(value) + _spaceFiltersFlow.emit(value) } override suspend fun joinedParents(spaceId: RoomId): Result> { diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt index 245f841d1a..261ec3834d 100644 --- a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt @@ -89,6 +89,7 @@ class KonsistPreviewTest { "GradientFloatingActionButtonCircleShapePreview", "HeaderFooterPageScrollablePreview", "HomeTopBarMultiAccountPreview", + "HomeTopBarSpaceFiltersSelectedPreview", "HomeTopBarSpacesPreview", "HomeTopBarWithIndicatorPreview", "IconsOtherPreview", From dcc2776cd14be158a6faad372dcfb07b89a4542f Mon Sep 17 00:00:00 2001 From: ElementBot Date: Wed, 4 Feb 2026 14:02:54 +0000 Subject: [PATCH 17/22] Update screenshots --- ...s.home.impl.components_HomeTopBarMultiAccount_Day_0_en.png | 4 ++-- ...home.impl.components_HomeTopBarMultiAccount_Night_0_en.png | 4 ++-- ...mpl.components_HomeTopBarSpaceFiltersSelected_Day_0_en.png | 3 +++ ...l.components_HomeTopBarSpaceFiltersSelected_Night_0_en.png | 3 +++ ....home.impl.components_HomeTopBarWithIndicator_Day_0_en.png | 4 ++-- ...ome.impl.components_HomeTopBarWithIndicator_Night_0_en.png | 4 ++-- .../features.home.impl.components_HomeTopBar_Day_0_en.png | 4 ++-- .../features.home.impl.components_HomeTopBar_Night_0_en.png | 4 ++-- ...tures.home.impl.spacefilters_SpaceFiltersView_Day_0_en.png | 3 +++ ...tures.home.impl.spacefilters_SpaceFiltersView_Day_1_en.png | 3 +++ ...res.home.impl.spacefilters_SpaceFiltersView_Night_0_en.png | 3 +++ ...res.home.impl.spacefilters_SpaceFiltersView_Night_1_en.png | 3 +++ 12 files changed, 30 insertions(+), 12 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarSpaceFiltersSelected_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarSpaceFiltersSelected_Night_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.home.impl.spacefilters_SpaceFiltersView_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.home.impl.spacefilters_SpaceFiltersView_Day_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.home.impl.spacefilters_SpaceFiltersView_Night_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.home.impl.spacefilters_SpaceFiltersView_Night_1_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarMultiAccount_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarMultiAccount_Day_0_en.png index ea9abe74c0..1d20b765e7 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarMultiAccount_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarMultiAccount_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6992a2fbaebed88e22c58e393d086bbcba50afd822ac869b9e27dc68ed3a493e -size 22007 +oid sha256:d03c47b707264a6aad1ff7f75419eee07a379c51cbcb07ac504d48983b362225 +size 22182 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarMultiAccount_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarMultiAccount_Night_0_en.png index e3f6f0ea4e..e88eb16bfa 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarMultiAccount_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarMultiAccount_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:67a87f2231268e26e06e62aada18c4c62e8ee53e1eb1ddc538f8c7e98881acbc -size 20256 +oid sha256:af5561d67d9d28418c1f9d106527c674ffbd63aacc793e371942e9216d2aadc8 +size 20425 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarSpaceFiltersSelected_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarSpaceFiltersSelected_Day_0_en.png new file mode 100644 index 0000000000..d2f36e66d7 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarSpaceFiltersSelected_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:601cff975cb714b8fafdbee6741f6f642ea744880b31bf060540f821fc5f911e +size 23195 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarSpaceFiltersSelected_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarSpaceFiltersSelected_Night_0_en.png new file mode 100644 index 0000000000..719a95f1a3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarSpaceFiltersSelected_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8722652998d147e103dfb84934787b406c93508aa6a13815b82f36a424e02863 +size 21316 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarWithIndicator_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarWithIndicator_Day_0_en.png index 93b29cd67f..dee99e6989 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarWithIndicator_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarWithIndicator_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0fdbe7e7ab22f44cc60a04582d5a5159055d4af3af0d3d2a7cd012f48abd9592 -size 22443 +oid sha256:76867d38b4b78d36837bbafc93c442b714fc1f0941a1cbe0cf46f7befb4f5347 +size 22611 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarWithIndicator_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarWithIndicator_Night_0_en.png index a796c3a10e..db984f351e 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarWithIndicator_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarWithIndicator_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96942af60064898a853268f8aa8103f04a836332fef09fbfa83ad01cccae54d3 -size 20651 +oid sha256:1fb40adfd3139dd952b9707825e4938d630dba7f00d3192b3aef300de90ca0ba +size 20823 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBar_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBar_Day_0_en.png index 1df7a1d9ec..4bb3b53cea 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBar_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBar_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e38f16c942bcfb31103b5e80d168a5425f181632ea4440a054aa4d0985bd335a -size 22106 +oid sha256:a03be8035fc5e5a4a653744504389d9bd2c9f35e1ab9a4070d73dbb08f934e4d +size 22277 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBar_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBar_Night_0_en.png index ef2d6e3500..947371e698 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBar_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBar_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:677c21f745e4b9c7984f6c8fe3f84a27fabe6aeaf5f8cac82bb5ac692bd6a797 -size 20308 +oid sha256:67f04b8baa89527233d9cfbe0faf6e57b2ad384d35935cf866e46cf3d05f0fc7 +size 20474 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.spacefilters_SpaceFiltersView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.spacefilters_SpaceFiltersView_Day_0_en.png new file mode 100644 index 0000000000..d189f2ea85 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.spacefilters_SpaceFiltersView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5ed03e5c6103dd4d96a0c4a8fda97808801f853cc3e05595e339a67e0b228027 +size 30472 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.spacefilters_SpaceFiltersView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.spacefilters_SpaceFiltersView_Day_1_en.png new file mode 100644 index 0000000000..df44e09e95 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.spacefilters_SpaceFiltersView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dfeca128a35edebe19f59749bedcd7335422377fd2627f95f81d7e3e28dd61e5 +size 18173 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.spacefilters_SpaceFiltersView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.spacefilters_SpaceFiltersView_Night_0_en.png new file mode 100644 index 0000000000..8601473ecb --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.spacefilters_SpaceFiltersView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:543f80c91d04f44c4c62a06c9e44b334826d5bd92f2b17a4eeeb7e15dfbed0c1 +size 29510 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.spacefilters_SpaceFiltersView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.spacefilters_SpaceFiltersView_Night_1_en.png new file mode 100644 index 0000000000..7ac4ff5a03 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.spacefilters_SpaceFiltersView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8735caab11d89a689591c1d1ad5c1483d0c1fc01ce7b5933c7546ce2c2641d4a +size 17162 From 0f668370b70277b6f569a5d9cf8299ee3a9727fe Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 4 Feb 2026 16:12:52 +0100 Subject: [PATCH 18/22] Clear selection when selected space is removed from filters --- .../spacefilters/SpaceFiltersPresenter.kt | 17 +++++---- .../spacefilters/SpaceFiltersPresenterTest.kt | 37 +++++++++++++++++++ 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenter.kt index a3e283b341..92a2f33e7c 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenter.kt @@ -9,8 +9,8 @@ package io.element.android.features.home.impl.spacefilters import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -86,12 +86,15 @@ class SpaceFiltersPresenter( ) } is SelectionMode.Selected -> { - // Keep in sync with the available filters if rooms are added/removed - val selectedFilter by remember { - derivedStateOf { - availableFilters - .firstOrNull { it.spaceRoom.roomId == mode.filter.spaceRoom.roomId } - ?: mode.filter + var selectedFilter by remember { mutableStateOf(mode.filter) } + // Makes sure the selectedFilter stays in sync with the available filters + LaunchedEffect(availableFilters) { + val upToDateFilter = availableFilters + .firstOrNull { it.spaceRoom.roomId == mode.filter.spaceRoom.roomId } + if (upToDateFilter == null) { + selectionMode = SelectionMode.Unselected + } else { + selectedFilter = upToDateFilter } } SpaceFiltersState.Selected( diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenterTest.kt index 8621721e2a..4d4b908624 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenterTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenterTest.kt @@ -164,6 +164,43 @@ class SpaceFiltersPresenterTest { } } + @Test + fun `present - selected filter is cleared when space is removed from available filters`() = runTest { + val spaceFilter = aSpaceServiceFilter(displayName = "Work", roomId = RoomId("!work:example.com")) + val otherSpaceFilter = aSpaceServiceFilter(displayName = "Personal", roomId = RoomId("!personal:example.com")) + + val spaceService = FakeSpaceService() + val matrixClient = FakeMatrixClient(spaceService = spaceService) + + val presenter = createSpaceFiltersPresenter( + featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true) + ), + matrixClient = matrixClient, + ) + presenter.test { + // Go to Selecting and emit filters + val unselectedState = awaitLastSequentialItem() as SpaceFiltersState.Unselected + unselectedState.eventSink(SpaceFiltersEvent.Unselected.ShowFilters) + spaceService.emitSpaceFilters(listOf(spaceFilter, otherSpaceFilter)) + + // Select the filter + val selectingState = awaitLastSequentialItem() as SpaceFiltersState.Selecting + selectingState.eventSink(SpaceFiltersEvent.Selecting.SelectFilter(spaceFilter)) + + // Verify in Selected state + val selectedState = awaitLastSequentialItem() as SpaceFiltersState.Selected + assertThat(selectedState.selectedFilter).isEqualTo(spaceFilter) + + // Remove the selected space from available filters (but keep other spaces) + spaceService.emitSpaceFilters(listOf(otherSpaceFilter)) + + // Should auto-transition to Unselected + val finalState = awaitLastSequentialItem() + assertThat(finalState).isInstanceOf(SpaceFiltersState.Unselected::class.java) + } + } + @Test fun `present - selected filter stays in sync when available filters update`() = runTest { val originalFilter = aSpaceServiceFilter( From 5e0e1f8f4f3d1396d325e78b6c5f2f682aa9062c Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 5 Feb 2026 10:35:27 +0100 Subject: [PATCH 19/22] SpaceFiltersView : try to fix the BottomSheet hide animation --- .../impl/spacefilters/SpaceFiltersView.kt | 68 ++++++++++++------- 1 file changed, 42 insertions(+), 26 deletions(-) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt index 5a8c583e57..a753e78a87 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt @@ -49,6 +49,7 @@ import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.ui.strings.CommonStrings +import timber.log.Timber @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -57,34 +58,50 @@ fun SpaceFiltersView( modifier: Modifier = Modifier ) { val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val isSelecting = state is SpaceFiltersState.Selecting var showSheet by remember { mutableStateOf(false) } - LaunchedEffect(state) { - when (state) { - is SpaceFiltersState.Selecting -> showSheet = true - else -> { - sheetState.hide() - showSheet = false - } + LaunchedEffect(isSelecting) { + if (isSelecting) { + showSheet = true + } else { + sheetState.hide() } } - Box(modifier = modifier) { - if (showSheet && state is SpaceFiltersState.Selecting) { - ModalBottomSheet( + // This is necessary because the animation can get cancelled + // then the sheetState is hidden but the showSheet is still true. + LaunchedEffect(sheetState.isVisible, sheetState.isAnimationRunning) { + if (!sheetState.isVisible && !sheetState.isAnimationRunning) { + showSheet = false + } + } + if (showSheet) { + ModalBottomSheet( + modifier = modifier + .systemBarsPadding() + .navigationBarsPadding(), + sheetState = sheetState, + onDismissRequest = { + if (isSelecting) { + state.eventSink(SpaceFiltersEvent.Selecting.Cancel) + } + } + ) { + Box( modifier = Modifier - .systemBarsPadding() - .navigationBarsPadding(), - sheetState = sheetState, - onDismissRequest = { state.eventSink(SpaceFiltersEvent.Selecting.Cancel) }, + .fillMaxWidth() + .fillMaxHeight(0.9f) ) { - SpaceFiltersBottomSheetContent( - filters = state.visibleFilters, - searchQuery = state.searchQuery, - onFilterSelected = { filter -> - state.eventSink(SpaceFiltersEvent.Selecting.SelectFilter(filter)) - } - ) + if (isSelecting) { + SpaceFiltersBottomSheetContent( + filters = state.visibleFilters, + searchQuery = state.searchQuery, + onFilterSelected = { filter -> + state.eventSink(SpaceFiltersEvent.Selecting.SelectFilter(filter)) + } + ) + } } } } @@ -98,10 +115,7 @@ private fun SpaceFiltersBottomSheetContent( modifier: Modifier = Modifier ) { Column( - modifier = modifier - .fillMaxWidth() - .fillMaxHeight(0.9f) - .padding(vertical = 16.dp) + modifier = modifier.padding(vertical = 16.dp) ) { Text( text = stringResource(R.string.screen_roomlist_your_spaces), @@ -113,7 +127,9 @@ private fun SpaceFiltersBottomSheetContent( Spacer(modifier = Modifier.height(12.dp)) SearchField( state = searchQuery, - modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), placeholder = stringResource(CommonStrings.action_search), ) Spacer(modifier = Modifier.height(16.dp)) From 0466663d64875d359bf5fe58f2af7ab754902290 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 5 Feb 2026 11:33:24 +0100 Subject: [PATCH 20/22] Improve SpaceFilters: hide when empty and fix tests --- .../spacefilters/SpaceFiltersPresenter.kt | 8 +- .../spacefilters/SpaceFiltersStateProvider.kt | 4 +- .../impl/spacefilters/SpaceFiltersView.kt | 1 - .../spacefilters/SpaceFiltersPresenterTest.kt | 79 ++++++++++++++++--- 4 files changed, 72 insertions(+), 20 deletions(-) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenter.kt index 92a2f33e7c..9813732bdb 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenter.kt @@ -36,14 +36,14 @@ class SpaceFiltersPresenter( .isFeatureEnabledFlow(FeatureFlags.RoomListSpaceFilters) .collectAsState(initial = false) - if (!isFeatureEnabled) { - return SpaceFiltersState.Disabled - } - val availableFilters by remember { matrixClient.spaceService.spaceFiltersFlow.map { it.toImmutableList() } }.collectAsState(initial = persistentListOf()) + if (!isFeatureEnabled || availableFilters.isEmpty()) { + return SpaceFiltersState.Disabled + } + var selectionMode by remember { mutableStateOf(SelectionMode.Unselected) } fun handleUnselectedEvent(event: SpaceFiltersEvent.Unselected) { diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersStateProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersStateProvider.kt index 931d39aef4..264d122836 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersStateProvider.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersStateProvider.kt @@ -13,7 +13,7 @@ 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.spaces.SpaceServiceFilter import io.element.android.libraries.previewutils.room.aSpaceRoom -import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList class SpaceFiltersStateProvider : PreviewParameterProvider { override val values: Sequence @@ -55,7 +55,7 @@ fun aSelectingSpaceFiltersState( searchQuery: String = "", eventSink: (SpaceFiltersEvent.Selecting) -> Unit = {}, ) = SpaceFiltersState.Selecting( - availableFilters = persistentListOf(*availableFilters.toTypedArray()), + availableFilters = availableFilters.toImmutableList(), searchQuery = TextFieldState(searchQuery), eventSink = eventSink, ) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt index a753e78a87..3ce03dbdd3 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt @@ -49,7 +49,6 @@ import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.ui.strings.CommonStrings -import timber.log.Timber @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenterTest.kt index 4d4b908624..278a268864 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenterTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenterTest.kt @@ -35,13 +35,34 @@ class SpaceFiltersPresenterTest { } @Test - fun `present - when feature flag is enabled returns Unselected state initially`() = runTest { + fun `present - when available filters is empty returns Disabled state`() = runTest { val presenter = createSpaceFiltersPresenter( featureFlagService = FakeFeatureFlagService( initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true) ) ) presenter.test { + val state = awaitLastSequentialItem() + assertThat(state).isEqualTo(SpaceFiltersState.Disabled) + } + } + + @Test + fun `present - when feature flag is enabled and filters exist returns Unselected state`() = runTest { + val spaceFilter = aSpaceServiceFilter(displayName = "Test Space") + val spaceService = FakeSpaceService() + val matrixClient = FakeMatrixClient(spaceService = spaceService) + + val presenter = createSpaceFiltersPresenter( + featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true) + ), + matrixClient = matrixClient, + ) + presenter.test { + // Emit filters + spaceService.emitSpaceFilters(listOf(spaceFilter)) + val state = awaitLastSequentialItem() assertThat(state).isInstanceOf(SpaceFiltersState.Unselected::class.java) } @@ -49,12 +70,20 @@ class SpaceFiltersPresenterTest { @Test fun `present - ShowFilters event transitions from Unselected to Selecting`() = runTest { + val spaceFilter = aSpaceServiceFilter(displayName = "Test Space") + val spaceService = FakeSpaceService() + val matrixClient = FakeMatrixClient(spaceService = spaceService) + val presenter = createSpaceFiltersPresenter( featureFlagService = FakeFeatureFlagService( initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true) - ) + ), + matrixClient = matrixClient, ) presenter.test { + // Emit filters first + spaceService.emitSpaceFilters(listOf(spaceFilter)) + val unselectedState = awaitLastSequentialItem() as SpaceFiltersState.Unselected unselectedState.eventSink(SpaceFiltersEvent.Unselected.ShowFilters) @@ -65,12 +94,20 @@ class SpaceFiltersPresenterTest { @Test fun `present - Cancel event in Selecting state transitions back to Unselected`() = runTest { + val spaceFilter = aSpaceServiceFilter(displayName = "Test Space") + val spaceService = FakeSpaceService() + val matrixClient = FakeMatrixClient(spaceService = spaceService) + val presenter = createSpaceFiltersPresenter( featureFlagService = FakeFeatureFlagService( initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true) - ) + ), + matrixClient = matrixClient, ) presenter.test { + // Emit filters first + spaceService.emitSpaceFilters(listOf(spaceFilter)) + // Start in Unselected val unselectedState = awaitLastSequentialItem() as SpaceFiltersState.Unselected unselectedState.eventSink(SpaceFiltersEvent.Unselected.ShowFilters) @@ -88,12 +125,19 @@ class SpaceFiltersPresenterTest { @Test fun `present - SelectFilter event in Selecting state transitions to Selected`() = runTest { val spaceFilter = aSpaceServiceFilter(displayName = "Test Space") + val spaceService = FakeSpaceService() + val matrixClient = FakeMatrixClient(spaceService = spaceService) + val presenter = createSpaceFiltersPresenter( featureFlagService = FakeFeatureFlagService( initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true) - ) + ), + matrixClient = matrixClient, ) presenter.test { + // Emit filters first + spaceService.emitSpaceFilters(listOf(spaceFilter)) + // Start in Unselected val unselectedState = awaitLastSequentialItem() as SpaceFiltersState.Unselected unselectedState.eventSink(SpaceFiltersEvent.Unselected.ShowFilters) @@ -111,12 +155,19 @@ class SpaceFiltersPresenterTest { @Test fun `present - ClearSelection event in Selected state transitions back to Unselected`() = runTest { val spaceFilter = aSpaceServiceFilter(displayName = "Test Space") + val spaceService = FakeSpaceService() + val matrixClient = FakeMatrixClient(spaceService = spaceService) + val presenter = createSpaceFiltersPresenter( featureFlagService = FakeFeatureFlagService( initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true) - ) + ), + matrixClient = matrixClient, ) presenter.test { + // Emit filters first + spaceService.emitSpaceFilters(listOf(spaceFilter)) + // Start in Unselected val unselectedState = awaitLastSequentialItem() as SpaceFiltersState.Unselected unselectedState.eventSink(SpaceFiltersEvent.Unselected.ShowFilters) @@ -151,13 +202,13 @@ class SpaceFiltersPresenterTest { matrixClient = matrixClient, ) presenter.test { + // Emit space filters + spaceService.emitSpaceFilters(spaceFilters) + // Start in Unselected val unselectedState = awaitLastSequentialItem() as SpaceFiltersState.Unselected unselectedState.eventSink(SpaceFiltersEvent.Unselected.ShowFilters) - // Emit space filters - spaceService.emitSpaceFilters(spaceFilters) - // Now in Selecting with available filters val selectingState = awaitLastSequentialItem() as SpaceFiltersState.Selecting assertThat(selectingState.availableFilters).containsExactly(spaceFilter1, spaceFilter2).inOrder() @@ -179,10 +230,12 @@ class SpaceFiltersPresenterTest { matrixClient = matrixClient, ) presenter.test { - // Go to Selecting and emit filters + // Emit filters first + spaceService.emitSpaceFilters(listOf(spaceFilter, otherSpaceFilter)) + + // Go to Selecting val unselectedState = awaitLastSequentialItem() as SpaceFiltersState.Unselected unselectedState.eventSink(SpaceFiltersEvent.Unselected.ShowFilters) - spaceService.emitSpaceFilters(listOf(spaceFilter, otherSpaceFilter)) // Select the filter val selectingState = awaitLastSequentialItem() as SpaceFiltersState.Selecting @@ -224,13 +277,13 @@ class SpaceFiltersPresenterTest { matrixClient = matrixClient, ) presenter.test { + // Emit initial space filters + spaceService.emitSpaceFilters(listOf(originalFilter)) + // Start in Unselected val unselectedState = awaitLastSequentialItem() as SpaceFiltersState.Unselected unselectedState.eventSink(SpaceFiltersEvent.Unselected.ShowFilters) - // Emit initial space filters - spaceService.emitSpaceFilters(listOf(originalFilter)) - // Now in Selecting val selectingState = awaitLastSequentialItem() as SpaceFiltersState.Selecting selectingState.eventSink(SpaceFiltersEvent.Selecting.SelectFilter(originalFilter)) From e64f0af4e49269ea27f6abe17b33830eed01f7d1 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 5 Feb 2026 13:52:41 +0100 Subject: [PATCH 21/22] Space filters fix previews --- .../impl/roomlist/RoomListStateProvider.kt | 4 +- .../impl/spacefilters/SpaceFiltersView.kt | 38 +++++++++---------- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateProvider.kt index 7f83eb7eb0..97600e0247 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateProvider.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateProvider.kt @@ -19,7 +19,7 @@ import io.element.android.features.home.impl.model.anInviteSender import io.element.android.features.home.impl.search.RoomListSearchState import io.element.android.features.home.impl.search.aRoomListSearchState import io.element.android.features.home.impl.spacefilters.SpaceFiltersState -import io.element.android.features.home.impl.spacefilters.aDisabledSpaceFiltersState +import io.element.android.features.home.impl.spacefilters.anUnselectedSpaceFiltersState import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState import io.element.android.features.invite.api.acceptdecline.anAcceptDeclineInviteState import io.element.android.features.leaveroom.api.LeaveRoomEvent @@ -54,7 +54,7 @@ internal fun aRoomListState( leaveRoomState: LeaveRoomState = aLeaveRoomState(), searchState: RoomListSearchState = aRoomListSearchState(), filtersState: RoomListFiltersState = aRoomListFiltersState(), - spaceFiltersState: SpaceFiltersState = aDisabledSpaceFiltersState(), + spaceFiltersState: SpaceFiltersState = anUnselectedSpaceFiltersState(), contentState: RoomListContentState = aRoomsContentState(), acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(), hideInvitesAvatars: Boolean = false, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt index 3ce03dbdd3..fb77c74203 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt @@ -23,13 +23,12 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SheetValue import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -56,33 +55,30 @@ fun SpaceFiltersView( state: SpaceFiltersState, modifier: Modifier = Modifier ) { - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) - val isSelecting = state is SpaceFiltersState.Selecting - var showSheet by remember { mutableStateOf(false) } - + val isSelecting by rememberUpdatedState(state is SpaceFiltersState.Selecting) + val sheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = true, + confirmValueChange = { sheetValueTarget -> + // This ensures the hide animation is not cancelled + when (sheetValueTarget) { + SheetValue.Expanded -> isSelecting + else -> true + } + } + ) LaunchedEffect(isSelecting) { - if (isSelecting) { - showSheet = true - } else { + if (!isSelecting) { sheetState.hide() } } - - // This is necessary because the animation can get cancelled - // then the sheetState is hidden but the showSheet is still true. - LaunchedEffect(sheetState.isVisible, sheetState.isAnimationRunning) { - if (!sheetState.isVisible && !sheetState.isAnimationRunning) { - showSheet = false - } - } - if (showSheet) { + if (sheetState.isVisible || isSelecting) { ModalBottomSheet( modifier = modifier .systemBarsPadding() .navigationBarsPadding(), sheetState = sheetState, onDismissRequest = { - if (isSelecting) { + if (state is SpaceFiltersState.Selecting) { state.eventSink(SpaceFiltersEvent.Selecting.Cancel) } } @@ -92,7 +88,7 @@ fun SpaceFiltersView( .fillMaxWidth() .fillMaxHeight(0.9f) ) { - if (isSelecting) { + if (state is SpaceFiltersState.Selecting) { SpaceFiltersBottomSheetContent( filters = state.visibleFilters, searchQuery = state.searchQuery, From 4d5872ceaf1c227bd3bfea9417b5c70bc67e8b3a Mon Sep 17 00:00:00 2001 From: ElementBot Date: Thu, 5 Feb 2026 14:21:11 +0000 Subject: [PATCH 22/22] Update screenshots --- .../snapshots/images/features.home.impl_HomeViewA11y_en.png | 4 ++-- .../snapshots/images/features.home.impl_HomeView_Day_0_en.png | 4 ++-- .../images/features.home.impl_HomeView_Day_10_en.png | 4 ++-- .../images/features.home.impl_HomeView_Day_11_en.png | 4 ++-- .../images/features.home.impl_HomeView_Day_13_en.png | 4 ++-- .../images/features.home.impl_HomeView_Day_14_en.png | 4 ++-- .../images/features.home.impl_HomeView_Day_15_en.png | 4 ++-- .../snapshots/images/features.home.impl_HomeView_Day_1_en.png | 4 ++-- .../snapshots/images/features.home.impl_HomeView_Day_2_en.png | 4 ++-- .../snapshots/images/features.home.impl_HomeView_Day_3_en.png | 4 ++-- .../snapshots/images/features.home.impl_HomeView_Day_5_en.png | 4 ++-- .../snapshots/images/features.home.impl_HomeView_Day_6_en.png | 4 ++-- .../snapshots/images/features.home.impl_HomeView_Day_7_en.png | 4 ++-- .../snapshots/images/features.home.impl_HomeView_Day_8_en.png | 4 ++-- .../snapshots/images/features.home.impl_HomeView_Day_9_en.png | 4 ++-- .../images/features.home.impl_HomeView_Night_0_en.png | 4 ++-- .../images/features.home.impl_HomeView_Night_10_en.png | 4 ++-- .../images/features.home.impl_HomeView_Night_11_en.png | 4 ++-- .../images/features.home.impl_HomeView_Night_13_en.png | 4 ++-- .../images/features.home.impl_HomeView_Night_14_en.png | 4 ++-- .../images/features.home.impl_HomeView_Night_15_en.png | 4 ++-- .../images/features.home.impl_HomeView_Night_1_en.png | 4 ++-- .../images/features.home.impl_HomeView_Night_2_en.png | 4 ++-- .../images/features.home.impl_HomeView_Night_3_en.png | 4 ++-- .../images/features.home.impl_HomeView_Night_5_en.png | 4 ++-- .../images/features.home.impl_HomeView_Night_6_en.png | 4 ++-- .../images/features.home.impl_HomeView_Night_7_en.png | 4 ++-- .../images/features.home.impl_HomeView_Night_8_en.png | 4 ++-- .../images/features.home.impl_HomeView_Night_9_en.png | 4 ++-- 29 files changed, 58 insertions(+), 58 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeViewA11y_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeViewA11y_en.png index 98ce0d3bc1..4226e61fae 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeViewA11y_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeViewA11y_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cc3fcb72de86766e3b97ffe0802551d7597b688f2a52ef73938d91c9cfdf0633 -size 141868 +oid sha256:1bf2830f59241a4f4fe7b6d5a53576d5b5738f036044dc925794c4a035d1284b +size 143545 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_0_en.png index edc3a6f1f1..e2bbfae52f 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:02f12178991ed984da4b2d0a9883250750b6e37e7c9ce8494ca12bb987f29ad4 -size 65455 +oid sha256:fb6a0347fa3b5639fa30cf4e353c85b5cab45bb07375bf622d46c896a6ff1c36 +size 65612 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_10_en.png index eb4002392c..4cdc19760e 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4eb7b8997069e2a4b37cd27c33e2862cba1871e1b58e85feb0b1bc4da6a3fce8 -size 33631 +oid sha256:f3ad2e84a242788e35b221311348d3cf7053ae14501b668ba91015b4a51b81fd +size 33797 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_11_en.png index 886d567126..4c09e8a3f4 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ff84f90db0deea1d8edc52a2067855cf277cc747f5c357dc0758ce72bbc6d0c8 -size 28014 +oid sha256:daeec92b6da82df24261e400005662a2cf367328837256873615bf5357d36ddc +size 28212 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_13_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_13_en.png index 583ae6e976..f7de233e1a 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_13_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_13_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e8cd2089f07a93e3dc62e41d80a3253d800b4463947276ccd461428280aff6b8 -size 84644 +oid sha256:46ea20dea8e41276db4062ee86348833192398a826836a2bfbb7c065e8f91a46 +size 84802 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_14_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_14_en.png index faee0bc22d..76f4fa0d78 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_14_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_14_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:71f336f93dd3fe736187a38aba6c1f14420bb0da9d574603f6df2042bce8f8d1 -size 83116 +oid sha256:8bae76f03ed015e34032a531b21d238f0553a09335bc7ea545c9690631965754 +size 83259 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_15_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_15_en.png index 7d6ed57883..5901b196ac 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_15_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_15_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d7840847edf48b171393418d6fc26d846d772fa8be919f55dfee086d85cab814 -size 51404 +oid sha256:90ff370cbe3d2e7ea25471dc29e2593100dcfbef4f25dec51f03003871e55e4e +size 51563 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_1_en.png index edc3a6f1f1..e2bbfae52f 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:02f12178991ed984da4b2d0a9883250750b6e37e7c9ce8494ca12bb987f29ad4 -size 65455 +oid sha256:fb6a0347fa3b5639fa30cf4e353c85b5cab45bb07375bf622d46c896a6ff1c36 +size 65612 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_2_en.png index edc3a6f1f1..e2bbfae52f 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:02f12178991ed984da4b2d0a9883250750b6e37e7c9ce8494ca12bb987f29ad4 -size 65455 +oid sha256:fb6a0347fa3b5639fa30cf4e353c85b5cab45bb07375bf622d46c896a6ff1c36 +size 65612 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_3_en.png index b14e265de2..0c6414f84f 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:93b7d7b0d7df69921015ece40ae3ab859c81cd3e78828143f660a481a72b73e2 -size 62305 +oid sha256:d5674b09940d6dfd1361a37289bcdeeb2a1ac7f04d9cff287552540a9ddae95c +size 62470 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_5_en.png index edc3a6f1f1..e2bbfae52f 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:02f12178991ed984da4b2d0a9883250750b6e37e7c9ce8494ca12bb987f29ad4 -size 65455 +oid sha256:fb6a0347fa3b5639fa30cf4e353c85b5cab45bb07375bf622d46c896a6ff1c36 +size 65612 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_6_en.png index e1b311a970..dd3aa8cd85 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6bb7a5a23947cd3a311c8d9526508c121beb4e0ee37f337bf8361845be10d172 -size 54415 +oid sha256:429218b356c23d6cb934ebbcb144e838e75332b6f67f61e96c3a88893d33d5c1 +size 54556 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_7_en.png index 1e79e25769..3717d09c9d 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:11e9f28e6131eae66e268b72daab6d404d1f1f8b0eba78eb11d4376284eb11e3 -size 54220 +oid sha256:94aa6527f1fbebb1a3c5ca4a7f09ddbd7b9d8d6f7c0ee1dcbb34a83180391f6b +size 54352 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_8_en.png index eb5ead6254..530a3806ce 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:50e8fedd31aaa5dbdb63e088709a5e237f8c5400ca7209390d837a07d9b86973 -size 52404 +oid sha256:80c54edb1449c682fd21efb6593d22e0a5b936a7ba4ff879cf38c19deaf9acc4 +size 52538 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_9_en.png index 0f387ca179..d0b7b52ab7 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9fb19cc1e169e81f301a12ff3e30b8118682248ec3c13ea29b54f87398a54c3e -size 82977 +oid sha256:b434402aac34cb7888714911af5f2fb710d41e932fad31660b2dac5bbda6a748 +size 83136 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_0_en.png index e011cf5f07..3d7fe2d829 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:62f84bd6941a00ee6b006318b8ac9e659694c2749a3bfa1d8ca72ca2ce413c90 -size 62152 +oid sha256:38f16bdafa24411b2b4e33980871972073500c2e86bb74e7277d8b9d8155413a +size 62300 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_10_en.png index 1052c94a6e..39b3383938 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ab4a977d119ae80ddb92198b9c26b4b42d842eba0fd0543896f55914018d34f2 -size 30548 +oid sha256:f411dee9755509ebf93d9452bcbd3979555775245798d12f819b515701d09556 +size 30696 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_11_en.png index 40455a3752..65f49e179a 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:66824bcff1cf8fdd0fd88c3e0272bd11eaf6fdd16d24120c9346c99378226eee -size 24633 +oid sha256:fbc5042340e85c931531525fe05ef820e6d92a10f53d2c94b6878afb1ab776e0 +size 24796 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_13_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_13_en.png index 69ec4c780c..ac5dac47cf 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_13_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_13_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7d1745b2665a9329808f69e2750b50d59eac2d92216a25bae03a52907b6c7681 -size 80341 +oid sha256:fe2614c4255ff8ea6a4e25219f396b3a69da0283373cd8c06b8e3f9396e1c1b8 +size 80491 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_14_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_14_en.png index ffba98b54e..6a33b13a2e 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_14_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_14_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9017e66bb482de6a517065efe2e9d66ce4eb561fb848ca29d3245a2abc02d4c0 -size 79209 +oid sha256:7c74fa9af01f3d7e3762416d6c40b3abff7159b70a8328db42be68c622cb93ce +size 79359 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_15_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_15_en.png index c58e0fe485..432628718d 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_15_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_15_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6fd8d9d809b65df1690bf132351eec86736224d50fdd1bc9f9d2e731a4669ad6 -size 47687 +oid sha256:af4734d49824617816471fd49a64b49c568b77780b1c84fc0ab9c6248ae57c92 +size 47843 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_1_en.png index e011cf5f07..3d7fe2d829 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:62f84bd6941a00ee6b006318b8ac9e659694c2749a3bfa1d8ca72ca2ce413c90 -size 62152 +oid sha256:38f16bdafa24411b2b4e33980871972073500c2e86bb74e7277d8b9d8155413a +size 62300 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_2_en.png index e011cf5f07..3d7fe2d829 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:62f84bd6941a00ee6b006318b8ac9e659694c2749a3bfa1d8ca72ca2ce413c90 -size 62152 +oid sha256:38f16bdafa24411b2b4e33980871972073500c2e86bb74e7277d8b9d8155413a +size 62300 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_3_en.png index 044c2a9fbc..a538bf6454 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a8db3006e7ea4826022d47a4ae37dab2a7ddd064d4cc2c48b44deca0f158541c -size 59301 +oid sha256:850ea9b0435ce5f46f6fb4c717a8746e864ec1a1a94a7be327b33b8b3156cecc +size 59458 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_5_en.png index e011cf5f07..3d7fe2d829 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:62f84bd6941a00ee6b006318b8ac9e659694c2749a3bfa1d8ca72ca2ce413c90 -size 62152 +oid sha256:38f16bdafa24411b2b4e33980871972073500c2e86bb74e7277d8b9d8155413a +size 62300 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_6_en.png index 442eba42bc..0b1af1d838 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:957b8712456f13d83857616e2ffab05c5d60f50e6b39037bb47af0d272c53b02 -size 51863 +oid sha256:307f3be8a656c4f66f30fc6aa7ae9b97871cb62084b52e66f4cc81186b521838 +size 52001 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_7_en.png index 34ae9c102a..8a98b7f0c1 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:beac305c3cc0661633873276a99798b1fa693342bd4ab9424bc44759a1f745ec -size 51695 +oid sha256:bf8dca9a57cc3b867a010568612c8f9f25983e27543e4980bd31183e6b6c0539 +size 51839 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_8_en.png index 06d338c4c6..0d41fac91e 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a83be8296a2abd4a2bd28f08c8687cac1dddaaedce4ccf784538892da7b069c -size 49831 +oid sha256:11ce6dfc9128bf150eedd9113272ff4a9b2eb9afd3262cab4b7d4c9ec0c8279b +size 49980 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_9_en.png index a3e0ad8645..dae4931e9e 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6c7790afd67222ce61ea0b72f1af3c1877b7e88580a644524c33f18d6fe76137 -size 79096 +oid sha256:84f78878963f2ead0ca82985bb5686f56cdf33f5ee45470b066d9e3f4c13db03 +size 79239