Merge pull request #6063 from element-hq/feature/fga/space_add_existing_room
Implement Space 'Add existing rooms' feature
This commit is contained in:
commit
3f624c601c
43 changed files with 1362 additions and 12 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:72074f44d83f1772e83de96fd86265a43cdd2123023ac65bee8ddf7abf4af37e
|
||||
size 38249
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2a295c4f8c9e784861a3db01597be406615a4adb6c4d8997478336e3a3f30003
|
||||
size 11682
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d7c006740c4fd75d7efbd5abe59804d8d5fabbfd7f11b6d14c0b00fcb83850be
|
||||
size 21434
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:841cfff6c0c0219e83e6ee7ca03060e143ddff72f7946e578498ffb6918fa37d
|
||||
size 14200
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:83d9e5eb832d1f52b17e35e18d1c834120a7b510c2a3cc1647dc35dfeeb8b0a9
|
||||
size 42762
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5d5fd28a7971e2f25fdf6d3e1b0af9a1d279369d2f1d656ff72a36a1badfd774
|
||||
size 29089
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7d191722630f4eede4606f63053d29996ae32c728af85e198a390be82b84d274
|
||||
size 40323
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:dd5f99d986f37e962322060add381b334c50a6fbc05a23fa09319b37cc86a090
|
||||
size 38078
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e011e95c98c9694980b031207b82c6feb0b9b51b06b233672bee2ee3ceb5b18c
|
||||
size 11382
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:71bc3b3b49e37690e92fe23ab3c48cc286341e64c427471b7c940b65243bd998
|
||||
size 21776
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:76ab4de8587bac55872b2807b342fdbdf72f771aed65ee342705f49a89c1f196
|
||||
size 13865
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:50898da4de06be0d2430d4635b7c6880701d60fb78940bb2135c943e2d9634b5
|
||||
size 42908
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8c4da20d886e0f2fadd57edaea13c2bae4d19fcf4c8a397a2dec92f68e14aad7
|
||||
size 27733
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2ceeea93e8cd825cf82971abe76c5a0d28e0522ffa4694db588c87f9473cc117
|
||||
size 37422
|
||||
|
|
@ -234,7 +234,8 @@
|
|||
"includeRegex" : [
|
||||
"screen\\.leave_space\\..*",
|
||||
"screen\\.space_settings\\..*",
|
||||
"screen\\.space\\..*"
|
||||
"screen\\.space\\..*",
|
||||
"screen\\.space_add_rooms\\..*"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue