Room list contextual menu (#427)
- Adds `ModalBottomSheet` to our design components (it wraps the homonimous Material3 one). - Adds a bottom sheet to the Room list using the aforementioned design component. - Adds navigation from the room list to a room detail (context menu "Settings" action). - Consolidates the "leave room flow" into a new `leaveroom` module used by both the room list and the room details. - Adds progress indicator to the leave room flow - Uses new `leaveroom` module in `roomdetails` module too. Parent issue: - https://github.com/vector-im/element-x-android/issues/261
This commit is contained in:
parent
897540ed04
commit
0dee0784ba
60 changed files with 1462 additions and 250 deletions
|
|
@ -0,0 +1,148 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomlist.impl
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.VectorIcons
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.ElementTheme
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun RoomListContextMenu(
|
||||
contextMenu: RoomListState.ContextMenu.Shown,
|
||||
eventSink: (RoomListEvents) -> Unit,
|
||||
onRoomSettingsClicked: (roomId: RoomId) -> Unit,
|
||||
) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { eventSink(RoomListEvents.HideContextMenu) },
|
||||
) {
|
||||
RoomListModalBottomSheetContent(
|
||||
contextMenu = contextMenu,
|
||||
onRoomSettingsClicked = {
|
||||
eventSink(RoomListEvents.HideContextMenu)
|
||||
onRoomSettingsClicked(it)
|
||||
},
|
||||
onLeaveRoomClicked = {
|
||||
eventSink(RoomListEvents.HideContextMenu)
|
||||
eventSink(RoomListEvents.LeaveRoom(contextMenu.roomId))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RoomListModalBottomSheetContent(
|
||||
contextMenu: RoomListState.ContextMenu.Shown,
|
||||
onRoomSettingsClicked: (roomId: RoomId) -> Unit,
|
||||
onLeaveRoomClicked: (roomId: RoomId) -> Unit,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = contextMenu.roomName,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
}
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(text = stringResource(id = StringR.string.common_settings))
|
||||
},
|
||||
modifier = Modifier.clickable { onRoomSettingsClicked(contextMenu.roomId) },
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Settings,
|
||||
contentDescription = stringResource(id = StringR.string.common_settings),
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = stringResource(id = StringR.string.action_leave_room),
|
||||
color = ElementTheme.colors.textActionCritical,
|
||||
)
|
||||
},
|
||||
modifier = Modifier.clickable { onLeaveRoomClicked(contextMenu.roomId) },
|
||||
leadingContent = {
|
||||
Icon(
|
||||
resourceId = VectorIcons.DoorOpen,
|
||||
contentDescription = stringResource(id = StringR.string.action_leave_room),
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = ElementTheme.colors.textActionCritical,
|
||||
)
|
||||
}
|
||||
)
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
}
|
||||
}
|
||||
|
||||
// TODO This component should be seen in [RoomListView] @Preview but it doesn't show up.
|
||||
// see: https://issuetracker.google.com/issues/283843380
|
||||
// Remove this preview when the issue is fixed.
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun RoomListModalBottomSheetContentLightPreview() =
|
||||
ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
// TODO This component should be seen in [RoomListView] @Preview but it doesn't show up.
|
||||
// see: https://issuetracker.google.com/issues/283843380
|
||||
// Remove this preview when the issue is fixed.
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun RoomListModalBottomSheetContentDarkPreview() =
|
||||
ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
RoomListModalBottomSheetContent(
|
||||
contextMenu = RoomListState.ContextMenu.Shown(
|
||||
roomId = RoomId(value = "!aRoom:aDomain"),
|
||||
roomName = "aRoom"
|
||||
),
|
||||
onRoomSettingsClicked = {},
|
||||
onLeaveRoomClicked = {}
|
||||
)
|
||||
}
|
||||
|
|
@ -16,9 +16,15 @@
|
|||
|
||||
package io.element.android.features.roomlist.impl
|
||||
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
sealed interface RoomListEvents {
|
||||
data class UpdateFilter(val newFilter: String) : RoomListEvents
|
||||
data class UpdateVisibleRange(val range: IntRange) : RoomListEvents
|
||||
object DismissRequestVerificationPrompt : RoomListEvents
|
||||
object ToggleSearchResults : RoomListEvents
|
||||
data class ShowContextMenu(val roomListRoomSummary: RoomListRoomSummary) : RoomListEvents
|
||||
object HideContextMenu : RoomListEvents
|
||||
data class LeaveRoom(val roomId: RoomId) : RoomListEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,17 +56,22 @@ class RoomListNode @AssistedInject constructor(
|
|||
plugins<RoomListEntryPoint.Callback>().forEach { it.onInvitesClicked() }
|
||||
}
|
||||
|
||||
private fun onRoomSettingsClicked(roomId: RoomId) {
|
||||
plugins<RoomListEntryPoint.Callback>().forEach { it.onRoomSettingsClicked(roomId) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
RoomListView(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
onRoomClicked = this::onRoomClicked,
|
||||
onOpenSettings = this::onOpenSettings,
|
||||
onSettingsClicked = this::onOpenSettings,
|
||||
onCreateRoomClicked = this::onCreateRoomClicked,
|
||||
onVerifyClicked = this::onSessionVerificationClicked,
|
||||
onInvitesClicked = this::onInvitesClicked,
|
||||
onRoomSettingsClicked = this::onRoomSettingsClicked,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||
import androidx.compose.runtime.setValue
|
||||
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomPresenter
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummaryPlaceholders
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
|
|
@ -62,10 +64,12 @@ class RoomListPresenter @Inject constructor(
|
|||
private val networkMonitor: NetworkMonitor,
|
||||
private val snackbarDispatcher: SnackbarDispatcher,
|
||||
private val inviteStateDataSource: InviteStateDataSource,
|
||||
private val leaveRoomPresenter: LeaveRoomPresenter,
|
||||
) : Presenter<RoomListState> {
|
||||
|
||||
@Composable
|
||||
override fun present(): RoomListState {
|
||||
val leaveRoomState = leaveRoomPresenter.present()
|
||||
val matrixUser: MutableState<MatrixUser?> = rememberSaveable {
|
||||
mutableStateOf(null)
|
||||
}
|
||||
|
|
@ -97,6 +101,8 @@ class RoomListPresenter @Inject constructor(
|
|||
|
||||
var displaySearchResults by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
var contextMenu by remember { mutableStateOf<RoomListState.ContextMenu>(RoomListState.ContextMenu.Hidden) }
|
||||
|
||||
fun handleEvents(event: RoomListEvents) {
|
||||
when (event) {
|
||||
is RoomListEvents.UpdateFilter -> filter = event.newFilter
|
||||
|
|
@ -108,6 +114,14 @@ class RoomListPresenter @Inject constructor(
|
|||
}
|
||||
displaySearchResults = !displaySearchResults
|
||||
}
|
||||
is RoomListEvents.ShowContextMenu -> {
|
||||
contextMenu = RoomListState.ContextMenu.Shown(
|
||||
roomId = event.roomListRoomSummary.roomId,
|
||||
roomName = event.roomListRoomSummary.name
|
||||
)
|
||||
}
|
||||
is RoomListEvents.HideContextMenu -> contextMenu = RoomListState.ContextMenu.Hidden
|
||||
is RoomListEvents.LeaveRoom -> leaveRoomState.eventSink(LeaveRoomEvent.ShowConfirmation(event.roomId))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -132,6 +146,8 @@ class RoomListPresenter @Inject constructor(
|
|||
hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online,
|
||||
invitesState = inviteStateDataSource.inviteState(),
|
||||
displaySearchResults = displaySearchResults,
|
||||
contextMenu = contextMenu,
|
||||
leaveRoomState = leaveRoomState,
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,8 +17,10 @@
|
|||
package io.element.android.features.roomlist.impl
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||
import io.element.android.libraries.designsystem.utils.SnackbarMessage
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
|
|
@ -33,8 +35,18 @@ data class RoomListState(
|
|||
val snackbarMessage: SnackbarMessage?,
|
||||
val invitesState: InvitesState,
|
||||
val displaySearchResults: Boolean,
|
||||
val eventSink: (RoomListEvents) -> Unit
|
||||
)
|
||||
val contextMenu: ContextMenu,
|
||||
val leaveRoomState: LeaveRoomState,
|
||||
val eventSink: (RoomListEvents) -> Unit,
|
||||
) {
|
||||
sealed interface ContextMenu {
|
||||
object Hidden : ContextMenu
|
||||
data class Shown(
|
||||
val roomId: RoomId,
|
||||
val roomName: String,
|
||||
) : ContextMenu
|
||||
}
|
||||
}
|
||||
|
||||
enum class InvitesState {
|
||||
NoInvites,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
package io.element.android.features.roomlist.impl
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummaryPlaceholders
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
|
|
@ -39,6 +40,9 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
|
|||
aRoomListState().copy(invitesState = InvitesState.NewInvites),
|
||||
aRoomListState().copy(displaySearchResults = true, filter = "", filteredRoomList = persistentListOf()),
|
||||
aRoomListState().copy(displaySearchResults = true),
|
||||
aRoomListState().copy(contextMenu = RoomListState.ContextMenu.Shown(
|
||||
roomId = RoomId("!aRoom:aDomain"), roomName = "A nice room name"
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -52,6 +56,8 @@ internal fun aRoomListState() = RoomListState(
|
|||
displayVerificationPrompt = false,
|
||||
invitesState = InvitesState.NoInvites,
|
||||
displaySearchResults = false,
|
||||
contextMenu = RoomListState.ContextMenu.Hidden,
|
||||
leaveRoomState = LeaveRoomState(),
|
||||
eventSink = {}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
|
||||
package io.element.android.features.roomlist.impl
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
|
|
@ -62,6 +61,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomView
|
||||
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView
|
||||
import io.element.android.features.roomlist.impl.components.RoomListTopBar
|
||||
import io.element.android.features.roomlist.impl.components.RoomSummaryRow
|
||||
|
|
@ -88,20 +88,38 @@ import io.element.android.libraries.ui.strings.R as StringR
|
|||
@Composable
|
||||
fun RoomListView(
|
||||
state: RoomListState,
|
||||
onRoomClicked: (RoomId) -> Unit,
|
||||
onSettingsClicked: () -> Unit,
|
||||
onVerifyClicked: () -> Unit,
|
||||
onCreateRoomClicked: () -> Unit,
|
||||
onInvitesClicked: () -> Unit,
|
||||
onRoomSettingsClicked: (roomId: RoomId) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
onRoomClicked: (RoomId) -> Unit = {},
|
||||
onOpenSettings: () -> Unit = {},
|
||||
onVerifyClicked: () -> Unit = {},
|
||||
onCreateRoomClicked: () -> Unit = {},
|
||||
onInvitesClicked: () -> Unit = {},
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
ConnectivityIndicatorView(isOnline = state.hasNetworkConnection)
|
||||
Box {
|
||||
fun onRoomLongClicked(
|
||||
roomListRoomSummary: RoomListRoomSummary
|
||||
) {
|
||||
state.eventSink(RoomListEvents.ShowContextMenu(roomListRoomSummary))
|
||||
}
|
||||
|
||||
if (state.contextMenu is RoomListState.ContextMenu.Shown) {
|
||||
RoomListContextMenu(
|
||||
contextMenu = state.contextMenu,
|
||||
eventSink = state.eventSink,
|
||||
onRoomSettingsClicked = onRoomSettingsClicked,
|
||||
)
|
||||
}
|
||||
|
||||
LeaveRoomView(state = state.leaveRoomState)
|
||||
|
||||
RoomListContent(
|
||||
state = state,
|
||||
onRoomClicked = onRoomClicked,
|
||||
onOpenSettings = onOpenSettings,
|
||||
onRoomLongClicked = { onRoomLongClicked(it) },
|
||||
onOpenSettings = onSettingsClicked,
|
||||
onVerifyClicked = onVerifyClicked,
|
||||
onCreateRoomClicked = onCreateRoomClicked,
|
||||
onInvitesClicked = onInvitesClicked,
|
||||
|
|
@ -110,6 +128,7 @@ fun RoomListView(
|
|||
RoomListSearchResultView(
|
||||
state = state,
|
||||
onRoomClicked = onRoomClicked,
|
||||
onRoomLongClicked = { onRoomLongClicked(it) },
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
|
|
@ -125,12 +144,12 @@ fun RoomListContent(
|
|||
modifier: Modifier = Modifier,
|
||||
onVerifyClicked: () -> Unit = {},
|
||||
onRoomClicked: (RoomId) -> Unit = {},
|
||||
onRoomLongClicked: (RoomListRoomSummary) -> Unit = {},
|
||||
onOpenSettings: () -> Unit = {},
|
||||
onCreateRoomClicked: () -> Unit = {},
|
||||
onInvitesClicked: () -> Unit = {},
|
||||
) {
|
||||
fun onRoomClicked(room: RoomListRoomSummary) {
|
||||
if (room.roomId == null) return
|
||||
onRoomClicked(room.roomId)
|
||||
}
|
||||
|
||||
|
|
@ -237,7 +256,11 @@ fun RoomListContent(
|
|||
items = state.roomList,
|
||||
contentType = { room -> room.contentType() },
|
||||
) { room ->
|
||||
RoomSummaryRow(room = room, onClick = ::onRoomClicked)
|
||||
RoomSummaryRow(
|
||||
room = room,
|
||||
onClick = ::onRoomClicked,
|
||||
onLongClick = onRoomLongClicked,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -339,13 +362,25 @@ internal fun RoomListViewDarkPreview(@PreviewParameter(RoomListStateProvider::cl
|
|||
|
||||
@Composable
|
||||
private fun ContentToPreview(state: RoomListState) {
|
||||
RoomListView(state)
|
||||
RoomListView(
|
||||
state = state,
|
||||
onRoomClicked = {},
|
||||
onSettingsClicked = {},
|
||||
onVerifyClicked = {},
|
||||
onCreateRoomClicked = {},
|
||||
onInvitesClicked = {},
|
||||
onRoomSettingsClicked = {}
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun RoomListSearchResultContentPreview() {
|
||||
ElementPreviewLight {
|
||||
RoomListSearchResultContent(state = aRoomListState(), onRoomClicked = {})
|
||||
RoomListSearchResultContent(
|
||||
state = aRoomListState(),
|
||||
onRoomClicked = {},
|
||||
onRoomLongClicked = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,8 +16,9 @@
|
|||
|
||||
package io.element.android.features.roomlist.impl.components
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -70,17 +71,20 @@ import io.element.android.libraries.designsystem.theme.roomListUnreadIndicator
|
|||
|
||||
private val minHeight = 72.dp
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
internal fun RoomSummaryRow(
|
||||
room: RoomListRoomSummary,
|
||||
onClick: (RoomListRoomSummary) -> Unit,
|
||||
onLongClick: (RoomListRoomSummary) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: (RoomListRoomSummary) -> Unit = {},
|
||||
) {
|
||||
val clickModifier = if (room.isPlaceholder) {
|
||||
modifier
|
||||
} else {
|
||||
modifier.clickable(
|
||||
modifier.combinedClickable(
|
||||
onClick = { onClick(room) },
|
||||
onLongClick = { onLongClick(room) },
|
||||
indication = rememberRipple(),
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
)
|
||||
|
|
@ -214,5 +218,9 @@ internal fun RoomSummaryRowDarkPreview(@PreviewParameter(RoomListRoomSummaryProv
|
|||
|
||||
@Composable
|
||||
private fun ContentToPreview(data: RoomListRoomSummary) {
|
||||
RoomSummaryRow(data)
|
||||
RoomSummaryRow(
|
||||
room = data,
|
||||
onClick = {},
|
||||
onLongClick = {}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
|||
@Immutable
|
||||
data class RoomListRoomSummary(
|
||||
val id: String,
|
||||
val roomId: RoomId?,
|
||||
val roomId: RoomId,
|
||||
val name: String = "",
|
||||
val hasUnread: Boolean = false,
|
||||
val timestamp: String? = null,
|
||||
|
|
|
|||
|
|
@ -17,13 +17,14 @@
|
|||
package io.element.android.features.roomlist.impl.model
|
||||
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
object RoomListRoomSummaryPlaceholders {
|
||||
|
||||
fun create(id: String): RoomListRoomSummary {
|
||||
return RoomListRoomSummary(
|
||||
id = id,
|
||||
roomId = null,
|
||||
roomId = RoomId("!aRoom:domain"),
|
||||
isPlaceholder = true,
|
||||
name = "Short name",
|
||||
timestamp = "hh:mm",
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ import io.element.android.libraries.ui.strings.R
|
|||
internal fun RoomListSearchResultView(
|
||||
state: RoomListState,
|
||||
onRoomClicked: (RoomId) -> Unit,
|
||||
onRoomLongClicked: (RoomListRoomSummary) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
|
|
@ -85,7 +86,11 @@ internal fun RoomListSearchResultView(
|
|||
})
|
||||
) {
|
||||
if (state.displaySearchResults) {
|
||||
RoomListSearchResultContent(state = state, onRoomClicked = onRoomClicked)
|
||||
RoomListSearchResultContent(
|
||||
state = state,
|
||||
onRoomClicked = onRoomClicked,
|
||||
onRoomLongClicked = onRoomLongClicked,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -96,6 +101,7 @@ internal fun RoomListSearchResultView(
|
|||
internal fun RoomListSearchResultContent(
|
||||
state: RoomListState,
|
||||
onRoomClicked: (RoomId) -> Unit,
|
||||
onRoomLongClicked: (RoomListRoomSummary) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val borderColor = MaterialTheme.colorScheme.tertiary
|
||||
|
|
@ -104,7 +110,6 @@ internal fun RoomListSearchResultContent(
|
|||
state.eventSink(RoomListEvents.ToggleSearchResults)
|
||||
}
|
||||
fun onRoomClicked(room: RoomListRoomSummary) {
|
||||
if (room.roomId == null) return
|
||||
onRoomClicked(room.roomId)
|
||||
}
|
||||
Scaffold(
|
||||
|
|
@ -197,7 +202,11 @@ internal fun RoomListSearchResultContent(
|
|||
items = state.filteredRoomList,
|
||||
contentType = { room -> room.contentType() },
|
||||
) { room ->
|
||||
RoomSummaryRow(room = room, onClick = ::onRoomClicked)
|
||||
RoomSummaryRow(
|
||||
room = room,
|
||||
onClick = ::onRoomClicked,
|
||||
onLongClick = onRoomLongClicked,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,8 +20,11 @@ import app.cash.molecule.RecompositionClock
|
|||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||
import io.element.android.features.leaveroom.fake.LeaveRoomPresenterFake
|
||||
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||
import io.element.android.features.roomlist.impl.model.aRoomListRoomSummary
|
||||
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
|
||||
import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
|
|
@ -54,6 +57,7 @@ class RoomListPresenterTests {
|
|||
FakeNetworkMonitor(),
|
||||
SnackbarDispatcher(),
|
||||
FakeInviteDataSource(),
|
||||
LeaveRoomPresenterFake(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -82,6 +86,7 @@ class RoomListPresenterTests {
|
|||
FakeNetworkMonitor(),
|
||||
SnackbarDispatcher(),
|
||||
FakeInviteDataSource(),
|
||||
LeaveRoomPresenterFake(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -103,6 +108,7 @@ class RoomListPresenterTests {
|
|||
FakeNetworkMonitor(),
|
||||
SnackbarDispatcher(),
|
||||
FakeInviteDataSource(),
|
||||
LeaveRoomPresenterFake(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -132,6 +138,7 @@ class RoomListPresenterTests {
|
|||
FakeNetworkMonitor(),
|
||||
SnackbarDispatcher(),
|
||||
FakeInviteDataSource(),
|
||||
LeaveRoomPresenterFake(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -164,6 +171,7 @@ class RoomListPresenterTests {
|
|||
FakeNetworkMonitor(),
|
||||
SnackbarDispatcher(),
|
||||
FakeInviteDataSource(),
|
||||
LeaveRoomPresenterFake(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -202,6 +210,7 @@ class RoomListPresenterTests {
|
|||
FakeNetworkMonitor(),
|
||||
SnackbarDispatcher(),
|
||||
FakeInviteDataSource(),
|
||||
LeaveRoomPresenterFake(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -253,6 +262,7 @@ class RoomListPresenterTests {
|
|||
FakeNetworkMonitor(),
|
||||
SnackbarDispatcher(),
|
||||
FakeInviteDataSource(),
|
||||
LeaveRoomPresenterFake(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -278,6 +288,7 @@ class RoomListPresenterTests {
|
|||
FakeNetworkMonitor(),
|
||||
SnackbarDispatcher(),
|
||||
FakeInviteDataSource(inviteStateFlow),
|
||||
LeaveRoomPresenterFake(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -297,6 +308,89 @@ class RoomListPresenterTests {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - show context menu`() = runTest {
|
||||
val presenter = RoomListPresenter(
|
||||
FakeMatrixClient(A_SESSION_ID),
|
||||
createDateFormatter(),
|
||||
FakeRoomLastMessageFormatter(),
|
||||
FakeSessionVerificationService(),
|
||||
FakeNetworkMonitor(),
|
||||
SnackbarDispatcher(),
|
||||
FakeInviteDataSource(),
|
||||
LeaveRoomPresenterFake(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
|
||||
val initialState = awaitItem()
|
||||
val summary = aRoomListRoomSummary()
|
||||
initialState.eventSink(RoomListEvents.ShowContextMenu(summary))
|
||||
|
||||
val shownState = awaitItem()
|
||||
Truth.assertThat(shownState.contextMenu)
|
||||
.isEqualTo(RoomListState.ContextMenu.Shown(summary.roomId, summary.name))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - hide context menu`() = runTest {
|
||||
val presenter = RoomListPresenter(
|
||||
FakeMatrixClient(A_SESSION_ID),
|
||||
createDateFormatter(),
|
||||
FakeRoomLastMessageFormatter(),
|
||||
FakeSessionVerificationService(),
|
||||
FakeNetworkMonitor(),
|
||||
SnackbarDispatcher(),
|
||||
FakeInviteDataSource(),
|
||||
LeaveRoomPresenterFake(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
|
||||
val initialState = awaitItem()
|
||||
val summary = aRoomListRoomSummary()
|
||||
initialState.eventSink(RoomListEvents.ShowContextMenu(summary))
|
||||
|
||||
val shownState = awaitItem()
|
||||
Truth.assertThat(shownState.contextMenu)
|
||||
.isEqualTo(RoomListState.ContextMenu.Shown(summary.roomId, summary.name))
|
||||
shownState.eventSink(RoomListEvents.HideContextMenu)
|
||||
|
||||
val hiddenState = awaitItem()
|
||||
Truth.assertThat(hiddenState.contextMenu).isEqualTo(RoomListState.ContextMenu.Hidden)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - leave room calls into leave room presenter`() = runTest {
|
||||
val leaveRoomPresenter = LeaveRoomPresenterFake()
|
||||
val presenter = RoomListPresenter(
|
||||
FakeMatrixClient(A_SESSION_ID),
|
||||
createDateFormatter(),
|
||||
FakeRoomLastMessageFormatter(),
|
||||
FakeSessionVerificationService(),
|
||||
FakeNetworkMonitor(),
|
||||
SnackbarDispatcher(),
|
||||
FakeInviteDataSource(),
|
||||
leaveRoomPresenter,
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(RoomListEvents.LeaveRoom(A_ROOM_ID))
|
||||
|
||||
Truth.assertThat(leaveRoomPresenter.events).containsExactly(LeaveRoomEvent.ShowConfirmation(A_ROOM_ID))
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDateFormatter(): LastMessageTimestampFormatter {
|
||||
return FakeLastMessageTimestampFormatter().apply {
|
||||
givenFormat(A_FORMATTED_DATE)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue