feat: finish swipe-to-reply impl
Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
parent
df3a1ab26f
commit
5d5744e180
|
|
@ -0,0 +1,286 @@
|
||||||
|
package chat.revolt.components.screens.chat.atoms
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.animation.animateColorAsState
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.slideInVertically
|
||||||
|
import androidx.compose.animation.slideOutVertically
|
||||||
|
import androidx.compose.animation.togetherWith
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.requiredHeightIn
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
|
import androidx.compose.ui.input.pointer.PointerInputChange
|
||||||
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
|
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.unit.dp
|
||||||
|
import androidx.compose.ui.unit.min
|
||||||
|
import chat.revolt.R
|
||||||
|
import chat.revolt.api.RevoltAPI
|
||||||
|
import chat.revolt.api.schemas.Channel
|
||||||
|
import chat.revolt.api.schemas.Message
|
||||||
|
import chat.revolt.api.settings.LoadedSettings
|
||||||
|
import chat.revolt.api.settings.MessageReplyStyle
|
||||||
|
import chat.revolt.callbacks.Action
|
||||||
|
import chat.revolt.callbacks.ActionChannel
|
||||||
|
import chat.revolt.components.chat.Message
|
||||||
|
import chat.revolt.internals.extensions.supportSwipeReply
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
const val SWIPE_TO_REPLY_THRESHOLD = -450f
|
||||||
|
|
||||||
|
// Display a regular message in the LazyColumn of the chat screen.
|
||||||
|
@SuppressLint("UnusedBoxWithConstraintsScope") // we do use it, but the IDE is stupid
|
||||||
|
@Composable
|
||||||
|
fun RegularMessage(
|
||||||
|
message: Message,
|
||||||
|
channel: Channel?,
|
||||||
|
setDrawerGestureEnabled: (Boolean) -> Unit,
|
||||||
|
setDisableScroll: (Boolean) -> Unit,
|
||||||
|
showMessageBottomSheet: (String) -> Unit,
|
||||||
|
showReactBottomSheet: () -> Unit,
|
||||||
|
putTextAtCursorPosition: (String) -> Unit,
|
||||||
|
replyToMessage: suspend (String) -> Unit,
|
||||||
|
scope: CoroutineScope = rememberCoroutineScope()
|
||||||
|
) {
|
||||||
|
val haptic = LocalHapticFeedback.current
|
||||||
|
|
||||||
|
var offsetX by remember { mutableFloatStateOf(0f) }
|
||||||
|
val animOffsetX by animateFloatAsState(
|
||||||
|
when {
|
||||||
|
offsetX > -20f -> 0f
|
||||||
|
else -> offsetX
|
||||||
|
},
|
||||||
|
label = "X offset of message for Swipe to Reply"
|
||||||
|
)
|
||||||
|
var markGestureInvalid by remember { mutableStateOf(false) }
|
||||||
|
var hapticFeedbackPerformed by remember { mutableStateOf(false) }
|
||||||
|
var messageHeight by remember { mutableIntStateOf(0) }
|
||||||
|
|
||||||
|
val canReleaseToSend = remember(offsetX) { offsetX <= SWIPE_TO_REPLY_THRESHOLD }
|
||||||
|
val indicatorBackground by animateColorAsState(
|
||||||
|
when {
|
||||||
|
canReleaseToSend -> MaterialTheme.colorScheme.inversePrimary
|
||||||
|
else -> MaterialTheme.colorScheme.primaryContainer
|
||||||
|
},
|
||||||
|
label = "Swipe to Reply indicator background"
|
||||||
|
)
|
||||||
|
val indicatorForeground by animateColorAsState(
|
||||||
|
when {
|
||||||
|
canReleaseToSend -> MaterialTheme.colorScheme.primary
|
||||||
|
else -> MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
},
|
||||||
|
label = "Swipe to Reply indicator foreground"
|
||||||
|
)
|
||||||
|
|
||||||
|
var onFingerMoveHandler: (List<PointerInputChange>) -> Unit =
|
||||||
|
{ changeList: List<PointerInputChange> ->
|
||||||
|
changeList.firstOrNull()
|
||||||
|
?.let {
|
||||||
|
val deltaX = it.position.x - it.previousPosition.x
|
||||||
|
val deltaY = it.position.y - it.previousPosition.y
|
||||||
|
|
||||||
|
val couldBeTopDownScroll =
|
||||||
|
deltaX > -30f && abs(deltaY) > 30f && offsetX >= -100f
|
||||||
|
|
||||||
|
if (couldBeTopDownScroll) {
|
||||||
|
offsetX = 0f
|
||||||
|
markGestureInvalid = true
|
||||||
|
return@let
|
||||||
|
}
|
||||||
|
|
||||||
|
val goesTowardsLeft = it.position.x < it.previousPosition.x
|
||||||
|
if (goesTowardsLeft || offsetX <= -20f) {
|
||||||
|
if (markGestureInvalid) return@let
|
||||||
|
|
||||||
|
offsetX += deltaX
|
||||||
|
setDrawerGestureEnabled(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (goesTowardsLeft && offsetX <= -30f) {
|
||||||
|
setDisableScroll(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (goesTowardsLeft && offsetX <= SWIPE_TO_REPLY_THRESHOLD && !hapticFeedbackPerformed) {
|
||||||
|
hapticFeedbackPerformed = true
|
||||||
|
haptic.performHapticFeedback(
|
||||||
|
HapticFeedbackType.GestureThresholdActivate
|
||||||
|
)
|
||||||
|
} else if (hapticFeedbackPerformed && offsetX >= -100f) {
|
||||||
|
hapticFeedbackPerformed = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box {
|
||||||
|
Message(
|
||||||
|
message = message,
|
||||||
|
onMessageContextMenu = {
|
||||||
|
message.id?.let { messageId ->
|
||||||
|
showMessageBottomSheet(messageId)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onAvatarClick = {
|
||||||
|
if (message.webhook != null) {
|
||||||
|
scope.launch {
|
||||||
|
ActionChannel.send(Action.OpenWebhookSheet)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message.author?.let { author ->
|
||||||
|
scope.launch {
|
||||||
|
ActionChannel.send(Action.OpenUserSheet(author, channel?.server))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onNameClick = {
|
||||||
|
val author = message.author?.let { RevoltAPI.userCache[it] } ?: return@Message
|
||||||
|
putTextAtCursorPosition("@${author.username}#${author.discriminator}")
|
||||||
|
},
|
||||||
|
canReply = true,
|
||||||
|
onReply = {
|
||||||
|
message.id?.let { messageId ->
|
||||||
|
scope.launch {
|
||||||
|
replyToMessage(messageId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onAddReaction = {
|
||||||
|
message.id?.let { messageId ->
|
||||||
|
showReactBottomSheet()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fromWebhook = message.webhook != null,
|
||||||
|
webhookName = message.webhook?.name,
|
||||||
|
modifier = Modifier
|
||||||
|
.offset(
|
||||||
|
x = with(LocalDensity.current) { animOffsetX.toDp() }
|
||||||
|
)
|
||||||
|
.then(
|
||||||
|
if (LoadedSettings.messageReplyStyle == MessageReplyStyle.SwipeFromEnd)
|
||||||
|
Modifier.supportSwipeReply(
|
||||||
|
onDown = {},
|
||||||
|
onMove = onFingerMoveHandler,
|
||||||
|
onUp = {
|
||||||
|
if (offsetX <= SWIPE_TO_REPLY_THRESHOLD) {
|
||||||
|
scope.launch {
|
||||||
|
message.id?.let {
|
||||||
|
replyToMessage(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setDrawerGestureEnabled(true)
|
||||||
|
markGestureInvalid = false
|
||||||
|
setDisableScroll(false)
|
||||||
|
hapticFeedbackPerformed = false
|
||||||
|
offsetX = 0f
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else Modifier
|
||||||
|
)
|
||||||
|
.onSizeChanged {
|
||||||
|
// FIXME:
|
||||||
|
// This whole onSizeChanged pattern is, technically a workaround. LazyColumn
|
||||||
|
// doesn't support the usual idiomatic ways to make an item as tall as the
|
||||||
|
// tallest item in the row (intrinsic sizing; fill parent etc.)
|
||||||
|
// This workaround may bite us performance-wise!
|
||||||
|
if (messageHeight != it.height) messageHeight = it.height
|
||||||
|
}
|
||||||
|
)
|
||||||
|
with(LocalDensity.current) {
|
||||||
|
val msgHeightAsDp = messageHeight.toDp()
|
||||||
|
BoxWithConstraints(Modifier.height(msgHeightAsDp)) {
|
||||||
|
AnimatedContent(
|
||||||
|
targetState = canReleaseToSend,
|
||||||
|
transitionSpec = {
|
||||||
|
if (canReleaseToSend) {
|
||||||
|
slideInVertically { -it } togetherWith slideOutVertically { it }
|
||||||
|
} else {
|
||||||
|
slideInVertically { it } togetherWith slideOutVertically { -it }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label = "Swipe to Reply indicator content slide"
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
Modifier
|
||||||
|
.height(msgHeightAsDp)
|
||||||
|
.requiredHeightIn(max = msgHeightAsDp) // must not cause message to be taller
|
||||||
|
.offset(
|
||||||
|
x = with(LocalDensity.current) {
|
||||||
|
maxWidth - abs(
|
||||||
|
animOffsetX
|
||||||
|
).toDp()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.background(
|
||||||
|
// indicatorBackground
|
||||||
|
if (it) MaterialTheme.colorScheme.inversePrimary
|
||||||
|
else MaterialTheme.colorScheme.primaryContainer
|
||||||
|
)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(
|
||||||
|
8.dp,
|
||||||
|
Alignment.Start
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_reply_24dp),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(
|
||||||
|
min(
|
||||||
|
msgHeightAsDp - 4.dp,
|
||||||
|
24.dp
|
||||||
|
)
|
||||||
|
),
|
||||||
|
tint = // indicatorForeground
|
||||||
|
if (it) MaterialTheme.colorScheme.primary
|
||||||
|
else MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
when (it) {
|
||||||
|
true -> stringResource(
|
||||||
|
R.string.swipe_to_reply_release
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> stringResource(
|
||||||
|
R.string.swipe_to_reply_keep_swiping
|
||||||
|
)
|
||||||
|
},
|
||||||
|
color = indicatorForeground
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,6 +17,7 @@ import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.Crossfade
|
import androidx.compose.animation.Crossfade
|
||||||
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.animation.core.animateIntAsState
|
import androidx.compose.animation.core.animateIntAsState
|
||||||
|
|
@ -36,7 +37,6 @@ import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
|
@ -49,6 +49,7 @@ import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
import androidx.compose.foundation.layout.offset
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.requiredHeight
|
import androidx.compose.foundation.layout.requiredHeight
|
||||||
|
import androidx.compose.foundation.layout.requiredHeightIn
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.statusBars
|
import androidx.compose.foundation.layout.statusBars
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
|
@ -88,6 +89,7 @@ import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
|
@ -99,6 +101,7 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.input.pointer.PointerInputChange
|
import androidx.compose.ui.input.pointer.PointerInputChange
|
||||||
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
|
@ -115,6 +118,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.em
|
import androidx.compose.ui.unit.em
|
||||||
|
import androidx.compose.ui.unit.min
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
|
@ -150,6 +154,7 @@ import chat.revolt.components.screens.chat.AttachmentManager
|
||||||
import chat.revolt.components.screens.chat.ChannelIcon
|
import chat.revolt.components.screens.chat.ChannelIcon
|
||||||
import chat.revolt.components.screens.chat.ReplyManager
|
import chat.revolt.components.screens.chat.ReplyManager
|
||||||
import chat.revolt.components.screens.chat.TypingIndicator
|
import chat.revolt.components.screens.chat.TypingIndicator
|
||||||
|
import chat.revolt.components.screens.chat.atoms.RegularMessage
|
||||||
import chat.revolt.components.skeletons.MessageSkeleton
|
import chat.revolt.components.skeletons.MessageSkeleton
|
||||||
import chat.revolt.components.skeletons.MessageSkeletonVariant
|
import chat.revolt.components.skeletons.MessageSkeletonVariant
|
||||||
import chat.revolt.internals.extensions.rememberChannelPermissions
|
import chat.revolt.internals.extensions.rememberChannelPermissions
|
||||||
|
|
@ -716,189 +721,29 @@ fun ChannelScreen(
|
||||||
) { index ->
|
) { index ->
|
||||||
when (val item = viewModel.items[index]) {
|
when (val item = viewModel.items[index]) {
|
||||||
is ChannelScreenItem.RegularMessage -> {
|
is ChannelScreenItem.RegularMessage -> {
|
||||||
var offsetX by remember { mutableFloatStateOf(0f) }
|
RegularMessage(
|
||||||
val animOffsetX by animateFloatAsState(
|
item.message,
|
||||||
when {
|
viewModel.channel,
|
||||||
offsetX > -20f -> 0f
|
setDrawerGestureEnabled = {
|
||||||
else -> offsetX
|
setDrawerGestureEnabled(it)
|
||||||
},
|
},
|
||||||
label = "X offset of message for replies"
|
setDisableScroll = {
|
||||||
)
|
disableScroll = it
|
||||||
var markGestureInvalid by remember { mutableStateOf(false) }
|
},
|
||||||
var hapticFeedbackPerformed by remember {
|
showMessageBottomSheet = {
|
||||||
mutableStateOf(
|
messageContextSheetTarget = it
|
||||||
false
|
messageContextSheetShown = true
|
||||||
)
|
},
|
||||||
}
|
showReactBottomSheet = {
|
||||||
|
item.message.id?.let {
|
||||||
var onMoveHandler: (List<PointerInputChange>) -> Unit =
|
reactSheetTarget = it
|
||||||
{ changeList: List<PointerInputChange> ->
|
reactSheetShown = true
|
||||||
changeList
|
|
||||||
.firstOrNull()
|
|
||||||
?.let {
|
|
||||||
val deltaX =
|
|
||||||
it.position.x - it.previousPosition.x
|
|
||||||
val deltaY =
|
|
||||||
it.position.y - it.previousPosition.y
|
|
||||||
|
|
||||||
val couldBeTopDownScroll =
|
|
||||||
deltaX > -30f
|
|
||||||
&& abs(deltaY) > 30f
|
|
||||||
// too far in to consider it an accident
|
|
||||||
&& offsetX >= -100f
|
|
||||||
if (couldBeTopDownScroll) {
|
|
||||||
offsetX = 0f
|
|
||||||
markGestureInvalid = true
|
|
||||||
return@let
|
|
||||||
}
|
|
||||||
|
|
||||||
val goesTowardsLeft =
|
|
||||||
it.position.x < it.previousPosition.x
|
|
||||||
if (goesTowardsLeft || offsetX <= -20f) {
|
|
||||||
if (markGestureInvalid) {
|
|
||||||
return@let
|
|
||||||
}
|
|
||||||
offsetX += deltaX
|
|
||||||
setDrawerGestureEnabled(
|
|
||||||
false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (goesTowardsLeft && offsetX <= -30f) {
|
|
||||||
disableScroll = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
goesTowardsLeft
|
|
||||||
&& offsetX <= -300f
|
|
||||||
&& !hapticFeedbackPerformed
|
|
||||||
) {
|
|
||||||
hapticFeedbackPerformed = true
|
|
||||||
haptic.performHapticFeedback(
|
|
||||||
HapticFeedbackType.GestureThresholdActivate
|
|
||||||
)
|
|
||||||
} else if (
|
|
||||||
hapticFeedbackPerformed
|
|
||||||
&& offsetX >= -100f
|
|
||||||
) {
|
|
||||||
hapticFeedbackPerformed = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Box {
|
|
||||||
Message(
|
|
||||||
message = item.message,
|
|
||||||
onMessageContextMenu = {
|
|
||||||
item.message.id?.let { messageId ->
|
|
||||||
messageContextSheetTarget = messageId
|
|
||||||
messageContextSheetShown = true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onAvatarClick = {
|
|
||||||
if (item.message.webhook != null) {
|
|
||||||
scope.launch {
|
|
||||||
ActionChannel.send(Action.OpenWebhookSheet)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
item.message.author?.let { author ->
|
|
||||||
scope.launch {
|
|
||||||
ActionChannel.send(
|
|
||||||
Action.OpenUserSheet(
|
|
||||||
author,
|
|
||||||
viewModel.channel?.server
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onNameClick = {
|
|
||||||
val author =
|
|
||||||
item.message.author?.let { RevoltAPI.userCache[it] }
|
|
||||||
?: return@Message
|
|
||||||
viewModel.putAtCursorPosition("@${author.username}#${author.discriminator}")
|
|
||||||
},
|
|
||||||
canReply = true,
|
|
||||||
onReply = {
|
|
||||||
item.message.id?.let { messageId ->
|
|
||||||
scope.launch {
|
|
||||||
viewModel.addReplyTo(
|
|
||||||
messageId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onAddReaction = {
|
|
||||||
item.message.id?.let { messageId ->
|
|
||||||
reactSheetTarget = messageId
|
|
||||||
reactSheetShown = true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fromWebhook = item.message.webhook != null,
|
|
||||||
webhookName = item.message.webhook?.name,
|
|
||||||
modifier = Modifier
|
|
||||||
.offset(
|
|
||||||
x = with(LocalDensity.current) { animOffsetX.toDp() }
|
|
||||||
)
|
|
||||||
.then(
|
|
||||||
if (LoadedSettings.messageReplyStyle == MessageReplyStyle.SwipeFromEnd)
|
|
||||||
Modifier.supportSwipeReply(
|
|
||||||
onDown = {},
|
|
||||||
onMove = onMoveHandler,
|
|
||||||
onUp = {
|
|
||||||
if (offsetX <= -300f) {
|
|
||||||
scope.launch {
|
|
||||||
item.message.id?.let {
|
|
||||||
viewModel.addReplyTo(
|
|
||||||
it
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setDrawerGestureEnabled(true)
|
|
||||||
markGestureInvalid = false
|
|
||||||
disableScroll = false
|
|
||||||
hapticFeedbackPerformed = false
|
|
||||||
offsetX = 0f
|
|
||||||
}
|
|
||||||
)
|
|
||||||
else Modifier
|
|
||||||
)
|
|
||||||
)
|
|
||||||
BoxWithConstraints(Modifier.fillMaxHeight()) {
|
|
||||||
Row(
|
|
||||||
Modifier
|
|
||||||
.fillMaxHeight()
|
|
||||||
.offset(
|
|
||||||
x = with(LocalDensity.current) {
|
|
||||||
maxWidth - abs(
|
|
||||||
animOffsetX
|
|
||||||
).toDp()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.background(
|
|
||||||
MaterialTheme.colorScheme.primary.copy(
|
|
||||||
alpha = 0.1f
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.fillMaxWidth(),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(
|
|
||||||
8.dp,
|
|
||||||
Alignment.Start
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
when {
|
|
||||||
offsetX <= -300f -> "stop"
|
|
||||||
else -> "keep"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
putTextAtCursorPosition = viewModel::putAtCursorPosition,
|
||||||
|
replyToMessage = viewModel::addReplyTo,
|
||||||
|
scope = scope
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is ChannelScreenItem.ProspectiveMessage -> {
|
is ChannelScreenItem.ProspectiveMessage -> {
|
||||||
|
|
|
||||||
|
|
@ -268,6 +268,9 @@
|
||||||
<string name="x_days_ago">%1$d days ago</string>
|
<string name="x_days_ago">%1$d days ago</string>
|
||||||
<string name="edited">(edited)</string>
|
<string name="edited">(edited)</string>
|
||||||
|
|
||||||
|
<string name="swipe_to_reply_keep_swiping">Reply</string>
|
||||||
|
<string name="swipe_to_reply_release">Release to reply</string>
|
||||||
|
|
||||||
<string name="disconnected">Disconnected</string>
|
<string name="disconnected">Disconnected</string>
|
||||||
<string name="tap_to_reconnect">Tap to reconnect</string>
|
<string name="tap_to_reconnect">Tap to reconnect</string>
|
||||||
<string name="reconnecting">Reconnecting…</string>
|
<string name="reconnecting">Reconnecting…</string>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue