diff --git a/composeApp/src/androidMain/composeResources/drawable/ic_copy.xml b/composeApp/src/androidMain/composeResources/drawable/ic_copy.xml
new file mode 100644
index 0000000..d57c759
--- /dev/null
+++ b/composeApp/src/androidMain/composeResources/drawable/ic_copy.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/composeApp/src/androidMain/composeResources/drawable/ic_info.xml b/composeApp/src/androidMain/composeResources/drawable/ic_info.xml
new file mode 100644
index 0000000..bf5dcf6
--- /dev/null
+++ b/composeApp/src/androidMain/composeResources/drawable/ic_info.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/composeApp/src/androidMain/composeResources/drawable/ic_reply.xml b/composeApp/src/androidMain/composeResources/drawable/ic_reply.xml
new file mode 100644
index 0000000..d45bfc5
--- /dev/null
+++ b/composeApp/src/androidMain/composeResources/drawable/ic_reply.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/chat/ChatScreen.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/chat/ChatScreen.kt
index 3670449..c0baab6 100644
--- a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/chat/ChatScreen.kt
+++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/chat/ChatScreen.kt
@@ -1,6 +1,7 @@
package su.reya.coop.screens.chat
import android.app.Activity
+import android.content.ClipData
import android.content.Intent
import android.net.Uri
import android.speech.RecognizerIntent
@@ -38,8 +39,10 @@ import androidx.compose.material3.Badge
import androidx.compose.material3.BadgedBox
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilledTonalButton
+import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LoadingIndicator
@@ -55,6 +58,7 @@ import androidx.compose.runtime.Composable
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
@@ -65,6 +69,9 @@ import androidx.compose.ui.draw.blur
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.ClipEntry
+import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalWindowInfo
@@ -74,6 +81,9 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coop.composeapp.generated.resources.Res
import coop.composeapp.generated.resources.ic_arrow_back
+import coop.composeapp.generated.resources.ic_copy
+import coop.composeapp.generated.resources.ic_info
+import coop.composeapp.generated.resources.ic_reply
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -100,6 +110,7 @@ fun ChatScreen(
) {
val context = LocalContext.current
val snackbarHostState = LocalSnackbarHostState.current
+ val clipboardManager = LocalClipboard.current
val navigator = LocalNavigator.current
val profileCache = LocalProfileCache.current
val scope = rememberCoroutineScope()
@@ -410,11 +421,10 @@ fun ChatScreen(
val windowHeight = windowInfo.containerSize.height
val scrollState = rememberScrollState()
- val menuHeightDp = 336.dp
- val spacingDp = 12.dp
- val totalExtraHeightPx = with(density) { (menuHeightDp + spacingDp).toPx() }
+ var menuHeight by remember { mutableFloatStateOf(0f) }
+ val spacing = with(density) { 12.dp.toPx() }
val showAbove =
- (windowHeight - bounds.bottom) < totalExtraHeightPx && bounds.top > totalExtraHeightPx
+ (windowHeight - bounds.bottom) < (menuHeight + spacing) && bounds.top > (menuHeight + spacing)
Box(
modifier = Modifier
@@ -423,8 +433,10 @@ fun ChatScreen(
.clickable { selectedMessage = null }
.verticalScroll(scrollState),
) {
- val contentBottomDp = with(density) { (bounds.bottom + totalExtraHeightPx).toDp() }
- Spacer(modifier = Modifier.height(contentBottomDp + 200.dp))
+ val totalExtraHeight = if (menuHeight > 0) menuHeight + spacing else 300f
+ val contentBottom = with(density) { (bounds.bottom + totalExtraHeight).toDp() }
+
+ Spacer(modifier = Modifier.height(contentBottom + 200.dp))
ChatMessage(
model = model,
@@ -434,19 +446,33 @@ fun ChatScreen(
)
val menuOffset = if (showAbove) {
- bounds.top - totalExtraHeightPx
+ bounds.top - menuHeight - spacing
} else {
- bounds.bottom + with(density) { 12.dp.toPx() } // 12dp spacing below
+ bounds.bottom + spacing
}
Box(
modifier = Modifier
.offset { IntOffset(0, menuOffset.toInt().coerceAtLeast(0)) }
+ .onGloballyPositioned { menuHeight = it.size.height.toFloat() }
.fillMaxWidth()
.padding(horizontal = 16.dp),
contentAlignment = if (model.isMine) Alignment.CenterEnd else Alignment.CenterStart
) {
- ContextMenu { selectedMessage = null }
+ ContextMenu { action ->
+ when (action) {
+ "Copy" -> {
+ scope.launch {
+ val content = model.annotatedContent
+ val data = ClipData.newPlainText(content, content)
+ clipboardManager.setClipEntry(ClipEntry(data))
+ }
+ }
+
+ else -> {}
+ }
+ selectedMessage = null
+ }
}
}
}
@@ -455,29 +481,35 @@ fun ChatScreen(
@Composable
private fun ContextMenu(onAction: (String) -> Unit) {
+ val menuItems = listOf(
+ Triple("Copy", Res.drawable.ic_copy, false),
+ Triple("Reply", Res.drawable.ic_reply, false),
+ Triple("Info", Res.drawable.ic_info, false),
+ )
+
Surface(
- shape = RoundedCornerShape(16.dp),
- color = MaterialTheme.colorScheme.surfaceContainerLow,
+ shape = RoundedCornerShape(24.dp),
+ color = MaterialTheme.colorScheme.tertiaryContainer,
modifier = Modifier.width(220.dp)
) {
- Column {
- listOf(
- "Reply",
- "Forward",
- "Copy",
- "Select",
- "Info",
- "Pin",
- "Delete"
- ).forEach { action ->
- Text(
- text = action,
- modifier = Modifier
- .fillMaxWidth()
- .clickable { onAction(action) }
- .padding(horizontal = 16.dp, vertical = 12.dp),
- style = MaterialTheme.typography.bodyLarge
+ Column(modifier = Modifier.padding(8.dp)) {
+ menuItems.forEach { (label, icon, hasDivider) ->
+ DropdownMenuItem(
+ text = { Text(label) },
+ onClick = { onAction(label) },
+ leadingIcon = {
+ Icon(
+ painter = painterResource(icon),
+ contentDescription = label,
+ )
+ }
)
+ if (hasDivider) {
+ HorizontalDivider(
+ modifier = Modifier.padding(vertical = 4.dp),
+ color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)
+ )
+ }
}
}
}