Merge pull request #6063 from element-hq/feature/fga/space_add_existing_room
Implement Space 'Add existing rooms' feature
This commit is contained in:
@@ -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<SpaceNode>(buildContext, listOf(callback))
|
||||
}
|
||||
@@ -132,6 +140,14 @@ class SpaceFlowNode(
|
||||
}
|
||||
createNode<SpaceSettingsFlowNode>(buildContext, listOf(callback))
|
||||
}
|
||||
NavTarget.AddRoom -> {
|
||||
val callback = object : AddRoomToSpaceNode.Callback {
|
||||
override fun onFinish() {
|
||||
backstack.pop()
|
||||
}
|
||||
}
|
||||
createNode<AddRoomToSpaceNode>(buildContext, listOf(callback))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.space.impl.addroom
|
||||
|
||||
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.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
|
||||
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.appyx.launchMolecule
|
||||
import io.element.android.libraries.architecture.callback
|
||||
|
||||
@ContributesNode(SpaceFlowScope::class)
|
||||
@AssistedInject
|
||||
class AddRoomToSpaceNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: AddRoomToSpacePresenter,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
interface Callback : Plugin {
|
||||
fun onFinish()
|
||||
}
|
||||
|
||||
private val callback: Callback = callback()
|
||||
private val stateFlow = launchMolecule { presenter.present() }
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state by stateFlow.collectAsState()
|
||||
AddRoomToSpaceView(
|
||||
state = state,
|
||||
onBackClick = ::navigateUp,
|
||||
onRoomsAdded = callback::onFinish,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.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.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
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.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.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Inject
|
||||
class AddRoomToSpacePresenter(
|
||||
private val spaceRoomList: SpaceRoomList,
|
||||
private val spaceService: SpaceService,
|
||||
private val dataSourceFactory: AddRoomToSpaceSearchDataSource.Factory,
|
||||
) : Presenter<AddRoomToSpaceState> {
|
||||
@Composable
|
||||
override fun present(): AddRoomToSpaceState {
|
||||
var selectedRooms: ImmutableList<SelectRoomInfo> by remember { mutableStateOf(persistentListOf()) }
|
||||
var searchQuery by remember { mutableStateOf("") }
|
||||
var isSearchActive by remember { mutableStateOf(false) }
|
||||
val saveAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val dataSource = remember { dataSourceFactory.create(coroutineScope) }
|
||||
|
||||
// Update search query in data source
|
||||
LaunchedEffect(searchQuery) {
|
||||
dataSource.setSearchQuery(searchQuery)
|
||||
}
|
||||
LaunchedEffect(isSearchActive) {
|
||||
dataSource.setIsActive(isSearchActive)
|
||||
}
|
||||
|
||||
val suggestions by dataSource.suggestions.collectAsState(initial = persistentListOf())
|
||||
|
||||
val filteredRooms by dataSource.roomInfoList.collectAsState(initial = persistentListOf())
|
||||
val searchResults by remember<State<SearchBarResultState<ImmutableList<SelectRoomInfo>>>> {
|
||||
derivedStateOf {
|
||||
when {
|
||||
filteredRooms.isNotEmpty() -> SearchBarResultState.Results(filteredRooms)
|
||||
isSearchActive && searchQuery.isNotEmpty() -> SearchBarResultState.NoResultsFound()
|
||||
else -> SearchBarResultState.Initial()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleEvent(event: AddRoomToSpaceEvent) {
|
||||
when (event) {
|
||||
is AddRoomToSpaceEvent.ToggleRoom -> {
|
||||
selectedRooms = if (selectedRooms.any { it.roomId == event.room.roomId }) {
|
||||
selectedRooms.filterNot { it.roomId == event.room.roomId }.toImmutableList()
|
||||
} else {
|
||||
(selectedRooms + event.room).toImmutableList()
|
||||
}
|
||||
}
|
||||
is AddRoomToSpaceEvent.UpdateSearchQuery -> {
|
||||
searchQuery = event.query
|
||||
}
|
||||
is AddRoomToSpaceEvent.OnSearchActiveChanged -> {
|
||||
isSearchActive = event.active
|
||||
if (!event.active) {
|
||||
searchQuery = ""
|
||||
}
|
||||
}
|
||||
AddRoomToSpaceEvent.CloseSearch -> {
|
||||
isSearchActive = false
|
||||
searchQuery = ""
|
||||
}
|
||||
AddRoomToSpaceEvent.Save -> {
|
||||
coroutineScope.addRoomsToSpace(
|
||||
selectedRooms = selectedRooms,
|
||||
addAction = saveAction,
|
||||
)
|
||||
}
|
||||
AddRoomToSpaceEvent.ResetSaveAction -> {
|
||||
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<SelectRoomInfo>,
|
||||
addAction: MutableState<AsyncAction<Unit>>,
|
||||
) = 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.space.impl.addroom
|
||||
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
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.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
|
||||
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.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, rooms already in the space, and only includes rooms the user has joined.
|
||||
*/
|
||||
@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 ->
|
||||
spaceChildren.map { it.roomId }.toSet()
|
||||
}
|
||||
|
||||
private val filterRoomPredicate: (RoomInfo, Set<RoomId>) -> Boolean = { info, childIds ->
|
||||
!info.isSpace &&
|
||||
!info.isDm &&
|
||||
info.currentUserMembership == CurrentUserMembership.JOINED &&
|
||||
info.id !in childIds
|
||||
}
|
||||
|
||||
val roomInfoList: Flow<ImmutableList<SelectRoomInfo>> = combine(
|
||||
roomList.filteredSummaries,
|
||||
spaceChildrenFlow,
|
||||
) { roomSummaries, childIds ->
|
||||
roomSummaries
|
||||
.filter { filterRoomPredicate(it.info, childIds) }
|
||||
.map { it.toSelectRoomInfo() }
|
||||
.toImmutableList()
|
||||
}.flowOn(coroutineDispatchers.computation)
|
||||
|
||||
val suggestions: Flow<ImmutableList<SelectRoomInfo>> = spaceChildrenFlow.map { childIds ->
|
||||
matrixClient
|
||||
.getRecentlyVisitedRoomInfoFlow { 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) {
|
||||
val filter = if (searchQuery.isBlank()) {
|
||||
RoomListFilter.None
|
||||
} else {
|
||||
RoomListFilter.NormalizedMatchRoomName(searchQuery)
|
||||
}
|
||||
roomList.updateFilter(filter)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.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<ImmutableList<SelectRoomInfo>>,
|
||||
val selectedRooms: ImmutableList<SelectRoomInfo>,
|
||||
val suggestions: ImmutableList<SelectRoomInfo>,
|
||||
val saveAction: AsyncAction<Unit>,
|
||||
val eventSink: (AddRoomToSpaceEvent) -> Unit,
|
||||
) {
|
||||
val canSave: Boolean = selectedRooms.isNotEmpty() && !saveAction.isLoading()
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.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<AddRoomToSpaceState> {
|
||||
override val values: Sequence<AddRoomToSpaceState>
|
||||
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")),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun anAddRoomToSpaceState(
|
||||
searchQuery: String = "",
|
||||
searchResults: SearchBarResultState<ImmutableList<SelectRoomInfo>> = SearchBarResultState.Initial(),
|
||||
selectedRooms: ImmutableList<SelectRoomInfo> = persistentListOf(),
|
||||
isSearchActive: Boolean = false,
|
||||
saveAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
suggestions: ImmutableList<SelectRoomInfo> = persistentListOf(),
|
||||
eventSink: (AddRoomToSpaceEvent) -> Unit = {},
|
||||
): AddRoomToSpaceState {
|
||||
return AddRoomToSpaceState(
|
||||
searchQuery = searchQuery,
|
||||
searchResults = searchResults,
|
||||
selectedRooms = selectedRooms,
|
||||
isSearchActive = isSearchActive,
|
||||
saveAction = saveAction,
|
||||
suggestions = suggestions,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
}
|
||||
|
||||
internal fun aSelectRoomInfoList(): ImmutableList<SelectRoomInfo> = 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()
|
||||
@@ -0,0 +1,246 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.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.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
|
||||
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.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.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
|
||||
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(AddRoomToSpaceEvent.ToggleRoom(roomInfo))
|
||||
}
|
||||
|
||||
fun onBack() {
|
||||
if (state.isSearchActive) {
|
||||
state.eventSink(AddRoomToSpaceEvent.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(AddRoomToSpaceEvent.Save) }
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
Modifier
|
||||
.padding(paddingValues)
|
||||
.consumeWindowInsets(paddingValues)
|
||||
) {
|
||||
SearchBar(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeHolderTitle = stringResource(CommonStrings.action_search),
|
||||
query = state.searchQuery,
|
||||
onQueryChange = { state.eventSink(AddRoomToSpaceEvent.UpdateSearchQuery(it)) },
|
||||
active = state.isSearchActive,
|
||||
onActiveChange = { state.eventSink(AddRoomToSpaceEvent.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(AddRoomToSpaceEvent.ToggleRoom(it)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
onRemoveRoom = ::onRoomRemoved,
|
||||
modifier = Modifier.padding(vertical = 16.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(AddRoomToSpaceEvent.ToggleRoom(it)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SaveActionView(
|
||||
saveAction = state.saveAction,
|
||||
onRetry = { state.eventSink(AddRoomToSpaceEvent.Save) },
|
||||
onDismiss = { state.eventSink(AddRoomToSpaceEvent.ResetSaveAction) }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SaveActionView(
|
||||
saveAction: AsyncAction<Unit>,
|
||||
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
|
||||
private fun SelectedRoomsRow(
|
||||
selectedRooms: ImmutableList<SelectRoomInfo>,
|
||||
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 = {},
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,13 @@
|
||||
<string name="screen_leave_space_subtitle_only_last_admin">"Du vil ikke blive fjernet fra følgende rum, fordi du er den eneste administrator:"</string>
|
||||
<string name="screen_leave_space_title">"Forlad %1$s?"</string>
|
||||
<string name="screen_leave_space_title_last_admin">"Du er den eneste administrator for %1$s"</string>
|
||||
<string name="screen_space_add_rooms_room_access_description">"Tilføjelse af et rum påvirker ikke adgangen til rummet. For at ændre adgangen, gå til Rumindstillinger > Sikkerhed og privatliv."</string>
|
||||
<string name="screen_space_menu_action_members">"Vis medlemmer"</string>
|
||||
<string name="screen_space_remove_rooms_confirmation_content">"Fjernelse af et rum påvirker ikke adgangen til rummet. For at ændre adgangen, gå til Rum-info > Privatliv og sikkerhed."</string>
|
||||
<plurals name="screen_space_remove_rooms_confirmation_title">
|
||||
<item quantity="one">"Fjern %1$d rum fra %2$s"</item>
|
||||
<item quantity="other">"Fjern %1$d rum fra %2$s"</item>
|
||||
</plurals>
|
||||
<string name="screen_space_settings_leave_space">"Forlad gruppe"</string>
|
||||
<string name="screen_space_settings_roles_and_permissions">"Roller og tilladelser"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Sikkerhed og privatliv"</string>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<string name="screen_leave_space_subtitle_only_last_admin">"Du wirst aus den folgenden Chats nicht entfernt, weil du der einzige Admin bist:"</string>
|
||||
<string name="screen_leave_space_title">"%1$s verlassen?"</string>
|
||||
<string name="screen_leave_space_title_last_admin">"Du bist der einzige Administrator für %1$s"</string>
|
||||
<string name="screen_space_add_rooms_room_access_description">"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\""</string>
|
||||
<string name="screen_space_menu_action_members">"Mitglieder anzeigen"</string>
|
||||
<string name="screen_space_remove_rooms_confirmation_content">"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\""</string>
|
||||
<plurals name="screen_space_remove_rooms_confirmation_title">
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<string name="screen_leave_space_subtitle_only_last_admin">"Vous ne quitterez pas le ou les salons suivants car vous y êtes le seul administrateur:"</string>
|
||||
<string name="screen_leave_space_title">"Quitter %1$s?"</string>
|
||||
<string name="screen_leave_space_title_last_admin">"Vous êtes le seul administrateur de %1$s"</string>
|
||||
<string name="screen_space_add_rooms_room_access_description">"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é."</string>
|
||||
<string name="screen_space_menu_action_members">"Voir les membres"</string>
|
||||
<string name="screen_space_remove_rooms_confirmation_content">"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é\"."</string>
|
||||
<plurals name="screen_space_remove_rooms_confirmation_title">
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<string name="screen_leave_space_subtitle_only_last_admin">"Вы не будете удалены из следующих комнат, поскольку вы являетесь единственным администратором:"</string>
|
||||
<string name="screen_leave_space_title">"Выйти из %1$s?"</string>
|
||||
<string name="screen_leave_space_title_last_admin">"Вы единственный администратор для %1$s"</string>
|
||||
<string name="screen_space_add_rooms_room_access_description">"Добавление комнаты не повлияет на доступ к ней. Чтобы изменить доступ к комнате, перейдите в Настройки > Безопасность и конфиденциальность."</string>
|
||||
<string name="screen_space_menu_action_members">"Просмотреть участников"</string>
|
||||
<string name="screen_space_remove_rooms_confirmation_content">"Удаление комнаты не повлияет на доступ к ней. Чтобы изменить доступ, перейдите в раздел «Информация о комнате > Конфиденциальность и безопасность."</string>
|
||||
<plurals name="screen_space_remove_rooms_confirmation_title">
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<string name="screen_leave_space_subtitle_only_last_admin">"You will not be removed from the following room(s) because you\'re the only administrator:"</string>
|
||||
<string name="screen_leave_space_title">"Leave %1$s?"</string>
|
||||
<string name="screen_leave_space_title_last_admin">"You are the only admin for %1$s"</string>
|
||||
<string name="screen_space_add_rooms_room_access_description">"Adding a room will not affect the room access. To change the access go to Room settings > Security & privacy."</string>
|
||||
<string name="screen_space_menu_action_members">"View members"</string>
|
||||
<string name="screen_space_remove_rooms_confirmation_content">"Removing a room will not affect the room access. To change the access go to Room info > Privacy & security."</string>
|
||||
<plurals name="screen_space_remove_rooms_confirmation_title">
|
||||
|
||||
@@ -0,0 +1,332 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@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.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.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.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 = createAddRoomToSpacePresenter()
|
||||
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 = createAddRoomToSpacePresenter()
|
||||
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 = createAddRoomToSpacePresenter()
|
||||
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 = createAddRoomToSpacePresenter()
|
||||
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 = createAddRoomToSpacePresenter()
|
||||
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 = createAddRoomToSpacePresenter()
|
||||
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 = createAddRoomToSpacePresenter()
|
||||
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 = createAddRoomToSpacePresenter(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 = createAddRoomToSpacePresenter()
|
||||
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<RoomId, RoomId, Result<Unit>> { _, _ ->
|
||||
Result.success(Unit)
|
||||
}
|
||||
val spaceService = FakeSpaceService(
|
||||
addChildToSpaceResult = addChildToSpaceResult,
|
||||
)
|
||||
val presenter = createAddRoomToSpacePresenter(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 = createAddRoomToSpacePresenter(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 = createAddRoomToSpacePresenter(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 = createAddRoomToSpacePresenter(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 = createAddRoomToSpacePresenter()
|
||||
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 = createAddRoomToSpacePresenter(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.createAddRoomToSpacePresenter(
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.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<ComponentActivity>()
|
||||
|
||||
@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<AddRoomToSpaceEvent>()
|
||||
rule.setAddRoomToSpaceView(
|
||||
anAddRoomToSpaceState(
|
||||
isSearchActive = true,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.pressBack()
|
||||
eventsRecorder.assertSingle(AddRoomToSpaceEvent.CloseSearch)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking save emits Save event`() {
|
||||
val eventsRecorder = EventsRecorder<AddRoomToSpaceEvent>()
|
||||
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<AddRoomToSpaceEvent>()
|
||||
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 <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setAddRoomToSpaceView(
|
||||
state: AddRoomToSpaceState,
|
||||
onBackClick: () -> Unit = EnsureNeverCalled(),
|
||||
onRoomsAdded: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
setContent {
|
||||
AddRoomToSpaceView(
|
||||
state = state,
|
||||
onBackClick = onBackClick,
|
||||
onRoomsAdded = onRoomsAdded,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -210,6 +210,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setSpace
|
||||
onLeaveSpaceClick: () -> Unit = EnsureNeverCalled(),
|
||||
onSettingsClick: () -> Unit = EnsureNeverCalled(),
|
||||
onViewMembersClick: () -> Unit = EnsureNeverCalled(),
|
||||
onAddRoomClick: () -> Unit = EnsureNeverCalled(),
|
||||
acceptDeclineInviteView: @Composable () -> Unit = {},
|
||||
) {
|
||||
setContent {
|
||||
@@ -221,6 +222,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setSpace
|
||||
onLeaveSpaceClick = onLeaveSpaceClick,
|
||||
onSettingsClick = onSettingsClick,
|
||||
onViewMembersClick = onViewMembersClick,
|
||||
onAddRoomClick = onAddRoomClick,
|
||||
acceptDeclineInviteView = acceptDeclineInviteView,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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.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,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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 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.getRecentlyVisitedRoomInfoFlow(
|
||||
predicate: (RoomInfo) -> Boolean,
|
||||
): Flow<RoomInfo> = flow {
|
||||
val recentlyVisitedRooms = getRecentlyVisitedRooms().getOrDefault(emptyList())
|
||||
for (roomId in recentlyVisitedRooms) {
|
||||
getRoom(roomId)?.use { room ->
|
||||
val info = room.info()
|
||||
if (predicate(info)) {
|
||||
emit(info)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Unit>
|
||||
|
||||
/**
|
||||
* Remove a child room from a space.
|
||||
* @param spaceId The space ID from which to remove the child.
|
||||
|
||||
@@ -98,6 +98,12 @@ class RustSpaceService(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun addChildToSpace(spaceId: RoomId, childId: RoomId): Result<Unit> = withContext(sessionDispatcher) {
|
||||
runCatchingExceptions {
|
||||
innerSpaceService.addChildToSpace(childId = childId.value, spaceId = spaceId.value)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun removeChildFromSpace(spaceId: RoomId, childId: RoomId): Result<Unit> = withContext(sessionDispatcher) {
|
||||
runCatchingExceptions {
|
||||
innerSpaceService.removeChildFromSpace(childId = childId.value, spaceId = spaceId.value)
|
||||
|
||||
@@ -23,6 +23,7 @@ class FakeSpaceService(
|
||||
private val joinedSpacesResult: () -> Result<List<SpaceRoom>> = { lambdaError() },
|
||||
private val spaceRoomListResult: (RoomId) -> SpaceRoomList = { lambdaError() },
|
||||
private val leaveSpaceHandleResult: (RoomId) -> LeaveSpaceHandle = { lambdaError() },
|
||||
private val addChildToSpaceResult: (RoomId, RoomId) -> Result<Unit> = { _, _ -> lambdaError() },
|
||||
private val removeChildFromSpaceResult: (RoomId, RoomId) -> Result<Unit> = { _, _ -> lambdaError() },
|
||||
private val joinedParentsResult: (RoomId) -> Result<List<SpaceRoom>> = { 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<Unit> = simulateLongTask {
|
||||
addChildToSpaceResult(spaceId, childId)
|
||||
}
|
||||
|
||||
override suspend fun removeChildFromSpace(spaceId: RoomId, childId: RoomId): Result<Unit> = simulateLongTask {
|
||||
removeChildFromSpaceResult(spaceId, childId)
|
||||
}
|
||||
|
||||
@@ -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,13 @@ 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,
|
||||
)
|
||||
|
||||
@@ -477,7 +477,6 @@ Möchtest du wirklich fortfahren?"</string>
|
||||
<string name="screen_share_open_google_maps">"In Google Maps öffnen"</string>
|
||||
<string name="screen_share_open_osm_maps">"In OpenStreetMap öffnen"</string>
|
||||
<string name="screen_share_this_location_action">"Diesen Standort teilen"</string>
|
||||
<string name="screen_space_add_rooms_room_access_description">"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\""</string>
|
||||
<string name="screen_space_list_description">"Von dir erstellte oder beigetretene Spaces."</string>
|
||||
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
||||
<string name="screen_space_list_empty_state_title">"Erstelle einen Space, um Chats zu organisieren"</string>
|
||||
|
||||
@@ -483,7 +483,6 @@ Raison : %1$s."</string>
|
||||
<string name="screen_share_open_google_maps">"Ouvrir dans Google Maps"</string>
|
||||
<string name="screen_share_open_osm_maps">"Ouvrir dans OpenStreetMap"</string>
|
||||
<string name="screen_share_this_location_action">"Partager cette position"</string>
|
||||
<string name="screen_space_add_rooms_room_access_description">"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é."</string>
|
||||
<string name="screen_space_list_description">"Espaces que vous avez créés ou rejoints."</string>
|
||||
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
||||
<string name="screen_space_list_empty_state_title">"Créer des espaces pour organiser les salons"</string>
|
||||
|
||||
@@ -492,7 +492,6 @@
|
||||
<string name="screen_share_open_google_maps">"Открыть в Google Maps"</string>
|
||||
<string name="screen_share_open_osm_maps">"Открыть в OpenStreetMap"</string>
|
||||
<string name="screen_share_this_location_action">"Поделиться этим местоположением"</string>
|
||||
<string name="screen_space_add_rooms_room_access_description">"Добавление комнаты не повлияет на доступ к ней. Чтобы изменить доступ к комнате, перейдите в Настройки > Безопасность и конфиденциальность."</string>
|
||||
<string name="screen_space_list_description">"Пространства, которые вы создали или к которым присоединились."</string>
|
||||
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
||||
<string name="screen_space_list_empty_state_title">"Создавайте пространства для организации комнат"</string>
|
||||
|
||||
@@ -484,7 +484,6 @@ Are you sure you want to continue?"</string>
|
||||
<string name="screen_share_open_google_maps">"Open in Google Maps"</string>
|
||||
<string name="screen_share_open_osm_maps">"Open in OpenStreetMap"</string>
|
||||
<string name="screen_share_this_location_action">"Share this location"</string>
|
||||
<string name="screen_space_add_rooms_room_access_description">"Adding a room will not affect the room access. To change the access go to Room settings > Security & privacy."</string>
|
||||
<string name="screen_space_list_description">"Spaces you have created or joined."</string>
|
||||
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
||||
<string name="screen_space_list_empty_state_title">"Create spaces to organize rooms"</string>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -234,7 +234,8 @@
|
||||
"includeRegex" : [
|
||||
"screen\\.leave_space\\..*",
|
||||
"screen\\.space_settings\\..*",
|
||||
"screen\\.space\\..*"
|
||||
"screen\\.space\\..*",
|
||||
"screen\\.space_add_rooms\\..*"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user