parent
922ca6b874
commit
df3a1ab26f
|
|
@ -184,7 +184,8 @@ fun Message(
|
|||
onReply: () -> Unit = {},
|
||||
onAddReaction: () -> Unit = {},
|
||||
fromWebhook: Boolean = false,
|
||||
webhookName: String? = null
|
||||
webhookName: String? = null,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val author = RevoltAPI.userCache[message.author] ?: return CircularProgressIndicator()
|
||||
val context = LocalContext.current
|
||||
|
|
@ -200,7 +201,7 @@ fun Message(
|
|||
|
||||
val authorIsBlocked = remember(author) { author.relationship == "Blocked" }
|
||||
|
||||
Column(Modifier.animateContentSize()) {
|
||||
Column(modifier.animateContentSize()) {
|
||||
if (message.tail == false) {
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
package chat.revolt.internals.extensions
|
||||
|
||||
import androidx.compose.foundation.gestures.awaitEachGesture
|
||||
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.pointer.PointerEvent
|
||||
import androidx.compose.ui.input.pointer.PointerEventPass
|
||||
import androidx.compose.ui.input.pointer.PointerInputChange
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
|
||||
fun Modifier.supportSwipeReply(
|
||||
pass: PointerEventPass = PointerEventPass.Main,
|
||||
onDown: (pointer: PointerInputChange) -> Unit,
|
||||
onMove: (changes: List<PointerInputChange>) -> Unit,
|
||||
onUp: () -> Unit,
|
||||
) = this.then(
|
||||
Modifier.pointerInput(pass) {
|
||||
awaitEachGesture {
|
||||
val down = awaitFirstDown(pass = pass, requireUnconsumed = false)
|
||||
onDown(down)
|
||||
do {
|
||||
val event: PointerEvent = awaitPointerEvent(
|
||||
pass = pass
|
||||
)
|
||||
|
||||
onMove(event.changes)
|
||||
|
||||
} while (event.changes.any { it.pressed })
|
||||
onUp()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
@ -804,8 +804,10 @@ fun ChatRouterScreen(
|
|||
)
|
||||
}
|
||||
} else {
|
||||
var useSidebarGesture by remember { mutableStateOf(true) }
|
||||
DismissibleNavigationDrawer(
|
||||
drawerState = drawerState,
|
||||
gesturesEnabled = useSidebarGesture,
|
||||
drawerContent = {
|
||||
DismissibleDrawerSheet(
|
||||
drawerContainerColor = Color.Transparent,
|
||||
|
|
@ -842,7 +844,11 @@ fun ChatRouterScreen(
|
|||
toggleDrawer = {
|
||||
toggleDrawerLambda()
|
||||
},
|
||||
drawerState = drawerState
|
||||
drawerState = drawerState,
|
||||
drawerGestureEnabled = useSidebarGesture,
|
||||
setDrawerGestureEnabled = {
|
||||
useSidebarGesture = it
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -884,7 +890,9 @@ fun ChannelNavigator(
|
|||
topNav: NavController,
|
||||
useDrawer: Boolean,
|
||||
toggleDrawer: () -> Unit,
|
||||
drawerState: DrawerState? = null
|
||||
drawerState: DrawerState? = null,
|
||||
drawerGestureEnabled: Boolean = true,
|
||||
setDrawerGestureEnabled: (Boolean) -> Unit = {},
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
|
|
@ -922,7 +930,10 @@ fun ChannelNavigator(
|
|||
}
|
||||
}
|
||||
},
|
||||
useDrawer = useDrawer
|
||||
useDrawer = useDrawer,
|
||||
drawerGestureEnabled = drawerGestureEnabled,
|
||||
setDrawerGestureEnabled = setDrawerGestureEnabled,
|
||||
drawerState = drawerState,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package chat.revolt.screens.chat.views.channel
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.ContentValues
|
||||
import android.content.res.Configuration
|
||||
|
|
@ -17,6 +18,7 @@ import androidx.compose.animation.AnimatedContent
|
|||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.animateIntAsState
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
|
|
@ -27,12 +29,14 @@ import androidx.compose.foundation.background
|
|||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
|
|
@ -42,6 +46,7 @@ import androidx.compose.foundation.layout.imeAnimationTarget
|
|||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredHeight
|
||||
import androidx.compose.foundation.layout.size
|
||||
|
|
@ -60,6 +65,7 @@ import androidx.compose.material3.AssistChip
|
|||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.DrawerState
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
|
|
@ -81,6 +87,7 @@ import androidx.compose.runtime.DisposableEffect
|
|||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
|
|
@ -90,8 +97,12 @@ import androidx.compose.runtime.snapshotFlow
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.input.pointer.PointerInputChange
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
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.text.Placeholder
|
||||
|
|
@ -120,6 +131,8 @@ import chat.revolt.api.routes.channel.react
|
|||
import chat.revolt.api.routes.microservices.autumn.FileArgs
|
||||
import chat.revolt.api.schemas.ChannelType
|
||||
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.DateDivider
|
||||
|
|
@ -140,6 +153,7 @@ import chat.revolt.components.screens.chat.TypingIndicator
|
|||
import chat.revolt.components.skeletons.MessageSkeleton
|
||||
import chat.revolt.components.skeletons.MessageSkeletonVariant
|
||||
import chat.revolt.internals.extensions.rememberChannelPermissions
|
||||
import chat.revolt.internals.extensions.supportSwipeReply
|
||||
import chat.revolt.internals.extensions.zero
|
||||
import chat.revolt.sheets.ChannelInfoSheet
|
||||
import chat.revolt.sheets.MessageContextSheet
|
||||
|
|
@ -151,6 +165,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.datetime.Instant
|
||||
import java.io.File
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
|
||||
sealed class ChannelScreenItem {
|
||||
|
|
@ -182,6 +197,7 @@ private fun pxAsDp(px: Int): Dp {
|
|||
|
||||
private const val NOT_ENOUGH_SPACE_FOR_PANES_THRESHOLD = 500
|
||||
|
||||
@SuppressLint("UnusedBoxWithConstraintsScope")
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun ChannelScreen(
|
||||
|
|
@ -189,6 +205,9 @@ fun ChannelScreen(
|
|||
onToggleDrawer: () -> Unit,
|
||||
useDrawer: Boolean,
|
||||
useBackButton: Boolean = false,
|
||||
drawerGestureEnabled: Boolean = true,
|
||||
setDrawerGestureEnabled: (Boolean) -> Unit = {},
|
||||
drawerState: DrawerState? = null,
|
||||
backButtonAction: (() -> Unit)? = null,
|
||||
useChatUI: Boolean = false,
|
||||
viewModel: ChannelScreenViewModel = hiltViewModel()
|
||||
|
|
@ -196,6 +215,8 @@ fun ChannelScreen(
|
|||
// <editor-fold desc="State and effects">
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
val haptic = LocalHapticFeedback.current
|
||||
val config = LocalConfiguration.current
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.listenToWsEvents()
|
||||
|
|
@ -381,6 +402,7 @@ fun ChannelScreen(
|
|||
// </editor-fold>
|
||||
// <editor-fold desc="UI elements">
|
||||
val lazyListState = rememberLazyListState()
|
||||
var disableScroll by remember { mutableStateOf(false) }
|
||||
|
||||
val isScrolledToBottom = remember(lazyListState) {
|
||||
derivedStateOf {
|
||||
|
|
@ -654,6 +676,7 @@ fun ChannelScreen(
|
|||
) {
|
||||
LazyColumn(
|
||||
state = lazyListState,
|
||||
userScrollEnabled = !disableScroll,
|
||||
reverseLayout = true,
|
||||
contentPadding = PaddingValues(top = 16.dp, bottom = 32.dp)
|
||||
) {
|
||||
|
|
@ -693,53 +716,189 @@ fun ChannelScreen(
|
|||
) { index ->
|
||||
when (val item = viewModel.items[index]) {
|
||||
is ChannelScreenItem.RegularMessage -> {
|
||||
Message(
|
||||
message = item.message,
|
||||
onMessageContextMenu = {
|
||||
item.message.id?.let { messageId ->
|
||||
messageContextSheetTarget = messageId
|
||||
messageContextSheetShown = true
|
||||
}
|
||||
var offsetX by remember { mutableFloatStateOf(0f) }
|
||||
val animOffsetX by animateFloatAsState(
|
||||
when {
|
||||
offsetX > -20f -> 0f
|
||||
else -> offsetX
|
||||
},
|
||||
onAvatarClick = {
|
||||
if (item.message.webhook != null) {
|
||||
scope.launch {
|
||||
ActionChannel.send(Action.OpenWebhookSheet)
|
||||
label = "X offset of message for replies"
|
||||
)
|
||||
var markGestureInvalid by remember { mutableStateOf(false) }
|
||||
var hapticFeedbackPerformed by remember {
|
||||
mutableStateOf(
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
var onMoveHandler: (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
|
||||
// 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
|
||||
}
|
||||
}
|
||||
} else {
|
||||
item.message.author?.let { author ->
|
||||
}
|
||||
|
||||
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.OpenUserSheet(
|
||||
author,
|
||||
viewModel.channel?.server
|
||||
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"
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is ChannelScreenItem.ProspectiveMessage -> {
|
||||
|
|
|
|||
Loading…
Reference in New Issue