This commit is contained in:
2026-07-13 15:51:39 +07:00
parent 2e290cce0c
commit 352863ea7c
4 changed files with 88 additions and 28 deletions

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#000000"
android:pathData="M360,720Q327,720 303.5,696.5Q280,673 280,640L280,160Q280,127 303.5,103.5Q327,80 360,80L720,80Q753,80 776.5,103.5Q800,127 800,160L800,640Q800,673 776.5,696.5Q753,720 720,720L360,720ZM360,640L720,640Q720,640 720,640Q720,640 720,640L720,160Q720,160 720,160Q720,160 720,160L360,160Q360,160 360,160Q360,160 360,160L360,640Q360,640 360,640Q360,640 360,640ZM200,880Q167,880 143.5,856.5Q120,833 120,800L120,240L200,240L200,800Q200,800 200,800Q200,800 200,800L640,800L640,880L200,880ZM360,640Q360,640 360,640Q360,640 360,640L360,160Q360,160 360,160Q360,160 360,160L360,160Q360,160 360,160Q360,160 360,160L360,640Q360,640 360,640Q360,640 360,640Z" />
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#000000"
android:pathData="M440,680L520,680L520,440L440,440L440,680ZM508.5,348.5Q520,337 520,320Q520,303 508.5,291.5Q497,280 480,280Q463,280 451.5,291.5Q440,303 440,320Q440,337 451.5,348.5Q463,360 480,360Q497,360 508.5,348.5ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z" />
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#000000"
android:pathData="M760,760L760,600Q760,550 725,515Q690,480 640,480L273,480L417,624L360,680L120,440L360,200L417,256L273,400L640,400Q723,400 781.5,458.5Q840,517 840,600L840,760L760,760Z" />
</vector>

View File

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