From b0703bf46676eb4737c4b8c4df130ca2e87bd296 Mon Sep 17 00:00:00 2001 From: Vitor Hugo Schwaab Date: Tue, 30 Apr 2024 18:25:34 +0200 Subject: [PATCH 1/8] feat: allow swiping to reply Users can swipe a message to the start of the screen in order to reply to it --- .../home/conversations/ConversationScreen.kt | 5 ++ .../conversations/media/FileAssetsContent.kt | 3 +- .../messages/item/MessageContainerItem.kt | 2 + .../messages/item/MessageItemTemplate.kt | 3 +- .../messages/item/RegularMessageItem.kt | 89 ++++++++++++++++++- .../ui/home/conversations/model/UIMessage.kt | 10 ++- ...SearchConversationMessagesResultsScreen.kt | 1 + 7 files changed, 106 insertions(+), 7 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt index 06e54bde0e1..6e75593d2eb 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt @@ -799,6 +799,7 @@ private fun ConversationScreen( onOpenProfile = onOpenProfile, onUpdateConversationReadDate = onUpdateConversationReadDate, onShowEditingOptions = conversationScreenState::showEditContextMenu, + onSwipedToReply = messageComposerStateHolder::toReply, onSelfDeletingMessageRead = onSelfDeletingMessageRead, onFailedMessageCancelClicked = remember { { onDeleteMessage(it, false) } }, onFailedMessageRetryClicked = onFailedMessageRetryClicked, @@ -847,6 +848,7 @@ private fun ConversationScreenContent( onOpenProfile: (String) -> Unit, onUpdateConversationReadDate: (String) -> Unit, onShowEditingOptions: (UIMessage.Regular) -> Unit, + onSwipedToReply: (UIMessage.Regular) -> Unit, onSelfDeletingMessageRead: (UIMessage) -> Unit, conversationDetailsData: ConversationDetailsData, onFailedMessageRetryClicked: (String, ConversationId) -> Unit, @@ -888,6 +890,7 @@ private fun ConversationScreenContent( onResetSessionClicked = onResetSessionClicked, onSelfDeletingMessageRead = onSelfDeletingMessageRead, onShowEditingOption = onShowEditingOptions, + onSwipedToReply = onSwipedToReply, conversationDetailsData = conversationDetailsData, onFailedMessageCancelClicked = onFailedMessageCancelClicked, onFailedMessageRetryClicked = onFailedMessageRetryClicked, @@ -961,6 +964,7 @@ fun MessageList( onReactionClicked: (String, String) -> Unit, onResetSessionClicked: (senderUserId: UserId, clientId: String?) -> Unit, onShowEditingOption: (UIMessage.Regular) -> Unit, + onSwipedToReply: (UIMessage.Regular) -> Unit, onSelfDeletingMessageRead: (UIMessage) -> Unit, conversationDetailsData: ConversationDetailsData, onFailedMessageRetryClicked: (String, ConversationId) -> Unit, @@ -1054,6 +1058,7 @@ fun MessageList( onAudioClick = onAudioItemClicked, onChangeAudioPosition = onChangeAudioPosition, onLongClicked = onShowEditingOption, + onSwipedToReply = onSwipedToReply, onAssetMessageClicked = onAssetItemClicked, onImageMessageClicked = onImageFullScreenMode, onOpenProfile = onOpenProfile, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/FileAssetsContent.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/FileAssetsContent.kt index 8eebbdd90f7..17be9d60ed9 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/FileAssetsContent.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/FileAssetsContent.kt @@ -134,7 +134,8 @@ private fun AssetMessagesListContent( defaultBackgroundColor = colorsScheme().backgroundVariant, shouldDisplayMessageStatus = false, shouldDisplayFooter = false, - onReplyClickable = null + onReplyClickable = null, + onSwipedToReply = { } ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/MessageContainerItem.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/MessageContainerItem.kt index b23a89dbd5e..f8cb5cc70d2 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/MessageContainerItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/MessageContainerItem.kt @@ -60,6 +60,7 @@ fun MessageContainerItem( audioMessagesState: PersistentMap, assetStatus: AssetTransferStatus? = null, onLongClicked: (UIMessage.Regular) -> Unit, + onSwipedToReply: (UIMessage.Regular) -> Unit, onAssetMessageClicked: (String) -> Unit, onAudioClick: (String) -> Unit, onChangeAudioPosition: (String, Int) -> Unit, @@ -142,6 +143,7 @@ fun MessageContainerItem( onAudioClick = onAudioClick, onChangeAudioPosition = onChangeAudioPosition, onLongClicked = onLongClicked, + onSwipedToReply = onSwipedToReply, onAssetMessageClicked = onAssetMessageClicked, onImageMessageClicked = onImageMessageClicked, onOpenProfile = onOpenProfile, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/MessageItemTemplate.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/MessageItemTemplate.kt index fea61a7533a..cf33b0cda93 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/MessageItemTemplate.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/MessageItemTemplate.kt @@ -34,11 +34,12 @@ fun MessageItemTemplate( showAuthor: Boolean = true, useSmallBottomPadding: Boolean = false, fullAvatarOuterPadding: Dp, + modifier: Modifier = Modifier, leading: @Composable () -> Unit, content: @Composable () -> Unit ) { Row( - Modifier + modifier .fillMaxWidth() .padding( end = dimensions().messageItemHorizontalPadding, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt index e6d2fed4e62..5a9c2a60914 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt @@ -18,16 +18,26 @@ package com.wire.android.ui.home.conversations.messages.item +import androidx.compose.animation.core.FastOutLinearInEasing import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SwipeToDismissBox +import androidx.compose.material3.SwipeToDismissBoxState +import androidx.compose.material3.SwipeToDismissBoxValue import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -36,14 +46,18 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.IntOffset import com.wire.android.R import com.wire.android.media.audiomessage.AudioState import com.wire.android.model.Clickable import com.wire.android.ui.common.LegalHoldIndicator import com.wire.android.ui.common.StatusBox import com.wire.android.ui.common.UserBadge +import com.wire.android.ui.common.colorsScheme import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.spacers.HorizontalSpace import com.wire.android.ui.common.spacers.VerticalSpace @@ -84,8 +98,10 @@ import com.wire.kalium.logic.data.user.UserId import kotlinx.collections.immutable.PersistentMap import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow +import kotlin.math.min // TODO: a definite candidate for a refactor and cleanup +@OptIn(ExperimentalMaterial3Api::class) @Suppress("ComplexMethod") @Composable fun RegularMessageItem( @@ -96,6 +112,7 @@ fun RegularMessageItem( audioMessagesState: PersistentMap, assetStatus: AssetTransferStatus? = null, onLongClicked: (UIMessage.Regular) -> Unit, + onSwipedToReply: (UIMessage.Regular) -> Unit = {}, onAssetMessageClicked: (String) -> Unit, onAudioClick: (String) -> Unit, onChangeAudioPosition: (String, Int) -> Unit, @@ -114,8 +131,12 @@ fun RegularMessageItem( useSmallBottomPadding: Boolean = false, currentTimeInMillisFlow: Flow = flow { }, selfDeletionTimerState: SelfDeletionTimerHelper.SelfDeletionTimerState = SelfDeletionTimerHelper.SelfDeletionTimerState.NotExpirable -) { - with(message) { +): Unit = with(message) { + val onSwipe = remember(message) { { onSwipedToReply(message) } } + SwipableToReplyBox( + isSwipable = isReplyable, + onSwipedToReply = onSwipe + ) { MessageItemTemplate( showAuthor, useSmallBottomPadding = useSmallBottomPadding, @@ -240,6 +261,70 @@ fun RegularMessageItem( } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun SwipableToReplyBox( + isSwipable: Boolean, + modifier: Modifier = Modifier, + onSwipedToReply: () -> Unit = {}, + content: @Composable RowScope.() -> Unit +) { + val density = LocalDensity.current + val dismissState = remember { + SwipeToDismissBoxState( + SwipeToDismissBoxValue.Settled, + density, + positionalThreshold = { distance: Float -> distance * 0.33f }, + confirmValueChange = { changedValue -> + if (changedValue == SwipeToDismissBoxValue.EndToStart) { + onSwipedToReply() + } + // Go back to rest position + changedValue == SwipeToDismissBoxValue.Settled + } + ) + } + SwipeToDismissBox( + state = dismissState, + modifier = modifier, + enableDismissFromStartToEnd = false, + content = content, + enableDismissFromEndToStart = isSwipable, + backgroundContent = { + Row( + modifier = Modifier.fillMaxSize(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End + ) { + if (dismissState.dismissDirection == SwipeToDismissBoxValue.EndToStart + && dismissState.progress < 1f + ) { + // Finish the animation in the first 33% of the drag + val progressUntilAnimationCompletion = 0.33f + val adjustedProgress = min(1f, (dismissState.progress / progressUntilAnimationCompletion)) + val iconSize = dimensions().fabIconSize + val spacing = dimensions().spacing16x + val progress = FastOutLinearInEasing.transform(adjustedProgress) + val xOffset = with(LocalDensity.current) { + val offsetFromScreenEnd = spacing.toPx() + val offsetAfterScreenEnd = iconSize.toPx() + val totalTravelDistance = offsetFromScreenEnd + offsetAfterScreenEnd + offsetAfterScreenEnd - (totalTravelDistance * progress) + } + Icon( + painter = painterResource(id = R.drawable.ic_reply), + contentDescription = "", + modifier = Modifier + .size(iconSize) + .offset { IntOffset(xOffset.toInt(), 0) }, + tint = colorsScheme().onBackground + ) + } + } + } + ) +} + @Composable fun EphemeralMessageExpiredLabel(isSelfMessage: Boolean, conversationDetailsData: ConversationDetailsData) { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIMessage.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIMessage.kt index ea03e70aaa1..f8416655b98 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIMessage.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIMessage.kt @@ -77,6 +77,10 @@ sealed interface UIMessage { val isAssetMessage = messageContent is UIMessageContent.AssetMessage || messageContent is UIMessageContent.ImageMessage || messageContent is UIMessageContent.AudioAssetMessage + val isReplyable = messageContent is UIMessageContent.TextMessage && + (header.messageStatus.flowStatus is MessageFlowStatus.Delivered || + header.messageStatus.flowStatus is MessageFlowStatus.Sent || + header.messageStatus.flowStatus is MessageFlowStatus.Read) val isTextContentWithoutQuote = messageContent is UIMessageContent.TextMessage && messageContent.messageBody.quotedMessage == null val isLocation: Boolean = messageContent is UIMessageContent.Location } @@ -133,8 +137,8 @@ sealed class MessageEditStatus { sealed class MessageFlowStatus { - object Sending : MessageFlowStatus() - object Sent : MessageFlowStatus() + data object Sending : MessageFlowStatus() + data object Sent : MessageFlowStatus() sealed class Failure(val errorText: UIText) : MessageFlowStatus() { sealed class Send(errorText: UIText) : Failure(errorText) { data class Locally(val isEdited: Boolean) : Send( @@ -165,7 +169,7 @@ sealed class MessageFlowStatus { ) } - object Delivered : MessageFlowStatus() + data object Delivered : MessageFlowStatus() data class Read(val count: Long) : MessageFlowStatus() } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesResultsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesResultsScreen.kt index 86481b84862..4851ef9db54 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesResultsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesResultsScreen.kt @@ -71,6 +71,7 @@ fun SearchConversationMessagesResultsScreen( shouldDisplayMessageStatus = false, shouldDisplayFooter = false, onReplyClickable = null, + onSwipedToReply = {} ) } From 5b2d2d47d3a9de57837217d3a6ddf0856865d9e1 Mon Sep 17 00:00:00 2001 From: Vitor Hugo Schwaab Date: Thu, 2 May 2024 11:57:00 +0200 Subject: [PATCH 2/8] chore: change swipe background color --- .../messages/item/RegularMessageItem.kt | 48 ++++++++++++++++--- 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt index 5a9c2a60914..2147b18f985 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt @@ -42,15 +42,24 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp import com.wire.android.R import com.wire.android.media.audiomessage.AudioState import com.wire.android.model.Clickable @@ -98,10 +107,10 @@ import com.wire.kalium.logic.data.user.UserId import kotlinx.collections.immutable.PersistentMap import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow +import kotlin.math.absoluteValue import kotlin.math.min // TODO: a definite candidate for a refactor and cleanup -@OptIn(ExperimentalMaterial3Api::class) @Suppress("ComplexMethod") @Composable fun RegularMessageItem( @@ -270,6 +279,13 @@ private fun SwipableToReplyBox( content: @Composable RowScope.() -> Unit ) { val density = LocalDensity.current + val haptic = LocalHapticFeedback.current + val configuration = LocalConfiguration.current + val screenWidth = with(density) { configuration.screenWidthDp.dp.toPx() } + var didVibrateOnCurrentDrag by remember { mutableStateOf(false) } + + // Finish the animation in the first 25% of the drag + val progressUntilAnimationCompletion = 0.25f val dismissState = remember { SwipeToDismissBoxState( SwipeToDismissBoxValue.Settled, @@ -279,11 +295,18 @@ private fun SwipableToReplyBox( if (changedValue == SwipeToDismissBoxValue.EndToStart) { onSwipedToReply() } + if (changedValue == SwipeToDismissBoxValue.Settled) { + // Reset the haptic feedback when drag is stopped + didVibrateOnCurrentDrag = false + } // Go back to rest position changedValue == SwipeToDismissBoxValue.Settled } ) } + val primaryColor = colorsScheme().primary + // TODO: RTL is currently broken https://issuetracker.google.com/issues/321600474 + // Maybe addressed in compose3 1.3.0 (currently in alpha) SwipeToDismissBox( state = dismissState, modifier = modifier, @@ -292,32 +315,45 @@ private fun SwipableToReplyBox( enableDismissFromEndToStart = isSwipable, backgroundContent = { Row( - modifier = Modifier.fillMaxSize(), + modifier = Modifier.fillMaxSize() + .drawBehind { + // TODO(RTL): Might need adjusting once RTL is supported (also lacking in SwipeToDismissBox) + drawRect( + color = primaryColor, + topLeft = Offset(screenWidth + dismissState.requireOffset(), 0f), + size = Size(dismissState.requireOffset().absoluteValue, size.height), + ) + }, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.End ) { if (dismissState.dismissDirection == SwipeToDismissBoxValue.EndToStart + // Sometimes this is called with progress 1f when the user stops the interaction, causing a blink. + // Ignore these cases as it doesn't make any difference && dismissState.progress < 1f ) { - // Finish the animation in the first 33% of the drag - val progressUntilAnimationCompletion = 0.33f val adjustedProgress = min(1f, (dismissState.progress / progressUntilAnimationCompletion)) val iconSize = dimensions().fabIconSize val spacing = dimensions().spacing16x val progress = FastOutLinearInEasing.transform(adjustedProgress) - val xOffset = with(LocalDensity.current) { + val xOffset = with(density) { val offsetFromScreenEnd = spacing.toPx() val offsetAfterScreenEnd = iconSize.toPx() val totalTravelDistance = offsetFromScreenEnd + offsetAfterScreenEnd offsetAfterScreenEnd - (totalTravelDistance * progress) } + // Got to the end, user can release to + if (progress == 1f && !didVibrateOnCurrentDrag) { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + didVibrateOnCurrentDrag = true + } Icon( painter = painterResource(id = R.drawable.ic_reply), contentDescription = "", modifier = Modifier .size(iconSize) .offset { IntOffset(xOffset.toInt(), 0) }, - tint = colorsScheme().onBackground + tint = colorsScheme().onPrimary ) } } From 81316cb7440906937830f5604f62aa5fed716d3a Mon Sep 17 00:00:00 2001 From: Vitor Hugo Schwaab Date: Thu, 2 May 2024 12:00:54 +0200 Subject: [PATCH 3/8] style: better split condition grouping --- .../wire/android/ui/home/conversations/model/UIMessage.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIMessage.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIMessage.kt index f8416655b98..68c731f2233 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIMessage.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIMessage.kt @@ -77,10 +77,11 @@ sealed interface UIMessage { val isAssetMessage = messageContent is UIMessageContent.AssetMessage || messageContent is UIMessageContent.ImageMessage || messageContent is UIMessageContent.AudioAssetMessage - val isReplyable = messageContent is UIMessageContent.TextMessage && - (header.messageStatus.flowStatus is MessageFlowStatus.Delivered || + val isReplyable = messageContent is UIMessageContent.TextMessage && ( + header.messageStatus.flowStatus is MessageFlowStatus.Delivered || header.messageStatus.flowStatus is MessageFlowStatus.Sent || - header.messageStatus.flowStatus is MessageFlowStatus.Read) + header.messageStatus.flowStatus is MessageFlowStatus.Read + ) val isTextContentWithoutQuote = messageContent is UIMessageContent.TextMessage && messageContent.messageBody.quotedMessage == null val isLocation: Boolean = messageContent is UIMessageContent.Location } From ac4885dfd046c43009c9ad8e451dfbb77947325f Mon Sep 17 00:00:00 2001 From: Vitor Hugo Schwaab Date: Thu, 2 May 2024 13:58:00 +0200 Subject: [PATCH 4/8] chore: revert the swipe direction --- .../messages/item/RegularMessageItem.kt | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt index 2147b18f985..a37c0c28e1d 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt @@ -292,7 +292,7 @@ private fun SwipableToReplyBox( density, positionalThreshold = { distance: Float -> distance * 0.33f }, confirmValueChange = { changedValue -> - if (changedValue == SwipeToDismissBoxValue.EndToStart) { + if (changedValue == SwipeToDismissBoxValue.StartToEnd) { onSwipedToReply() } if (changedValue == SwipeToDismissBoxValue.Settled) { @@ -310,9 +310,9 @@ private fun SwipableToReplyBox( SwipeToDismissBox( state = dismissState, modifier = modifier, - enableDismissFromStartToEnd = false, + enableDismissFromStartToEnd = isSwipable, content = content, - enableDismissFromEndToStart = isSwipable, + enableDismissFromEndToStart = false, backgroundContent = { Row( modifier = Modifier.fillMaxSize() @@ -320,14 +320,14 @@ private fun SwipableToReplyBox( // TODO(RTL): Might need adjusting once RTL is supported (also lacking in SwipeToDismissBox) drawRect( color = primaryColor, - topLeft = Offset(screenWidth + dismissState.requireOffset(), 0f), + topLeft = Offset(0f, 0f), size = Size(dismissState.requireOffset().absoluteValue, size.height), ) }, verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.End + horizontalArrangement = Arrangement.Start ) { - if (dismissState.dismissDirection == SwipeToDismissBoxValue.EndToStart + if (dismissState.dismissDirection == SwipeToDismissBoxValue.StartToEnd // Sometimes this is called with progress 1f when the user stops the interaction, causing a blink. // Ignore these cases as it doesn't make any difference && dismissState.progress < 1f @@ -337,10 +337,10 @@ private fun SwipableToReplyBox( val spacing = dimensions().spacing16x val progress = FastOutLinearInEasing.transform(adjustedProgress) val xOffset = with(density) { - val offsetFromScreenEnd = spacing.toPx() - val offsetAfterScreenEnd = iconSize.toPx() - val totalTravelDistance = offsetFromScreenEnd + offsetAfterScreenEnd - offsetAfterScreenEnd - (totalTravelDistance * progress) + val offsetBeforeScreenStart = iconSize.toPx() + val offsetAfterScreenStart = spacing.toPx() + val totalTravelDistance = offsetBeforeScreenStart + offsetAfterScreenStart + -offsetBeforeScreenStart + (totalTravelDistance * progress) } // Got to the end, user can release to if (progress == 1f && !didVibrateOnCurrentDrag) { From b026f887cd148a522051cf529c7df445fca9159c Mon Sep 17 00:00:00 2001 From: Vitor Hugo Schwaab Date: Thu, 2 May 2024 14:27:21 +0200 Subject: [PATCH 5/8] style: remove unused code --- .../ui/home/conversations/messages/item/RegularMessageItem.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt index a37c0c28e1d..a29aefbf63a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt @@ -51,7 +51,6 @@ import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalHapticFeedback @@ -59,7 +58,6 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.dp import com.wire.android.R import com.wire.android.media.audiomessage.AudioState import com.wire.android.model.Clickable @@ -280,8 +278,6 @@ private fun SwipableToReplyBox( ) { val density = LocalDensity.current val haptic = LocalHapticFeedback.current - val configuration = LocalConfiguration.current - val screenWidth = with(density) { configuration.screenWidthDp.dp.toPx() } var didVibrateOnCurrentDrag by remember { mutableStateOf(false) } // Finish the animation in the first 25% of the drag From 6cd7303bdb7e3514d24fd470530913510c20e7bf Mon Sep 17 00:00:00 2001 From: Vitor Hugo Schwaab Date: Thu, 2 May 2024 15:31:27 +0200 Subject: [PATCH 6/8] fix: allow other messages to be replied to --- .../ui/home/conversations/model/UIMessage.kt | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIMessage.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIMessage.kt index 68c731f2233..c17fd3d4e1a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIMessage.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIMessage.kt @@ -74,15 +74,34 @@ sealed interface UIMessage { val isAvailable: Boolean = !isDeleted && !sendingFailed && !decryptionFailed override val isPending: Boolean = header.messageStatus.flowStatus == MessageFlowStatus.Sending val isMyMessage = source == MessageSource.Self + val isAssetMessage = messageContent is UIMessageContent.AssetMessage || messageContent is UIMessageContent.ImageMessage || messageContent is UIMessageContent.AudioAssetMessage - val isReplyable = messageContent is UIMessageContent.TextMessage && ( - header.messageStatus.flowStatus is MessageFlowStatus.Delivered || - header.messageStatus.flowStatus is MessageFlowStatus.Sent || - header.messageStatus.flowStatus is MessageFlowStatus.Read - ) + + private val isReplyableContent: Boolean + get() = messageContent is UIMessageContent.TextMessage || + messageContent is UIMessageContent.AssetMessage || + messageContent is UIMessageContent.AudioAssetMessage || + messageContent is UIMessageContent.Location || + messageContent is UIMessageContent.Regular + + /** + * The message was sent from the sender (either self or others), and is available for other users + * to retrieve from the backend, or is already retrieved. + */ + private val isTheMessageAvailableToOtherUsers: Boolean + get() = header.messageStatus.flowStatus is MessageFlowStatus.Delivered || + header.messageStatus.flowStatus is MessageFlowStatus.Sent || + header.messageStatus.flowStatus is MessageFlowStatus.Read + + val isReplyable: Boolean + get() = isReplyableContent && + isTheMessageAvailableToOtherUsers && + header.messageStatus.expirationStatus is ExpirationStatus.NotExpirable + val isTextContentWithoutQuote = messageContent is UIMessageContent.TextMessage && messageContent.messageBody.quotedMessage == null + val isLocation: Boolean = messageContent is UIMessageContent.Location } From bd7f768c4d48f0df4cf3bc174bd040ff414c1194 Mon Sep 17 00:00:00 2001 From: Vitor Hugo Schwaab Date: Thu, 2 May 2024 15:59:17 +0200 Subject: [PATCH 7/8] fix: align positionalThreshold to match visual --- .../ui/home/conversations/messages/item/RegularMessageItem.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt index a29aefbf63a..ba5184a67a1 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt @@ -286,7 +286,7 @@ private fun SwipableToReplyBox( SwipeToDismissBoxState( SwipeToDismissBoxValue.Settled, density, - positionalThreshold = { distance: Float -> distance * 0.33f }, + positionalThreshold = { distance: Float -> distance * progressUntilAnimationCompletion }, confirmValueChange = { changedValue -> if (changedValue == SwipeToDismissBoxValue.StartToEnd) { onSwipedToReply() From 91d388618a90df165f33878b76fb7095941bfd36 Mon Sep 17 00:00:00 2001 From: Vitor Hugo Schwaab Date: Thu, 2 May 2024 16:01:46 +0200 Subject: [PATCH 8/8] docs: improve comment clarity --- .../ui/home/conversations/messages/item/RegularMessageItem.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt index ba5184a67a1..5758e4601b0 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt @@ -289,13 +289,14 @@ private fun SwipableToReplyBox( positionalThreshold = { distance: Float -> distance * progressUntilAnimationCompletion }, confirmValueChange = { changedValue -> if (changedValue == SwipeToDismissBoxValue.StartToEnd) { + // Attempt to finish dismiss, notify reply intention onSwipedToReply() } if (changedValue == SwipeToDismissBoxValue.Settled) { // Reset the haptic feedback when drag is stopped didVibrateOnCurrentDrag = false } - // Go back to rest position + // Reject state change, only allow returning back to rest position changedValue == SwipeToDismissBoxValue.Settled } )