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.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
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.IntOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import dev.chrisbanes.haze.HazeProgressive
|
import dev.chrisbanes.haze.HazeProgressive
|
||||||
@@ -331,16 +334,25 @@ fun ChatScreen(
|
|||||||
modifier = Modifier.fillMaxSize(), // Fill the entire space of the Box
|
modifier = Modifier.fillMaxSize(), // Fill the entire space of the Box
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp, alignment = Alignment.Bottom)
|
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(
|
||||||
items = panelState.messages,
|
items = panelState.messages,
|
||||||
key = { it.id }
|
key = { it.id }
|
||||||
) { message ->
|
) { message ->
|
||||||
val isAuthor = message.user_id == currentUserId
|
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(
|
Box(
|
||||||
modifier = Modifier.hazeSource(hazeState)
|
modifier = Modifier
|
||||||
|
.hazeSource(hazeState)
|
||||||
|
.onGloballyPositioned { coordinates ->
|
||||||
|
messagePosition = IntOffset(
|
||||||
|
coordinates.positionInRoot().x.toInt(),
|
||||||
|
coordinates.positionInRoot().y.toInt()
|
||||||
|
)
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
MessageItem(
|
MessageItem(
|
||||||
message = message,
|
message = message,
|
||||||
@@ -349,16 +361,20 @@ fun ChatScreen(
|
|||||||
contextMenuState = ContextMenuState(
|
contextMenuState = ContextMenuState(
|
||||||
isOpen = true,
|
isOpen = true,
|
||||||
message = message,
|
message = message,
|
||||||
position = tapPosition
|
position = IntOffset(
|
||||||
|
messagePosition.x + tapOffset.x.toInt(),
|
||||||
|
messagePosition.y + tapOffset.y.toInt()
|
||||||
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onTapPosition = { position ->
|
onTapPosition = { offset ->
|
||||||
tapPosition = position
|
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)
|
panel.handleDeleteMessage(message.id)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
hazeState = hazeState
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
package ru.fromchat.ui.chat
|
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.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.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
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.automirrored.filled.Reply
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material.icons.filled.Edit
|
import androidx.compose.material.icons.filled.Edit
|
||||||
import androidx.compose.material3.HorizontalDivider
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
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.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.shadow
|
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.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.IntOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Popup
|
import androidx.compose.ui.window.Popup
|
||||||
import androidx.compose.ui.window.PopupProperties
|
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
|
import ru.fromchat.api.Message
|
||||||
|
|
||||||
data class ContextMenuState(
|
data class ContextMenuState(
|
||||||
@@ -46,7 +48,6 @@ data class ContextMenuState(
|
|||||||
val position: IntOffset = IntOffset(0, 0)
|
val position: IntOffset = IntOffset(0, 0)
|
||||||
)
|
)
|
||||||
|
|
||||||
@OptIn(ExperimentalHazeMaterialsApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MessageContextMenu(
|
fun MessageContextMenu(
|
||||||
state: ContextMenuState,
|
state: ContextMenuState,
|
||||||
@@ -56,71 +57,124 @@ fun MessageContextMenu(
|
|||||||
onEdit: (Message) -> Unit,
|
onEdit: (Message) -> Unit,
|
||||||
onDelete: (Message) -> Unit,
|
onDelete: (Message) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
hazeState: HazeState
|
|
||||||
) {
|
) {
|
||||||
AnimatedVisibility(
|
var shouldShowPopup by remember(state.message) {
|
||||||
visible = state.isOpen,
|
mutableStateOf(state.isOpen && state.message != null)
|
||||||
enter = fadeIn(tween(200)) + scaleIn(initialScale = 0.8f, animationSpec = tween(250)),
|
}
|
||||||
exit = fadeOut(tween(150)) + scaleOut(targetScale = 0.8f, animationSpec = tween(150))
|
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) {
|
if (state.isOpen && state.message != null) {
|
||||||
Popup(
|
shouldShowPopup = true
|
||||||
onDismissRequest = onDismiss,
|
animationProgress.floatValue = 0f
|
||||||
alignment = Alignment.TopStart,
|
}
|
||||||
offset = state.position,
|
}
|
||||||
properties = PopupProperties(
|
|
||||||
dismissOnBackPress = true,
|
if (shouldShowPopup && state.message != null) {
|
||||||
dismissOnClickOutside = true
|
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(
|
Column {
|
||||||
modifier = modifier
|
// Reply button (always shown)
|
||||||
.width(160.dp)
|
ContextMenuItem(
|
||||||
.shadow(8.dp, RoundedCornerShape(8.dp))
|
icon = Icons.AutoMirrored.Filled.Reply,
|
||||||
.hazeEffect(
|
text = "Reply",
|
||||||
state = hazeState,
|
onClick = {
|
||||||
style = HazeMaterials.thick()
|
onReply(state.message)
|
||||||
),
|
onDismiss()
|
||||||
shape = RoundedCornerShape(8.dp),
|
}
|
||||||
color = Color.Transparent
|
)
|
||||||
) {
|
|
||||||
Column {
|
// Edit button (only for own messages)
|
||||||
// Reply button (always shown)
|
if (isAuthor) {
|
||||||
ContextMenuItem(
|
ContextMenuItem(
|
||||||
icon = Icons.AutoMirrored.Filled.Reply,
|
icon = Icons.Default.Edit,
|
||||||
text = "Reply",
|
text = "Edit",
|
||||||
onClick = {
|
onClick = {
|
||||||
state.message?.let { onReply(it) }
|
onEdit(state.message)
|
||||||
onDismiss()
|
onDismiss()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Edit button (only for own messages)
|
// Delete button (only for own messages)
|
||||||
if (isAuthor) {
|
if (isAuthor) {
|
||||||
HorizontalDivider()
|
ContextMenuItem(
|
||||||
ContextMenuItem(
|
icon = Icons.Default.Delete,
|
||||||
icon = Icons.Default.Edit,
|
text = "Delete",
|
||||||
text = "Edit",
|
onClick = {
|
||||||
onClick = {
|
onDelete(state.message)
|
||||||
state.message?.let { onEdit(it) }
|
onDismiss()
|
||||||
onDismiss()
|
},
|
||||||
}
|
isError = true
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,6 +203,7 @@ private fun ContextMenuItem(
|
|||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
.clickable(onClick = onClick)
|
.clickable(onClick = onClick)
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
contentAlignment = Alignment.CenterStart
|
contentAlignment = Alignment.CenterStart
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import androidx.compose.ui.graphics.Brush
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
@@ -45,7 +46,7 @@ fun MessageItem(
|
|||||||
message: Message,
|
message: Message,
|
||||||
isAuthor: Boolean,
|
isAuthor: Boolean,
|
||||||
onLongPress: () -> Unit,
|
onLongPress: () -> Unit,
|
||||||
onTapPosition: (IntOffset) -> Unit = {},
|
onTapPosition: (Offset) -> Unit = {},
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
@@ -67,12 +68,7 @@ fun MessageItem(
|
|||||||
.pointerInput(Unit) {
|
.pointerInput(Unit) {
|
||||||
detectTapGestures(
|
detectTapGestures(
|
||||||
onLongPress = { offset ->
|
onLongPress = { offset ->
|
||||||
onTapPosition(
|
onTapPosition(offset)
|
||||||
IntOffset(
|
|
||||||
offset.x.toInt(),
|
|
||||||
offset.y.toInt()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
onLongPress()
|
onLongPress()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user