diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesScreen.kt b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesScreen.kt index 91b3eb663f..44320e3e70 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesScreen.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesScreen.kt @@ -1,4 +1,8 @@ -@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) +@file:OptIn( + ExperimentalMaterial3Api::class, + ExperimentalMaterialApi::class, + ExperimentalComposeUiApi::class +) package io.element.android.x.features.messages @@ -21,8 +25,10 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.End import androidx.compose.ui.Alignment.Companion.Start +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.layout.LastBaseline +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -48,8 +54,6 @@ import kotlinx.coroutines.launch import timber.log.Timber import java.lang.Math.random -private val COMPOSER_HEIGHT = 112.dp - @Composable fun MessagesScreen( roomId: String, @@ -64,10 +68,12 @@ fun MessagesScreen( } LogCompositions(tag = "MessagesScreen", msg = "Root") - val actionsSheetState = rememberModalBottomSheetState( + val itemActionsBottomSheetState = rememberModalBottomSheetState( initialValue = ModalBottomSheetValue.Hidden, ) + val snackbarHostState = remember { SnackbarHostState() } val coroutineScope = rememberCoroutineScope() + val focusManager = LocalFocusManager.current val roomTitle by viewModel.collectAsState(MessagesViewState::roomName) val roomAvatar by viewModel.collectAsState(MessagesViewState::roomAvatar) val timelineItems by viewModel.collectAsState(MessagesViewState::timelineItems) @@ -78,7 +84,7 @@ fun MessagesScreen( val composerFullScreen by composerViewModel.collectAsState(MessageComposerViewState::isFullScreen) val composerCanSendMessage by composerViewModel.collectAsState(MessageComposerViewState::isSendButtonVisible) val composerText by composerViewModel.collectAsState(MessageComposerViewState::text) - val snackbarHostState = remember { SnackbarHostState() } + MessagesScreenContent( roomTitle = roomTitle, roomAvatar = roomAvatar, @@ -99,33 +105,18 @@ fun MessagesScreen( Timber.v("onClick on timeline item: ${it.id}") }, onLongClick = { + focusManager.clearFocus(force = true) viewModel.computeActionsSheetState(it) coroutineScope.launch { - actionsSheetState.show() + itemActionsBottomSheetState.show() } }, snackbarHostState = snackbarHostState, ) - val itemActionsSheetState by viewModel.collectAsState(prop1 = MessagesViewState::itemActionsSheetState) TimelineItemActionsScreen( - sheetState = actionsSheetState, - actionsSheetState = itemActionsSheetState(), - onActionClicked = { - viewModel.handleItemAction(it) - coroutineScope.launch { - val targetEvent = viewModel.getTargetEvent() - when (it) { - is MessagesItemAction.Edit -> { - // Entering Edit mode, update the text in the composer. - val newComposerText = - (targetEvent?.content as? MessagesTimelineItemTextBasedContent)?.body.orEmpty() - composerViewModel.updateText(newComposerText) - } - else -> Unit - } - actionsSheetState.hide() - } - } + viewModel = viewModel, + composerViewModel = composerViewModel, + modalBottomSheetState = itemActionsBottomSheetState, ) snackBarContent?.let { coroutineScope.launch { @@ -185,7 +176,12 @@ fun MessagesScreenContent( composerText = composerText ) }, - snackbarHost = { SnackbarHost(snackbarHostState) }, + snackbarHost = { + SnackbarHost( + snackbarHostState, + modifier = Modifier.navigationBarsPadding() + ) + }, ) } diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesViewModel.kt b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesViewModel.kt index 18570f1581..a37bd153f0 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesViewModel.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesViewModel.kt @@ -2,6 +2,7 @@ package io.element.android.x.features.messages import com.airbnb.mvrx.MavericksViewModel import com.airbnb.mvrx.MavericksViewModelFactory +import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext import io.element.android.x.designsystem.components.avatar.AvatarData import io.element.android.x.designsystem.components.avatar.AvatarSize @@ -92,12 +93,8 @@ class MessagesViewModel( return currentState.itemActionsSheetState.invoke()?.targetItem } - fun handleItemAction(action: MessagesItemAction) { + fun handleItemAction(action: MessagesItemAction, targetEvent: MessagesTimelineItemState.MessageEvent) { viewModelScope.launch(Dispatchers.Default) { - val currentState = awaitState() - Timber.v("Handle $action for ${currentState.itemActionsSheetState}") - val targetEvent = getTargetEvent() - ?: return@launch when (action) { MessagesItemAction.Copy -> notImplementedYet() MessagesItemAction.Forward -> notImplementedYet() @@ -152,7 +149,11 @@ class MessagesViewModel( } } - fun computeActionsSheetState(messagesTimelineItemState: MessagesTimelineItemState.MessageEvent) { + fun computeActionsSheetState(messagesTimelineItemState: MessagesTimelineItemState.MessageEvent?) { + if (messagesTimelineItemState == null) { + setState { copy(itemActionsSheetState = Uninitialized) } + return + } suspend { val actions = if (messagesTimelineItemState.content is MessagesTimelineItemRedactedContent) { diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/components/MessagesTimelineItemActionsSheet.kt b/features/messages/src/main/java/io/element/android/x/features/messages/components/MessagesTimelineItemActionsSheet.kt index 7f84bda256..3d1b6cede9 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/components/MessagesTimelineItemActionsSheet.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/components/MessagesTimelineItemActionsSheet.kt @@ -7,29 +7,71 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.* -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.unit.dp +import com.airbnb.mvrx.compose.collectAsState import io.element.android.x.designsystem.components.VectorIcon +import io.element.android.x.features.messages.MessagesViewModel import io.element.android.x.features.messages.model.MessagesItemAction import io.element.android.x.features.messages.model.MessagesItemActionsSheetState +import io.element.android.x.features.messages.model.MessagesTimelineItemState +import io.element.android.x.features.messages.model.MessagesViewState +import io.element.android.x.features.messages.model.content.MessagesTimelineItemTextBasedContent +import io.element.android.x.features.messages.textcomposer.MessageComposerViewModel +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.launch @Composable fun TimelineItemActionsScreen( - sheetState: ModalBottomSheetState, - actionsSheetState: MessagesItemActionsSheetState?, - onActionClicked: (MessagesItemAction) -> Unit, + viewModel: MessagesViewModel, + composerViewModel: MessageComposerViewModel, + modalBottomSheetState: ModalBottomSheetState, modifier: Modifier = Modifier ) { + val coroutineScope = rememberCoroutineScope() + LaunchedEffect(modalBottomSheetState) { + snapshotFlow { modalBottomSheetState.currentValue } + .filter { it == ModalBottomSheetValue.Hidden } + .collect { + viewModel.computeActionsSheetState(null) + } + } + + val itemActionsSheetState by viewModel.collectAsState(MessagesViewState::itemActionsSheetState) + + fun onItemActionClicked( + itemAction: MessagesItemAction, + targetItem: MessagesTimelineItemState.MessageEvent + ) { + viewModel.handleItemAction(itemAction, targetItem) + coroutineScope.launch { + val targetEvent = viewModel.getTargetEvent() + when (itemAction) { + is MessagesItemAction.Edit -> { + // Entering Edit mode, update the text in the composer. + val newComposerText = + (targetEvent?.content as? MessagesTimelineItemTextBasedContent)?.body.orEmpty() + composerViewModel.updateText(newComposerText) + } + else -> Unit + } + modalBottomSheetState.hide() + } + } + + ModalBottomSheetLayout( modifier = modifier, - sheetState = sheetState, + sheetState = modalBottomSheetState, sheetContent = { SheetContent( - actionsSheetState = actionsSheetState, - onActionClicked = onActionClicked, - modifier = Modifier.navigationBarsPadding() + actionsSheetState = itemActionsSheetState(), + onActionClicked = ::onItemActionClicked, + modifier = Modifier.navigationBarsPadding().imePadding() ) } ) {} @@ -39,7 +81,7 @@ fun TimelineItemActionsScreen( @Composable private fun SheetContent( actionsSheetState: MessagesItemActionsSheetState?, - onActionClicked: (MessagesItemAction) -> Unit, + onActionClicked: (MessagesItemAction, MessagesTimelineItemState.MessageEvent) -> Unit, modifier: Modifier = Modifier ) { if (actionsSheetState == null || actionsSheetState.actions.isEmpty()) { @@ -54,7 +96,7 @@ private fun SheetContent( items(actionsSheetState.actions) { ListItem( modifier = Modifier.clickable { - onActionClicked(it) + onActionClicked(it, actionsSheetState.targetItem) }, text = { Text(