Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -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) {
Expand Down Expand Up @@ -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) },
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
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,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
Expand All @@ -28,14 +30,14 @@ 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

@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun FastReplyIndicator(
modifier: Modifier = Modifier,
Expand All @@ -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)
)
}
}
}
}
Expand Down Expand Up @@ -135,7 +135,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
Loading