Move logic of different BottomSheets in MessagesView to presenters (#600)
* Move bottom sheet logic in `MessagesView` to presenters. * Make the block inside `SheetState.hide` suspend.
This commit is contained in:
parent
9be0587b8b
commit
9bdf53f43d
14 changed files with 294 additions and 84 deletions
|
|
@ -23,4 +23,5 @@ import io.element.android.libraries.matrix.api.core.EventId
|
||||||
sealed interface MessagesEvents {
|
sealed interface MessagesEvents {
|
||||||
data class HandleAction(val action: TimelineItemAction, val event: TimelineItem.Event) : MessagesEvents
|
data class HandleAction(val action: TimelineItemAction, val event: TimelineItem.Event) : MessagesEvents
|
||||||
data class SendReaction(val emoji: String, val eventId: EventId) : MessagesEvents
|
data class SendReaction(val emoji: String, val eventId: EventId) : MessagesEvents
|
||||||
|
object Dismiss : MessagesEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import io.element.android.features.messages.impl.actionlist.ActionListEvents
|
||||||
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
|
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
|
||||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
|
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
|
||||||
|
|
@ -32,6 +33,7 @@ import io.element.android.features.messages.impl.messagecomposer.MessageComposer
|
||||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
|
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
|
||||||
import io.element.android.features.messages.impl.timeline.TimelineEvents
|
import io.element.android.features.messages.impl.timeline.TimelineEvents
|
||||||
import io.element.android.features.messages.impl.timeline.TimelinePresenter
|
import io.element.android.features.messages.impl.timeline.TimelinePresenter
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter
|
||||||
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuPresenter
|
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuPresenter
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent
|
||||||
|
|
@ -66,6 +68,7 @@ class MessagesPresenter @Inject constructor(
|
||||||
private val composerPresenter: MessageComposerPresenter,
|
private val composerPresenter: MessageComposerPresenter,
|
||||||
private val timelinePresenter: TimelinePresenter,
|
private val timelinePresenter: TimelinePresenter,
|
||||||
private val actionListPresenter: ActionListPresenter,
|
private val actionListPresenter: ActionListPresenter,
|
||||||
|
private val customReactionPresenter: CustomReactionPresenter,
|
||||||
private val retrySendMenuPresenter: RetrySendMenuPresenter,
|
private val retrySendMenuPresenter: RetrySendMenuPresenter,
|
||||||
private val networkMonitor: NetworkMonitor,
|
private val networkMonitor: NetworkMonitor,
|
||||||
private val snackbarDispatcher: SnackbarDispatcher,
|
private val snackbarDispatcher: SnackbarDispatcher,
|
||||||
|
|
@ -79,6 +82,7 @@ class MessagesPresenter @Inject constructor(
|
||||||
val composerState = composerPresenter.present()
|
val composerState = composerPresenter.present()
|
||||||
val timelineState = timelinePresenter.present()
|
val timelineState = timelinePresenter.present()
|
||||||
val actionListState = actionListPresenter.present()
|
val actionListState = actionListPresenter.present()
|
||||||
|
val customReactionState = customReactionPresenter.present()
|
||||||
val retryState = retrySendMenuPresenter.present()
|
val retryState = retrySendMenuPresenter.present()
|
||||||
|
|
||||||
val syncUpdateFlow = room.syncUpdateFlow().collectAsState(0L)
|
val syncUpdateFlow = room.syncUpdateFlow().collectAsState(0L)
|
||||||
|
|
@ -108,8 +112,13 @@ class MessagesPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
fun handleEvents(event: MessagesEvents) {
|
fun handleEvents(event: MessagesEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is MessagesEvents.HandleAction -> localCoroutineScope.handleTimelineAction(event.action, event.event, composerState)
|
is MessagesEvents.HandleAction -> {
|
||||||
is MessagesEvents.SendReaction -> localCoroutineScope.sendReaction(event.emoji, event.eventId)
|
localCoroutineScope.handleTimelineAction(event.action, event.event, composerState)
|
||||||
|
}
|
||||||
|
is MessagesEvents.SendReaction -> {
|
||||||
|
localCoroutineScope.sendReaction(event.emoji, event.eventId)
|
||||||
|
}
|
||||||
|
is MessagesEvents.Dismiss -> actionListState.eventSink(ActionListEvents.Clear)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return MessagesState(
|
return MessagesState(
|
||||||
|
|
@ -119,6 +128,7 @@ class MessagesPresenter @Inject constructor(
|
||||||
composerState = composerState,
|
composerState = composerState,
|
||||||
timelineState = timelineState,
|
timelineState = timelineState,
|
||||||
actionListState = actionListState,
|
actionListState = actionListState,
|
||||||
|
customReactionState = customReactionState,
|
||||||
retrySendMenuState = retryState,
|
retrySendMenuState = retryState,
|
||||||
hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online,
|
hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online,
|
||||||
snackbarMessage = snackbarMessage,
|
snackbarMessage = snackbarMessage,
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import androidx.compose.runtime.Immutable
|
||||||
import io.element.android.features.messages.impl.actionlist.ActionListState
|
import io.element.android.features.messages.impl.actionlist.ActionListState
|
||||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
|
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
|
||||||
import io.element.android.features.messages.impl.timeline.TimelineState
|
import io.element.android.features.messages.impl.timeline.TimelineState
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState
|
||||||
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState
|
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.utils.SnackbarMessage
|
import io.element.android.libraries.designsystem.utils.SnackbarMessage
|
||||||
|
|
@ -33,6 +34,7 @@ data class MessagesState(
|
||||||
val composerState: MessageComposerState,
|
val composerState: MessageComposerState,
|
||||||
val timelineState: TimelineState,
|
val timelineState: TimelineState,
|
||||||
val actionListState: ActionListState,
|
val actionListState: ActionListState,
|
||||||
|
val customReactionState: CustomReactionState,
|
||||||
val retrySendMenuState: RetrySendMenuState,
|
val retrySendMenuState: RetrySendMenuState,
|
||||||
val hasNetworkConnection: Boolean,
|
val hasNetworkConnection: Boolean,
|
||||||
val snackbarMessage: SnackbarMessage?,
|
val snackbarMessage: SnackbarMessage?,
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import io.element.android.features.messages.impl.actionlist.anActionListState
|
||||||
import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState
|
import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState
|
||||||
import io.element.android.features.messages.impl.timeline.aTimelineItemList
|
import io.element.android.features.messages.impl.timeline.aTimelineItemList
|
||||||
import io.element.android.features.messages.impl.timeline.aTimelineState
|
import io.element.android.features.messages.impl.timeline.aTimelineState
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState
|
||||||
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState
|
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
||||||
import io.element.android.libraries.core.data.StableCharSequence
|
import io.element.android.libraries.core.data.StableCharSequence
|
||||||
|
|
@ -54,6 +55,10 @@ fun aMessagesState() = MessagesState(
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
),
|
),
|
||||||
actionListState = anActionListState(),
|
actionListState = anActionListState(),
|
||||||
|
customReactionState = CustomReactionState(
|
||||||
|
selectedEventId = null,
|
||||||
|
eventSink = {},
|
||||||
|
),
|
||||||
hasNetworkConnection = true,
|
hasNetworkConnection = true,
|
||||||
snackbarMessage = null,
|
snackbarMessage = null,
|
||||||
eventSink = {}
|
eventSink = {}
|
||||||
|
|
|
||||||
|
|
@ -35,15 +35,8 @@ import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.ArrowBack
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
|
|
@ -61,7 +54,8 @@ import io.element.android.features.messages.impl.attachments.Attachment
|
||||||
import io.element.android.features.messages.impl.messagecomposer.AttachmentsState
|
import io.element.android.features.messages.impl.messagecomposer.AttachmentsState
|
||||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerView
|
import io.element.android.features.messages.impl.messagecomposer.MessageComposerView
|
||||||
import io.element.android.features.messages.impl.timeline.TimelineView
|
import io.element.android.features.messages.impl.timeline.TimelineView
|
||||||
import io.element.android.features.messages.impl.timeline.components.CustomReactionBottomSheet
|
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionBottomSheet
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionEvents
|
||||||
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuEvents
|
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuEvents
|
||||||
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMessageMenu
|
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMessageMenu
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
|
|
@ -84,7 +78,6 @@ import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import io.element.android.libraries.ui.strings.R as StringsR
|
import io.element.android.libraries.ui.strings.R as StringsR
|
||||||
|
|
||||||
|
|
@ -100,13 +93,7 @@ fun MessagesView(
|
||||||
onItemDebugInfoClicked: (EventId, TimelineItemDebugInfo) -> Unit,
|
onItemDebugInfoClicked: (EventId, TimelineItemDebugInfo) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
val actionListViewBottomSheetState = rememberModalBottomSheetState()
|
|
||||||
val customReactionBottomSheetState = rememberModalBottomSheetState()
|
|
||||||
|
|
||||||
LogCompositions(tag = "MessagesScreen", msg = "Root")
|
LogCompositions(tag = "MessagesScreen", msg = "Root")
|
||||||
var isMessageActionsBottomSheetVisible by rememberSaveable { mutableStateOf(false) }
|
|
||||||
var isCustomReactionBottomSheetVisible by rememberSaveable { mutableStateOf(false) }
|
|
||||||
|
|
||||||
AttachmentStateView(state.composerState.attachmentsState, onPreviewAttachments)
|
AttachmentStateView(state.composerState.attachmentsState, onPreviewAttachments)
|
||||||
|
|
||||||
|
|
@ -126,19 +113,11 @@ fun MessagesView(
|
||||||
Timber.v("OnMessageLongClicked= ${event.id}")
|
Timber.v("OnMessageLongClicked= ${event.id}")
|
||||||
localView.hideKeyboard()
|
localView.hideKeyboard()
|
||||||
state.actionListState.eventSink(ActionListEvents.ComputeForMessage(event))
|
state.actionListState.eventSink(ActionListEvents.ComputeForMessage(event))
|
||||||
isMessageActionsBottomSheetVisible = true
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun onDismissActionListBottomSheet() {
|
|
||||||
state.actionListState.eventSink(ActionListEvents.Clear)
|
|
||||||
actionListViewBottomSheetState.hide()
|
|
||||||
isMessageActionsBottomSheetVisible = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onActionSelected(action: TimelineItemAction, event: TimelineItem.Event) {
|
fun onActionSelected(action: TimelineItemAction, event: TimelineItem.Event) {
|
||||||
coroutineScope.launch { onDismissActionListBottomSheet() }
|
|
||||||
when (action) {
|
when (action) {
|
||||||
is TimelineItemAction.Developer -> if (event.eventId != null && event.debugInfo != null) {
|
is TimelineItemAction.Developer -> if (event.eventId != null) {
|
||||||
onItemDebugInfoClicked(event.eventId, event.debugInfo)
|
onItemDebugInfoClicked(event.eventId, event.debugInfo)
|
||||||
}
|
}
|
||||||
else -> state.eventSink(MessagesEvents.HandleAction(action, event))
|
else -> state.eventSink(MessagesEvents.HandleAction(action, event))
|
||||||
|
|
@ -147,7 +126,6 @@ fun MessagesView(
|
||||||
|
|
||||||
fun onEmojiReactionClicked(emoji: String, event: TimelineItem.Event) {
|
fun onEmojiReactionClicked(emoji: String, event: TimelineItem.Event) {
|
||||||
if (event.eventId == null) return
|
if (event.eventId == null) return
|
||||||
coroutineScope.launch { onDismissActionListBottomSheet() }
|
|
||||||
state.eventSink(MessagesEvents.SendReaction(emoji, event.eventId))
|
state.eventSink(MessagesEvents.SendReaction(emoji, event.eventId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -189,42 +167,21 @@ fun MessagesView(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
var reactingToEventId: EventId? by remember { mutableStateOf(null) }
|
|
||||||
ActionListView(
|
ActionListView(
|
||||||
state = state.actionListState,
|
state = state.actionListState,
|
||||||
sheetState = actionListViewBottomSheetState,
|
|
||||||
isVisible = isMessageActionsBottomSheetVisible,
|
|
||||||
onDismiss = { coroutineScope.launch { onDismissActionListBottomSheet() } },
|
|
||||||
onActionSelected = ::onActionSelected,
|
onActionSelected = ::onActionSelected,
|
||||||
onCustomReactionClicked = { event ->
|
onCustomReactionClicked = { event ->
|
||||||
reactingToEventId = event.eventId
|
state.customReactionState.eventSink(CustomReactionEvents.UpdateSelectedEvent(event.eventId))
|
||||||
coroutineScope.launch {
|
|
||||||
onDismissActionListBottomSheet()
|
|
||||||
isCustomReactionBottomSheetVisible = true
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onEmojiReactionClicked = ::onEmojiReactionClicked,
|
onEmojiReactionClicked = ::onEmojiReactionClicked,
|
||||||
)
|
)
|
||||||
|
|
||||||
CustomReactionBottomSheet(
|
CustomReactionBottomSheet(
|
||||||
isVisible = isCustomReactionBottomSheetVisible,
|
state = state.customReactionState,
|
||||||
sheetState = customReactionBottomSheetState,
|
|
||||||
onDismiss = {
|
|
||||||
reactingToEventId = null
|
|
||||||
coroutineScope.launch {
|
|
||||||
customReactionBottomSheetState.hide()
|
|
||||||
isCustomReactionBottomSheetVisible = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onEmojiSelected = { emoji ->
|
onEmojiSelected = { emoji ->
|
||||||
val eventId = reactingToEventId
|
state.customReactionState.selectedEventId?.let { eventId ->
|
||||||
if (eventId != null) {
|
|
||||||
state.eventSink(MessagesEvents.SendReaction(emoji.unicode, eventId))
|
state.eventSink(MessagesEvents.SendReaction(emoji.unicode, eventId))
|
||||||
reactingToEventId = null
|
state.customReactionState.eventSink(CustomReactionEvents.UpdateSelectedEvent(null))
|
||||||
coroutineScope.launch {
|
|
||||||
customReactionBottomSheetState.hide()
|
|
||||||
isCustomReactionBottomSheetVisible = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,10 @@ import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.SheetState
|
import androidx.compose.material3.SheetState
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
|
@ -74,6 +77,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Divider
|
import io.element.android.libraries.designsystem.theme.components.Divider
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
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.ModalBottomSheet
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.hide
|
||||||
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnail
|
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnail
|
||||||
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo
|
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo
|
||||||
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
|
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
|
||||||
|
|
@ -82,37 +86,51 @@ import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
|
||||||
@Composable
|
@Composable
|
||||||
fun ActionListView(
|
fun ActionListView(
|
||||||
state: ActionListState,
|
state: ActionListState,
|
||||||
isVisible: Boolean,
|
|
||||||
onActionSelected: (action: TimelineItemAction, TimelineItem.Event) -> Unit,
|
onActionSelected: (action: TimelineItemAction, TimelineItem.Event) -> Unit,
|
||||||
onEmojiReactionClicked: (String, TimelineItem.Event) -> Unit,
|
onEmojiReactionClicked: (String, TimelineItem.Event) -> Unit,
|
||||||
onCustomReactionClicked: (TimelineItem.Event) -> Unit,
|
onCustomReactionClicked: (TimelineItem.Event) -> Unit,
|
||||||
onDismiss: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
sheetState: SheetState = rememberModalBottomSheetState()
|
sheetState: SheetState = rememberModalBottomSheetState()
|
||||||
) {
|
) {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val targetItem = (state.target as? ActionListState.Target.Success)?.event
|
val targetItem = (state.target as? ActionListState.Target.Success)?.event
|
||||||
|
|
||||||
fun onItemActionClicked(
|
fun onItemActionClicked(
|
||||||
itemAction: TimelineItemAction
|
itemAction: TimelineItemAction
|
||||||
) {
|
) {
|
||||||
if (targetItem == null) return
|
if (targetItem == null) return
|
||||||
onActionSelected(itemAction, targetItem)
|
sheetState.hide(coroutineScope) {
|
||||||
|
state.eventSink(ActionListEvents.Clear)
|
||||||
|
onActionSelected(itemAction, targetItem)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onEmojiReactionClicked(emoji: String) {
|
fun onEmojiReactionClicked(emoji: String) {
|
||||||
if (targetItem == null) return
|
if (targetItem == null) return
|
||||||
onEmojiReactionClicked(emoji, targetItem)
|
sheetState.hide(coroutineScope) {
|
||||||
|
state.eventSink(ActionListEvents.Clear)
|
||||||
|
onEmojiReactionClicked(emoji, targetItem)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onCustomReactionClicked() {
|
fun onCustomReactionClicked() {
|
||||||
if (targetItem == null) return
|
if (targetItem == null) return
|
||||||
onCustomReactionClicked(targetItem)
|
sheetState.hide(coroutineScope) {
|
||||||
|
state.eventSink(ActionListEvents.Clear)
|
||||||
|
onCustomReactionClicked(targetItem)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isVisible) {
|
fun onDismiss() {
|
||||||
|
sheetState.hide(coroutineScope) {
|
||||||
|
state.eventSink(ActionListEvents.Clear)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetItem != null) {
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
sheetState = sheetState,
|
sheetState = sheetState,
|
||||||
onDismissRequest = onDismiss
|
onDismissRequest = ::onDismiss,
|
||||||
) {
|
) {
|
||||||
SheetContent(
|
SheetContent(
|
||||||
state = state,
|
state = state,
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,6 @@ import androidx.compose.foundation.lazy.grid.items
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.material.ripple.rememberRipple
|
import androidx.compose.material.ripple.rememberRipple
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.SheetState
|
|
||||||
import androidx.compose.material3.Tab
|
import androidx.compose.material3.Tab
|
||||||
import androidx.compose.material3.TabRow
|
import androidx.compose.material3.TabRow
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -49,26 +47,9 @@ import com.vanniktech.emoji.google.GoogleEmojiProvider
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
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.designsystem.theme.components.Text
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun CustomReactionBottomSheet(
|
|
||||||
isVisible: Boolean,
|
|
||||||
sheetState: SheetState,
|
|
||||||
onDismiss: () -> Unit,
|
|
||||||
onEmojiSelected: (Emoji) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
if (isVisible) {
|
|
||||||
ModalBottomSheet(onDismissRequest = onDismiss, sheetState = sheetState, modifier = modifier) {
|
|
||||||
EmojiPicker(onEmojiSelected = onEmojiSelected, modifier = Modifier.fillMaxSize())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun EmojiPicker(
|
fun EmojiPicker(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
/*
|
||||||
|
* 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.messages.impl.timeline.components.customreaction
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import com.vanniktech.emoji.Emoji
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.EmojiPicker
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.hide
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun CustomReactionBottomSheet(
|
||||||
|
state: CustomReactionState,
|
||||||
|
onEmojiSelected: (Emoji) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val sheetState = rememberModalBottomSheetState()
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
fun onDismiss() {
|
||||||
|
sheetState.hide(coroutineScope) {
|
||||||
|
state.eventSink(CustomReactionEvents.UpdateSelectedEvent(null))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onEmojiSelectedDismiss(emoji: Emoji) {
|
||||||
|
sheetState.hide(coroutineScope) {
|
||||||
|
state.eventSink(CustomReactionEvents.UpdateSelectedEvent(null))
|
||||||
|
onEmojiSelected(emoji)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val isVisible = state.selectedEventId != null
|
||||||
|
if (isVisible) {
|
||||||
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = ::onDismiss,
|
||||||
|
sheetState = sheetState,
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
EmojiPicker(
|
||||||
|
onEmojiSelected = ::onEmojiSelectedDismiss,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* 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.messages.impl.timeline.components.customreaction
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
|
||||||
|
sealed interface CustomReactionEvents {
|
||||||
|
data class UpdateSelectedEvent(val eventId: EventId?) : CustomReactionEvents
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* 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.messages.impl.timeline.components.customreaction
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class CustomReactionPresenter @Inject constructor() : Presenter<CustomReactionState> {
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun present(): CustomReactionState {
|
||||||
|
var selectedEventId by remember { mutableStateOf<EventId?>(null) }
|
||||||
|
|
||||||
|
fun handleEvents(event: CustomReactionEvents) {
|
||||||
|
when (event) {
|
||||||
|
is CustomReactionEvents.UpdateSelectedEvent -> selectedEventId = event.eventId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return CustomReactionState(selectedEventId = selectedEventId, eventSink = ::handleEvents)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* 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.messages.impl.timeline.components.customreaction
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
|
||||||
|
data class CustomReactionState(
|
||||||
|
val selectedEventId: EventId?,
|
||||||
|
val eventSink: (CustomReactionEvents) -> Unit,
|
||||||
|
)
|
||||||
|
|
@ -26,9 +26,11 @@ import io.element.android.features.messages.fixtures.aTimelineItemsFactory
|
||||||
import io.element.android.features.messages.impl.MessagesEvents
|
import io.element.android.features.messages.impl.MessagesEvents
|
||||||
import io.element.android.features.messages.impl.MessagesPresenter
|
import io.element.android.features.messages.impl.MessagesPresenter
|
||||||
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
|
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
|
||||||
|
import io.element.android.features.messages.impl.actionlist.ActionListState
|
||||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
|
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
|
||||||
import io.element.android.features.messages.impl.timeline.TimelinePresenter
|
import io.element.android.features.messages.impl.timeline.TimelinePresenter
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter
|
||||||
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuPresenter
|
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuPresenter
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
||||||
|
|
@ -90,6 +92,7 @@ class MessagesPresenterTest {
|
||||||
room.givenSendReactionResult(Result.failure(IllegalStateException("Failed to send reaction")))
|
room.givenSendReactionResult(Result.failure(IllegalStateException("Failed to send reaction")))
|
||||||
initialState.eventSink.invoke(MessagesEvents.SendReaction("👍", AN_EVENT_ID))
|
initialState.eventSink.invoke(MessagesEvents.SendReaction("👍", AN_EVENT_ID))
|
||||||
assertThat(room.sendReactionCount).isEqualTo(2)
|
assertThat(room.sendReactionCount).isEqualTo(2)
|
||||||
|
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,7 +105,7 @@ class MessagesPresenterTest {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Forward, aMessageEvent()))
|
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Forward, aMessageEvent()))
|
||||||
// Still a TODO in the code
|
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -115,7 +118,7 @@ class MessagesPresenterTest {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Copy, aMessageEvent()))
|
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Copy, aMessageEvent()))
|
||||||
// Still a TODO in the code
|
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -131,6 +134,7 @@ class MessagesPresenterTest {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
val finalState = awaitItem()
|
val finalState = awaitItem()
|
||||||
assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java)
|
assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java)
|
||||||
|
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -143,7 +147,7 @@ class MessagesPresenterTest {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, aMessageEvent(eventId = null)))
|
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, aMessageEvent(eventId = null)))
|
||||||
skipItems(1)
|
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
|
||||||
// Otherwise we would have some extra items here
|
// Otherwise we would have some extra items here
|
||||||
ensureAllEventsConsumed()
|
ensureAllEventsConsumed()
|
||||||
}
|
}
|
||||||
|
|
@ -177,6 +181,7 @@ class MessagesPresenterTest {
|
||||||
assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java)
|
assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java)
|
||||||
val replyMode = finalState.composerState.mode as MessageComposerMode.Reply
|
val replyMode = finalState.composerState.mode as MessageComposerMode.Reply
|
||||||
assertThat(replyMode.attachmentThumbnailInfo).isNotNull()
|
assertThat(replyMode.attachmentThumbnailInfo).isNotNull()
|
||||||
|
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -209,6 +214,7 @@ class MessagesPresenterTest {
|
||||||
assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java)
|
assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java)
|
||||||
val replyMode = finalState.composerState.mode as MessageComposerMode.Reply
|
val replyMode = finalState.composerState.mode as MessageComposerMode.Reply
|
||||||
assertThat(replyMode.attachmentThumbnailInfo).isNotNull()
|
assertThat(replyMode.attachmentThumbnailInfo).isNotNull()
|
||||||
|
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -236,6 +242,7 @@ class MessagesPresenterTest {
|
||||||
assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java)
|
assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java)
|
||||||
val replyMode = finalState.composerState.mode as MessageComposerMode.Reply
|
val replyMode = finalState.composerState.mode as MessageComposerMode.Reply
|
||||||
assertThat(replyMode.attachmentThumbnailInfo).isNotNull()
|
assertThat(replyMode.attachmentThumbnailInfo).isNotNull()
|
||||||
|
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -251,6 +258,7 @@ class MessagesPresenterTest {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
val finalState = awaitItem()
|
val finalState = awaitItem()
|
||||||
assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Edit::class.java)
|
assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Edit::class.java)
|
||||||
|
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -265,6 +273,7 @@ class MessagesPresenterTest {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Redact, aMessageEvent()))
|
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Redact, aMessageEvent()))
|
||||||
assertThat(matrixRoom.redactEventEventIdParam).isEqualTo(AN_EVENT_ID)
|
assertThat(matrixRoom.redactEventEventIdParam).isEqualTo(AN_EVENT_ID)
|
||||||
|
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -277,7 +286,20 @@ class MessagesPresenterTest {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.ReportContent, aMessageEvent()))
|
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.ReportContent, aMessageEvent()))
|
||||||
// Still a TODO in the code
|
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - handle dismiss action`() = runTest {
|
||||||
|
val presenter = createMessagePresenter()
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
skipItems(1)
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink.invoke(MessagesEvents.Dismiss)
|
||||||
|
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -290,7 +312,7 @@ class MessagesPresenterTest {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Developer, aMessageEvent()))
|
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Developer, aMessageEvent()))
|
||||||
// Still a TODO in the code
|
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -324,18 +346,19 @@ class MessagesPresenterTest {
|
||||||
flavorShortDescription = "",
|
flavorShortDescription = "",
|
||||||
)
|
)
|
||||||
val actionListPresenter = ActionListPresenter(buildMeta = buildMeta)
|
val actionListPresenter = ActionListPresenter(buildMeta = buildMeta)
|
||||||
|
val customReactionPresenter = CustomReactionPresenter()
|
||||||
val retrySendMenuPresenter = RetrySendMenuPresenter(room = matrixRoom)
|
val retrySendMenuPresenter = RetrySendMenuPresenter(room = matrixRoom)
|
||||||
return MessagesPresenter(
|
return MessagesPresenter(
|
||||||
room = matrixRoom,
|
room = matrixRoom,
|
||||||
composerPresenter = messageComposerPresenter,
|
composerPresenter = messageComposerPresenter,
|
||||||
timelinePresenter = timelinePresenter,
|
timelinePresenter = timelinePresenter,
|
||||||
actionListPresenter = actionListPresenter,
|
actionListPresenter = actionListPresenter,
|
||||||
|
customReactionPresenter = customReactionPresenter,
|
||||||
retrySendMenuPresenter = retrySendMenuPresenter,
|
retrySendMenuPresenter = retrySendMenuPresenter,
|
||||||
networkMonitor = FakeNetworkMonitor(),
|
networkMonitor = FakeNetworkMonitor(),
|
||||||
snackbarDispatcher = SnackbarDispatcher(),
|
snackbarDispatcher = SnackbarDispatcher(),
|
||||||
messageSummaryFormatter = FakeMessageSummaryFormatter(),
|
messageSummaryFormatter = FakeMessageSummaryFormatter(),
|
||||||
dispatchers = testCoroutineDispatchers(),
|
dispatchers = testCoroutineDispatchers(),
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
* 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.messages.timeline.components.customreaction
|
||||||
|
|
||||||
|
import app.cash.molecule.RecompositionClock
|
||||||
|
import app.cash.molecule.moleculeFlow
|
||||||
|
import app.cash.turbine.test
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionEvents
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter
|
||||||
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class CustomReactionPresenterTests {
|
||||||
|
|
||||||
|
private val presenter = CustomReactionPresenter()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - handle selecting and de-selecting an event`() = runTest {
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.selectedEventId).isNull()
|
||||||
|
|
||||||
|
initialState.eventSink(CustomReactionEvents.UpdateSelectedEvent(AN_EVENT_ID))
|
||||||
|
assertThat(awaitItem().selectedEventId).isEqualTo(AN_EVENT_ID)
|
||||||
|
|
||||||
|
initialState.eventSink(CustomReactionEvents.UpdateSelectedEvent(null))
|
||||||
|
assertThat(awaitItem().selectedEventId).isNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -37,6 +37,8 @@ import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewGroup
|
import io.element.android.libraries.designsystem.preview.PreviewGroup
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -66,6 +68,14 @@ fun ModalBottomSheet(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
fun SheetState.hide(coroutineScope: CoroutineScope, then: suspend () -> Unit) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
hide()
|
||||||
|
then()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// This preview and its screenshots are blank, see: https://issuetracker.google.com/issues/283843380
|
// This preview and its screenshots are blank, see: https://issuetracker.google.com/issues/283843380
|
||||||
@Preview(group = PreviewGroup.BottomSheets)
|
@Preview(group = PreviewGroup.BottomSheets)
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue