Implement Space "Add existing rooms" logic and ui
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.space.impl.addroom
|
||||
|
||||
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
|
||||
|
||||
sealed interface AddRoomToSpaceEvents {
|
||||
data class ToggleRoom(val room: SelectRoomInfo) : AddRoomToSpaceEvents
|
||||
data class UpdateSearchQuery(val query: String) : AddRoomToSpaceEvents
|
||||
data class OnSearchActiveChanged(val active: Boolean) : AddRoomToSpaceEvents
|
||||
data object Save : AddRoomToSpaceEvents
|
||||
data object CloseSearch : AddRoomToSpaceEvents
|
||||
data object ClearError : AddRoomToSpaceEvents
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import io.element.android.libraries.architecture.callback
|
||||
class AddRoomToSpaceNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: AddRoomToSpacePresenter,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
interface Callback : Plugin {
|
||||
fun onFinish()
|
||||
@@ -33,6 +34,12 @@ class AddRoomToSpaceNode(
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
|
||||
val state = presenter.present()
|
||||
AddRoomToSpaceView(
|
||||
state = state,
|
||||
onBackClick = ::navigateUp,
|
||||
onRoomsAdded = callback::onFinish,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.space.impl.addroom
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runUpdatingState
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||
import io.element.android.libraries.matrix.api.room.isDm
|
||||
import io.element.android.libraries.matrix.api.room.recent.getRecentRooms
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceService
|
||||
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
|
||||
import io.element.android.libraries.matrix.ui.model.toSelectRoomInfo
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.take
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private const val MAX_SUGGESTIONS_COUNT = 5
|
||||
|
||||
@Inject
|
||||
class AddRoomToSpacePresenter(
|
||||
private val spaceRoomList: SpaceRoomList,
|
||||
private val dataSource: AddRoomToSpaceSearchDataSource,
|
||||
private val spaceService: SpaceService,
|
||||
private val matrixClient: MatrixClient,
|
||||
@SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
|
||||
) : Presenter<AddRoomToSpaceState> {
|
||||
|
||||
@Composable
|
||||
override fun present(): AddRoomToSpaceState {
|
||||
var selectedRooms by remember { mutableStateOf(persistentListOf<SelectRoomInfo>()) }
|
||||
var searchQuery by remember { mutableStateOf("") }
|
||||
var isSearchActive by remember { mutableStateOf(false) }
|
||||
val saveAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||
|
||||
// Load data source
|
||||
LaunchedEffect(Unit) { dataSource.load() }
|
||||
|
||||
// Update search query in data source
|
||||
LaunchedEffect(searchQuery) {
|
||||
dataSource.setSearchQuery(searchQuery)
|
||||
}
|
||||
|
||||
// Get rooms already in space
|
||||
val spaceChildrenIds by remember {
|
||||
spaceRoomList.spaceRoomsFlow.map { spaceChildren ->
|
||||
spaceChildren.map { it.roomId }
|
||||
}
|
||||
}.collectAsState(initial = emptyList())
|
||||
|
||||
// Suggestions from recently visited rooms (excluding DMs, spaces, and rooms already in space)
|
||||
val suggestions by produceState(persistentListOf(), spaceChildrenIds) {
|
||||
value = matrixClient
|
||||
.getRecentRooms { info ->
|
||||
!info.isSpace && !info.isDm && info.currentUserMembership == CurrentUserMembership.JOINED
|
||||
}
|
||||
.take(MAX_SUGGESTIONS_COUNT)
|
||||
.map { info -> info.toSelectRoomInfo() }
|
||||
.toList()
|
||||
.toImmutableList()
|
||||
}
|
||||
|
||||
val allRooms by dataSource.roomInfoList.collectAsState(initial = persistentListOf())
|
||||
val searchResults by remember<State<SearchBarResultState<ImmutableList<SelectRoomInfo>>>> {
|
||||
derivedStateOf {
|
||||
val filtered = allRooms.filterNot { it.roomId in spaceChildrenIds }
|
||||
when {
|
||||
filtered.isNotEmpty() -> SearchBarResultState.Results(filtered.toImmutableList())
|
||||
isSearchActive && searchQuery.isNotEmpty() -> SearchBarResultState.NoResultsFound()
|
||||
else -> SearchBarResultState.Initial()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleEvent(event: AddRoomToSpaceEvents) {
|
||||
when (event) {
|
||||
is AddRoomToSpaceEvents.ToggleRoom -> {
|
||||
selectedRooms = if (selectedRooms.any { it.roomId == event.room.roomId }) {
|
||||
selectedRooms.filterNot { it.roomId == event.room.roomId }.toPersistentList()
|
||||
} else {
|
||||
(selectedRooms + event.room).toPersistentList()
|
||||
}
|
||||
}
|
||||
is AddRoomToSpaceEvents.UpdateSearchQuery -> {
|
||||
searchQuery = event.query
|
||||
}
|
||||
is AddRoomToSpaceEvents.OnSearchActiveChanged -> {
|
||||
isSearchActive = event.active
|
||||
if (!event.active) {
|
||||
searchQuery = ""
|
||||
}
|
||||
}
|
||||
AddRoomToSpaceEvents.CloseSearch -> {
|
||||
isSearchActive = false
|
||||
searchQuery = ""
|
||||
}
|
||||
AddRoomToSpaceEvents.Save -> {
|
||||
sessionCoroutineScope.addRoomsToSpace(
|
||||
selectedRooms = selectedRooms,
|
||||
addAction = saveAction,
|
||||
)
|
||||
}
|
||||
AddRoomToSpaceEvents.ClearError -> {
|
||||
saveAction.value = AsyncAction.Uninitialized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return AddRoomToSpaceState(
|
||||
searchQuery = searchQuery,
|
||||
isSearchActive = isSearchActive,
|
||||
searchResults = searchResults,
|
||||
selectedRooms = selectedRooms,
|
||||
suggestions = suggestions,
|
||||
saveAction = saveAction.value,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.addRoomsToSpace(
|
||||
selectedRooms: ImmutableList<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,71 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.space.impl.addroom
|
||||
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||
import io.element.android.libraries.matrix.api.room.isDm
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomList
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||
import io.element.android.libraries.matrix.api.roomlist.loadAllIncrementally
|
||||
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
|
||||
import io.element.android.libraries.matrix.ui.model.toSelectRoomInfo
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
private const val PAGE_SIZE = 30
|
||||
|
||||
/**
|
||||
* DataSource for rooms that can be added to a space.
|
||||
* Filters out DMs, spaces, and only includes rooms the user has joined.
|
||||
*/
|
||||
@Inject
|
||||
class AddRoomToSpaceSearchDataSource(
|
||||
roomListService: RoomListService,
|
||||
coroutineDispatchers: CoroutineDispatchers,
|
||||
) {
|
||||
private val roomList = roomListService.createRoomList(
|
||||
pageSize = PAGE_SIZE,
|
||||
initialFilter = RoomListFilter.all(),
|
||||
source = RoomList.Source.All,
|
||||
)
|
||||
|
||||
val roomInfoList: Flow<ImmutableList<SelectRoomInfo>> = roomList.filteredSummaries
|
||||
.map { roomSummaries ->
|
||||
roomSummaries
|
||||
.filter {
|
||||
it.info.currentUserMembership == CurrentUserMembership.JOINED &&
|
||||
!it.info.isDm &&
|
||||
!it.info.isSpace
|
||||
}
|
||||
.distinctBy { it.roomId }
|
||||
.map { roomSummary -> roomSummary.toSelectRoomInfo() }
|
||||
.toImmutableList()
|
||||
}
|
||||
.flowOn(coroutineDispatchers.computation)
|
||||
|
||||
suspend fun load() = coroutineScope {
|
||||
roomList.loadAllIncrementally(this)
|
||||
}
|
||||
|
||||
suspend fun setSearchQuery(searchQuery: String) {
|
||||
val filter = if (searchQuery.isBlank()) {
|
||||
RoomListFilter.None
|
||||
} else {
|
||||
RoomListFilter.NormalizedMatchRoomName(searchQuery)
|
||||
}
|
||||
roomList.updateFilter(filter)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.space.impl.addroom
|
||||
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class AddRoomToSpaceState(
|
||||
val searchQuery: String,
|
||||
val isSearchActive: Boolean,
|
||||
val searchResults: SearchBarResultState<ImmutableList<SelectRoomInfo>>,
|
||||
val selectedRooms: ImmutableList<SelectRoomInfo>,
|
||||
val suggestions: ImmutableList<SelectRoomInfo>,
|
||||
val saveAction: AsyncAction<Unit>,
|
||||
val eventSink: (AddRoomToSpaceEvents) -> Unit,
|
||||
) {
|
||||
val canSave: Boolean = selectedRooms.isNotEmpty() && !saveAction.isLoading()
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.space.impl.addroom
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
internal class AddRoomToSpaceStateProvider : PreviewParameterProvider<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")),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private 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(),
|
||||
): AddRoomToSpaceState {
|
||||
return AddRoomToSpaceState(
|
||||
searchQuery = searchQuery,
|
||||
searchResults = searchResults,
|
||||
selectedRooms = selectedRooms,
|
||||
isSearchActive = isSearchActive,
|
||||
saveAction = saveAction,
|
||||
suggestions = suggestions,
|
||||
eventSink = {},
|
||||
)
|
||||
}
|
||||
|
||||
private 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,228 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.space.impl.addroom
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarType
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||
import io.element.android.libraries.designsystem.components.list.AvatarListItem
|
||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.ListSectionHeader
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBar
|
||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.matrix.ui.components.SelectedRoom
|
||||
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
|
||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AddRoomToSpaceView(
|
||||
state: AddRoomToSpaceState,
|
||||
onBackClick: () -> Unit,
|
||||
onRoomsAdded: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
fun onRoomRemoved(roomInfo: SelectRoomInfo) {
|
||||
state.eventSink(AddRoomToSpaceEvents.ToggleRoom(roomInfo))
|
||||
}
|
||||
|
||||
fun onBack() {
|
||||
if (state.isSearchActive) {
|
||||
state.eventSink(AddRoomToSpaceEvents.CloseSearch)
|
||||
} else {
|
||||
onBackClick()
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler(onBack = ::onBack)
|
||||
|
||||
// Navigate back on success
|
||||
LaunchedEffect(state.saveAction) {
|
||||
if (state.saveAction is AsyncAction.Success) {
|
||||
onRoomsAdded()
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
titleStr = stringResource(CommonStrings.action_add_existing_rooms),
|
||||
navigationIcon = {
|
||||
BackButton(onClick = ::onBack)
|
||||
},
|
||||
actions = {
|
||||
TextButton(
|
||||
text = stringResource(CommonStrings.action_save),
|
||||
enabled = state.canSave,
|
||||
onClick = { state.eventSink(AddRoomToSpaceEvents.Save) }
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
Modifier
|
||||
.padding(paddingValues)
|
||||
.consumeWindowInsets(paddingValues)
|
||||
) {
|
||||
SearchBar(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeHolderTitle = stringResource(CommonStrings.action_search),
|
||||
query = state.searchQuery,
|
||||
onQueryChange = { state.eventSink(AddRoomToSpaceEvents.UpdateSearchQuery(it)) },
|
||||
active = state.isSearchActive,
|
||||
onActiveChange = { state.eventSink(AddRoomToSpaceEvents.OnSearchActiveChanged(it)) },
|
||||
resultState = state.searchResults,
|
||||
showBackButton = false,
|
||||
contentPrefix = {
|
||||
if (state.selectedRooms.isNotEmpty()) {
|
||||
SelectedRoomsRow(
|
||||
selectedRooms = state.selectedRooms,
|
||||
onRemoveRoom = ::onRoomRemoved,
|
||||
modifier = Modifier.padding(vertical = 16.dp)
|
||||
)
|
||||
}
|
||||
},
|
||||
) { rooms ->
|
||||
LazyColumn {
|
||||
items(rooms, key = { it.roomId.value }) { roomInfo ->
|
||||
RoomListItem(
|
||||
roomInfo = roomInfo,
|
||||
isSelected = state.selectedRooms.any { it.roomId == roomInfo.roomId },
|
||||
onToggle = { state.eventSink(AddRoomToSpaceEvents.ToggleRoom(it)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!state.isSearchActive) {
|
||||
if (state.selectedRooms.isNotEmpty()) {
|
||||
SelectedRoomsRow(
|
||||
selectedRooms = state.selectedRooms,
|
||||
onRemoveRoom = ::onRoomRemoved,
|
||||
modifier = Modifier.padding(vertical = 16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
if (state.suggestions.isNotEmpty()) {
|
||||
LazyColumn {
|
||||
item {
|
||||
ListSectionHeader(
|
||||
title = stringResource(id = CommonStrings.common_suggestions),
|
||||
hasDivider = true,
|
||||
)
|
||||
}
|
||||
items(state.suggestions, key = { it.roomId.value }) { roomInfo ->
|
||||
RoomListItem(
|
||||
roomInfo = roomInfo,
|
||||
isSelected = state.selectedRooms.any { it.roomId == roomInfo.roomId },
|
||||
onToggle = { state.eventSink(AddRoomToSpaceEvents.ToggleRoom(it)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Loading dialog
|
||||
if (state.saveAction.isLoading()) {
|
||||
ProgressDialog()
|
||||
}
|
||||
|
||||
// Error dialog
|
||||
if (state.saveAction.isFailure()) {
|
||||
ErrorDialog(
|
||||
content = stringResource(CommonStrings.error_unknown),
|
||||
onSubmit = { state.eventSink(AddRoomToSpaceEvents.ClearError) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SelectedRoomsRow(
|
||||
selectedRooms: ImmutableList<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 = {},
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user