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:
committed by
GitHub
parent
9be0587b8b
commit
9bdf53f43d
@@ -23,4 +23,5 @@ import io.element.android.libraries.matrix.api.core.EventId
|
||||
sealed interface MessagesEvents {
|
||||
data class HandleAction(val action: TimelineItemAction, val event: TimelineItem.Event) : 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.rememberCoroutineScope
|
||||
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.model.TimelineItemAction
|
||||
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.timeline.TimelineEvents
|
||||
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.model.TimelineItem
|
||||
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 timelinePresenter: TimelinePresenter,
|
||||
private val actionListPresenter: ActionListPresenter,
|
||||
private val customReactionPresenter: CustomReactionPresenter,
|
||||
private val retrySendMenuPresenter: RetrySendMenuPresenter,
|
||||
private val networkMonitor: NetworkMonitor,
|
||||
private val snackbarDispatcher: SnackbarDispatcher,
|
||||
@@ -79,6 +82,7 @@ class MessagesPresenter @Inject constructor(
|
||||
val composerState = composerPresenter.present()
|
||||
val timelineState = timelinePresenter.present()
|
||||
val actionListState = actionListPresenter.present()
|
||||
val customReactionState = customReactionPresenter.present()
|
||||
val retryState = retrySendMenuPresenter.present()
|
||||
|
||||
val syncUpdateFlow = room.syncUpdateFlow().collectAsState(0L)
|
||||
@@ -108,8 +112,13 @@ class MessagesPresenter @Inject constructor(
|
||||
}
|
||||
fun handleEvents(event: MessagesEvents) {
|
||||
when (event) {
|
||||
is MessagesEvents.HandleAction -> localCoroutineScope.handleTimelineAction(event.action, event.event, composerState)
|
||||
is MessagesEvents.SendReaction -> localCoroutineScope.sendReaction(event.emoji, event.eventId)
|
||||
is MessagesEvents.HandleAction -> {
|
||||
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(
|
||||
@@ -119,6 +128,7 @@ class MessagesPresenter @Inject constructor(
|
||||
composerState = composerState,
|
||||
timelineState = timelineState,
|
||||
actionListState = actionListState,
|
||||
customReactionState = customReactionState,
|
||||
retrySendMenuState = retryState,
|
||||
hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online,
|
||||
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.messagecomposer.MessageComposerState
|
||||
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.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.utils.SnackbarMessage
|
||||
@@ -33,6 +34,7 @@ data class MessagesState(
|
||||
val composerState: MessageComposerState,
|
||||
val timelineState: TimelineState,
|
||||
val actionListState: ActionListState,
|
||||
val customReactionState: CustomReactionState,
|
||||
val retrySendMenuState: RetrySendMenuState,
|
||||
val hasNetworkConnection: Boolean,
|
||||
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.timeline.aTimelineItemList
|
||||
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.model.event.aTimelineItemTextContent
|
||||
import io.element.android.libraries.core.data.StableCharSequence
|
||||
@@ -54,6 +55,10 @@ fun aMessagesState() = MessagesState(
|
||||
eventSink = {},
|
||||
),
|
||||
actionListState = anActionListState(),
|
||||
customReactionState = CustomReactionState(
|
||||
selectedEventId = null,
|
||||
eventSink = {},
|
||||
),
|
||||
hasNetworkConnection = true,
|
||||
snackbarMessage = null,
|
||||
eventSink = {}
|
||||
|
||||
@@ -35,15 +35,8 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
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.Modifier
|
||||
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.MessageComposerView
|
||||
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.RetrySendMessageMenu
|
||||
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.event.EventSendState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import io.element.android.libraries.ui.strings.R as StringsR
|
||||
|
||||
@@ -100,13 +93,7 @@ fun MessagesView(
|
||||
onItemDebugInfoClicked: (EventId, TimelineItemDebugInfo) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val actionListViewBottomSheetState = rememberModalBottomSheetState()
|
||||
val customReactionBottomSheetState = rememberModalBottomSheetState()
|
||||
|
||||
LogCompositions(tag = "MessagesScreen", msg = "Root")
|
||||
var isMessageActionsBottomSheetVisible by rememberSaveable { mutableStateOf(false) }
|
||||
var isCustomReactionBottomSheetVisible by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
AttachmentStateView(state.composerState.attachmentsState, onPreviewAttachments)
|
||||
|
||||
@@ -126,19 +113,11 @@ fun MessagesView(
|
||||
Timber.v("OnMessageLongClicked= ${event.id}")
|
||||
localView.hideKeyboard()
|
||||
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) {
|
||||
coroutineScope.launch { onDismissActionListBottomSheet() }
|
||||
when (action) {
|
||||
is TimelineItemAction.Developer -> if (event.eventId != null && event.debugInfo != null) {
|
||||
is TimelineItemAction.Developer -> if (event.eventId != null) {
|
||||
onItemDebugInfoClicked(event.eventId, event.debugInfo)
|
||||
}
|
||||
else -> state.eventSink(MessagesEvents.HandleAction(action, event))
|
||||
@@ -147,7 +126,6 @@ fun MessagesView(
|
||||
|
||||
fun onEmojiReactionClicked(emoji: String, event: TimelineItem.Event) {
|
||||
if (event.eventId == null) return
|
||||
coroutineScope.launch { onDismissActionListBottomSheet() }
|
||||
state.eventSink(MessagesEvents.SendReaction(emoji, event.eventId))
|
||||
}
|
||||
|
||||
@@ -189,42 +167,21 @@ fun MessagesView(
|
||||
},
|
||||
)
|
||||
|
||||
var reactingToEventId: EventId? by remember { mutableStateOf(null) }
|
||||
ActionListView(
|
||||
state = state.actionListState,
|
||||
sheetState = actionListViewBottomSheetState,
|
||||
isVisible = isMessageActionsBottomSheetVisible,
|
||||
onDismiss = { coroutineScope.launch { onDismissActionListBottomSheet() } },
|
||||
onActionSelected = ::onActionSelected,
|
||||
onCustomReactionClicked = { event ->
|
||||
reactingToEventId = event.eventId
|
||||
coroutineScope.launch {
|
||||
onDismissActionListBottomSheet()
|
||||
isCustomReactionBottomSheetVisible = true
|
||||
}
|
||||
state.customReactionState.eventSink(CustomReactionEvents.UpdateSelectedEvent(event.eventId))
|
||||
},
|
||||
onEmojiReactionClicked = ::onEmojiReactionClicked,
|
||||
)
|
||||
|
||||
CustomReactionBottomSheet(
|
||||
isVisible = isCustomReactionBottomSheetVisible,
|
||||
sheetState = customReactionBottomSheetState,
|
||||
onDismiss = {
|
||||
reactingToEventId = null
|
||||
coroutineScope.launch {
|
||||
customReactionBottomSheetState.hide()
|
||||
isCustomReactionBottomSheetVisible = false
|
||||
}
|
||||
},
|
||||
state = state.customReactionState,
|
||||
onEmojiSelected = { emoji ->
|
||||
val eventId = reactingToEventId
|
||||
if (eventId != null) {
|
||||
state.customReactionState.selectedEventId?.let { eventId ->
|
||||
state.eventSink(MessagesEvents.SendReaction(emoji.unicode, eventId))
|
||||
reactingToEventId = null
|
||||
coroutineScope.launch {
|
||||
customReactionBottomSheetState.hide()
|
||||
isCustomReactionBottomSheetVisible = false
|
||||
}
|
||||
state.customReactionState.eventSink(CustomReactionEvents.UpdateSelectedEvent(null))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -42,7 +42,10 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SheetState
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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.Icon
|
||||
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.AttachmentThumbnailInfo
|
||||
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
|
||||
@@ -82,37 +86,51 @@ import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
|
||||
@Composable
|
||||
fun ActionListView(
|
||||
state: ActionListState,
|
||||
isVisible: Boolean,
|
||||
onActionSelected: (action: TimelineItemAction, TimelineItem.Event) -> Unit,
|
||||
onEmojiReactionClicked: (String, TimelineItem.Event) -> Unit,
|
||||
onCustomReactionClicked: (TimelineItem.Event) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
sheetState: SheetState = rememberModalBottomSheetState()
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val targetItem = (state.target as? ActionListState.Target.Success)?.event
|
||||
|
||||
fun onItemActionClicked(
|
||||
itemAction: TimelineItemAction
|
||||
) {
|
||||
if (targetItem == null) return
|
||||
onActionSelected(itemAction, targetItem)
|
||||
sheetState.hide(coroutineScope) {
|
||||
state.eventSink(ActionListEvents.Clear)
|
||||
onActionSelected(itemAction, targetItem)
|
||||
}
|
||||
}
|
||||
|
||||
fun onEmojiReactionClicked(emoji: String) {
|
||||
if (targetItem == null) return
|
||||
onEmojiReactionClicked(emoji, targetItem)
|
||||
sheetState.hide(coroutineScope) {
|
||||
state.eventSink(ActionListEvents.Clear)
|
||||
onEmojiReactionClicked(emoji, targetItem)
|
||||
}
|
||||
}
|
||||
|
||||
fun onCustomReactionClicked() {
|
||||
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(
|
||||
sheetState = sheetState,
|
||||
onDismissRequest = onDismiss
|
||||
onDismissRequest = ::onDismiss,
|
||||
) {
|
||||
SheetContent(
|
||||
state = state,
|
||||
|
||||
@@ -32,8 +32,6 @@ import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
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.TabRow
|
||||
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.ElementPreviewLight
|
||||
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 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)
|
||||
@Composable
|
||||
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.MessagesPresenter
|
||||
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.messagecomposer.MessageComposerPresenter
|
||||
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.model.event.TimelineItemFileContent
|
||||
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")))
|
||||
initialState.eventSink.invoke(MessagesEvents.SendReaction("👍", AN_EVENT_ID))
|
||||
assertThat(room.sendReactionCount).isEqualTo(2)
|
||||
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +105,7 @@ class MessagesPresenterTest {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
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)
|
||||
val initialState = awaitItem()
|
||||
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)
|
||||
val finalState = awaitItem()
|
||||
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)
|
||||
val initialState = awaitItem()
|
||||
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
|
||||
ensureAllEventsConsumed()
|
||||
}
|
||||
@@ -177,6 +181,7 @@ class MessagesPresenterTest {
|
||||
assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java)
|
||||
val replyMode = finalState.composerState.mode as MessageComposerMode.Reply
|
||||
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)
|
||||
val replyMode = finalState.composerState.mode as MessageComposerMode.Reply
|
||||
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)
|
||||
val replyMode = finalState.composerState.mode as MessageComposerMode.Reply
|
||||
assertThat(replyMode.attachmentThumbnailInfo).isNotNull()
|
||||
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,6 +258,7 @@ class MessagesPresenterTest {
|
||||
skipItems(1)
|
||||
val finalState = awaitItem()
|
||||
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()
|
||||
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Redact, aMessageEvent()))
|
||||
assertThat(matrixRoom.redactEventEventIdParam).isEqualTo(AN_EVENT_ID)
|
||||
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,7 +286,20 @@ class MessagesPresenterTest {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
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)
|
||||
val initialState = awaitItem()
|
||||
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 = "",
|
||||
)
|
||||
val actionListPresenter = ActionListPresenter(buildMeta = buildMeta)
|
||||
val customReactionPresenter = CustomReactionPresenter()
|
||||
val retrySendMenuPresenter = RetrySendMenuPresenter(room = matrixRoom)
|
||||
return MessagesPresenter(
|
||||
room = matrixRoom,
|
||||
composerPresenter = messageComposerPresenter,
|
||||
timelinePresenter = timelinePresenter,
|
||||
actionListPresenter = actionListPresenter,
|
||||
customReactionPresenter = customReactionPresenter,
|
||||
retrySendMenuPresenter = retrySendMenuPresenter,
|
||||
networkMonitor = FakeNetworkMonitor(),
|
||||
snackbarDispatcher = SnackbarDispatcher(),
|
||||
messageSummaryFormatter = FakeMessageSummaryFormatter(),
|
||||
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.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.preview.PreviewGroup
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@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
|
||||
@Preview(group = PreviewGroup.BottomSheets)
|
||||
@Composable
|
||||
|
||||
Reference in New Issue
Block a user