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 4f0ce731..eeb26113 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 @@ -559,6 +559,60 @@ private fun MessageBubbleSwitcher( val isChannel = state.isChannel && state.currentTopicId == null val isTopicClosed = state.topics.find { it.id.toLong() == state.currentTopicId }?.isClosed?: false + val canWriteText by remember(state.isAdmin, state.permissions.canSendBasicMessages) { + derivedStateOf { state.isAdmin || state.permissions.canSendBasicMessages } + } + val canSendPhotos by remember(state.isAdmin, state.permissions.canSendPhotos) { + derivedStateOf { state.isAdmin || state.permissions.canSendPhotos } + } + val canSendVideos by remember(state.isAdmin, state.permissions.canSendVideos) { + derivedStateOf { state.isAdmin || state.permissions.canSendVideos } + } + val canSendDocuments by remember(state.isAdmin, state.permissions.canSendDocuments) { + derivedStateOf { state.isAdmin || state.permissions.canSendDocuments } + } + val canSendAudios by remember(state.isAdmin, state.permissions.canSendAudios) { + derivedStateOf { state.isAdmin || state.permissions.canSendAudios } + } + val canUseMediaPicker by remember(canSendPhotos, canSendVideos) { + derivedStateOf { canSendPhotos || canSendVideos } + } + val canUseDocumentPicker by remember(canSendDocuments, canSendAudios) { + derivedStateOf { canSendDocuments || canSendAudios } + } + val canSendPolls by remember(state.isAdmin, state.permissions.canSendPolls) { + derivedStateOf { state.isAdmin || state.permissions.canSendPolls } + } + val canOpenAttachSheet by remember( + canUseMediaPicker, + canUseDocumentPicker, + canSendPolls, + state.attachMenuBots + ) { + derivedStateOf { canUseMediaPicker || canUseDocumentPicker || canSendPolls || state.attachMenuBots.isNotEmpty() } + } + val canSendStickers by remember(state.isAdmin, state.permissions.canSendOtherMessages) { + derivedStateOf { state.isAdmin || state.permissions.canSendOtherMessages } + } + val canSendVoice by remember(state.isAdmin, state.permissions.canSendVoiceNotes) { + derivedStateOf { state.isAdmin || state.permissions.canSendVoiceNotes } + } + val canSendVideoNotes by remember(state.isAdmin, state.permissions.canSendVideoNotes) { + derivedStateOf { state.isAdmin || state.permissions.canSendVideoNotes } + } + val canSendAnything by remember( + canWriteText, + canOpenAttachSheet, + canSendStickers, + canSendVoice, + canSendVideoNotes, + canSendPolls + ) { + derivedStateOf { + canWriteText || canOpenAttachSheet || canSendStickers || canSendVoice || canSendVideoNotes || canSendPolls + } + } + when (item) { is GroupedMessageItem.Single -> { if (item.message.content is MessageContent.Service) { @@ -655,7 +709,7 @@ private fun MessageBubbleSwitcher( onCommentsClick = { component.onCommentsClick(it) }, toProfile = toProfile, onViaBotClick = onViaBotClick, - canReply = state.canWrite && !isSelectionMode, + canReply = state.canWrite && !isSelectionMode && canSendAnything, onReplySwipe = { component.onReplyMessage(it) }, onYouTubeClick = { component.onOpenYouTube(it) }, onInstantViewClick = { component.onOpenInstantView(it) }, @@ -760,7 +814,7 @@ private fun MessageBubbleSwitcher( onPositionChange = { _, pos, size -> onMessagePositionChange(pos, size) }, toProfile = toProfile, onViaBotClick = onViaBotClick, - canReply = state.canWrite && !isSelectionMode && (!isTopicClosed || state.isAdmin), + canReply = state.canWrite && !isSelectionMode && (!isTopicClosed || state.isAdmin) && canSendAnything, onReplySwipe = { component.onReplyMessage(it) }, swipeEnabled = !isSelectionMode, downloadUtils = downloadUtils, @@ -828,7 +882,7 @@ private fun MessageBubbleSwitcher( onCommentsClick = { component.onCommentsClick(it) }, toProfile = toProfile, onViaBotClick = onViaBotClick, - canReply = state.canWrite && !isSelectionMode && (!isTopicClosed || state.isAdmin), + canReply = state.canWrite && !isSelectionMode && (!isTopicClosed || state.isAdmin) && canSendAnything, onReplySwipe = { component.onReplyMessage(it) }, swipeEnabled = !isSelectionMode, downloadUtils = downloadUtils, 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 28104ca8..89888607 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 @@ -312,8 +312,7 @@ fun AlbumMessageBubbleContainer( } FastReplyIndicator( - modifier = Modifier - .align(if (isOutgoing) Alignment.CenterEnd else Alignment.CenterStart), + modifier = Modifier.align(Alignment.CenterEnd), dragOffsetX = dragOffsetX, isOutgoing = isOutgoing, 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 index 9cf04035..bcc5a3ef 100644 --- 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 @@ -1,24 +1,26 @@ package org.monogram.presentation.features.chats.currentChat.components +import androidx.compose.animation.AnimatedVisibility 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.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut 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.CircularWavyProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi 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 @@ -28,7 +30,6 @@ 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 @@ -36,6 +37,7 @@ const val REPLY_TRIGGER_FRACTION = 0.35f const val MAX_SWIPE_FRACTION = 0.7f const val ICON_OFFSET_FRACTION = 0.1f +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun FastReplyIndicator( modifier: Modifier = Modifier, @@ -46,46 +48,44 @@ fun FastReplyIndicator( ) { 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 progress = ((dragged - 48.dp.value) / (triggerDistance - 48.dp.value)).coerceIn(0f, 1f) + 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 + Box( + modifier = modifier + .offset(x = iconOffset) + .size(34.dp) + .graphicsLayer { + translationX = when { + isOutgoing -> (-dragOffsetX.value - iconOffset.value) * 0.5f + inverseOffset -> -iconOffset.value + else -> iconOffset.value } - .background( - color = MaterialTheme.colorScheme.surfaceContainerHigh.copy(alpha = 0.7f), - shape = CircleShape - ), - contentAlignment = Alignment.Center + }, + contentAlignment = Alignment.Center + ) { + AnimatedVisibility( + visible = dragged > 48.dp.value, + enter = fadeIn() + scaleIn(initialScale = 0.6f), + exit = fadeOut() + scaleOut(targetScale = 0.6f) ) { - Icon( - imageVector = Icons.AutoMirrored.Filled.Reply, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(18.dp) - ) + Box( + modifier = Modifier.matchParentSize(), + contentAlignment = Alignment.Center + ) { + CircularWavyProgressIndicator( + progress = { progress }, + color = MaterialTheme.colorScheme.primary, + ) + Icon( + imageVector = Icons.AutoMirrored.Filled.Reply, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(18.dp) + ) + } } } } @@ -135,7 +135,13 @@ fun Modifier.fastReplyPointer( onReplySwipe() } scope.launch { - dragOffsetX.animateTo(0f, spring()) + dragOffsetX.animateTo( + 0f, + spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessLow + ) + ) } } } 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 bd774a8a..67f7737b 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 @@ -294,8 +294,7 @@ fun MessageBubbleContainer( } FastReplyIndicator( - modifier = Modifier - .align(if (isOutgoing) Alignment.CenterEnd else Alignment.CenterStart), + modifier = Modifier.align(Alignment.CenterEnd), dragOffsetX = dragOffsetX, isOutgoing = isOutgoing, 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 79bae263..dfeeb4ed 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 @@ -418,7 +418,7 @@ fun ChannelMessageBubbleContainer( } FastReplyIndicator( - modifier = Modifier.align(Alignment.CenterStart), + modifier = Modifier.align(Alignment.CenterEnd), dragOffsetX = dragOffsetX, inverseOffset = isLandscape, maxWidth = maxWidth,