diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/ChatContent.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/ChatContent.kt index 18db097b..34f9a147 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/ChatContent.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/ChatContent.kt @@ -444,26 +444,6 @@ fun ChatContent( .background(MaterialTheme.colorScheme.background) .onGloballyPositioned { containerSize = it.size } ) { - /*if (isDragToBackEnabled && !isTablet && !isCustomBackHandlingEnabled && dragOffsetX.value > 0 && previousChild != null) { - Box( - modifier = Modifier.fillMaxSize() - ) { - renderChild(previousChild) - Box( - modifier = Modifier - .fillMaxSize() - .background( - Color.Black.copy( - alpha = 0.3f * (1f - (dragOffsetX.value / containerSize.width.toFloat()).coerceIn( - 0f, - 1f - )) - ) - ) - ) - } - }*/ - Box( modifier = Modifier .fillMaxSize() diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentList.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentList.kt index d897f3dd..09182a0c 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentList.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentList.kt @@ -494,6 +494,7 @@ private fun MessageBubbleSwitcher( isAnyViewerOpen: Boolean = false ) { val isChannel = state.isChannel && state.currentTopicId == null + val isTopicClosed = state.topics.find { it.id.toLong() == state.currentTopicId }?.isClosed?: false when (item) { is GroupedMessageItem.Single -> { @@ -591,6 +592,8 @@ private fun MessageBubbleSwitcher( onCommentsClick = { component.onCommentsClick(it) }, toProfile = toProfile, onViaBotClick = onViaBotClick, + canReply = state.canWrite && !isSelectionMode, + onReplySwipe = { component.onReplyMessage(it) }, onYouTubeClick = { component.onOpenYouTube(it) }, onInstantViewClick = { component.onOpenInstantView(it) }, downloadUtils = downloadUtils, @@ -694,7 +697,7 @@ private fun MessageBubbleSwitcher( onPositionChange = { _, pos, size -> onMessagePositionChange(pos, size) }, toProfile = toProfile, onViaBotClick = onViaBotClick, - canReply = state.canWrite && !isSelectionMode, + canReply = state.canWrite && !isSelectionMode && (!isTopicClosed || state.isAdmin), onReplySwipe = { component.onReplyMessage(it) }, swipeEnabled = !isSelectionMode, downloadUtils = downloadUtils, @@ -762,7 +765,7 @@ private fun MessageBubbleSwitcher( onCommentsClick = { component.onCommentsClick(it) }, toProfile = toProfile, onViaBotClick = onViaBotClick, - canReply = state.canWrite && !isSelectionMode, + canReply = state.canWrite && !isSelectionMode && (!isTopicClosed || state.isAdmin), onReplySwipe = { component.onReplyMessage(it) }, swipeEnabled = !isSelectionMode, downloadUtils = downloadUtils, @@ -874,7 +877,8 @@ private fun RootMessageSection( onRetractVote = { component.onRetractVote(it) }, onShowVoters = { id, opt -> component.onShowVoters(id, opt) }, onClosePoll = { component.onClosePoll(it) }, - toProfile = toProfile, swipeEnabled = false, + toProfile = toProfile, + swipeEnabled = false, onViaBotClick = onViaBotClick, onInstantViewClick = { component.onOpenInstantView(it) }, onYouTubeClick = { component.onOpenYouTube(it) }, diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/AlbumMessageBubbleContainer.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/AlbumMessageBubbleContainer.kt index 91555392..a2a0da73 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/AlbumMessageBubbleContainer.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/AlbumMessageBubbleContainer.kt @@ -1,6 +1,7 @@ package org.monogram.presentation.features.chats.currentChat.components import android.content.res.Configuration +import androidx.compose.animation.core.Animatable import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.* import androidx.compose.material3.MaterialTheme @@ -14,6 +15,7 @@ import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.positionInWindow import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.toSize @@ -97,11 +99,21 @@ fun AlbumMessageBubbleContainer( var bubblePosition by remember { mutableStateOf(Offset.Zero) } var bubbleSize by remember { mutableStateOf(IntSize.Zero) } + val dragOffsetX = remember { Animatable(0f) } + Column( modifier = Modifier .fillMaxWidth() .onGloballyPositioned { outerColumnPosition = it.positionInWindow() } .padding(top = topSpacing, bottom = 2.dp) + .offset { IntOffset(dragOffsetX.value.toInt(), 0) } + .fastReplyPointer( + canReply = canReply, + dragOffsetX = dragOffsetX, + scope = rememberCoroutineScope(), + onReplySwipe = { onReplySwipe(messages.first()) }, + maxWidth = maxWidth.value + ) .pointerInput(Unit) { detectTapGestures( onTap = { offset -> @@ -137,109 +149,121 @@ fun AlbumMessageBubbleContainer( Spacer(modifier = Modifier.width(8.dp)) } - Column( - modifier = Modifier - .then(if (isChannel) Modifier.padding(horizontal = 8.dp) else Modifier) - .widthIn(max = maxWidth) - .then(if (isChannel) Modifier.fillMaxWidth() else Modifier) - .onGloballyPositioned { coordinates -> - bubblePosition = coordinates.positionInWindow() - bubbleSize = coordinates.size - if (shouldReportPosition) { - onPositionChange(lastMsg.id, bubblePosition, bubbleSize) + Box( + modifier = Modifier.wrapContentSize() + ) { + Column( + modifier = Modifier + .then(if (isChannel) Modifier.padding(horizontal = 8.dp) else Modifier) + .widthIn(max = maxWidth) + .then(if (isChannel) Modifier.fillMaxWidth() else Modifier) + .onGloballyPositioned { coordinates -> + bubblePosition = coordinates.positionInWindow() + bubbleSize = coordinates.size + if (shouldReportPosition) { + onPositionChange(lastMsg.id, bubblePosition, bubbleSize) + } } + ) { + if (isGroup && !isOutgoing && !isChannel) { + Text( + text = firstMsg.senderName, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 12.dp, bottom = 4.dp) + ) } - ) { - if (isGroup && !isOutgoing && !isChannel) { - Text( - text = firstMsg.senderName, - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(start = 12.dp, bottom = 4.dp) - ) - } - if (isChannel) { - ChannelAlbumMessageBubble( - messages = messages, - isSameSenderAbove = isSameSenderAbove, - isSameSenderBelow = isSameSenderBelow, - autoplayGifs = autoplayGifs, - autoplayVideos = autoplayVideos, - autoDownloadMobile = autoDownloadMobile, - autoDownloadWifi = autoDownloadWifi, - autoDownloadRoaming = autoDownloadRoaming, - onPhotoClick = onPhotoClick, - onDownloadPhoto = onDownloadPhoto, - onVideoClick = onVideoClick, - onDocumentClick = onDocumentClick, - onAudioClick = onAudioClick, - onCancelDownload = onCancelDownload, - onLongClick = { offset -> - onReplyClick( - bubblePosition, - bubbleSize, - bubblePosition + offset - ) - }, - onReplyClick = onGoToReply, - onReactionClick = { onReactionClick(lastMsg.id, it) }, - onCommentsClick = onCommentsClick, - showComments = showComments, - toProfile = toProfile, - modifier = Modifier.fillMaxWidth(), - fontSize = fontSize, - bubbleRadius = bubbleRadius, - downloadUtils = downloadUtils, - isAnyViewerOpen = isAnyViewerOpen - ) - } else { - ChatAlbumMessageBubble( - messages = messages, - isOutgoing = isOutgoing, - isGroup = isGroup, - isSameSenderAbove = isSameSenderAbove, - isSameSenderBelow = isSameSenderBelow, - autoplayGifs = autoplayGifs, - autoplayVideos = autoplayVideos, - autoDownloadMobile = autoDownloadMobile, - autoDownloadWifi = autoDownloadWifi, - autoDownloadRoaming = autoDownloadRoaming, - onPhotoClick = onPhotoClick, - onDownloadPhoto = onDownloadPhoto, - onVideoClick = onVideoClick, - onDocumentClick = onDocumentClick, - onAudioClick = onAudioClick, - onCancelDownload = onCancelDownload, - onLongClick = { offset -> - onReplyClick( - bubblePosition, - bubbleSize, - bubblePosition + offset - ) - }, - onReplyClick = onGoToReply, - onReactionClick = { onReactionClick(lastMsg.id, it) }, - toProfile = toProfile, - modifier = Modifier, - fontSize = fontSize, - downloadUtils = downloadUtils, - isAnyViewerOpen = isAnyViewerOpen - ) - } + if (isChannel) { + ChannelAlbumMessageBubble( + messages = messages, + isSameSenderAbove = isSameSenderAbove, + isSameSenderBelow = isSameSenderBelow, + autoplayGifs = autoplayGifs, + autoplayVideos = autoplayVideos, + autoDownloadMobile = autoDownloadMobile, + autoDownloadWifi = autoDownloadWifi, + autoDownloadRoaming = autoDownloadRoaming, + onPhotoClick = onPhotoClick, + onDownloadPhoto = onDownloadPhoto, + onVideoClick = onVideoClick, + onDocumentClick = onDocumentClick, + onAudioClick = onAudioClick, + onCancelDownload = onCancelDownload, + onLongClick = { offset -> + onReplyClick( + bubblePosition, + bubbleSize, + bubblePosition + offset + ) + }, + onReplyClick = onGoToReply, + onReactionClick = { onReactionClick(lastMsg.id, it) }, + onCommentsClick = onCommentsClick, + showComments = showComments, + toProfile = toProfile, + modifier = Modifier.fillMaxWidth(), + fontSize = fontSize, + bubbleRadius = bubbleRadius, + downloadUtils = downloadUtils, + isAnyViewerOpen = isAnyViewerOpen + ) + } else { + ChatAlbumMessageBubble( + messages = messages, + isOutgoing = isOutgoing, + isGroup = isGroup, + isSameSenderAbove = isSameSenderAbove, + isSameSenderBelow = isSameSenderBelow, + autoplayGifs = autoplayGifs, + autoplayVideos = autoplayVideos, + autoDownloadMobile = autoDownloadMobile, + autoDownloadWifi = autoDownloadWifi, + autoDownloadRoaming = autoDownloadRoaming, + onPhotoClick = onPhotoClick, + onDownloadPhoto = onDownloadPhoto, + onVideoClick = onVideoClick, + onDocumentClick = onDocumentClick, + onAudioClick = onAudioClick, + onCancelDownload = onCancelDownload, + onLongClick = { offset -> + onReplyClick( + bubblePosition, + bubbleSize, + bubblePosition + offset + ) + }, + onReplyClick = onGoToReply, + onReactionClick = { onReactionClick(lastMsg.id, it) }, + toProfile = toProfile, + modifier = Modifier, + fontSize = fontSize, + downloadUtils = downloadUtils, + isAnyViewerOpen = isAnyViewerOpen + ) + } - lastMsg.replyMarkup?.let { markup -> - ReplyMarkupView( - replyMarkup = markup, - onButtonClick = { onReplyMarkupButtonClick(lastMsg.id, it) } + lastMsg.replyMarkup?.let { markup -> + ReplyMarkupView( + replyMarkup = markup, + onButtonClick = { onReplyMarkupButtonClick(lastMsg.id, it) } + ) + } + + MessageViaBotAttribution( + msg = lastMsg, + isOutgoing = isOutgoing, + onViaBotClick = onViaBotClick, + modifier = Modifier.align(if (isOutgoing) Alignment.End else Alignment.Start) ) } - MessageViaBotAttribution( - msg = lastMsg, + FastReplyIndicator( + modifier = Modifier + .align(if (isOutgoing) Alignment.CenterEnd else Alignment.CenterStart), + dragOffsetX = dragOffsetX, isOutgoing = isOutgoing, - onViaBotClick = onViaBotClick, - modifier = Modifier.align(if (isOutgoing) Alignment.End else Alignment.Start) + maxWidth = maxWidth, ) } } diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/FastReplyIndicator.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/FastReplyIndicator.kt new file mode 100644 index 00000000..9cf04035 --- /dev/null +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/FastReplyIndicator.kt @@ -0,0 +1,142 @@ +package org.monogram.presentation.features.chats.currentChat.components + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.AnimationVector1D +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Reply +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.changedToUp +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.input.pointer.positionChange +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.lerp +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +const val REPLY_TRIGGER_FRACTION = 0.35f +const val MAX_SWIPE_FRACTION = 0.7f +const val ICON_OFFSET_FRACTION = 0.1f + +@Composable +fun FastReplyIndicator( + modifier: Modifier = Modifier, + dragOffsetX: Animatable, + isOutgoing: Boolean = false, + inverseOffset: Boolean = false, + maxWidth: Dp, +) { + val triggerDistance = maxWidth.value * REPLY_TRIGGER_FRACTION + val dragged = (-dragOffsetX.value).coerceAtLeast(0f) + val progress = ((dragged - 48.dp.value) / (triggerDistance - 48.dp.value)) + .coerceIn(0f, 1f) + + val iconAlpha by animateFloatAsState( + targetValue = progress, + animationSpec = tween(durationMillis = 150) + ) + val iconScale by animateFloatAsState( + targetValue = lerp(0.5f, 1f, progress), + animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessLow) + ) + val iconOffset = maxWidth * ICON_OFFSET_FRACTION + + if (dragged > 48.dp.value) { + Box( + modifier = modifier + .offset(x = if (isOutgoing) iconOffset else maxWidth) + .size(30.dp) + .graphicsLayer { + translationX = when { + isOutgoing -> (-dragOffsetX.value - iconOffset.value) * 0.5f + inverseOffset -> -iconOffset.value + else -> iconOffset.value + } + scaleX = iconScale + scaleY = iconScale + alpha = iconAlpha + } + .background( + color = MaterialTheme.colorScheme.surfaceContainerHigh.copy(alpha = 0.7f), + shape = CircleShape + ), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.Reply, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(18.dp) + ) + } + } +} + +fun Modifier.fastReplyPointer( + canReply: Boolean, + dragOffsetX: Animatable, + scope: CoroutineScope, + onReplySwipe: () -> Unit, + maxWidth: Float +): Modifier = pointerInput(canReply) { + if (!canReply) return@pointerInput + + awaitEachGesture { + val down = awaitFirstDown(requireUnconsumed = false) + var isDragging = false + var totalDragX = 0f + + while (down.pressed) { + val event = awaitPointerEvent(pass = PointerEventPass.Main) + val change = event.changes.firstOrNull { it.id == down.id } ?: break + + if (change.changedToUp()) break + + val deltaX = change.positionChange().x + totalDragX += deltaX + + if (!isDragging) { + if (totalDragX < -48.dp.toPx()) { + isDragging = true + } else if (totalDragX > 48.dp.toPx()) { + break + } + } + + if (isDragging) { + change.consume() + val newOffset = dragOffsetX.value + deltaX + scope.launch { + dragOffsetX.snapTo(newOffset.coerceIn(-(maxWidth * MAX_SWIPE_FRACTION), 0f)) + } + } + } + + if (isDragging) { + if (-dragOffsetX.value >= maxWidth * REPLY_TRIGGER_FRACTION) { + onReplySwipe() + } + scope.launch { + dragOffsetX.animateTo(0f, spring()) + } + } + } +} \ No newline at end of file diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/MessageBubbleContainer.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/MessageBubbleContainer.kt index e03b6b36..fa6ef60d 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/MessageBubbleContainer.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/MessageBubbleContainer.kt @@ -2,6 +2,7 @@ package org.monogram.presentation.features.chats.currentChat.components import android.content.res.Configuration import androidx.compose.animation.Animatable +import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.gestures.detectTapGestures @@ -18,6 +19,7 @@ import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.positionInWindow import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.toSize @@ -114,12 +116,22 @@ fun MessageBubbleContainer( var bubblePosition by remember { mutableStateOf(Offset.Zero) } var bubbleSize by remember { mutableStateOf(IntSize.Zero) } + val dragOffsetX = remember { Animatable(0f) } + Column( modifier = Modifier .fillMaxWidth() .background(animatedColor.value, RoundedCornerShape(12.dp)) .onGloballyPositioned { outerColumnPosition = it.positionInWindow() } .padding(top = topSpacing) + .offset { IntOffset(dragOffsetX.value.toInt(), 0) } + .fastReplyPointer( + canReply = canReply, + dragOffsetX = dragOffsetX, + scope = rememberCoroutineScope(), + onReplySwipe = { onReplySwipe(msg) }, + maxWidth = maxWidth.value + ) .pointerInput(Unit) { detectTapGestures( onTap = { offset -> @@ -152,70 +164,82 @@ fun MessageBubbleContainer( toProfile = toProfile ) - Column( - modifier = Modifier - .width(IntrinsicSize.Max) - .widthIn(max = maxWidth) - .onGloballyPositioned { coordinates -> - bubblePosition = coordinates.positionInWindow() - bubbleSize = coordinates.size - if (shouldReportPosition) { - onPositionChange(msg.id, bubblePosition, bubbleSize) - } - }, - horizontalAlignment = if (isOutgoing) Alignment.End else Alignment.Start + Box( + modifier = Modifier.wrapContentSize() ) { - MessageContentSelector( - msg = msg, - newerMsg = newerMsg, - isOutgoing = isOutgoing, - isSameSenderAbove = isSameSenderAbove, - isSameSenderBelow = isSameSenderBelow, - isGroup = isGroup, - fontSize = fontSize, - letterSpacing = letterSpacing, - bubbleRadius = bubbleRadius, - stSize = stSize, - autoDownloadMobile = autoDownloadMobile, - autoDownloadWifi = autoDownloadWifi, - autoDownloadRoaming = autoDownloadRoaming, - autoDownloadFiles = autoDownloadFiles, - autoplayGifs = autoplayGifs, - autoplayVideos = autoplayVideos, - showLinkPreviews = showLinkPreviews, - onPhotoClick = onPhotoClick, - onDownloadPhoto = onDownloadPhoto, - onVideoClick = onVideoClick, - onDocumentClick = onDocumentClick, - onAudioClick = onAudioClick, - onCancelDownload = onCancelDownload, - onReplyClick = onReplyClick, - onGoToReply = onGoToReply, - onReactionClick = onReactionClick, - onStickerClick = onStickerClick, - onPollOptionClick = onPollOptionClick, - onRetractVote = onRetractVote, - onShowVoters = onShowVoters, - onClosePoll = onClosePoll, - onInstantViewClick = onInstantViewClick, - onYouTubeClick = onYouTubeClick, - toProfile = toProfile, - bubblePosition = bubblePosition, - bubbleSize = bubbleSize, - downloadUtils = downloadUtils, - isAnyViewerOpen = isAnyViewerOpen - ) + Column( + modifier = Modifier + .width(IntrinsicSize.Max) + .widthIn(max = maxWidth) + .onGloballyPositioned { coordinates -> + bubblePosition = coordinates.positionInWindow() + bubbleSize = coordinates.size + if (shouldReportPosition) { + onPositionChange(msg.id, bubblePosition, bubbleSize) + } + }, + horizontalAlignment = if (isOutgoing) Alignment.End else Alignment.Start + ) { + MessageContentSelector( + msg = msg, + newerMsg = newerMsg, + isOutgoing = isOutgoing, + isSameSenderAbove = isSameSenderAbove, + isSameSenderBelow = isSameSenderBelow, + isGroup = isGroup, + fontSize = fontSize, + letterSpacing = letterSpacing, + bubbleRadius = bubbleRadius, + stSize = stSize, + autoDownloadMobile = autoDownloadMobile, + autoDownloadWifi = autoDownloadWifi, + autoDownloadRoaming = autoDownloadRoaming, + autoDownloadFiles = autoDownloadFiles, + autoplayGifs = autoplayGifs, + autoplayVideos = autoplayVideos, + showLinkPreviews = showLinkPreviews, + onPhotoClick = onPhotoClick, + onDownloadPhoto = onDownloadPhoto, + onVideoClick = onVideoClick, + onDocumentClick = onDocumentClick, + onAudioClick = onAudioClick, + onCancelDownload = onCancelDownload, + onReplyClick = onReplyClick, + onGoToReply = onGoToReply, + onReactionClick = onReactionClick, + onStickerClick = onStickerClick, + onPollOptionClick = onPollOptionClick, + onRetractVote = onRetractVote, + onShowVoters = onShowVoters, + onClosePoll = onClosePoll, + onInstantViewClick = onInstantViewClick, + onYouTubeClick = onYouTubeClick, + toProfile = toProfile, + bubblePosition = bubblePosition, + bubbleSize = bubbleSize, + downloadUtils = downloadUtils, + isAnyViewerOpen = isAnyViewerOpen + ) - MessageReplyMarkup( - msg = msg, - onReplyMarkupButtonClick = onReplyMarkupButtonClick - ) + MessageReplyMarkup( + msg = msg, + onReplyMarkupButtonClick = onReplyMarkupButtonClick + ) - MessageViaBotAttribution( - msg = msg, + MessageViaBotAttribution( + msg = msg, + isOutgoing = isOutgoing, + onViaBotClick = onViaBotClick, + modifier = Modifier.align(if (isOutgoing) Alignment.End else Alignment.Start) + ) + } + + FastReplyIndicator( + modifier = Modifier + .align(if (isOutgoing) Alignment.CenterEnd else Alignment.CenterStart), + dragOffsetX = dragOffsetX, isOutgoing = isOutgoing, - onViaBotClick = onViaBotClick, - modifier = Modifier.align(if (isOutgoing) Alignment.End else Alignment.Start) + maxWidth = maxWidth ) } } diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/channels/ChannelMessageBubbleContainer.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/channels/ChannelMessageBubbleContainer.kt index 1c742a7d..79bae263 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/channels/ChannelMessageBubbleContainer.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/channels/ChannelMessageBubbleContainer.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.positionInWindow import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.toSize @@ -27,7 +28,9 @@ import org.monogram.domain.models.MessageContent import org.monogram.domain.models.MessageModel import org.monogram.presentation.core.util.IDownloadUtils import org.monogram.presentation.features.chats.currentChat.chatContent.shouldShowDate +import org.monogram.presentation.features.chats.currentChat.components.FastReplyIndicator import org.monogram.presentation.features.chats.currentChat.components.chats.* +import org.monogram.presentation.features.chats.currentChat.components.fastReplyPointer @Composable fun ChannelMessageBubbleContainer( @@ -70,8 +73,10 @@ fun ChannelMessageBubbleContainer( showComments: Boolean = true, toProfile: (Long) -> Unit = {}, onViaBotClick: (String) -> Unit = {}, + canReply: Boolean = true, + onReplySwipe: (MessageModel) -> Unit = {}, downloadUtils: IDownloadUtils, - isAnyViewerOpen: Boolean = false + isAnyViewerOpen: Boolean = false, ) { val configuration = LocalConfiguration.current val screenWidth = configuration.screenWidthDp.dp @@ -104,12 +109,22 @@ fun ChannelMessageBubbleContainer( var bubblePosition by remember { mutableStateOf(Offset.Zero) } var bubbleSize by remember { mutableStateOf(IntSize.Zero) } + val dragOffsetX = remember { androidx.compose.animation.core.Animatable(0f) } + Column( modifier = Modifier .fillMaxWidth() .background(animatedColor.value, RoundedCornerShape(12.dp)) .onGloballyPositioned { outerColumnPosition = it.positionInWindow() } .padding(top = topSpacing) + .offset { IntOffset(dragOffsetX.value.toInt(), 0) } + .fastReplyPointer( + canReply = canReply, + dragOffsetX = dragOffsetX, + scope = rememberCoroutineScope(), + onReplySwipe = { onReplySwipe(msg) }, + maxWidth = maxWidth.value + ) .pointerInput(Unit) { detectTapGestures( onTap = { offset -> @@ -134,268 +149,279 @@ fun ChannelMessageBubbleContainer( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.Bottom ) { - Column( - modifier = Modifier - .padding(horizontal = 8.dp) - .widthIn(max = maxWidth) - .fillMaxWidth() - .onGloballyPositioned { coordinates -> - bubblePosition = coordinates.positionInWindow() - bubbleSize = coordinates.size - if (shouldReportPosition) { - onPositionChange(msg.id, bubblePosition, bubbleSize) - } - } + Box( + modifier = Modifier.wrapContentSize() ) { - when (val content = msg.content) { - is MessageContent.Text -> { - ChannelTextMessageBubble( - content = content, - msg = msg, - isSameSenderAbove = isSameSenderAbove, - isSameSenderBelow = isSameSenderBelow, - fontSize = fontSize, - letterSpacing = letterSpacing, - bubbleRadius = bubbleRadius, - showLinkPreviews = showLinkPreviews, - onReplyClick = onGoToReply, - onReactionClick = { onReactionClick(msg.id, it) }, - onInstantViewClick = onInstantViewClick, - onYouTubeClick = onYouTubeClick, - onClick = { offset -> - onReplyClick(bubblePosition, bubbleSize, bubblePosition + offset) - }, - onLongClick = { offset -> - onReplyClick(bubblePosition, bubbleSize, bubblePosition + offset) - }, - onCommentsClick = onCommentsClick, - showComments = showComments, - toProfile = toProfile, - modifier = Modifier.fillMaxWidth() - ) - } + Column( + modifier = Modifier + .padding(horizontal = 8.dp) + .widthIn(max = maxWidth) + .fillMaxWidth() + .onGloballyPositioned { coordinates -> + bubblePosition = coordinates.positionInWindow() + bubbleSize = coordinates.size + if (shouldReportPosition) { + onPositionChange(msg.id, bubblePosition, bubbleSize) + } + } + ) { + when (val content = msg.content) { + is MessageContent.Text -> { + ChannelTextMessageBubble( + content = content, + msg = msg, + isSameSenderAbove = isSameSenderAbove, + isSameSenderBelow = isSameSenderBelow, + fontSize = fontSize, + letterSpacing = letterSpacing, + bubbleRadius = bubbleRadius, + showLinkPreviews = showLinkPreviews, + onReplyClick = onGoToReply, + onReactionClick = { onReactionClick(msg.id, it) }, + onInstantViewClick = onInstantViewClick, + onYouTubeClick = onYouTubeClick, + onClick = { offset -> + onReplyClick(bubblePosition, bubbleSize, bubblePosition + offset) + }, + onLongClick = { offset -> + onReplyClick(bubblePosition, bubbleSize, bubblePosition + offset) + }, + onCommentsClick = onCommentsClick, + showComments = showComments, + toProfile = toProfile, + modifier = Modifier.fillMaxWidth() + ) + } - is MessageContent.Photo -> { - ChannelPhotoMessageBubble( - content = content, - msg = msg, - isSameSenderAbove = isSameSenderAbove, - isSameSenderBelow = isSameSenderBelow, - fontSize = fontSize, - letterSpacing = letterSpacing, - bubbleRadius = bubbleRadius, - autoDownloadMobile = autoDownloadMobile, - autoDownloadWifi = autoDownloadWifi, - autoDownloadRoaming = autoDownloadRoaming, - onPhotoClick = onPhotoClick, - onDownloadPhoto = onDownloadPhoto, - onCancelDownload = onCancelDownload, - onLongClick = { offset -> - onReplyClick( - bubblePosition, - bubbleSize, - bubblePosition + offset - ) - }, - onReplyClick = onGoToReply, - onReactionClick = { onReactionClick(msg.id, it) }, - onCommentsClick = onCommentsClick, - showComments = showComments, - toProfile = toProfile, - modifier = Modifier.fillMaxWidth(), - downloadUtils = downloadUtils - ) - } + is MessageContent.Photo -> { + ChannelPhotoMessageBubble( + content = content, + msg = msg, + isSameSenderAbove = isSameSenderAbove, + isSameSenderBelow = isSameSenderBelow, + fontSize = fontSize, + letterSpacing = letterSpacing, + bubbleRadius = bubbleRadius, + autoDownloadMobile = autoDownloadMobile, + autoDownloadWifi = autoDownloadWifi, + autoDownloadRoaming = autoDownloadRoaming, + onPhotoClick = onPhotoClick, + onDownloadPhoto = onDownloadPhoto, + onCancelDownload = onCancelDownload, + onLongClick = { offset -> + onReplyClick( + bubblePosition, + bubbleSize, + bubblePosition + offset + ) + }, + onReplyClick = onGoToReply, + onReactionClick = { onReactionClick(msg.id, it) }, + onCommentsClick = onCommentsClick, + showComments = showComments, + toProfile = toProfile, + modifier = Modifier.fillMaxWidth(), + downloadUtils = downloadUtils + ) + } - is MessageContent.Video -> { - ChannelVideoMessageBubble( - content = content, - msg = msg, - isSameSenderAbove = isSameSenderAbove, - isSameSenderBelow = isSameSenderBelow, - fontSize = fontSize, - letterSpacing = letterSpacing, - bubbleRadius = bubbleRadius, - autoDownloadMobile = autoDownloadMobile, - autoDownloadWifi = autoDownloadWifi, - autoDownloadRoaming = autoDownloadRoaming, - autoplayVideos = autoplayVideos, - onVideoClick = onVideoClick, - onCancelDownload = onCancelDownload, - onLongClick = { offset -> - onReplyClick( - bubblePosition, - bubbleSize, - bubblePosition + offset - ) - }, - onReplyClick = onGoToReply, - onReactionClick = { onReactionClick(msg.id, it) }, - onCommentsClick = onCommentsClick, - showComments = showComments, - toProfile = toProfile, - modifier = Modifier.fillMaxWidth(), - downloadUtils = downloadUtils, - isAnyViewerOpen = isAnyViewerOpen - ) - } + is MessageContent.Video -> { + ChannelVideoMessageBubble( + content = content, + msg = msg, + isSameSenderAbove = isSameSenderAbove, + isSameSenderBelow = isSameSenderBelow, + fontSize = fontSize, + letterSpacing = letterSpacing, + bubbleRadius = bubbleRadius, + autoDownloadMobile = autoDownloadMobile, + autoDownloadWifi = autoDownloadWifi, + autoDownloadRoaming = autoDownloadRoaming, + autoplayVideos = autoplayVideos, + onVideoClick = onVideoClick, + onCancelDownload = onCancelDownload, + onLongClick = { offset -> + onReplyClick( + bubblePosition, + bubbleSize, + bubblePosition + offset + ) + }, + onReplyClick = onGoToReply, + onReactionClick = { onReactionClick(msg.id, it) }, + onCommentsClick = onCommentsClick, + showComments = showComments, + toProfile = toProfile, + modifier = Modifier.fillMaxWidth(), + downloadUtils = downloadUtils, + isAnyViewerOpen = isAnyViewerOpen + ) + } - is MessageContent.Document -> { - DocumentMessageBubble( - content = content, - msg = msg, - isOutgoing = false, - isSameSenderAbove = isSameSenderAbove, - isSameSenderBelow = isSameSenderBelow, - fontSize = fontSize, - letterSpacing = letterSpacing, - autoDownloadFiles = autoDownloadFiles, - autoDownloadMobile = autoDownloadMobile, - autoDownloadWifi = autoDownloadWifi, - autoDownloadRoaming = autoDownloadRoaming, - onDocumentClick = onDocumentClick, - onCancelDownload = onCancelDownload, - onLongClick = { offset -> - onReplyClick( - bubblePosition, - bubbleSize, - bubblePosition + offset - ) - }, - toProfile = toProfile, - modifier = Modifier.fillMaxWidth(), - onReplyClick = onGoToReply, - onReactionClick = { onReactionClick(msg.id, it) }, - downloadUtils = downloadUtils - ) - } + is MessageContent.Document -> { + DocumentMessageBubble( + content = content, + msg = msg, + isOutgoing = false, + isSameSenderAbove = isSameSenderAbove, + isSameSenderBelow = isSameSenderBelow, + fontSize = fontSize, + letterSpacing = letterSpacing, + autoDownloadFiles = autoDownloadFiles, + autoDownloadMobile = autoDownloadMobile, + autoDownloadWifi = autoDownloadWifi, + autoDownloadRoaming = autoDownloadRoaming, + onDocumentClick = onDocumentClick, + onCancelDownload = onCancelDownload, + onLongClick = { offset -> + onReplyClick( + bubblePosition, + bubbleSize, + bubblePosition + offset + ) + }, + toProfile = toProfile, + modifier = Modifier.fillMaxWidth(), + onReplyClick = onGoToReply, + onReactionClick = { onReactionClick(msg.id, it) }, + downloadUtils = downloadUtils + ) + } - is MessageContent.Audio -> { - AudioMessageBubble( - content = content, - msg = msg, - isOutgoing = false, - isSameSenderAbove = isSameSenderAbove, - isSameSenderBelow = isSameSenderBelow, - fontSize = fontSize, - letterSpacing = letterSpacing, - autoDownloadFiles = autoDownloadFiles, - autoDownloadMobile = autoDownloadMobile, - autoDownloadWifi = autoDownloadWifi, - autoDownloadRoaming = autoDownloadRoaming, - onAudioClick = onAudioClick, - onCancelDownload = onCancelDownload, - onLongClick = { offset -> - onReplyClick( - bubblePosition, - bubbleSize, - bubblePosition + offset - ) - }, - toProfile = toProfile, - modifier = Modifier.fillMaxWidth(), - onReplyClick = onGoToReply, - onReactionClick = { onReactionClick(msg.id, it) }, - downloadUtils = downloadUtils - ) - } + is MessageContent.Audio -> { + AudioMessageBubble( + content = content, + msg = msg, + isOutgoing = false, + isSameSenderAbove = isSameSenderAbove, + isSameSenderBelow = isSameSenderBelow, + fontSize = fontSize, + letterSpacing = letterSpacing, + autoDownloadFiles = autoDownloadFiles, + autoDownloadMobile = autoDownloadMobile, + autoDownloadWifi = autoDownloadWifi, + autoDownloadRoaming = autoDownloadRoaming, + onAudioClick = onAudioClick, + onCancelDownload = onCancelDownload, + onLongClick = { offset -> + onReplyClick( + bubblePosition, + bubbleSize, + bubblePosition + offset + ) + }, + toProfile = toProfile, + modifier = Modifier.fillMaxWidth(), + onReplyClick = onGoToReply, + onReactionClick = { onReactionClick(msg.id, it) }, + downloadUtils = downloadUtils + ) + } - is MessageContent.Gif -> { - ChannelGifMessageBubble( - content = content, - msg = msg, - isSameSenderAbove = isSameSenderAbove, - isSameSenderBelow = isSameSenderBelow, - fontSize = fontSize, - letterSpacing = letterSpacing, - bubbleRadius = bubbleRadius, - autoDownloadMobile = autoDownloadMobile, - autoDownloadWifi = autoDownloadWifi, - autoDownloadRoaming = autoDownloadRoaming, - autoplayGifs = autoplayGifs, - onGifClick = onVideoClick, - onCancelDownload = onCancelDownload, - onLongClick = { offset -> - onReplyClick( - bubblePosition, - bubbleSize, - bubblePosition + offset - ) - }, - onReplyClick = onGoToReply, - onReactionClick = { onReactionClick(msg.id, it) }, - onCommentsClick = onCommentsClick, - showComments = showComments, - toProfile = toProfile, - modifier = Modifier.fillMaxWidth(), - downloadUtils = downloadUtils, - isAnyViewerOpen = isAnyViewerOpen - ) - } + is MessageContent.Gif -> { + ChannelGifMessageBubble( + content = content, + msg = msg, + isSameSenderAbove = isSameSenderAbove, + isSameSenderBelow = isSameSenderBelow, + fontSize = fontSize, + letterSpacing = letterSpacing, + bubbleRadius = bubbleRadius, + autoDownloadMobile = autoDownloadMobile, + autoDownloadWifi = autoDownloadWifi, + autoDownloadRoaming = autoDownloadRoaming, + autoplayGifs = autoplayGifs, + onGifClick = onVideoClick, + onCancelDownload = onCancelDownload, + onLongClick = { offset -> + onReplyClick( + bubblePosition, + bubbleSize, + bubblePosition + offset + ) + }, + onReplyClick = onGoToReply, + onReactionClick = { onReactionClick(msg.id, it) }, + onCommentsClick = onCommentsClick, + showComments = showComments, + toProfile = toProfile, + modifier = Modifier.fillMaxWidth(), + downloadUtils = downloadUtils, + isAnyViewerOpen = isAnyViewerOpen + ) + } - is MessageContent.Sticker -> { - StickerMessageBubble( - content = content, - msg = msg, - isOutgoing = false, - stickerSize = stickerSize, - onReplyClick = onGoToReply, - onReactionClick = { onReactionClick(msg.id, it) }, - onStickerClick = { onStickerClick(content.setId) }, - onLongClick = { - onReplyClick( - bubblePosition, - bubbleSize, - bubblePosition + (bubbleSize.toSize() / 2f).toOffset() - ) - }, - toProfile = toProfile - ) + is MessageContent.Sticker -> { + StickerMessageBubble( + content = content, + msg = msg, + isOutgoing = false, + stickerSize = stickerSize, + onReplyClick = onGoToReply, + onReactionClick = { onReactionClick(msg.id, it) }, + onStickerClick = { onStickerClick(content.setId) }, + onLongClick = { + onReplyClick( + bubblePosition, + bubbleSize, + bubblePosition + (bubbleSize.toSize() / 2f).toOffset() + ) + }, + toProfile = toProfile + ) + } + + is MessageContent.Poll -> { + ChannelPollMessageBubble( + content = content, + msg = msg, + isSameSenderAbove = isSameSenderAbove, + isSameSenderBelow = isSameSenderBelow, + fontSize = fontSize, + letterSpacing = letterSpacing, + bubbleRadius = bubbleRadius, + onOptionClick = { onPollOptionClick(msg.id, it) }, + onRetractVote = { onRetractVote(msg.id) }, + onShowVoters = { onShowVoters(msg.id, it) }, + onClosePoll = { onClosePoll(msg.id) }, + onReplyClick = onGoToReply, + onReactionClick = { onReactionClick(msg.id, it) }, + onLongClick = { offset -> + onReplyClick( + bubblePosition, + bubbleSize, + bubblePosition + offset + ) + }, + onCommentsClick = onCommentsClick, + showComments = showComments, + toProfile = toProfile + ) + } + + else -> {} } - is MessageContent.Poll -> { - ChannelPollMessageBubble( - content = content, - msg = msg, - isSameSenderAbove = isSameSenderAbove, - isSameSenderBelow = isSameSenderBelow, - fontSize = fontSize, - letterSpacing = letterSpacing, - bubbleRadius = bubbleRadius, - onOptionClick = { onPollOptionClick(msg.id, it) }, - onRetractVote = { onRetractVote(msg.id) }, - onShowVoters = { onShowVoters(msg.id, it) }, - onClosePoll = { onClosePoll(msg.id) }, - onReplyClick = onGoToReply, - onReactionClick = { onReactionClick(msg.id, it) }, - onLongClick = { offset -> - onReplyClick( - bubblePosition, - bubbleSize, - bubblePosition + offset - ) - }, - onCommentsClick = onCommentsClick, - showComments = showComments, - toProfile = toProfile + msg.replyMarkup?.let { markup -> + ReplyMarkupView( + replyMarkup = markup, + onButtonClick = { onReplyMarkupButtonClick(msg.id, it) } ) } - else -> {} - } - - msg.replyMarkup?.let { markup -> - ReplyMarkupView( - replyMarkup = markup, - onButtonClick = { onReplyMarkupButtonClick(msg.id, it) } + MessageViaBotAttribution( + msg = msg, + isOutgoing = msg.isOutgoing, + onViaBotClick = onViaBotClick, + modifier = Modifier.align(Alignment.Start) ) } - MessageViaBotAttribution( - msg = msg, - isOutgoing = msg.isOutgoing, - onViaBotClick = onViaBotClick, - modifier = Modifier.align(Alignment.Start) + FastReplyIndicator( + modifier = Modifier.align(Alignment.CenterStart), + dragOffsetX = dragOffsetX, + inverseOffset = isLandscape, + maxWidth = maxWidth, ) } } diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/pins/PinnedMessagesListSheet.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/pins/PinnedMessagesListSheet.kt index 85dd1454..8565d675 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/pins/PinnedMessagesListSheet.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/pins/PinnedMessagesListSheet.kt @@ -295,6 +295,7 @@ fun PinnedMessagesListSheet( onGoToReply = { onReplyClick(it) }, onReactionClick = onReactionClick, toProfile = {}, + canReply = false, downloadUtils = downloadUtils ) } @@ -322,6 +323,7 @@ fun PinnedMessagesListSheet( onGoToReply = { onReplyClick(it) }, onReactionClick = onReactionClick, toProfile = {}, + canReply = false, downloadUtils = downloadUtils ) } else if (item is GroupedMessageItem.Album) { @@ -339,6 +341,7 @@ fun PinnedMessagesListSheet( onGoToReply = { onReplyClick(it) }, onReactionClick = onReactionClick, toProfile = {}, + canReply = false, downloadUtils = downloadUtils ) }