Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow swipe to reply (WPB-982) #2955

Merged
merged 9 commits into from
May 2, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -888,6 +890,7 @@ private fun ConversationScreenContent(
onResetSessionClicked = onResetSessionClicked,
onSelfDeletingMessageRead = onSelfDeletingMessageRead,
onShowEditingOption = onShowEditingOptions,
onSwipedToReply = onSwipedToReply,
conversationDetailsData = conversationDetailsData,
onFailedMessageCancelClicked = onFailedMessageCancelClicked,
onFailedMessageRetryClicked = onFailedMessageRetryClicked,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -1054,6 +1058,7 @@ fun MessageList(
onAudioClick = onAudioItemClicked,
onChangeAudioPosition = onChangeAudioPosition,
onLongClicked = onShowEditingOption,
onSwipedToReply = onSwipedToReply,
onAssetMessageClicked = onAssetItemClicked,
onImageMessageClicked = onImageFullScreenMode,
onOpenProfile = onOpenProfile,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,8 @@ private fun AssetMessagesListContent(
defaultBackgroundColor = colorsScheme().backgroundVariant,
shouldDisplayMessageStatus = false,
shouldDisplayFooter = false,
onReplyClickable = null
onReplyClickable = null,
onSwipedToReply = { }
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ fun MessageContainerItem(
audioMessagesState: PersistentMap<String, AudioState>,
assetStatus: AssetTransferStatus? = null,
onLongClicked: (UIMessage.Regular) -> Unit,
onSwipedToReply: (UIMessage.Regular) -> Unit,
onAssetMessageClicked: (String) -> Unit,
onAudioClick: (String) -> Unit,
onChangeAudioPosition: (String, Int) -> Unit,
Expand Down Expand Up @@ -142,6 +143,7 @@ fun MessageContainerItem(
onAudioClick = onAudioClick,
onChangeAudioPosition = onChangeAudioPosition,
onLongClicked = onLongClicked,
onSwipedToReply = onSwipedToReply,
onAssetMessageClicked = onAssetMessageClicked,
onImageMessageClicked = onImageMessageClicked,
onOpenProfile = onOpenProfile,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,32 +18,53 @@

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
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.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 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
Expand Down Expand Up @@ -84,6 +105,8 @@ 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
@Suppress("ComplexMethod")
Expand All @@ -96,6 +119,7 @@ fun RegularMessageItem(
audioMessagesState: PersistentMap<String, AudioState>,
assetStatus: AssetTransferStatus? = null,
onLongClicked: (UIMessage.Regular) -> Unit,
onSwipedToReply: (UIMessage.Regular) -> Unit = {},
onAssetMessageClicked: (String) -> Unit,
onAudioClick: (String) -> Unit,
onChangeAudioPosition: (String, Int) -> Unit,
Expand All @@ -114,8 +138,12 @@ fun RegularMessageItem(
useSmallBottomPadding: Boolean = false,
currentTimeInMillisFlow: Flow<Long> = 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,
Expand Down Expand Up @@ -240,6 +268,95 @@ 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 haptic = LocalHapticFeedback.current
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,
density,
positionalThreshold = { distance: Float -> distance * 0.33f },
confirmValueChange = { changedValue ->
if (changedValue == SwipeToDismissBoxValue.StartToEnd) {
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,
enableDismissFromStartToEnd = isSwipable,
content = content,
enableDismissFromEndToStart = false,
backgroundContent = {
Row(
modifier = Modifier.fillMaxSize()
.drawBehind {
// TODO(RTL): Might need adjusting once RTL is supported (also lacking in SwipeToDismissBox)
drawRect(
color = primaryColor,
topLeft = Offset(0f, 0f),
size = Size(dismissState.requireOffset().absoluteValue, size.height),
)
},
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start
) {
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
) {
val adjustedProgress = min(1f, (dismissState.progress / progressUntilAnimationCompletion))
val iconSize = dimensions().fabIconSize
val spacing = dimensions().spacing16x
val progress = FastOutLinearInEasing.transform(adjustedProgress)
val xOffset = with(density) {
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) {
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().onPrimary
)
}
}
}
)
}

@Composable
fun EphemeralMessageExpiredLabel(isSelfMessage: Boolean, conversationDetailsData: ConversationDetailsData) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +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 ||
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
}
Expand Down Expand Up @@ -133,8 +138,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(
Expand Down Expand Up @@ -165,7 +170,7 @@ sealed class MessageFlowStatus {
)
}

object Delivered : MessageFlowStatus()
data object Delivered : MessageFlowStatus()

data class Read(val count: Long) : MessageFlowStatus()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ fun SearchConversationMessagesResultsScreen(
shouldDisplayMessageStatus = false,
shouldDisplayFooter = false,
onReplyClickable = null,
onSwipedToReply = {}
)
}

Expand Down
Loading