From c3a469544e2ac90b9a36bc2d01a0447f89c26aea Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 20 Jan 2026 21:04:23 +0100 Subject: [PATCH 01/11] Add addChildToSpace method to SpaceService --- .../android/libraries/matrix/api/spaces/SpaceService.kt | 8 ++++++++ .../libraries/matrix/impl/spaces/RustSpaceService.kt | 6 ++++++ .../libraries/matrix/test/spaces/FakeSpaceService.kt | 5 +++++ 3 files changed, 19 insertions(+) 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 927bec13e1..39ea699ae3 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 @@ -23,6 +23,14 @@ interface SpaceService { fun getLeaveSpaceHandle(spaceId: RoomId): LeaveSpaceHandle + /** + * Add a child room to a space. + * @param spaceId The space ID to which the child will be added. + * @param childId The room ID of the child to add. + * @return A result indicating success or failure. + */ + suspend fun addChildToSpace(spaceId: RoomId, childId: RoomId): Result + /** * Remove a child room from a space. * @param spaceId The space ID from which to remove the child. 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 2ce184484c..771da5a3d5 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 @@ -98,6 +98,12 @@ class RustSpaceService( } } + override suspend fun addChildToSpace(spaceId: RoomId, childId: RoomId): Result = withContext(sessionDispatcher) { + runCatchingExceptions { + innerSpaceService.addChildToSpace(childId = childId.value, spaceId = spaceId.value) + } + } + override suspend fun removeChildFromSpace(spaceId: RoomId, childId: RoomId): Result = withContext(sessionDispatcher) { runCatchingExceptions { innerSpaceService.removeChildFromSpace(childId = childId.value, spaceId = spaceId.value) 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 0cd9907db3..2db83b601a 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 @@ -23,6 +23,7 @@ class FakeSpaceService( private val joinedSpacesResult: () -> Result> = { lambdaError() }, private val spaceRoomListResult: (RoomId) -> SpaceRoomList = { lambdaError() }, private val leaveSpaceHandleResult: (RoomId) -> LeaveSpaceHandle = { lambdaError() }, + private val addChildToSpaceResult: (RoomId, RoomId) -> Result = { _, _ -> lambdaError() }, private val removeChildFromSpaceResult: (RoomId, RoomId) -> Result = { _, _ -> lambdaError() }, private val joinedParentsResult: (RoomId) -> Result> = { lambdaError() }, private val getSpaceRoomResult: (RoomId) -> SpaceRoom? = { lambdaError() }, @@ -55,6 +56,10 @@ class FakeSpaceService( return leaveSpaceHandleResult(spaceId) } + override suspend fun addChildToSpace(spaceId: RoomId, childId: RoomId): Result = simulateLongTask { + addChildToSpaceResult(spaceId, childId) + } + override suspend fun removeChildFromSpace(spaceId: RoomId, childId: RoomId): Result = simulateLongTask { removeChildFromSpaceResult(spaceId, childId) } From e840671bf20a98280808e3081a0482445b6096f8 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 20 Jan 2026 21:07:42 +0100 Subject: [PATCH 02/11] Add "Add existing rooms" entry to space --- .../features/space/impl/SpaceFlowNode.kt | 16 ++++++++ .../space/impl/addroom/AddRoomToSpaceNode.kt | 38 +++++++++++++++++++ .../features/space/impl/root/SpaceNode.kt | 4 ++ .../features/space/impl/root/SpaceView.kt | 12 ++++++ .../features/space/impl/root/SpaceViewTest.kt | 2 + 5 files changed, 72 insertions(+) create mode 100644 features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceNode.kt diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceFlowNode.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceFlowNode.kt index 4c91da0301..95d97156ff 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceFlowNode.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceFlowNode.kt @@ -25,6 +25,7 @@ import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedInject import io.element.android.annotations.ContributesNode import io.element.android.features.space.api.SpaceEntryPoint +import io.element.android.features.space.impl.addroom.AddRoomToSpaceNode import io.element.android.features.space.impl.di.SpaceFlowGraph import io.element.android.features.space.impl.leave.LeaveSpaceNode import io.element.android.features.space.impl.root.SpaceNode @@ -69,6 +70,9 @@ class SpaceFlowNode( @Parcelize data object Leave : NavTarget + + @Parcelize + data object AddRoom : NavTarget } override fun onBuilt() { @@ -111,6 +115,10 @@ class SpaceFlowNode( override fun startLeaveSpaceFlow() { backstack.push(NavTarget.Leave) } + + override fun navigateToAddRoom() { + backstack.push(NavTarget.AddRoom) + } } createNode(buildContext, listOf(callback)) } @@ -132,6 +140,14 @@ class SpaceFlowNode( } createNode(buildContext, listOf(callback)) } + NavTarget.AddRoom -> { + val callback = object : AddRoomToSpaceNode.Callback { + override fun onFinish() { + backstack.pop() + } + } + createNode(buildContext, listOf(callback)) + } } } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceNode.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceNode.kt new file mode 100644 index 0000000000..6d4bced691 --- /dev/null +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceNode.kt @@ -0,0 +1,38 @@ +/* + * 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.features.space.impl.addroom + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode +import io.element.android.features.space.impl.di.SpaceFlowScope +import io.element.android.libraries.architecture.callback + +@ContributesNode(SpaceFlowScope::class) +@AssistedInject +class AddRoomToSpaceNode( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, +) : Node(buildContext, plugins = plugins) { + interface Callback : Plugin { + fun onFinish() + } + + private val callback: Callback = callback() + + @Composable + override fun View(modifier: Modifier) { + + } +} diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceNode.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceNode.kt index 240bc89990..f56c3df8c2 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceNode.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceNode.kt @@ -46,6 +46,7 @@ class SpaceNode( fun navigateToSpaceSettings() fun navigateToRoomMemberList() fun startLeaveSpaceFlow() + fun navigateToAddRoom() } private val callback: Callback = callback() @@ -89,6 +90,9 @@ class SpaceNode( onViewMembersClick = { callback.navigateToRoomMemberList() }, + onAddRoomClick = { + callback.navigateToAddRoom() + }, acceptDeclineInviteView = { acceptDeclineInviteView.Render( state = state.acceptDeclineInviteState, diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt index 695b887496..9f91e0f463 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt @@ -98,6 +98,7 @@ fun SpaceView( onLeaveSpaceClick: () -> Unit, onSettingsClick: () -> Unit, onViewMembersClick: () -> Unit, + onAddRoomClick: () -> Unit, modifier: Modifier = Modifier, acceptDeclineInviteView: @Composable () -> Unit, ) { @@ -140,6 +141,7 @@ fun SpaceView( onShareSpace = onShareSpace, onViewMembersClick = onViewMembersClick, onManageRoomsClick = { state.eventSink(SpaceEvents.EnterManageMode) }, + onAddRoomClick = onAddRoomClick, ) } } @@ -344,6 +346,7 @@ private fun SpaceViewTopBar( onShareSpace: () -> Unit, onViewMembersClick: () -> Unit, onManageRoomsClick: () -> Unit, + onAddRoomClick: () -> Unit, modifier: Modifier = Modifier, ) { TopAppBar( @@ -376,6 +379,14 @@ private fun SpaceViewTopBar( onDismissRequest = { showMenu = false } ) { if (showManageRoomsAction) { + SpaceMenuItem( + titleRes = CommonStrings.action_add_existing_rooms, + icon = CompoundIcons.Room(), + onClick = { + showMenu = false + onAddRoomClick() + } + ) SpaceMenuItem( titleRes = CommonStrings.action_manage_rooms, icon = CompoundIcons.Edit(), @@ -600,6 +611,7 @@ internal fun SpaceViewPreview( acceptDeclineInviteView = {}, onSettingsClick = {}, onViewMembersClick = {}, + onAddRoomClick = {}, onBackClick = {}, ) } diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt index 27970e93f8..59041a97ed 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt @@ -210,6 +210,7 @@ private fun AndroidComposeTestRule.setSpace onLeaveSpaceClick: () -> Unit = EnsureNeverCalled(), onSettingsClick: () -> Unit = EnsureNeverCalled(), onViewMembersClick: () -> Unit = EnsureNeverCalled(), + onAddRoomClick: () -> Unit = EnsureNeverCalled(), acceptDeclineInviteView: @Composable () -> Unit = {}, ) { setContent { @@ -221,6 +222,7 @@ private fun AndroidComposeTestRule.setSpace onLeaveSpaceClick = onLeaveSpaceClick, onSettingsClick = onSettingsClick, onViewMembersClick = onViewMembersClick, + onAddRoomClick = onAddRoomClick, acceptDeclineInviteView = acceptDeclineInviteView, ) } From 9fe7c50972ef104fb7b92094b10976cb15e3facd Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 20 Jan 2026 22:15:42 +0100 Subject: [PATCH 03/11] Implement Space "Add existing rooms" logic and ui --- .../impl/addroom/AddRoomToSpaceEvents.kt | 20 ++ .../space/impl/addroom/AddRoomToSpaceNode.kt | 9 +- .../impl/addroom/AddRoomToSpacePresenter.kt | 171 +++++++++++++ .../addroom/AddRoomToSpaceSearchDataSource.kt | 71 ++++++ .../space/impl/addroom/AddRoomToSpaceState.kt | 26 ++ .../addroom/AddRoomToSpaceStateProvider.kt | 107 ++++++++ .../space/impl/addroom/AddRoomToSpaceView.kt | 228 ++++++++++++++++++ .../components/list/AvatarListItem.kt | 62 +++++ .../matrix/api/room/recent/RecentRoom.kt | 32 +++ .../matrix/ui/model/SelectRoomInfo.kt | 18 +- 10 files changed, 736 insertions(+), 8 deletions(-) create mode 100644 features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceEvents.kt create mode 100644 features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenter.kt create mode 100644 features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceSearchDataSource.kt create mode 100644 features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceState.kt create mode 100644 features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceStateProvider.kt create mode 100644 features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceView.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/AvatarListItem.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/recent/RecentRoom.kt diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceEvents.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceEvents.kt new file mode 100644 index 0000000000..5380b08b20 --- /dev/null +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceEvents.kt @@ -0,0 +1,20 @@ +/* + * 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.features.space.impl.addroom + +import io.element.android.libraries.matrix.ui.model.SelectRoomInfo + +sealed interface AddRoomToSpaceEvents { + data class ToggleRoom(val room: SelectRoomInfo) : AddRoomToSpaceEvents + data class UpdateSearchQuery(val query: String) : AddRoomToSpaceEvents + data class OnSearchActiveChanged(val active: Boolean) : AddRoomToSpaceEvents + data object Save : AddRoomToSpaceEvents + data object CloseSearch : AddRoomToSpaceEvents + data object ClearError : AddRoomToSpaceEvents +} diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceNode.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceNode.kt index 6d4bced691..939535cd4f 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceNode.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceNode.kt @@ -24,6 +24,7 @@ import io.element.android.libraries.architecture.callback class AddRoomToSpaceNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, + private val presenter: AddRoomToSpacePresenter, ) : Node(buildContext, plugins = plugins) { interface Callback : Plugin { fun onFinish() @@ -33,6 +34,12 @@ class AddRoomToSpaceNode( @Composable override fun View(modifier: Modifier) { - + val state = presenter.present() + AddRoomToSpaceView( + state = state, + onBackClick = ::navigateUp, + onRoomsAdded = callback::onFinish, + modifier = modifier + ) } } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenter.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenter.kt new file mode 100644 index 0000000000..9d89b98263 --- /dev/null +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenter.kt @@ -0,0 +1,171 @@ +/* + * 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.features.space.impl.addroom + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import dev.zacsweers.metro.Inject +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.architecture.runUpdatingState +import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.di.annotations.SessionCoroutineScope +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.room.CurrentUserMembership +import io.element.android.libraries.matrix.api.room.isDm +import io.element.android.libraries.matrix.api.room.recent.getRecentRooms +import io.element.android.libraries.matrix.api.spaces.SpaceRoomList +import io.element.android.libraries.matrix.api.spaces.SpaceService +import io.element.android.libraries.matrix.ui.model.SelectRoomInfo +import io.element.android.libraries.matrix.ui.model.toSelectRoomInfo +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch + +private const val MAX_SUGGESTIONS_COUNT = 5 + +@Inject +class AddRoomToSpacePresenter( + private val spaceRoomList: SpaceRoomList, + private val dataSource: AddRoomToSpaceSearchDataSource, + private val spaceService: SpaceService, + private val matrixClient: MatrixClient, + @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, +) : Presenter { + + @Composable + override fun present(): AddRoomToSpaceState { + var selectedRooms by remember { mutableStateOf(persistentListOf()) } + var searchQuery by remember { mutableStateOf("") } + var isSearchActive by remember { mutableStateOf(false) } + val saveAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } + + // Load data source + LaunchedEffect(Unit) { dataSource.load() } + + // Update search query in data source + LaunchedEffect(searchQuery) { + dataSource.setSearchQuery(searchQuery) + } + + // Get rooms already in space + val spaceChildrenIds by remember { + spaceRoomList.spaceRoomsFlow.map { spaceChildren -> + spaceChildren.map { it.roomId } + } + }.collectAsState(initial = emptyList()) + + // Suggestions from recently visited rooms (excluding DMs, spaces, and rooms already in space) + val suggestions by produceState(persistentListOf(), spaceChildrenIds) { + value = matrixClient + .getRecentRooms { info -> + !info.isSpace && !info.isDm && info.currentUserMembership == CurrentUserMembership.JOINED + } + .take(MAX_SUGGESTIONS_COUNT) + .map { info -> info.toSelectRoomInfo() } + .toList() + .toImmutableList() + } + + val allRooms by dataSource.roomInfoList.collectAsState(initial = persistentListOf()) + val searchResults by remember>>> { + derivedStateOf { + val filtered = allRooms.filterNot { it.roomId in spaceChildrenIds } + when { + filtered.isNotEmpty() -> SearchBarResultState.Results(filtered.toImmutableList()) + isSearchActive && searchQuery.isNotEmpty() -> SearchBarResultState.NoResultsFound() + else -> SearchBarResultState.Initial() + } + } + } + + fun handleEvent(event: AddRoomToSpaceEvents) { + when (event) { + is AddRoomToSpaceEvents.ToggleRoom -> { + selectedRooms = if (selectedRooms.any { it.roomId == event.room.roomId }) { + selectedRooms.filterNot { it.roomId == event.room.roomId }.toPersistentList() + } else { + (selectedRooms + event.room).toPersistentList() + } + } + is AddRoomToSpaceEvents.UpdateSearchQuery -> { + searchQuery = event.query + } + is AddRoomToSpaceEvents.OnSearchActiveChanged -> { + isSearchActive = event.active + if (!event.active) { + searchQuery = "" + } + } + AddRoomToSpaceEvents.CloseSearch -> { + isSearchActive = false + searchQuery = "" + } + AddRoomToSpaceEvents.Save -> { + sessionCoroutineScope.addRoomsToSpace( + selectedRooms = selectedRooms, + addAction = saveAction, + ) + } + AddRoomToSpaceEvents.ClearError -> { + saveAction.value = AsyncAction.Uninitialized + } + } + } + + return AddRoomToSpaceState( + searchQuery = searchQuery, + isSearchActive = isSearchActive, + searchResults = searchResults, + selectedRooms = selectedRooms, + suggestions = suggestions, + saveAction = saveAction.value, + eventSink = ::handleEvent, + ) + } + + private fun CoroutineScope.addRoomsToSpace( + selectedRooms: ImmutableList, + addAction: MutableState>, + ) = launch { + addAction.runUpdatingState { + val results = selectedRooms.map { selectedRoom -> + async { + spaceService.addChildToSpace( + spaceId = spaceRoomList.roomId, + childId = selectedRoom.roomId, + ) + } + }.awaitAll() + val anyFailure = results.any { it.isFailure } + if (anyFailure) { + Result.failure(Exception("Failed to add some rooms")) + } else { + Result.success(Unit) + } + } + } +} diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceSearchDataSource.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceSearchDataSource.kt new file mode 100644 index 0000000000..48740d1499 --- /dev/null +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceSearchDataSource.kt @@ -0,0 +1,71 @@ +/* + * 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.features.space.impl.addroom + +import dev.zacsweers.metro.Inject +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.matrix.api.room.CurrentUserMembership +import io.element.android.libraries.matrix.api.room.isDm +import io.element.android.libraries.matrix.api.roomlist.RoomList +import io.element.android.libraries.matrix.api.roomlist.RoomListFilter +import io.element.android.libraries.matrix.api.roomlist.RoomListService +import io.element.android.libraries.matrix.api.roomlist.loadAllIncrementally +import io.element.android.libraries.matrix.ui.model.SelectRoomInfo +import io.element.android.libraries.matrix.ui.model.toSelectRoomInfo +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map + +private const val PAGE_SIZE = 30 + +/** + * DataSource for rooms that can be added to a space. + * Filters out DMs, spaces, and only includes rooms the user has joined. + */ +@Inject +class AddRoomToSpaceSearchDataSource( + roomListService: RoomListService, + coroutineDispatchers: CoroutineDispatchers, +) { + private val roomList = roomListService.createRoomList( + pageSize = PAGE_SIZE, + initialFilter = RoomListFilter.all(), + source = RoomList.Source.All, + ) + + val roomInfoList: Flow> = roomList.filteredSummaries + .map { roomSummaries -> + roomSummaries + .filter { + it.info.currentUserMembership == CurrentUserMembership.JOINED && + !it.info.isDm && + !it.info.isSpace + } + .distinctBy { it.roomId } + .map { roomSummary -> roomSummary.toSelectRoomInfo() } + .toImmutableList() + } + .flowOn(coroutineDispatchers.computation) + + suspend fun load() = coroutineScope { + roomList.loadAllIncrementally(this) + } + + suspend fun setSearchQuery(searchQuery: String) { + val filter = if (searchQuery.isBlank()) { + RoomListFilter.None + } else { + RoomListFilter.NormalizedMatchRoomName(searchQuery) + } + roomList.updateFilter(filter) + } +} diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceState.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceState.kt new file mode 100644 index 0000000000..5bbea067cd --- /dev/null +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceState.kt @@ -0,0 +1,26 @@ +/* + * 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.features.space.impl.addroom + +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.matrix.ui.model.SelectRoomInfo +import kotlinx.collections.immutable.ImmutableList + +data class AddRoomToSpaceState( + val searchQuery: String, + val isSearchActive: Boolean, + val searchResults: SearchBarResultState>, + val selectedRooms: ImmutableList, + val suggestions: ImmutableList, + val saveAction: AsyncAction, + val eventSink: (AddRoomToSpaceEvents) -> Unit, +) { + val canSave: Boolean = selectedRooms.isNotEmpty() && !saveAction.isLoading() +} diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceStateProvider.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceStateProvider.kt new file mode 100644 index 0000000000..550555135a --- /dev/null +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceStateProvider.kt @@ -0,0 +1,107 @@ +/* + * 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.features.space.impl.addroom + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.ui.model.SelectRoomInfo +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList + +internal class AddRoomToSpaceStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + // Initial state with suggestions + anAddRoomToSpaceState( + suggestions = aSelectRoomInfoList(), + ), + // Search active, empty query + anAddRoomToSpaceState( + isSearchActive = true, + searchQuery = "", + suggestions = aSelectRoomInfoList(), + ), + // Search active with query and results + anAddRoomToSpaceState( + isSearchActive = true, + searchQuery = "general", + searchResults = SearchBarResultState.Results(aSelectRoomInfoList()), + ), + // Search active with query and no results + anAddRoomToSpaceState( + isSearchActive = true, + searchQuery = "unknown", + searchResults = SearchBarResultState.NoResultsFound(), + ), + // With selected rooms + anAddRoomToSpaceState( + suggestions = aSelectRoomInfoList(), + selectedRooms = aSelectRoomInfoList().take(1).toImmutableList(), + ), + // Loading state + anAddRoomToSpaceState( + selectedRooms = aSelectRoomInfoList().take(1).toImmutableList(), + saveAction = AsyncAction.Loading, + ), + // Error state + anAddRoomToSpaceState( + selectedRooms = aSelectRoomInfoList().take(1).toImmutableList(), + saveAction = AsyncAction.Failure(Exception("Failed to add rooms")), + ), + ) +} + +private fun anAddRoomToSpaceState( + searchQuery: String = "", + searchResults: SearchBarResultState> = SearchBarResultState.Initial(), + selectedRooms: ImmutableList = persistentListOf(), + isSearchActive: Boolean = false, + saveAction: AsyncAction = AsyncAction.Uninitialized, + suggestions: ImmutableList = persistentListOf(), +): AddRoomToSpaceState { + return AddRoomToSpaceState( + searchQuery = searchQuery, + searchResults = searchResults, + selectedRooms = selectedRooms, + isSearchActive = isSearchActive, + saveAction = saveAction, + suggestions = suggestions, + eventSink = {}, + ) +} + +private fun aSelectRoomInfoList(): ImmutableList = listOf( + SelectRoomInfo( + roomId = RoomId("!room1:server.org"), + name = "General", + canonicalAlias = null, + avatarUrl = null, + heroes = persistentListOf(), + isTombstoned = false, + ), + SelectRoomInfo( + roomId = RoomId("!room2:server.org"), + name = "Engineering", + canonicalAlias = null, + avatarUrl = null, + heroes = persistentListOf(), + isTombstoned = false, + ), + SelectRoomInfo( + roomId = RoomId("!room3:server.org"), + name = "Design", + canonicalAlias = null, + avatarUrl = null, + heroes = persistentListOf(), + isTombstoned = false, + ), +).toImmutableList() diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceView.kt new file mode 100644 index 0000000000..cef85255f5 --- /dev/null +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceView.kt @@ -0,0 +1,228 @@ +/* + * 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.features.space.impl.addroom + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +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.libraries.architecture.AsyncAction +import io.element.android.libraries.designsystem.components.ProgressDialog +import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.designsystem.components.avatar.AvatarType +import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog +import io.element.android.libraries.designsystem.components.list.AvatarListItem +import io.element.android.libraries.designsystem.components.list.ListItemContent +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.ListSectionHeader +import io.element.android.libraries.designsystem.theme.components.Scaffold +import io.element.android.libraries.designsystem.theme.components.SearchBar +import io.element.android.libraries.designsystem.theme.components.TextButton +import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.matrix.ui.components.SelectedRoom +import io.element.android.libraries.matrix.ui.model.SelectRoomInfo +import io.element.android.libraries.matrix.ui.model.getAvatarData +import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AddRoomToSpaceView( + state: AddRoomToSpaceState, + onBackClick: () -> Unit, + onRoomsAdded: () -> Unit, + modifier: Modifier = Modifier, +) { + fun onRoomRemoved(roomInfo: SelectRoomInfo) { + state.eventSink(AddRoomToSpaceEvents.ToggleRoom(roomInfo)) + } + + fun onBack() { + if (state.isSearchActive) { + state.eventSink(AddRoomToSpaceEvents.CloseSearch) + } else { + onBackClick() + } + } + + BackHandler(onBack = ::onBack) + + // Navigate back on success + LaunchedEffect(state.saveAction) { + if (state.saveAction is AsyncAction.Success) { + onRoomsAdded() + } + } + + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + titleStr = stringResource(CommonStrings.action_add_existing_rooms), + navigationIcon = { + BackButton(onClick = ::onBack) + }, + actions = { + TextButton( + text = stringResource(CommonStrings.action_save), + enabled = state.canSave, + onClick = { state.eventSink(AddRoomToSpaceEvents.Save) } + ) + } + ) + } + ) { paddingValues -> + Column( + Modifier + .padding(paddingValues) + .consumeWindowInsets(paddingValues) + ) { + SearchBar( + modifier = Modifier.fillMaxWidth(), + placeHolderTitle = stringResource(CommonStrings.action_search), + query = state.searchQuery, + onQueryChange = { state.eventSink(AddRoomToSpaceEvents.UpdateSearchQuery(it)) }, + active = state.isSearchActive, + onActiveChange = { state.eventSink(AddRoomToSpaceEvents.OnSearchActiveChanged(it)) }, + resultState = state.searchResults, + showBackButton = false, + contentPrefix = { + if (state.selectedRooms.isNotEmpty()) { + SelectedRoomsRow( + selectedRooms = state.selectedRooms, + onRemoveRoom = ::onRoomRemoved, + modifier = Modifier.padding(vertical = 16.dp) + ) + } + }, + ) { rooms -> + LazyColumn { + items(rooms, key = { it.roomId.value }) { roomInfo -> + RoomListItem( + roomInfo = roomInfo, + isSelected = state.selectedRooms.any { it.roomId == roomInfo.roomId }, + onToggle = { state.eventSink(AddRoomToSpaceEvents.ToggleRoom(it)) } + ) + } + } + } + + if (!state.isSearchActive) { + if (state.selectedRooms.isNotEmpty()) { + SelectedRoomsRow( + selectedRooms = state.selectedRooms, + onRemoveRoom = ::onRoomRemoved, + modifier = Modifier.padding(vertical = 16.dp) + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + if (state.suggestions.isNotEmpty()) { + LazyColumn { + item { + ListSectionHeader( + title = stringResource(id = CommonStrings.common_suggestions), + hasDivider = true, + ) + } + items(state.suggestions, key = { it.roomId.value }) { roomInfo -> + RoomListItem( + roomInfo = roomInfo, + isSelected = state.selectedRooms.any { it.roomId == roomInfo.roomId }, + onToggle = { state.eventSink(AddRoomToSpaceEvents.ToggleRoom(it)) } + ) + } + } + } + } + } + } + + // Loading dialog + if (state.saveAction.isLoading()) { + ProgressDialog() + } + + // Error dialog + if (state.saveAction.isFailure()) { + ErrorDialog( + content = stringResource(CommonStrings.error_unknown), + onSubmit = { state.eventSink(AddRoomToSpaceEvents.ClearError) }, + ) + } +} + +@Composable +private fun SelectedRoomsRow( + selectedRooms: ImmutableList, + onRemoveRoom: (SelectRoomInfo) -> Unit, + modifier: Modifier = Modifier, +) { + LazyRow( + modifier = modifier, + contentPadding = PaddingValues(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(32.dp) + ) { + items(selectedRooms, key = { it.roomId.value }) { roomInfo -> + SelectedRoom(roomInfo = roomInfo, onRemoveRoom = onRemoveRoom) + } + } +} + +@Composable +private fun RoomListItem( + roomInfo: SelectRoomInfo, + isSelected: Boolean, + onToggle: (SelectRoomInfo) -> Unit, +) { + AvatarListItem( + avatarData = roomInfo.getAvatarData(size = AvatarSize.RoomSelectRoomListItem), + avatarType = AvatarType.Room( + heroes = roomInfo.heroes.map { user -> + user.getAvatarData(size = AvatarSize.RoomSelectRoomListItem) + }.toImmutableList(), + isTombstoned = roomInfo.isTombstoned, + ), + headline = roomInfo.name ?: stringResource(id = CommonStrings.common_no_room_name), + supportingText = roomInfo.canonicalAlias?.value, + trailingContent = ListItemContent.Checkbox(checked = isSelected), + onClick = { onToggle(roomInfo) }, + ) +} + +@PreviewsDayNight +@Composable +internal fun AddRoomToSpaceViewPreview( + @PreviewParameter(AddRoomToSpaceStateProvider::class) state: AddRoomToSpaceState +) = ElementPreview { + AddRoomToSpaceView( + state = state, + onBackClick = {}, + onRoomsAdded = {}, + ) +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/AvatarListItem.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/AvatarListItem.kt new file mode 100644 index 0000000000..962da7af93 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/AvatarListItem.kt @@ -0,0 +1,62 @@ +/* + * 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.designsystem.components.list + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import io.element.android.libraries.designsystem.components.avatar.Avatar +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.components.avatar.AvatarType +import io.element.android.libraries.designsystem.theme.components.ListItem +import io.element.android.libraries.designsystem.theme.components.ListItemStyle +import io.element.android.libraries.designsystem.theme.components.Text + +/** + * A list item with an Avatar as leading content. + * + * Figma link : https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=1979-1894&m=dev + * + * @param avatarData The data for the avatar. + * @param avatarType The type of avatar to display. + * @param headline The main text of the list item. + * @param modifier The modifier to apply to the list item. + * @param supportingText The supporting text displayed below the headline. + * @param trailingContent The trailing content of the list item. + * @param enabled Whether the list item is enabled. + * @param style The style of the list item. + * @param onClick The callback to invoke when the list item is clicked. + */ +@Composable +fun AvatarListItem( + avatarData: AvatarData, + avatarType: AvatarType, + headline: String, + modifier: Modifier = Modifier, + supportingText: String? = null, + trailingContent: ListItemContent? = null, + enabled: Boolean = true, + style: ListItemStyle = ListItemStyle.Default, + onClick: (() -> Unit)? = null, +) { + ListItem( + modifier = modifier, + headlineContent = { Text(headline) }, + supportingContent = supportingText?.let { @Composable { Text(it) } }, + leadingContent = ListItemContent.Custom { _ -> + Avatar( + avatarData = avatarData, + avatarType = avatarType, + ) + }, + trailingContent = trailingContent, + style = style, + enabled = enabled, + onClick = onClick, + ) +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/recent/RecentRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/recent/RecentRoom.kt new file mode 100644 index 0000000000..d35696a8d1 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/recent/RecentRoom.kt @@ -0,0 +1,32 @@ +/* + * 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.libraries.matrix.api.room.recent + +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.room.RoomInfo +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +/** + * Returns a [Flow] of [RoomInfo] from recently visited DM rooms. + * The flow emits items lazily, allowing callers to filter and take only what they need. + * Use [kotlinx.coroutines.flow.take] to limit results and stop iteration early. + */ +fun MatrixClient.getRecentRooms( + predicate: (RoomInfo) -> Boolean, +): Flow = flow { + val recentlyVisitedRooms = getRecentlyVisitedRooms().getOrDefault(emptyList()) + for (roomId in recentlyVisitedRooms) { + getRoom(roomId)?.use { room -> + val info = room.info() + if (predicate(info)) { + emit(info) + } + } + } +} diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/SelectRoomInfo.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/SelectRoomInfo.kt index 49c8415fc7..cf66d040a9 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/SelectRoomInfo.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/SelectRoomInfo.kt @@ -12,6 +12,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.RoomInfo import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.api.user.MatrixUser import kotlinx.collections.immutable.ImmutableList @@ -32,11 +33,14 @@ data class SelectRoomInfo( ) } -fun RoomSummary.toSelectRoomInfo() = SelectRoomInfo( - roomId = roomId, - name = info.name, - avatarUrl = info.avatarUrl, - heroes = info.heroes, - canonicalAlias = info.canonicalAlias, - isTombstoned = info.successorRoom != null, +fun RoomSummary.toSelectRoomInfo() = info.toSelectRoomInfo() + +fun RoomInfo.toSelectRoomInfo() = SelectRoomInfo( + roomId = id, + name = name, + avatarUrl = avatarUrl, + heroes = heroes, + canonicalAlias = canonicalAlias, + isTombstoned = successorRoom != null, ) + From ca1d98928dbbdf86385c2349c03bac2a6dd07bbc Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 21 Jan 2026 17:33:17 +0100 Subject: [PATCH 04/11] Iterate on space "Add existing rooms" logic and ui --- ...oSpaceEvents.kt => AddRoomToSpaceEvent.kt} | 14 ++-- .../space/impl/addroom/AddRoomToSpaceNode.kt | 6 +- .../impl/addroom/AddRoomToSpacePresenter.kt | 57 ++++------------ .../addroom/AddRoomToSpaceSearchDataSource.kt | 65 ++++++++++++++----- .../space/impl/addroom/AddRoomToSpaceState.kt | 2 +- .../space/impl/addroom/AddRoomToSpaceView.kt | 53 +++++++++------ 6 files changed, 106 insertions(+), 91 deletions(-) rename features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/{AddRoomToSpaceEvents.kt => AddRoomToSpaceEvent.kt} (71%) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceEvents.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceEvent.kt similarity index 71% rename from features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceEvents.kt rename to features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceEvent.kt index 5380b08b20..fc6b1e216a 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceEvents.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceEvent.kt @@ -10,11 +10,11 @@ package io.element.android.features.space.impl.addroom import io.element.android.libraries.matrix.ui.model.SelectRoomInfo -sealed interface AddRoomToSpaceEvents { - data class ToggleRoom(val room: SelectRoomInfo) : AddRoomToSpaceEvents - data class UpdateSearchQuery(val query: String) : AddRoomToSpaceEvents - data class OnSearchActiveChanged(val active: Boolean) : AddRoomToSpaceEvents - data object Save : AddRoomToSpaceEvents - data object CloseSearch : AddRoomToSpaceEvents - data object ClearError : AddRoomToSpaceEvents +sealed interface AddRoomToSpaceEvent { + data class ToggleRoom(val room: SelectRoomInfo) : AddRoomToSpaceEvent + data class UpdateSearchQuery(val query: String) : AddRoomToSpaceEvent + data class OnSearchActiveChanged(val active: Boolean) : AddRoomToSpaceEvent + data object Save : AddRoomToSpaceEvent + data object CloseSearch : AddRoomToSpaceEvent + data object ResetSaveAction : AddRoomToSpaceEvent } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceNode.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceNode.kt index 939535cd4f..3699de797e 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceNode.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceNode.kt @@ -9,6 +9,8 @@ package io.element.android.features.space.impl.addroom import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node @@ -17,6 +19,7 @@ import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedInject import io.element.android.annotations.ContributesNode import io.element.android.features.space.impl.di.SpaceFlowScope +import io.element.android.libraries.architecture.appyx.launchMolecule import io.element.android.libraries.architecture.callback @ContributesNode(SpaceFlowScope::class) @@ -31,10 +34,11 @@ class AddRoomToSpaceNode( } private val callback: Callback = callback() + private val stateFlow = launchMolecule { presenter.present() } @Composable override fun View(modifier: Modifier) { - val state = presenter.present() + val state by stateFlow.collectAsState() AddRoomToSpaceView( state = state, onBackClick = ::navigateUp, diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenter.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenter.kt index 9d89b98263..c5d6f012cd 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenter.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenter.kt @@ -16,7 +16,6 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import dev.zacsweers.metro.Inject @@ -25,34 +24,22 @@ import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runUpdatingState import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.di.annotations.SessionCoroutineScope -import io.element.android.libraries.matrix.api.MatrixClient -import io.element.android.libraries.matrix.api.room.CurrentUserMembership -import io.element.android.libraries.matrix.api.room.isDm -import io.element.android.libraries.matrix.api.room.recent.getRecentRooms import io.element.android.libraries.matrix.api.spaces.SpaceRoomList import io.element.android.libraries.matrix.api.spaces.SpaceService import io.element.android.libraries.matrix.ui.model.SelectRoomInfo -import io.element.android.libraries.matrix.ui.model.toSelectRoomInfo import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.take -import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch -private const val MAX_SUGGESTIONS_COUNT = 5 - @Inject class AddRoomToSpacePresenter( private val spaceRoomList: SpaceRoomList, private val dataSource: AddRoomToSpaceSearchDataSource, private val spaceService: SpaceService, - private val matrixClient: MatrixClient, @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, ) : Presenter { @@ -63,74 +50,56 @@ class AddRoomToSpacePresenter( var isSearchActive by remember { mutableStateOf(false) } val saveAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } - // Load data source - LaunchedEffect(Unit) { dataSource.load() } - // Update search query in data source LaunchedEffect(searchQuery) { dataSource.setSearchQuery(searchQuery) } - - // Get rooms already in space - val spaceChildrenIds by remember { - spaceRoomList.spaceRoomsFlow.map { spaceChildren -> - spaceChildren.map { it.roomId } - } - }.collectAsState(initial = emptyList()) - - // Suggestions from recently visited rooms (excluding DMs, spaces, and rooms already in space) - val suggestions by produceState(persistentListOf(), spaceChildrenIds) { - value = matrixClient - .getRecentRooms { info -> - !info.isSpace && !info.isDm && info.currentUserMembership == CurrentUserMembership.JOINED - } - .take(MAX_SUGGESTIONS_COUNT) - .map { info -> info.toSelectRoomInfo() } - .toList() - .toImmutableList() + LaunchedEffect(isSearchActive) { + dataSource.setIsActive(isSearchActive) } - val allRooms by dataSource.roomInfoList.collectAsState(initial = persistentListOf()) + val suggestions by dataSource.suggestions.collectAsState(initial = persistentListOf()) + + val filteredRooms by dataSource.roomInfoList.collectAsState(initial = persistentListOf()) val searchResults by remember>>> { derivedStateOf { - val filtered = allRooms.filterNot { it.roomId in spaceChildrenIds } when { - filtered.isNotEmpty() -> SearchBarResultState.Results(filtered.toImmutableList()) + filteredRooms.isNotEmpty() -> SearchBarResultState.Results(filteredRooms) isSearchActive && searchQuery.isNotEmpty() -> SearchBarResultState.NoResultsFound() else -> SearchBarResultState.Initial() } } } - fun handleEvent(event: AddRoomToSpaceEvents) { + fun handleEvent(event: AddRoomToSpaceEvent) { when (event) { - is AddRoomToSpaceEvents.ToggleRoom -> { + is AddRoomToSpaceEvent.ToggleRoom -> { selectedRooms = if (selectedRooms.any { it.roomId == event.room.roomId }) { selectedRooms.filterNot { it.roomId == event.room.roomId }.toPersistentList() } else { (selectedRooms + event.room).toPersistentList() } } - is AddRoomToSpaceEvents.UpdateSearchQuery -> { + is AddRoomToSpaceEvent.UpdateSearchQuery -> { searchQuery = event.query } - is AddRoomToSpaceEvents.OnSearchActiveChanged -> { + is AddRoomToSpaceEvent.OnSearchActiveChanged -> { isSearchActive = event.active if (!event.active) { searchQuery = "" } } - AddRoomToSpaceEvents.CloseSearch -> { + AddRoomToSpaceEvent.CloseSearch -> { isSearchActive = false searchQuery = "" } - AddRoomToSpaceEvents.Save -> { + AddRoomToSpaceEvent.Save -> { sessionCoroutineScope.addRoomsToSpace( selectedRooms = selectedRooms, addAction = saveAction, ) } - AddRoomToSpaceEvents.ClearError -> { + AddRoomToSpaceEvent.ResetSaveAction -> { saveAction.value = AsyncAction.Uninitialized } } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceSearchDataSource.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceSearchDataSource.kt index 48740d1499..f6a69320c6 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceSearchDataSource.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceSearchDataSource.kt @@ -10,31 +10,42 @@ package io.element.android.features.space.impl.addroom import dev.zacsweers.metro.Inject import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.CurrentUserMembership +import io.element.android.libraries.matrix.api.room.RoomInfo import io.element.android.libraries.matrix.api.room.isDm +import io.element.android.libraries.matrix.api.room.recent.getRecentRooms 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.roomlist.RoomListService import io.element.android.libraries.matrix.api.roomlist.loadAllIncrementally +import io.element.android.libraries.matrix.api.spaces.SpaceRoomList import io.element.android.libraries.matrix.ui.model.SelectRoomInfo import io.element.android.libraries.matrix.ui.model.toSelectRoomInfo import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.flow.toList private const val PAGE_SIZE = 30 +private const val MAX_SUGGESTIONS_COUNT = 5 /** * DataSource for rooms that can be added to a space. - * Filters out DMs, spaces, and only includes rooms the user has joined. + * Filters out DMs, spaces, rooms already in the space, and only includes rooms the user has joined. */ @Inject class AddRoomToSpaceSearchDataSource( roomListService: RoomListService, - coroutineDispatchers: CoroutineDispatchers, + spaceRoomList: SpaceRoomList, + private val matrixClient: MatrixClient, + private val coroutineDispatchers: CoroutineDispatchers, ) { private val roomList = roomListService.createRoomList( pageSize = PAGE_SIZE, @@ -42,22 +53,42 @@ class AddRoomToSpaceSearchDataSource( source = RoomList.Source.All, ) - val roomInfoList: Flow> = roomList.filteredSummaries - .map { roomSummaries -> - roomSummaries - .filter { - it.info.currentUserMembership == CurrentUserMembership.JOINED && - !it.info.isDm && - !it.info.isSpace - } - .distinctBy { it.roomId } - .map { roomSummary -> roomSummary.toSelectRoomInfo() } - .toImmutableList() - } - .flowOn(coroutineDispatchers.computation) + private val spaceChildrenFlow = spaceRoomList.spaceRoomsFlow.map { spaceChildren -> + spaceChildren.map { it.roomId }.toSet() + } - suspend fun load() = coroutineScope { - roomList.loadAllIncrementally(this) + private val filterRoomPredicate: (RoomInfo, Set) -> Boolean = { info, childIds -> + !info.isSpace && + !info.isDm && + info.currentUserMembership == CurrentUserMembership.JOINED && + info.id !in childIds + } + + val roomInfoList: Flow> = combine( + roomList.filteredSummaries, + spaceChildrenFlow, + ) { roomSummaries, childIds -> + roomSummaries + .filter { filterRoomPredicate(it.info, childIds) } + .map { it.toSelectRoomInfo() } + .toImmutableList() + }.flowOn(coroutineDispatchers.computation) + + val suggestions: Flow> = spaceChildrenFlow.map { childIds -> + matrixClient + .getRecentRooms { filterRoomPredicate(it, childIds) } + .take(MAX_SUGGESTIONS_COUNT) + .map { it.toSelectRoomInfo() } + .toList() + .toImmutableList() + }.flowOn(coroutineDispatchers.computation) + + suspend fun setIsActive(isActive: Boolean) = coroutineScope { + if (isActive) { + roomList.loadAllIncrementally(this) + } else { + roomList.reset() + } } suspend fun setSearchQuery(searchQuery: String) { diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceState.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceState.kt index 5bbea067cd..2215bfecb9 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceState.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceState.kt @@ -20,7 +20,7 @@ data class AddRoomToSpaceState( val selectedRooms: ImmutableList, val suggestions: ImmutableList, val saveAction: AsyncAction, - val eventSink: (AddRoomToSpaceEvents) -> Unit, + val eventSink: (AddRoomToSpaceEvent) -> Unit, ) { val canSave: Boolean = selectedRooms.isNotEmpty() && !saveAction.isLoading() } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceView.kt index cef85255f5..94b97f6ff6 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceView.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceView.kt @@ -28,11 +28,10 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.designsystem.components.ProgressDialog +import io.element.android.libraries.designsystem.components.async.AsyncActionView import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarType import io.element.android.libraries.designsystem.components.button.BackButton -import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.components.list.AvatarListItem import io.element.android.libraries.designsystem.components.list.ListItemContent import io.element.android.libraries.designsystem.preview.ElementPreview @@ -58,12 +57,12 @@ fun AddRoomToSpaceView( modifier: Modifier = Modifier, ) { fun onRoomRemoved(roomInfo: SelectRoomInfo) { - state.eventSink(AddRoomToSpaceEvents.ToggleRoom(roomInfo)) + state.eventSink(AddRoomToSpaceEvent.ToggleRoom(roomInfo)) } fun onBack() { if (state.isSearchActive) { - state.eventSink(AddRoomToSpaceEvents.CloseSearch) + state.eventSink(AddRoomToSpaceEvent.CloseSearch) } else { onBackClick() } @@ -90,7 +89,7 @@ fun AddRoomToSpaceView( TextButton( text = stringResource(CommonStrings.action_save), enabled = state.canSave, - onClick = { state.eventSink(AddRoomToSpaceEvents.Save) } + onClick = { state.eventSink(AddRoomToSpaceEvent.Save) } ) } ) @@ -105,9 +104,9 @@ fun AddRoomToSpaceView( modifier = Modifier.fillMaxWidth(), placeHolderTitle = stringResource(CommonStrings.action_search), query = state.searchQuery, - onQueryChange = { state.eventSink(AddRoomToSpaceEvents.UpdateSearchQuery(it)) }, + onQueryChange = { state.eventSink(AddRoomToSpaceEvent.UpdateSearchQuery(it)) }, active = state.isSearchActive, - onActiveChange = { state.eventSink(AddRoomToSpaceEvents.OnSearchActiveChanged(it)) }, + onActiveChange = { state.eventSink(AddRoomToSpaceEvent.OnSearchActiveChanged(it)) }, resultState = state.searchResults, showBackButton = false, contentPrefix = { @@ -125,7 +124,7 @@ fun AddRoomToSpaceView( RoomListItem( roomInfo = roomInfo, isSelected = state.selectedRooms.any { it.roomId == roomInfo.roomId }, - onToggle = { state.eventSink(AddRoomToSpaceEvents.ToggleRoom(it)) } + onToggle = { state.eventSink(AddRoomToSpaceEvent.ToggleRoom(it)) } ) } } @@ -154,7 +153,7 @@ fun AddRoomToSpaceView( RoomListItem( roomInfo = roomInfo, isSelected = state.selectedRooms.any { it.roomId == roomInfo.roomId }, - onToggle = { state.eventSink(AddRoomToSpaceEvents.ToggleRoom(it)) } + onToggle = { state.eventSink(AddRoomToSpaceEvent.ToggleRoom(it)) } ) } } @@ -162,19 +161,31 @@ fun AddRoomToSpaceView( } } } + SaveActionView( + saveAction = state.saveAction, + onRetry = { state.eventSink(AddRoomToSpaceEvent.Save)}, + onDismiss = {state.eventSink(AddRoomToSpaceEvent.ResetSaveAction)} + ) +} - // Loading dialog - if (state.saveAction.isLoading()) { - ProgressDialog() - } - - // Error dialog - if (state.saveAction.isFailure()) { - ErrorDialog( - content = stringResource(CommonStrings.error_unknown), - onSubmit = { state.eventSink(AddRoomToSpaceEvents.ClearError) }, - ) - } +@Composable +private fun SaveActionView( + saveAction: AsyncAction, + onRetry: () -> Unit, + onDismiss: () -> Unit, +) { + AsyncActionView( + async = saveAction, + onRetry = onRetry, + errorTitle = { + stringResource(CommonStrings.common_something_went_wrong) + }, + errorMessage = { + stringResource(CommonStrings.error_network_or_server_issue) + }, + onSuccess = { onDismiss() }, + onErrorDismiss = onDismiss, + ) } @Composable From 125178c36f519a1113d2e6d6d7877a0894a96103 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 21 Jan 2026 17:44:53 +0100 Subject: [PATCH 05/11] Refactor AddRoomToSpaceSearchDataSource to use AssistedInject --- .../space/impl/addroom/AddRoomToSpacePresenter.kt | 9 ++++++--- .../impl/addroom/AddRoomToSpaceSearchDataSource.kt | 14 +++++++++++++- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenter.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenter.kt index c5d6f012cd..0516b102e5 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenter.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenter.kt @@ -17,6 +17,7 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import dev.zacsweers.metro.Inject import io.element.android.libraries.architecture.AsyncAction @@ -38,9 +39,8 @@ import kotlinx.coroutines.launch @Inject class AddRoomToSpacePresenter( private val spaceRoomList: SpaceRoomList, - private val dataSource: AddRoomToSpaceSearchDataSource, private val spaceService: SpaceService, - @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, + private val dataSourceFactory: AddRoomToSpaceSearchDataSource.Factory, ) : Presenter { @Composable @@ -50,6 +50,9 @@ class AddRoomToSpacePresenter( var isSearchActive by remember { mutableStateOf(false) } val saveAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } + val coroutineScope = rememberCoroutineScope() + val dataSource = remember { dataSourceFactory.create(coroutineScope) } + // Update search query in data source LaunchedEffect(searchQuery) { dataSource.setSearchQuery(searchQuery) @@ -94,7 +97,7 @@ class AddRoomToSpacePresenter( searchQuery = "" } AddRoomToSpaceEvent.Save -> { - sessionCoroutineScope.addRoomsToSpace( + coroutineScope.addRoomsToSpace( selectedRooms = selectedRooms, addAction = saveAction, ) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceSearchDataSource.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceSearchDataSource.kt index f6a69320c6..64603faaca 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceSearchDataSource.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceSearchDataSource.kt @@ -8,6 +8,9 @@ package io.element.android.features.space.impl.addroom +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.Inject import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.matrix.api.MatrixClient @@ -25,6 +28,7 @@ import io.element.android.libraries.matrix.ui.model.SelectRoomInfo import io.element.android.libraries.matrix.ui.model.toSelectRoomInfo import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine @@ -40,17 +44,25 @@ private const val MAX_SUGGESTIONS_COUNT = 5 * DataSource for rooms that can be added to a space. * Filters out DMs, spaces, rooms already in the space, and only includes rooms the user has joined. */ -@Inject +@AssistedInject class AddRoomToSpaceSearchDataSource( + @Assisted coroutineScope: CoroutineScope, roomListService: RoomListService, spaceRoomList: SpaceRoomList, private val matrixClient: MatrixClient, private val coroutineDispatchers: CoroutineDispatchers, ) { + + @AssistedFactory + interface Factory { + fun create(coroutineScope: CoroutineScope): AddRoomToSpaceSearchDataSource + } + private val roomList = roomListService.createRoomList( pageSize = PAGE_SIZE, initialFilter = RoomListFilter.all(), source = RoomList.Source.All, + coroutineScope = coroutineScope, ) private val spaceChildrenFlow = spaceRoomList.spaceRoomsFlow.map { spaceChildren -> From c03e052da45fe418969ae9cc082f0790577a9f85 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 21 Jan 2026 19:49:47 +0100 Subject: [PATCH 06/11] Sync strings --- features/space/impl/src/main/res/values-da/translations.xml | 6 ++++++ features/space/impl/src/main/res/values-de/translations.xml | 1 + features/space/impl/src/main/res/values-fr/translations.xml | 1 + features/space/impl/src/main/res/values-ru/translations.xml | 1 + features/space/impl/src/main/res/values/localazy.xml | 1 + .../ui-strings/src/main/res/values-de/translations.xml | 1 - .../ui-strings/src/main/res/values-fr/translations.xml | 1 - .../ui-strings/src/main/res/values-ru/translations.xml | 1 - libraries/ui-strings/src/main/res/values/localazy.xml | 1 - tools/localazy/config.json | 3 ++- 10 files changed, 12 insertions(+), 5 deletions(-) diff --git a/features/space/impl/src/main/res/values-da/translations.xml b/features/space/impl/src/main/res/values-da/translations.xml index 068712629d..8fa612ae43 100644 --- a/features/space/impl/src/main/res/values-da/translations.xml +++ b/features/space/impl/src/main/res/values-da/translations.xml @@ -10,7 +10,13 @@ "Du vil ikke blive fjernet fra følgende rum, fordi du er den eneste administrator:" "Forlad %1$s?" "Du er den eneste administrator for %1$s" + "Tilføjelse af et rum påvirker ikke adgangen til rummet. For at ændre adgangen, gå til Rumindstillinger > Sikkerhed og privatliv." "Vis medlemmer" + "Fjernelse af et rum påvirker ikke adgangen til rummet. For at ændre adgangen, gå til Rum-info > Privatliv og sikkerhed." + + "Fjern %1$d rum fra %2$s" + "Fjern %1$d rum fra %2$s" + "Forlad gruppe" "Roller og tilladelser" "Sikkerhed og privatliv" diff --git a/features/space/impl/src/main/res/values-de/translations.xml b/features/space/impl/src/main/res/values-de/translations.xml index 1d0238cf7f..e549e1c4bf 100644 --- a/features/space/impl/src/main/res/values-de/translations.xml +++ b/features/space/impl/src/main/res/values-de/translations.xml @@ -10,6 +10,7 @@ "Du wirst aus den folgenden Chats nicht entfernt, weil du der einzige Admin bist:" "%1$s verlassen?" "Du bist der einzige Administrator für %1$s" + "Das Hinzufügen eines Chats hat keinen Einfluss auf die Beitrittsregeln. Um die Regeln zu ändern, gehe zu \"Raum Info\" und dann zu \"Datenschutz und Sicherheit\"" "Mitglieder anzeigen" "Das Entfernen eines Chats hat keinen Einfluss auf die Beitrittsregeln. Um die Regeln zu ändern, gehe zu \"Raum Info\" und dann zu \"Datenschutz und Sicherheit\"" diff --git a/features/space/impl/src/main/res/values-fr/translations.xml b/features/space/impl/src/main/res/values-fr/translations.xml index ceeb3125f6..db406bd403 100644 --- a/features/space/impl/src/main/res/values-fr/translations.xml +++ b/features/space/impl/src/main/res/values-fr/translations.xml @@ -10,6 +10,7 @@ "Vous ne quitterez pas le ou les salons suivants car vous y êtes le seul administrateur:" "Quitter %1$s?" "Vous êtes le seul administrateur de %1$s" + "Ajouter un salon ne changera pas l’accès au salon. Pour modifier l’accès, aller dans les paramètres du salon puis dans Sécurité & confidentialité." "Voir les membres" "Supprimer un salon n’affectera pas ses paramètres d’accès. Pour modifier l’accès, aller dans les settings du salon puis \"Sécurité & confidentialité\"." diff --git a/features/space/impl/src/main/res/values-ru/translations.xml b/features/space/impl/src/main/res/values-ru/translations.xml index f18fa24e5d..f1b314dede 100644 --- a/features/space/impl/src/main/res/values-ru/translations.xml +++ b/features/space/impl/src/main/res/values-ru/translations.xml @@ -11,6 +11,7 @@ "Вы не будете удалены из следующих комнат, поскольку вы являетесь единственным администратором:" "Выйти из %1$s?" "Вы единственный администратор для %1$s" + "Добавление комнаты не повлияет на доступ к ней. Чтобы изменить доступ к комнате, перейдите в Настройки > Безопасность и конфиденциальность." "Просмотреть участников" "Удаление комнаты не повлияет на доступ к ней. Чтобы изменить доступ, перейдите в раздел «Информация о комнате > Конфиденциальность и безопасность." diff --git a/features/space/impl/src/main/res/values/localazy.xml b/features/space/impl/src/main/res/values/localazy.xml index 10aa0fb28c..8d32dc0572 100644 --- a/features/space/impl/src/main/res/values/localazy.xml +++ b/features/space/impl/src/main/res/values/localazy.xml @@ -10,6 +10,7 @@ "You will not be removed from the following room(s) because you\'re the only administrator:" "Leave %1$s?" "You are the only admin for %1$s" + "Adding a room will not affect the room access. To change the access go to Room settings > Security & privacy." "View members" "Removing a room will not affect the room access. To change the access go to Room info > Privacy & security." diff --git a/libraries/ui-strings/src/main/res/values-de/translations.xml b/libraries/ui-strings/src/main/res/values-de/translations.xml index 60a77143d6..a759af523f 100644 --- a/libraries/ui-strings/src/main/res/values-de/translations.xml +++ b/libraries/ui-strings/src/main/res/values-de/translations.xml @@ -477,7 +477,6 @@ Möchtest du wirklich fortfahren?" "In Google Maps öffnen" "In OpenStreetMap öffnen" "Diesen Standort teilen" - "Das Hinzufügen eines Chats hat keinen Einfluss auf die Beitrittsregeln. Um die Regeln zu ändern, gehe zu \"Raum Info\" und dann zu \"Datenschutz und Sicherheit\"" "Von dir erstellte oder beigetretene Spaces." "%1$s • %2$s" "Erstelle einen Space, um Chats zu organisieren" diff --git a/libraries/ui-strings/src/main/res/values-fr/translations.xml b/libraries/ui-strings/src/main/res/values-fr/translations.xml index 6d594b6655..0fcaeeab7d 100644 --- a/libraries/ui-strings/src/main/res/values-fr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fr/translations.xml @@ -483,7 +483,6 @@ Raison : %1$s." "Ouvrir dans Google Maps" "Ouvrir dans OpenStreetMap" "Partager cette position" - "Ajouter un salon ne changera pas l’accès au salon. Pour modifier l’accès, aller dans les paramètres du salon puis dans Sécurité & confidentialité." "Espaces que vous avez créés ou rejoints." "%1$s • %2$s" "Créer des espaces pour organiser les salons" diff --git a/libraries/ui-strings/src/main/res/values-ru/translations.xml b/libraries/ui-strings/src/main/res/values-ru/translations.xml index 628cce6438..e55f36a559 100644 --- a/libraries/ui-strings/src/main/res/values-ru/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ru/translations.xml @@ -492,7 +492,6 @@ "Открыть в Google Maps" "Открыть в OpenStreetMap" "Поделиться этим местоположением" - "Добавление комнаты не повлияет на доступ к ней. Чтобы изменить доступ к комнате, перейдите в Настройки > Безопасность и конфиденциальность." "Пространства, которые вы создали или к которым присоединились." "%1$s • %2$s" "Создавайте пространства для организации комнат" diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 332151e5f8..4bd87f256c 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -484,7 +484,6 @@ Are you sure you want to continue?" "Open in Google Maps" "Open in OpenStreetMap" "Share this location" - "Adding a room will not affect the room access. To change the access go to Room settings > Security & privacy." "Spaces you have created or joined." "%1$s • %2$s" "Create spaces to organize rooms" diff --git a/tools/localazy/config.json b/tools/localazy/config.json index e6ea1df3cb..3a7c568bb0 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -234,7 +234,8 @@ "includeRegex" : [ "screen\\.leave_space\\..*", "screen\\.space_settings\\..*", - "screen\\.space\\..*" + "screen\\.space\\..*", + "screen\\.space_add_rooms\\..*" ] }, { From d93c7db1d85dff41d9ef872e562a16455aca94fa Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 21 Jan 2026 19:58:45 +0100 Subject: [PATCH 07/11] Iterate on space "Add existing rooms" ui --- .../space/impl/addroom/AddRoomToSpaceView.kt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceView.kt index 94b97f6ff6..79e8a2b511 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceView.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceView.kt @@ -27,6 +27,8 @@ 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 +import io.element.android.features.space.impl.R import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.designsystem.components.async.AsyncActionView import io.element.android.libraries.designsystem.components.avatar.AvatarSize @@ -39,6 +41,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.ListSectionHeader import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.SearchBar +import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.matrix.ui.components.SelectedRoom @@ -131,6 +134,13 @@ fun AddRoomToSpaceView( } if (!state.isSearchActive) { + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = stringResource(R.string.screen_space_add_rooms_room_access_description), + color = ElementTheme.colors.textSecondary, + style = ElementTheme.typography.fontBodySmRegular, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) + ) if (state.selectedRooms.isNotEmpty()) { SelectedRoomsRow( selectedRooms = state.selectedRooms, @@ -139,8 +149,6 @@ fun AddRoomToSpaceView( ) } - Spacer(modifier = Modifier.height(8.dp)) - if (state.suggestions.isNotEmpty()) { LazyColumn { item { From a7928eb2e63fffce073c3146b05961437936d931 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 22 Jan 2026 10:24:09 +0100 Subject: [PATCH 08/11] Add tests to AddRoomToSpace feature --- .../addroom/AddRoomToSpaceStateProvider.kt | 7 +- .../addroom/AddRoomToSpacePresenterTest.kt | 336 ++++++++++++++++++ .../impl/addroom/AddRoomToSpaceViewTest.kt | 124 +++++++ 3 files changed, 464 insertions(+), 3 deletions(-) create mode 100644 features/space/impl/src/test/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenterTest.kt create mode 100644 features/space/impl/src/test/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceViewTest.kt diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceStateProvider.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceStateProvider.kt index 550555135a..6918f135f6 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceStateProvider.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceStateProvider.kt @@ -60,13 +60,14 @@ internal class AddRoomToSpaceStateProvider : PreviewParameterProvider> = SearchBarResultState.Initial(), selectedRooms: ImmutableList = persistentListOf(), isSearchActive: Boolean = false, saveAction: AsyncAction = AsyncAction.Uninitialized, suggestions: ImmutableList = persistentListOf(), + eventSink: (AddRoomToSpaceEvent) -> Unit = {}, ): AddRoomToSpaceState { return AddRoomToSpaceState( searchQuery = searchQuery, @@ -75,11 +76,11 @@ private fun anAddRoomToSpaceState( isSearchActive = isSearchActive, saveAction = saveAction, suggestions = suggestions, - eventSink = {}, + eventSink = eventSink, ) } -private fun aSelectRoomInfoList(): ImmutableList = listOf( +internal fun aSelectRoomInfoList(): ImmutableList = listOf( SelectRoomInfo( roomId = RoomId("!room1:server.org"), name = "General", diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenterTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenterTest.kt new file mode 100644 index 0000000000..17451cb492 --- /dev/null +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenterTest.kt @@ -0,0 +1,336 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.features.space.impl.addroom + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.CurrentUserMembership +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_ROOM_ID_2 +import io.element.android.libraries.matrix.test.AN_EXCEPTION +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.room.aRoomInfo +import io.element.android.libraries.matrix.test.room.aRoomSummary +import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService +import io.element.android.libraries.matrix.test.spaces.FakeSpaceRoomList +import io.element.android.libraries.matrix.test.spaces.FakeSpaceService +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import io.element.android.tests.testutils.test +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class AddRoomToSpacePresenterTest { + @Test + fun `present - initial state has empty selection and no search`() = runTest { + val presenter = createPresenter() + presenter.test { + val state = awaitItem() + assertThat(state.selectedRooms).isEmpty() + assertThat(state.searchQuery).isEmpty() + assertThat(state.isSearchActive).isFalse() + assertThat(state.saveAction).isEqualTo(AsyncAction.Uninitialized) + assertThat(state.canSave).isFalse() + } + } + + @Test + fun `present - ToggleRoom adds room to selection`() = runTest { + val presenter = createPresenter() + presenter.test { + val state = awaitItem() + val room = aSelectRoomInfoList().first() + state.eventSink(AddRoomToSpaceEvent.ToggleRoom(room)) + val updatedState = awaitItem() + assertThat(updatedState.selectedRooms).hasSize(1) + assertThat(updatedState.selectedRooms.first().roomId).isEqualTo(room.roomId) + assertThat(updatedState.canSave).isTrue() + } + } + + @Test + fun `present - ToggleRoom removes already selected room`() = runTest { + val presenter = createPresenter() + presenter.test { + val state = awaitItem() + val room = aSelectRoomInfoList().first() + // Add room + state.eventSink(AddRoomToSpaceEvent.ToggleRoom(room)) + val stateWithRoom = awaitItem() + assertThat(stateWithRoom.selectedRooms).hasSize(1) + // Remove room + stateWithRoom.eventSink(AddRoomToSpaceEvent.ToggleRoom(room)) + val stateWithoutRoom = awaitItem() + assertThat(stateWithoutRoom.selectedRooms).isEmpty() + assertThat(stateWithoutRoom.canSave).isFalse() + } + } + + @Test + fun `present - UpdateSearchQuery updates query`() = runTest { + val presenter = createPresenter() + presenter.test { + val state = awaitItem() + state.eventSink(AddRoomToSpaceEvent.UpdateSearchQuery("test")) + val updatedState = awaitItem() + assertThat(updatedState.searchQuery).isEqualTo("test") + } + } + + @Test + fun `present - OnSearchActiveChanged activates search`() = runTest { + val presenter = createPresenter() + presenter.test { + val state = awaitItem() + state.eventSink(AddRoomToSpaceEvent.OnSearchActiveChanged(true)) + val updatedState = awaitItem() + assertThat(updatedState.isSearchActive).isTrue() + } + } + + @Test + fun `present - OnSearchActiveChanged deactivates search and clears query`() = runTest { + val presenter = createPresenter() + presenter.test { + val state = awaitItem() + // Activate search and set query + state.eventSink(AddRoomToSpaceEvent.OnSearchActiveChanged(true)) + awaitItem() + state.eventSink(AddRoomToSpaceEvent.UpdateSearchQuery("test")) + awaitItem() + // Deactivate search + state.eventSink(AddRoomToSpaceEvent.OnSearchActiveChanged(false)) + advanceUntilIdle() + val finalState = expectMostRecentItem() + assertThat(finalState.isSearchActive).isFalse() + assertThat(finalState.searchQuery).isEmpty() + } + } + + @Test + fun `present - CloseSearch deactivates and clears query`() = runTest { + val presenter = createPresenter() + presenter.test { + val state = awaitItem() + // Activate search and set query + state.eventSink(AddRoomToSpaceEvent.OnSearchActiveChanged(true)) + awaitItem() + state.eventSink(AddRoomToSpaceEvent.UpdateSearchQuery("test")) + awaitItem() + // Close search + state.eventSink(AddRoomToSpaceEvent.CloseSearch) + advanceUntilIdle() + val finalState = expectMostRecentItem() + assertThat(finalState.isSearchActive).isFalse() + assertThat(finalState.searchQuery).isEmpty() + } + } + + @Test + fun `present - searchResults shows Results when rooms available`() = runTest { + val roomListService = FakeRoomListService() + val presenter = createPresenter(roomListService = roomListService) + presenter.test { + awaitItem() // Initial state + // Post rooms to the service + roomListService.postAllRooms( + listOf( + aRoomSummary( + roomId = A_ROOM_ID, + name = "Room 1", + isDirect = false, + isSpace = false, + currentUserMembership = CurrentUserMembership.JOINED, + ) + ) + ) + advanceUntilIdle() + val state = expectMostRecentItem() + assertThat(state.searchResults).isInstanceOf(SearchBarResultState.Results::class.java) + } + } + + @Test + fun `present - searchResults shows NoResultsFound when search active with query but no results`() = runTest { + val presenter = createPresenter() + presenter.test { + val state = awaitItem() + state.eventSink(AddRoomToSpaceEvent.OnSearchActiveChanged(true)) + awaitItem() + state.eventSink(AddRoomToSpaceEvent.UpdateSearchQuery("nonexistent")) + advanceUntilIdle() + val finalState = expectMostRecentItem() + assertThat(finalState.isSearchActive).isTrue() + assertThat(finalState.searchQuery).isEqualTo("nonexistent") + assertThat(finalState.searchResults).isInstanceOf(SearchBarResultState.NoResultsFound::class.java) + } + } + + @Test + fun `present - Save triggers addChildToSpace for all selected rooms`() = runTest { + val addChildToSpaceResult = lambdaRecorder> { _, _ -> + Result.success(Unit) + } + val spaceService = FakeSpaceService( + addChildToSpaceResult = addChildToSpaceResult, + ) + val presenter = createPresenter(spaceService = spaceService) + presenter.test { + val state = awaitItem() + // Select two rooms + val room1 = aSelectRoomInfoList()[0] + val room2 = aSelectRoomInfoList()[1] + state.eventSink(AddRoomToSpaceEvent.ToggleRoom(room1)) + awaitItem() + state.eventSink(AddRoomToSpaceEvent.ToggleRoom(room2)) + awaitItem() + // Save + state.eventSink(AddRoomToSpaceEvent.Save) + // Wait for loading and success states + skipItems(1) // Loading + advanceUntilIdle() + skipItems(1) // Success + // Verify service was called for both rooms + addChildToSpaceResult.assertions().isCalledExactly(2) + } + } + + @Test + fun `present - Save success updates saveAction to Success`() = runTest { + val spaceService = FakeSpaceService( + addChildToSpaceResult = { _, _ -> Result.success(Unit) }, + ) + val presenter = createPresenter(spaceService = spaceService) + presenter.test { + val state = awaitItem() + val room = aSelectRoomInfoList().first() + state.eventSink(AddRoomToSpaceEvent.ToggleRoom(room)) + awaitItem() + state.eventSink(AddRoomToSpaceEvent.Save) + // Wait for loading state + val loadingState = awaitItem() + assertThat(loadingState.saveAction).isEqualTo(AsyncAction.Loading) + // Wait for success state + advanceUntilIdle() + val successState = awaitItem() + assertThat(successState.saveAction).isInstanceOf(AsyncAction.Success::class.java) + } + } + + @Test + fun `present - Save failure updates saveAction to Failure`() = runTest { + val spaceService = FakeSpaceService( + addChildToSpaceResult = { _, _ -> Result.failure(AN_EXCEPTION) }, + ) + val presenter = createPresenter(spaceService = spaceService) + presenter.test { + val state = awaitItem() + val room = aSelectRoomInfoList().first() + state.eventSink(AddRoomToSpaceEvent.ToggleRoom(room)) + awaitItem() + state.eventSink(AddRoomToSpaceEvent.Save) + // Wait for loading state + val loadingState = awaitItem() + assertThat(loadingState.saveAction).isEqualTo(AsyncAction.Loading) + // Wait for failure state + advanceUntilIdle() + val failureState = awaitItem() + assertThat(failureState.saveAction).isInstanceOf(AsyncAction.Failure::class.java) + } + } + + @Test + fun `present - ResetSaveAction resets to Uninitialized`() = runTest { + val spaceService = FakeSpaceService( + addChildToSpaceResult = { _, _ -> Result.success(Unit) }, + ) + val presenter = createPresenter(spaceService = spaceService) + presenter.test { + val state = awaitItem() + val room = aSelectRoomInfoList().first() + state.eventSink(AddRoomToSpaceEvent.ToggleRoom(room)) + awaitItem() + state.eventSink(AddRoomToSpaceEvent.Save) + skipItems(1) // Loading + advanceUntilIdle() + val successState = awaitItem() + assertThat(successState.saveAction).isInstanceOf(AsyncAction.Success::class.java) + // Reset + successState.eventSink(AddRoomToSpaceEvent.ResetSaveAction) + val resetState = awaitItem() + assertThat(resetState.saveAction).isEqualTo(AsyncAction.Uninitialized) + } + } + + @Test + fun `canSave is false when no rooms selected`() = runTest { + val presenter = createPresenter() + presenter.test { + val state = awaitItem() + assertThat(state.selectedRooms).isEmpty() + assertThat(state.canSave).isFalse() + } + } + + @Test + fun `canSave is false when loading`() = runTest { + val spaceService = FakeSpaceService( + addChildToSpaceResult = { _, _ -> Result.success(Unit) }, + ) + val presenter = createPresenter(spaceService = spaceService) + presenter.test { + val state = awaitItem() + val room = aSelectRoomInfoList().first() + state.eventSink(AddRoomToSpaceEvent.ToggleRoom(room)) + val stateWithRoom = awaitItem() + assertThat(stateWithRoom.canSave).isTrue() + stateWithRoom.eventSink(AddRoomToSpaceEvent.Save) + val loadingState = awaitItem() + assertThat(loadingState.saveAction).isEqualTo(AsyncAction.Loading) + assertThat(loadingState.canSave).isFalse() + } + } + + private fun TestScope.createPresenter( + spaceRoomList: FakeSpaceRoomList = FakeSpaceRoomList( + paginateResult = { Result.success(Unit) }, + ), + spaceService: FakeSpaceService = FakeSpaceService( + addChildToSpaceResult = { _, _ -> Result.success(Unit) }, + ), + roomListService: FakeRoomListService = FakeRoomListService(), + matrixClient: FakeMatrixClient = FakeMatrixClient( + roomListService = roomListService, + ), + ): AddRoomToSpacePresenter { + val dataSourceFactory = object : AddRoomToSpaceSearchDataSource.Factory { + override fun create(coroutineScope: CoroutineScope) = AddRoomToSpaceSearchDataSource( + coroutineScope = coroutineScope, + roomListService = roomListService, + spaceRoomList = spaceRoomList, + matrixClient = matrixClient, + coroutineDispatchers = testCoroutineDispatchers(), + ) + } + return AddRoomToSpacePresenter( + spaceRoomList = spaceRoomList, + spaceService = spaceService, + dataSourceFactory = dataSourceFactory, + ) + } +} diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceViewTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceViewTest.kt new file mode 100644 index 0000000000..593a37aa00 --- /dev/null +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceViewTest.kt @@ -0,0 +1,124 @@ +/* + * 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.features.space.impl.addroom + +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.architecture.AsyncAction +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EnsureNeverCalled +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.ensureCalledOnce +import io.element.android.tests.testutils.pressBack +import kotlinx.collections.immutable.toImmutableList +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith +import org.robolectric.annotation.Config + +@RunWith(AndroidJUnit4::class) +class AddRoomToSpaceViewTest { + @get:Rule val rule = createAndroidComposeRule() + + @Test + fun `clicking back when search inactive invokes onBackClick`() { + ensureCalledOnce { + rule.setAddRoomToSpaceView( + anAddRoomToSpaceState( + isSearchActive = false, + ), + onBackClick = it, + ) + rule.pressBack() + } + } + + @Test + fun `clicking back when search active emits CloseSearch event`() { + val eventsRecorder = EventsRecorder() + rule.setAddRoomToSpaceView( + anAddRoomToSpaceState( + isSearchActive = true, + eventSink = eventsRecorder, + ), + ) + rule.pressBack() + eventsRecorder.assertSingle(AddRoomToSpaceEvent.CloseSearch) + } + + @Test + fun `clicking save emits Save event`() { + val eventsRecorder = EventsRecorder() + rule.setAddRoomToSpaceView( + anAddRoomToSpaceState( + selectedRooms = aSelectRoomInfoList().take(1).toImmutableList(), + eventSink = eventsRecorder, + ), + ) + rule.clickOn(CommonStrings.action_save) + eventsRecorder.assertList( + listOf( + AddRoomToSpaceEvent.UpdateSearchQuery(""), // SearchBar initialization + AddRoomToSpaceEvent.Save, + ) + ) + } + + @Config(qualifiers = "h1024dp") + @Test + fun `clicking room in suggestions emits ToggleRoom event`() { + val suggestions = aSelectRoomInfoList() + val eventsRecorder = EventsRecorder() + rule.setAddRoomToSpaceView( + anAddRoomToSpaceState( + suggestions = suggestions, + eventSink = eventsRecorder, + ), + ) + rule.onNodeWithText(suggestions.first().name!!).performClick() + eventsRecorder.assertList( + listOf( + AddRoomToSpaceEvent.UpdateSearchQuery(""), // SearchBar initialization + AddRoomToSpaceEvent.ToggleRoom(suggestions.first()), + ) + ) + } + + @Test + fun `onRoomsAdded called when saveAction is Success`() { + ensureCalledOnce { + rule.setAddRoomToSpaceView( + anAddRoomToSpaceState( + saveAction = AsyncAction.Success(Unit), + ), + onRoomsAdded = it, + ) + } + } +} + +private fun AndroidComposeTestRule.setAddRoomToSpaceView( + state: AddRoomToSpaceState, + onBackClick: () -> Unit = EnsureNeverCalled(), + onRoomsAdded: () -> Unit = EnsureNeverCalled(), +) { + setContent { + AddRoomToSpaceView( + state = state, + onBackClick = onBackClick, + onRoomsAdded = onRoomsAdded, + ) + } +} From 94876f2dee6c2e3c4c3e2c8b76962a59398daafd Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 22 Jan 2026 10:46:01 +0100 Subject: [PATCH 09/11] Fix quality --- .../impl/addroom/AddRoomToSpacePresenter.kt | 10 ++--- .../addroom/AddRoomToSpaceSearchDataSource.kt | 2 - .../space/impl/addroom/AddRoomToSpaceView.kt | 4 +- .../addroom/AddRoomToSpacePresenterTest.kt | 37 +++++++++---------- .../matrix/ui/model/SelectRoomInfo.kt | 1 - 5 files changed, 23 insertions(+), 31 deletions(-) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenter.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenter.kt index 0516b102e5..143cecd203 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenter.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenter.kt @@ -24,13 +24,12 @@ import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runUpdatingState import io.element.android.libraries.designsystem.theme.components.SearchBarResultState -import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.matrix.api.spaces.SpaceRoomList import io.element.android.libraries.matrix.api.spaces.SpaceService import io.element.android.libraries.matrix.ui.model.SelectRoomInfo import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toPersistentList +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -42,10 +41,9 @@ class AddRoomToSpacePresenter( private val spaceService: SpaceService, private val dataSourceFactory: AddRoomToSpaceSearchDataSource.Factory, ) : Presenter { - @Composable override fun present(): AddRoomToSpaceState { - var selectedRooms by remember { mutableStateOf(persistentListOf()) } + var selectedRooms: ImmutableList by remember { mutableStateOf(persistentListOf()) } var searchQuery by remember { mutableStateOf("") } var isSearchActive by remember { mutableStateOf(false) } val saveAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } @@ -78,9 +76,9 @@ class AddRoomToSpacePresenter( when (event) { is AddRoomToSpaceEvent.ToggleRoom -> { selectedRooms = if (selectedRooms.any { it.roomId == event.room.roomId }) { - selectedRooms.filterNot { it.roomId == event.room.roomId }.toPersistentList() + selectedRooms.filterNot { it.roomId == event.room.roomId }.toImmutableList() } else { - (selectedRooms + event.room).toPersistentList() + (selectedRooms + event.room).toImmutableList() } } is AddRoomToSpaceEvent.UpdateSearchQuery -> { diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceSearchDataSource.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceSearchDataSource.kt index 64603faaca..3d5722816c 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceSearchDataSource.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceSearchDataSource.kt @@ -11,7 +11,6 @@ package io.element.android.features.space.impl.addroom import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedFactory import dev.zacsweers.metro.AssistedInject -import dev.zacsweers.metro.Inject import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId @@ -52,7 +51,6 @@ class AddRoomToSpaceSearchDataSource( private val matrixClient: MatrixClient, private val coroutineDispatchers: CoroutineDispatchers, ) { - @AssistedFactory interface Factory { fun create(coroutineScope: CoroutineScope): AddRoomToSpaceSearchDataSource diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceView.kt index 79e8a2b511..0ea2bb7c70 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceView.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceView.kt @@ -171,8 +171,8 @@ fun AddRoomToSpaceView( } SaveActionView( saveAction = state.saveAction, - onRetry = { state.eventSink(AddRoomToSpaceEvent.Save)}, - onDismiss = {state.eventSink(AddRoomToSpaceEvent.ResetSaveAction)} + onRetry = { state.eventSink(AddRoomToSpaceEvent.Save) }, + onDismiss = { state.eventSink(AddRoomToSpaceEvent.ResetSaveAction) } ) } diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenterTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenterTest.kt index 17451cb492..0448bae7af 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenterTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenterTest.kt @@ -15,17 +15,14 @@ import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.CurrentUserMembership -import io.element.android.libraries.matrix.test.A_ROOM_ID -import io.element.android.libraries.matrix.test.A_ROOM_ID_2 import io.element.android.libraries.matrix.test.AN_EXCEPTION +import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.FakeMatrixClient -import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.aRoomSummary import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService import io.element.android.libraries.matrix.test.spaces.FakeSpaceRoomList import io.element.android.libraries.matrix.test.spaces.FakeSpaceService import io.element.android.tests.testutils.lambda.lambdaRecorder -import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.test import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.CoroutineScope @@ -38,7 +35,7 @@ import org.junit.Test class AddRoomToSpacePresenterTest { @Test fun `present - initial state has empty selection and no search`() = runTest { - val presenter = createPresenter() + val presenter = createAddRoomToSpacePresenter() presenter.test { val state = awaitItem() assertThat(state.selectedRooms).isEmpty() @@ -51,7 +48,7 @@ class AddRoomToSpacePresenterTest { @Test fun `present - ToggleRoom adds room to selection`() = runTest { - val presenter = createPresenter() + val presenter = createAddRoomToSpacePresenter() presenter.test { val state = awaitItem() val room = aSelectRoomInfoList().first() @@ -65,7 +62,7 @@ class AddRoomToSpacePresenterTest { @Test fun `present - ToggleRoom removes already selected room`() = runTest { - val presenter = createPresenter() + val presenter = createAddRoomToSpacePresenter() presenter.test { val state = awaitItem() val room = aSelectRoomInfoList().first() @@ -83,7 +80,7 @@ class AddRoomToSpacePresenterTest { @Test fun `present - UpdateSearchQuery updates query`() = runTest { - val presenter = createPresenter() + val presenter = createAddRoomToSpacePresenter() presenter.test { val state = awaitItem() state.eventSink(AddRoomToSpaceEvent.UpdateSearchQuery("test")) @@ -94,7 +91,7 @@ class AddRoomToSpacePresenterTest { @Test fun `present - OnSearchActiveChanged activates search`() = runTest { - val presenter = createPresenter() + val presenter = createAddRoomToSpacePresenter() presenter.test { val state = awaitItem() state.eventSink(AddRoomToSpaceEvent.OnSearchActiveChanged(true)) @@ -105,7 +102,7 @@ class AddRoomToSpacePresenterTest { @Test fun `present - OnSearchActiveChanged deactivates search and clears query`() = runTest { - val presenter = createPresenter() + val presenter = createAddRoomToSpacePresenter() presenter.test { val state = awaitItem() // Activate search and set query @@ -124,7 +121,7 @@ class AddRoomToSpacePresenterTest { @Test fun `present - CloseSearch deactivates and clears query`() = runTest { - val presenter = createPresenter() + val presenter = createAddRoomToSpacePresenter() presenter.test { val state = awaitItem() // Activate search and set query @@ -144,7 +141,7 @@ class AddRoomToSpacePresenterTest { @Test fun `present - searchResults shows Results when rooms available`() = runTest { val roomListService = FakeRoomListService() - val presenter = createPresenter(roomListService = roomListService) + val presenter = createAddRoomToSpacePresenter(roomListService = roomListService) presenter.test { awaitItem() // Initial state // Post rooms to the service @@ -167,7 +164,7 @@ class AddRoomToSpacePresenterTest { @Test fun `present - searchResults shows NoResultsFound when search active with query but no results`() = runTest { - val presenter = createPresenter() + val presenter = createAddRoomToSpacePresenter() presenter.test { val state = awaitItem() state.eventSink(AddRoomToSpaceEvent.OnSearchActiveChanged(true)) @@ -189,7 +186,7 @@ class AddRoomToSpacePresenterTest { val spaceService = FakeSpaceService( addChildToSpaceResult = addChildToSpaceResult, ) - val presenter = createPresenter(spaceService = spaceService) + val presenter = createAddRoomToSpacePresenter(spaceService = spaceService) presenter.test { val state = awaitItem() // Select two rooms @@ -215,7 +212,7 @@ class AddRoomToSpacePresenterTest { val spaceService = FakeSpaceService( addChildToSpaceResult = { _, _ -> Result.success(Unit) }, ) - val presenter = createPresenter(spaceService = spaceService) + val presenter = createAddRoomToSpacePresenter(spaceService = spaceService) presenter.test { val state = awaitItem() val room = aSelectRoomInfoList().first() @@ -237,7 +234,7 @@ class AddRoomToSpacePresenterTest { val spaceService = FakeSpaceService( addChildToSpaceResult = { _, _ -> Result.failure(AN_EXCEPTION) }, ) - val presenter = createPresenter(spaceService = spaceService) + val presenter = createAddRoomToSpacePresenter(spaceService = spaceService) presenter.test { val state = awaitItem() val room = aSelectRoomInfoList().first() @@ -259,7 +256,7 @@ class AddRoomToSpacePresenterTest { val spaceService = FakeSpaceService( addChildToSpaceResult = { _, _ -> Result.success(Unit) }, ) - val presenter = createPresenter(spaceService = spaceService) + val presenter = createAddRoomToSpacePresenter(spaceService = spaceService) presenter.test { val state = awaitItem() val room = aSelectRoomInfoList().first() @@ -279,7 +276,7 @@ class AddRoomToSpacePresenterTest { @Test fun `canSave is false when no rooms selected`() = runTest { - val presenter = createPresenter() + val presenter = createAddRoomToSpacePresenter() presenter.test { val state = awaitItem() assertThat(state.selectedRooms).isEmpty() @@ -292,7 +289,7 @@ class AddRoomToSpacePresenterTest { val spaceService = FakeSpaceService( addChildToSpaceResult = { _, _ -> Result.success(Unit) }, ) - val presenter = createPresenter(spaceService = spaceService) + val presenter = createAddRoomToSpacePresenter(spaceService = spaceService) presenter.test { val state = awaitItem() val room = aSelectRoomInfoList().first() @@ -306,7 +303,7 @@ class AddRoomToSpacePresenterTest { } } - private fun TestScope.createPresenter( + private fun TestScope.createAddRoomToSpacePresenter( spaceRoomList: FakeSpaceRoomList = FakeSpaceRoomList( paginateResult = { Result.success(Unit) }, ), diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/SelectRoomInfo.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/SelectRoomInfo.kt index cf66d040a9..d6981f62fd 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/SelectRoomInfo.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/SelectRoomInfo.kt @@ -43,4 +43,3 @@ fun RoomInfo.toSelectRoomInfo() = SelectRoomInfo( canonicalAlias = canonicalAlias, isTombstoned = successorRoom != null, ) - From 307436386388167bb51ba8a91445f0861e98cdec Mon Sep 17 00:00:00 2001 From: ElementBot Date: Thu, 22 Jan 2026 13:34:15 +0000 Subject: [PATCH 10/11] Update screenshots --- ...features.space.impl.addroom_AddRoomToSpaceView_Day_0_en.png | 3 +++ ...features.space.impl.addroom_AddRoomToSpaceView_Day_1_en.png | 3 +++ ...features.space.impl.addroom_AddRoomToSpaceView_Day_2_en.png | 3 +++ ...features.space.impl.addroom_AddRoomToSpaceView_Day_3_en.png | 3 +++ ...features.space.impl.addroom_AddRoomToSpaceView_Day_4_en.png | 3 +++ ...features.space.impl.addroom_AddRoomToSpaceView_Day_5_en.png | 3 +++ ...features.space.impl.addroom_AddRoomToSpaceView_Day_6_en.png | 3 +++ ...atures.space.impl.addroom_AddRoomToSpaceView_Night_0_en.png | 3 +++ ...atures.space.impl.addroom_AddRoomToSpaceView_Night_1_en.png | 3 +++ ...atures.space.impl.addroom_AddRoomToSpaceView_Night_2_en.png | 3 +++ ...atures.space.impl.addroom_AddRoomToSpaceView_Night_3_en.png | 3 +++ ...atures.space.impl.addroom_AddRoomToSpaceView_Night_4_en.png | 3 +++ ...atures.space.impl.addroom_AddRoomToSpaceView_Night_5_en.png | 3 +++ ...atures.space.impl.addroom_AddRoomToSpaceView_Night_6_en.png | 3 +++ 14 files changed, 42 insertions(+) create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Day_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Day_2_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Day_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Day_4_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Day_5_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Day_6_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Night_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Night_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Night_2_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Night_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Night_4_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Night_5_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Night_6_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Day_0_en.png new file mode 100644 index 0000000000..cfcec20e41 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:72074f44d83f1772e83de96fd86265a43cdd2123023ac65bee8ddf7abf4af37e +size 38249 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Day_1_en.png new file mode 100644 index 0000000000..a7e08a1c9f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2a295c4f8c9e784861a3db01597be406615a4adb6c4d8997478336e3a3f30003 +size 11682 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Day_2_en.png new file mode 100644 index 0000000000..2575b9e323 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d7c006740c4fd75d7efbd5abe59804d8d5fabbfd7f11b6d14c0b00fcb83850be +size 21434 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Day_3_en.png new file mode 100644 index 0000000000..a28bfd86a8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:841cfff6c0c0219e83e6ee7ca03060e143ddff72f7946e578498ffb6918fa37d +size 14200 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Day_4_en.png new file mode 100644 index 0000000000..a272355e5b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Day_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:83d9e5eb832d1f52b17e35e18d1c834120a7b510c2a3cc1647dc35dfeeb8b0a9 +size 42762 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Day_5_en.png new file mode 100644 index 0000000000..4c049df7e7 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Day_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5d5fd28a7971e2f25fdf6d3e1b0af9a1d279369d2f1d656ff72a36a1badfd774 +size 29089 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Day_6_en.png new file mode 100644 index 0000000000..d1135ccc34 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Day_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7d191722630f4eede4606f63053d29996ae32c728af85e198a390be82b84d274 +size 40323 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Night_0_en.png new file mode 100644 index 0000000000..bf6a20ec99 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dd5f99d986f37e962322060add381b334c50a6fbc05a23fa09319b37cc86a090 +size 38078 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Night_1_en.png new file mode 100644 index 0000000000..489a7dcb1b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e011e95c98c9694980b031207b82c6feb0b9b51b06b233672bee2ee3ceb5b18c +size 11382 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Night_2_en.png new file mode 100644 index 0000000000..e43d5e7f3d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71bc3b3b49e37690e92fe23ab3c48cc286341e64c427471b7c940b65243bd998 +size 21776 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Night_3_en.png new file mode 100644 index 0000000000..7917998cd2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:76ab4de8587bac55872b2807b342fdbdf72f771aed65ee342705f49a89c1f196 +size 13865 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Night_4_en.png new file mode 100644 index 0000000000..cb20383fc6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Night_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:50898da4de06be0d2430d4635b7c6880701d60fb78940bb2135c943e2d9634b5 +size 42908 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Night_5_en.png new file mode 100644 index 0000000000..b3bab96efb --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Night_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c4da20d886e0f2fadd57edaea13c2bae4d19fcf4c8a397a2dec92f68e14aad7 +size 27733 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Night_6_en.png new file mode 100644 index 0000000000..ebbc706877 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Night_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ceeea93e8cd825cf82971abe76c5a0d28e0522ffa4694db588c87f9473cc117 +size 37422 From 626479844f090cfb36e00c0f74fafbea0e8a814a Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 22 Jan 2026 19:57:04 +0100 Subject: [PATCH 11/11] Quality improvements after PR review --- .../features/space/impl/addroom/AddRoomToSpaceEvent.kt | 3 +-- .../features/space/impl/addroom/AddRoomToSpaceNode.kt | 3 +-- .../features/space/impl/addroom/AddRoomToSpacePresenter.kt | 3 +-- .../space/impl/addroom/AddRoomToSpaceSearchDataSource.kt | 7 +++---- .../features/space/impl/addroom/AddRoomToSpaceState.kt | 3 +-- .../space/impl/addroom/AddRoomToSpaceStateProvider.kt | 3 +-- .../features/space/impl/addroom/AddRoomToSpaceView.kt | 3 +-- .../space/impl/addroom/AddRoomToSpacePresenterTest.kt | 3 +-- .../features/space/impl/addroom/AddRoomToSpaceViewTest.kt | 3 +-- .../designsystem/components/list/AvatarListItem.kt | 3 +-- .../android/libraries/matrix/api/room/recent/RecentRoom.kt | 5 +++-- 11 files changed, 15 insertions(+), 24 deletions(-) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceEvent.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceEvent.kt index fc6b1e216a..eb3f2d0829 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceEvent.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceEvent.kt @@ -1,6 +1,5 @@ /* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2025 New Vector Ltd. + * 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. diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceNode.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceNode.kt index 3699de797e..71e7665844 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceNode.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceNode.kt @@ -1,6 +1,5 @@ /* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2025 New Vector Ltd. + * 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. diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenter.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenter.kt index 143cecd203..25fa421d1f 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenter.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenter.kt @@ -1,6 +1,5 @@ /* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2025 New Vector Ltd. + * 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. diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceSearchDataSource.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceSearchDataSource.kt index 3d5722816c..61f08d53ae 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceSearchDataSource.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceSearchDataSource.kt @@ -1,6 +1,5 @@ /* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2025 New Vector Ltd. + * 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. @@ -17,7 +16,7 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.RoomInfo import io.element.android.libraries.matrix.api.room.isDm -import io.element.android.libraries.matrix.api.room.recent.getRecentRooms +import io.element.android.libraries.matrix.api.room.recent.getRecentlyVisitedRoomInfoFlow 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.roomlist.RoomListService @@ -86,7 +85,7 @@ class AddRoomToSpaceSearchDataSource( val suggestions: Flow> = spaceChildrenFlow.map { childIds -> matrixClient - .getRecentRooms { filterRoomPredicate(it, childIds) } + .getRecentlyVisitedRoomInfoFlow { filterRoomPredicate(it, childIds) } .take(MAX_SUGGESTIONS_COUNT) .map { it.toSelectRoomInfo() } .toList() diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceState.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceState.kt index 2215bfecb9..8d7ff0ce6b 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceState.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceState.kt @@ -1,6 +1,5 @@ /* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2025 New Vector Ltd. + * 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. diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceStateProvider.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceStateProvider.kt index 6918f135f6..640e7ede7d 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceStateProvider.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceStateProvider.kt @@ -1,6 +1,5 @@ /* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2025 New Vector Ltd. + * 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. diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceView.kt index 0ea2bb7c70..c3696dcac0 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceView.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceView.kt @@ -1,6 +1,5 @@ /* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2025 New Vector Ltd. + * 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. diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenterTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenterTest.kt index 0448bae7af..c0b39c6a69 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenterTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenterTest.kt @@ -1,6 +1,5 @@ /* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2025 New Vector Ltd. + * 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. diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceViewTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceViewTest.kt index 593a37aa00..db322c3687 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceViewTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceViewTest.kt @@ -1,6 +1,5 @@ /* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2025 New Vector Ltd. + * 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. diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/AvatarListItem.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/AvatarListItem.kt index 962da7af93..c6a85e9b85 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/AvatarListItem.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/AvatarListItem.kt @@ -1,6 +1,5 @@ /* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2025 New Vector Ltd. + * 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. diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/recent/RecentRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/recent/RecentRoom.kt index d35696a8d1..a03952194c 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/recent/RecentRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/recent/RecentRoom.kt @@ -13,11 +13,12 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow /** - * Returns a [Flow] of [RoomInfo] from recently visited DM rooms. + * Returns a [Flow] of [RoomInfo] from recently visited rooms. * The flow emits items lazily, allowing callers to filter and take only what they need. * Use [kotlinx.coroutines.flow.take] to limit results and stop iteration early. + * */ -fun MatrixClient.getRecentRooms( +fun MatrixClient.getRecentlyVisitedRoomInfoFlow( predicate: (RoomInfo) -> Boolean, ): Flow = flow { val recentlyVisitedRooms = getRecentlyVisitedRooms().getOrDefault(emptyList())