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:
Jorge Martin Espinosa
2023-06-15 14:58:18 +02:00
committed by GitHub
parent 9be0587b8b
commit 9bdf53f43d
14 changed files with 294 additions and 84 deletions

View File

@@ -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
}

View File

@@ -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,

View File

@@ -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?,

View File

@@ -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 = {}

View File

@@ -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))
}
}
)

View File

@@ -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,

View File

@@ -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(

View File

@@ -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()
)
}
}
}

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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,
)

View File

@@ -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(),
)
}
}

View File

@@ -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()
}
}
}

View File

@@ -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