mirror of
https://github.com/fromchat-messenger/app.git
synced 2026-09-22 19:15:05 +03:00
Fix context menu
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
---
|
||||
alwaysApply: true
|
||||
---
|
||||
@@ -41,9 +41,12 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.layout.positionInRoot
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.chrisbanes.haze.HazeProgressive
|
||||
@@ -331,16 +334,25 @@ fun ChatScreen(
|
||||
modifier = Modifier.fillMaxSize(), // Fill the entire space of the Box
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp, alignment = Alignment.Bottom)
|
||||
) {
|
||||
item { Spacer(Modifier.height(innerPadding.calculateBottomPadding())) } // Spacer for chat input
|
||||
item { Spacer(Modifier.height(innerPadding.calculateTopPadding())) } // Spacer for TopAppBar
|
||||
|
||||
items(
|
||||
items = panelState.messages,
|
||||
key = { it.id }
|
||||
) { message ->
|
||||
val isAuthor = message.user_id == currentUserId
|
||||
var tapPosition by remember { mutableStateOf(IntOffset(0, 0)) }
|
||||
var messagePosition by remember { mutableStateOf(IntOffset(0, 0)) }
|
||||
var tapOffset by remember { mutableStateOf(Offset(0f, 0f)) }
|
||||
|
||||
Box(
|
||||
modifier = Modifier.hazeSource(hazeState)
|
||||
modifier = Modifier
|
||||
.hazeSource(hazeState)
|
||||
.onGloballyPositioned { coordinates ->
|
||||
messagePosition = IntOffset(
|
||||
coordinates.positionInRoot().x.toInt(),
|
||||
coordinates.positionInRoot().y.toInt()
|
||||
)
|
||||
}
|
||||
) {
|
||||
MessageItem(
|
||||
message = message,
|
||||
@@ -349,16 +361,20 @@ fun ChatScreen(
|
||||
contextMenuState = ContextMenuState(
|
||||
isOpen = true,
|
||||
message = message,
|
||||
position = tapPosition
|
||||
position = IntOffset(
|
||||
messagePosition.x + tapOffset.x.toInt(),
|
||||
messagePosition.y + tapOffset.y.toInt()
|
||||
)
|
||||
)
|
||||
},
|
||||
onTapPosition = { position ->
|
||||
tapPosition = position
|
||||
onTapPosition = { offset ->
|
||||
tapOffset = offset
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
item { Spacer(Modifier.height(innerPadding.calculateTopPadding())) } // Spacer for TopAppBar
|
||||
|
||||
item { Spacer(Modifier.height(innerPadding.calculateBottomPadding())) } // Spacer for chat input
|
||||
}
|
||||
}
|
||||
|
||||
@@ -381,7 +397,6 @@ fun ChatScreen(
|
||||
panel.handleDeleteMessage(message.id)
|
||||
}
|
||||
},
|
||||
hazeState = hazeState
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
package ru.fromchat.ui.chat
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animate
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.scaleIn
|
||||
import androidx.compose.animation.scaleOut
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
@@ -19,25 +16,30 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Reply
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.TransformOrigin
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.layout.positionInRoot
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Popup
|
||||
import androidx.compose.ui.window.PopupProperties
|
||||
import dev.chrisbanes.haze.HazeState
|
||||
import dev.chrisbanes.haze.hazeEffect
|
||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||
import dev.chrisbanes.haze.materials.HazeMaterials
|
||||
import ru.fromchat.api.Message
|
||||
|
||||
data class ContextMenuState(
|
||||
@@ -46,7 +48,6 @@ data class ContextMenuState(
|
||||
val position: IntOffset = IntOffset(0, 0)
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalHazeMaterialsApi::class)
|
||||
@Composable
|
||||
fun MessageContextMenu(
|
||||
state: ContextMenuState,
|
||||
@@ -56,71 +57,124 @@ fun MessageContextMenu(
|
||||
onEdit: (Message) -> Unit,
|
||||
onDelete: (Message) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
hazeState: HazeState
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = state.isOpen,
|
||||
enter = fadeIn(tween(200)) + scaleIn(initialScale = 0.8f, animationSpec = tween(250)),
|
||||
exit = fadeOut(tween(150)) + scaleOut(targetScale = 0.8f, animationSpec = tween(150))
|
||||
) {
|
||||
var shouldShowPopup by remember(state.message) {
|
||||
mutableStateOf(state.isOpen && state.message != null)
|
||||
}
|
||||
val animationProgress = remember { mutableFloatStateOf(0f) }
|
||||
|
||||
LaunchedEffect(state.isOpen) {
|
||||
if (state.isOpen) {
|
||||
// Enter animation
|
||||
animate(
|
||||
initialValue = 0f,
|
||||
targetValue = 1f,
|
||||
animationSpec = tween(200)
|
||||
) { value, _ ->
|
||||
animationProgress.floatValue = value
|
||||
}
|
||||
} else {
|
||||
// Exit animation
|
||||
animate(
|
||||
initialValue = 1f,
|
||||
targetValue = 0f,
|
||||
animationSpec = tween(150)
|
||||
) { value, _ ->
|
||||
animationProgress.floatValue = value
|
||||
}
|
||||
kotlinx.coroutines.delay(150)
|
||||
shouldShowPopup = false
|
||||
}
|
||||
}
|
||||
|
||||
// Show popup when opening
|
||||
LaunchedEffect(state.isOpen, state.message) {
|
||||
if (state.isOpen && state.message != null) {
|
||||
Popup(
|
||||
onDismissRequest = onDismiss,
|
||||
alignment = Alignment.TopStart,
|
||||
offset = state.position,
|
||||
properties = PopupProperties(
|
||||
dismissOnBackPress = true,
|
||||
dismissOnClickOutside = true
|
||||
)
|
||||
shouldShowPopup = true
|
||||
animationProgress.floatValue = 0f
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldShowPopup && state.message != null) {
|
||||
var popupSize by remember { mutableStateOf(Offset(0f, 0f)) }
|
||||
var popupPosition by remember { mutableStateOf(Offset(0f, 0f)) }
|
||||
|
||||
// Calculate transform origin based on click position relative to popup
|
||||
val transformOriginX = if (popupSize.x > 0f) {
|
||||
val clickXInPopup = state.position.x - popupPosition.x.toInt()
|
||||
(clickXInPopup / popupSize.x).coerceIn(0f, 1f)
|
||||
} else 0f
|
||||
val transformOriginY = if (popupSize.y > 0f) {
|
||||
val clickYInPopup = state.position.y - popupPosition.y.toInt()
|
||||
(clickYInPopup / popupSize.y).coerceIn(0f, 1f)
|
||||
} else 0f
|
||||
|
||||
val scale = animationProgress.floatValue * 0.2f + 0.8f
|
||||
val alpha = animationProgress.floatValue
|
||||
|
||||
Popup(
|
||||
onDismissRequest = onDismiss,
|
||||
alignment = Alignment.TopStart,
|
||||
offset = state.position,
|
||||
properties = PopupProperties(
|
||||
dismissOnBackPress = true,
|
||||
dismissOnClickOutside = true
|
||||
)
|
||||
) {
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.width(160.dp)
|
||||
.onGloballyPositioned { coordinates ->
|
||||
popupSize = Offset(
|
||||
coordinates.size.width.toFloat(),
|
||||
coordinates.size.height.toFloat()
|
||||
)
|
||||
popupPosition = coordinates.positionInRoot()
|
||||
}
|
||||
.graphicsLayer(
|
||||
scaleX = scale,
|
||||
scaleY = scale,
|
||||
alpha = alpha,
|
||||
transformOrigin = TransformOrigin(transformOriginX, transformOriginY)
|
||||
)
|
||||
.shadow(8.dp, RoundedCornerShape(8.dp)),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceContainerHighest
|
||||
) {
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.width(160.dp)
|
||||
.shadow(8.dp, RoundedCornerShape(8.dp))
|
||||
.hazeEffect(
|
||||
state = hazeState,
|
||||
style = HazeMaterials.thick()
|
||||
),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
color = Color.Transparent
|
||||
) {
|
||||
Column {
|
||||
// Reply button (always shown)
|
||||
Column {
|
||||
// Reply button (always shown)
|
||||
ContextMenuItem(
|
||||
icon = Icons.AutoMirrored.Filled.Reply,
|
||||
text = "Reply",
|
||||
onClick = {
|
||||
onReply(state.message)
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
|
||||
// Edit button (only for own messages)
|
||||
if (isAuthor) {
|
||||
ContextMenuItem(
|
||||
icon = Icons.AutoMirrored.Filled.Reply,
|
||||
text = "Reply",
|
||||
icon = Icons.Default.Edit,
|
||||
text = "Edit",
|
||||
onClick = {
|
||||
state.message?.let { onReply(it) }
|
||||
onEdit(state.message)
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Edit button (only for own messages)
|
||||
if (isAuthor) {
|
||||
HorizontalDivider()
|
||||
ContextMenuItem(
|
||||
icon = Icons.Default.Edit,
|
||||
text = "Edit",
|
||||
onClick = {
|
||||
state.message?.let { onEdit(it) }
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Delete button (only for own messages)
|
||||
if (isAuthor) {
|
||||
HorizontalDivider()
|
||||
ContextMenuItem(
|
||||
icon = Icons.Default.Delete,
|
||||
text = "Delete",
|
||||
onClick = {
|
||||
state.message?.let { onDelete(it) }
|
||||
onDismiss()
|
||||
},
|
||||
isError = true
|
||||
)
|
||||
}
|
||||
// Delete button (only for own messages)
|
||||
if (isAuthor) {
|
||||
ContextMenuItem(
|
||||
icon = Icons.Default.Delete,
|
||||
text = "Delete",
|
||||
onClick = {
|
||||
onDelete(state.message)
|
||||
onDismiss()
|
||||
},
|
||||
isError = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -149,6 +203,7 @@ private fun ContextMenuItem(
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
|
||||
@@ -30,6 +30,7 @@ import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -45,7 +46,7 @@ fun MessageItem(
|
||||
message: Message,
|
||||
isAuthor: Boolean,
|
||||
onLongPress: () -> Unit,
|
||||
onTapPosition: (IntOffset) -> Unit = {},
|
||||
onTapPosition: (Offset) -> Unit = {},
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
@@ -67,12 +68,7 @@ fun MessageItem(
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onLongPress = { offset ->
|
||||
onTapPosition(
|
||||
IntOffset(
|
||||
offset.x.toInt(),
|
||||
offset.y.toInt()
|
||||
)
|
||||
)
|
||||
onTapPosition(offset)
|
||||
onLongPress()
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user