Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,61 @@ fun ChatContent(
animationSpec = if (shouldAnimateContentEntrance) tween(300) else snap(),
label = "ContentOffset"
)

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
}
}

val messageListState = remember(
state.chatId,
state.currentTopicId,
Expand All @@ -630,6 +685,7 @@ fun ChatContent(
state.isChannel,
state.isAdmin,
state.canWrite,
canSendAnything,
state.highlightedMessageId,
state.fontSize,
state.letterSpacing,
Expand Down Expand Up @@ -666,6 +722,7 @@ fun ChatContent(
isChannel = state.isChannel,
isAdmin = state.isAdmin,
canWrite = state.canWrite,
canSendAnything = canSendAnything,
highlightedMessageId = state.highlightedMessageId,
fontSize = state.fontSize,
letterSpacing = state.letterSpacing,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ data class ChatMessageListUiState(
val isChannel: Boolean,
val isAdmin: Boolean,
val canWrite: Boolean,
val canSendAnything: Boolean,
val highlightedMessageId: Long?,
val fontSize: Float,
val letterSpacing: Float,
Expand Down Expand Up @@ -773,7 +774,7 @@ private fun MessageBubbleSwitcher(
onCommentsClick = { component.onCommentsClick(it) },
toProfile = toProfile,
onViaBotClick = onViaBotClick,
canReply = state.canWrite && !isSelectionMode,
canReply = state.canWrite && !isSelectionMode && state.canSendAnything,
onReplySwipe = { component.onReplyMessage(it) },
onYouTubeClick = { component.onOpenYouTube(it) },
onInstantViewClick = { component.onOpenInstantView(it) },
Expand Down Expand Up @@ -878,7 +879,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) && state.canSendAnything,
onReplySwipe = { component.onReplyMessage(it) },
swipeEnabled = !isSelectionMode,
downloadUtils = downloadUtils,
Expand Down Expand Up @@ -946,7 +947,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) && state.canSendAnything,
onReplySwipe = { component.onReplyMessage(it) },
swipeEnabled = !isSelectionMode,
downloadUtils = downloadUtils,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
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
Expand All @@ -28,14 +32,15 @@ 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
import kotlin.math.pow

const val REPLY_TRIGGER_FRACTION = 0.35f
const val MAX_SWIPE_FRACTION = 0.7f
const val REPLY_TRIGGER_FRACTION = 0.4f
const val MAX_SWIPE_FRACTION = 0.65f
const val ICON_OFFSET_FRACTION = 0.1f

@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun FastReplyIndicator(
modifier: Modifier = Modifier,
Expand All @@ -46,46 +51,57 @@ 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 startOffset = 48.dp.value
val effectiveDrag = (dragged - startOffset).coerceAtLeast(0f)
val effectiveRange = (triggerDistance - startOffset).coerceAtLeast(1f)

val progress = (effectiveDrag / effectiveRange).coerceIn(0f, 1f).pow(1.3f)

val animatedProgress by
animateFloatAsState(
targetValue = 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
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 > startOffset,
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 = { animatedProgress },
color = MaterialTheme.colorScheme.primary,
)
Icon(
imageVector = Icons.AutoMirrored.Filled.Reply,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(18.dp)
)
}
}
}
}
Expand Down Expand Up @@ -135,7 +151,13 @@ fun Modifier.fastReplyPointer(
onReplySwipe()
}
scope.launch {
dragOffsetX.animateTo(0f, spring())
dragOffsetX.animateTo(
0f,
spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,7 @@ fun ChannelMessageBubbleContainer(
}

FastReplyIndicator(
modifier = Modifier.align(Alignment.CenterStart),
modifier = Modifier.align(Alignment.CenterEnd),
dragOffsetX = dragOffsetX,
inverseOffset = isLandscape,
maxWidth = maxWidth,
Expand Down