feat(#136): visible copy button overlay on long-press of message bubbles (#143)

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
M57andM57 authored and GitHub committed 2026-06-20 09:46:10 +03:00
1 parent 113467a99d
commit 7248f66abf
1 file changed
+96 -2
@@ -6,6 +6,9 @@ import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
@@ -16,6 +19,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
@@ -25,15 +29,18 @@ import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Build
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material.icons.filled.Error
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
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.mutableStateOf
import androidx.compose.runtime.remember
@@ -65,6 +72,7 @@ import com.m57.hermescontrol.theme.SystemMessageColor
import com.m57.hermescontrol.theme.ToolChipColor
import com.m57.hermescontrol.theme.ToolChipColorLight
import com.m57.hermescontrol.theme.UserBubble
import kotlinx.coroutines.delay
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@@ -124,6 +132,15 @@ private fun UserBubble(
modifier: Modifier = Modifier,
) {
val clipboardManager = LocalClipboardManager.current
var showCopyButton by remember { mutableStateOf(false) }
// Auto-dismiss copy button after 4 seconds
LaunchedEffect(showCopyButton) {
if (showCopyButton) {
delay(4000)
showCopyButton = false
}
}
val highlightedText =
remember(message.content, searchQuery, isCurrentMatch) {
@@ -148,6 +165,7 @@ private fun UserBubble(
MaterialTheme.colorScheme.secondary,
),
)
Box {
Surface(
modifier =
Modifier
@@ -163,7 +181,7 @@ private fun UserBubble(
.pointerInput(message.content) {
detectTapGestures(
onLongPress = {
clipboardManager.setText(AnnotatedString(message.content))
showCopyButton = true
},
)
},
@@ -189,6 +207,39 @@ private fun UserBubble(
)
}
}
// Copy button overlay — top-right of the bubble
AnimatedVisibility(
visible = showCopyButton,
enter = fadeIn() + scaleIn(),
exit = fadeOut() + scaleOut(),
modifier =
Modifier
.align(Alignment.TopEnd)
.offset(x = 8.dp, y = (-8).dp),
) {
Surface(
shape = RoundedCornerShape(50),
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.95f),
shadowElevation = 6.dp,
) {
IconButton(
onClick = {
clipboardManager.setText(AnnotatedString(message.content))
showCopyButton = false
},
modifier = Modifier.size(32.dp),
) {
Icon(
Icons.Filled.ContentCopy,
contentDescription = "Copy",
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.onSurface,
)
}
}
}
}
}
}
@@ -204,6 +255,15 @@ private fun AssistantBubble(
val bubbleColor = if (isDarkTheme) AssistantBubble else AssistantBubbleLight
val textColor = MaterialTheme.colorScheme.onSurface
val clipboardManager = LocalClipboardManager.current
var showCopyButton by remember { mutableStateOf(false) }
// Auto-dismiss copy button after 4 seconds
LaunchedEffect(showCopyButton) {
if (showCopyButton) {
delay(4000)
showCopyButton = false
}
}
Box(
modifier =
@@ -212,6 +272,7 @@ private fun AssistantBubble(
.padding(horizontal = 12.dp, vertical = 3.dp),
contentAlignment = Alignment.CenterStart,
) {
Box {
Surface(
modifier =
Modifier
@@ -227,7 +288,7 @@ private fun AssistantBubble(
).pointerInput(message.content) {
detectTapGestures(
onLongPress = {
clipboardManager.setText(AnnotatedString(message.content))
showCopyButton = true
},
)
},
@@ -261,6 +322,39 @@ private fun AssistantBubble(
}
}
}
// Copy button overlay — top-right of the bubble
AnimatedVisibility(
visible = showCopyButton,
enter = fadeIn() + scaleIn(),
exit = fadeOut() + scaleOut(),
modifier =
Modifier
.align(Alignment.TopEnd)
.offset(x = (-8).dp, y = (-8).dp),
) {
Surface(
shape = RoundedCornerShape(50),
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.95f),
shadowElevation = 6.dp,
) {
IconButton(
onClick = {
clipboardManager.setText(AnnotatedString(message.content))
showCopyButton = false
},
modifier = Modifier.size(32.dp),
) {
Icon(
Icons.Filled.ContentCopy,
contentDescription = "Copy",
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.onSurface,
)
}
}
}
}
}
}