From 9a865b33a005ee2ab8fbb8bcdcc24c942166e04a Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 1 Jun 2026 14:29:39 +0300 Subject: [PATCH 1/2] feat(assistant): copy content Signed-off-by: alperozturk96 --- .../client/assistant/chat/ChatContent.kt | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/app/src/main/java/com/nextcloud/client/assistant/chat/ChatContent.kt b/app/src/main/java/com/nextcloud/client/assistant/chat/ChatContent.kt index 7c5f11dd205b..7e0f26b6bf11 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/chat/ChatContent.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/chat/ChatContent.kt @@ -9,6 +9,7 @@ package com.nextcloud.client.assistant.chat +import android.content.ClipData import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.animateFloat @@ -17,6 +18,7 @@ import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.tween import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -36,6 +38,8 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -43,11 +47,17 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.scale import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalClipboard +import androidx.compose.ui.platform.toClipEntry import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -60,6 +70,7 @@ import com.nextcloud.utils.TimeConstants import com.nextcloud.utils.extensions.time import com.owncloud.android.R import com.owncloud.android.lib.resources.assistant.chat.model.ChatMessage +import kotlinx.coroutines.launch import java.time.Instant private val MIN_CHAT_HEIGHT = 60.dp @@ -342,6 +353,10 @@ private fun TypingAnimation() { @Composable private fun AssistantMessageItem(message: ChatMessage) { + var showMenu by remember { mutableStateOf(false) } + val clipboard = LocalClipboard.current + val scope = rememberCoroutineScope() + Box( modifier = Modifier .padding(vertical = 12.dp) @@ -375,8 +390,28 @@ private fun AssistantMessageItem(message: ChatMessage) { ) ) .background(color = colorResource(R.color.bg_message_bubble)) + .combinedClickable( + onClick = {}, + onLongClick = { showMenu = true } + ) ) { MessageTextItem(message) + DropdownMenu( + expanded = showMenu, + onDismissRequest = { showMenu = false } + ) { + DropdownMenuItem( + text = { Text(stringResource(R.string.common_copy)) }, + onClick = { + scope.launch { + clipboard.setClipEntry( + ClipData.newPlainText(null, message.content).toClipEntry() + ) + } + showMenu = false + } + ) + } } } } @@ -384,6 +419,10 @@ private fun AssistantMessageItem(message: ChatMessage) { @Composable private fun UserMessageItem(message: ChatMessage) { + var showMenu by remember { mutableStateOf(false) } + val clipboard = LocalClipboard.current + val scope = rememberCoroutineScope() + Box( modifier = Modifier .padding(vertical = 12.dp) @@ -402,8 +441,28 @@ private fun UserMessageItem(message: ChatMessage) { ) ) .background(color = colorResource(R.color.bg_message_bubble)) + .combinedClickable( + onClick = {}, + onLongClick = { showMenu = true } + ) ) { MessageTextItem(message) + DropdownMenu( + expanded = showMenu, + onDismissRequest = { showMenu = false } + ) { + DropdownMenuItem( + text = { Text(stringResource(R.string.common_copy)) }, + onClick = { + scope.launch { + clipboard.setClipEntry( + ClipData.newPlainText(null, message.content).toClipEntry() + ) + } + showMenu = false + } + ) + } } } } From f05ab31068ef921d0a01fb1aab23ed0cb1ac01b4 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 1 Jun 2026 14:37:23 +0300 Subject: [PATCH 2/2] feat(assistant): copy content Signed-off-by: alperozturk96 --- .../client/assistant/chat/ChatContent.kt | 74 +++++++------------ 1 file changed, 27 insertions(+), 47 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/assistant/chat/ChatContent.kt b/app/src/main/java/com/nextcloud/client/assistant/chat/ChatContent.kt index 7e0f26b6bf11..65a446fabf3c 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/chat/ChatContent.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/chat/ChatContent.kt @@ -352,11 +352,33 @@ private fun TypingAnimation() { } @Composable -private fun AssistantMessageItem(message: ChatMessage) { +private fun CopyableMessageBubble(text: String, modifier: Modifier = Modifier, content: @Composable () -> Unit) { var showMenu by remember { mutableStateOf(false) } val clipboard = LocalClipboard.current val scope = rememberCoroutineScope() + Box( + modifier = modifier + .combinedClickable(onClick = {}, onLongClick = { showMenu = true }) + ) { + content() + DropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) { + DropdownMenuItem( + text = { Text(stringResource(R.string.common_copy)) }, + onClick = { + scope.launch { + val plainText = ClipData.newPlainText(null, text).toClipEntry() + clipboard.setClipEntry(plainText) + } + showMenu = false + } + ) + } + } +} + +@Composable +private fun AssistantMessageItem(message: ChatMessage) { Box( modifier = Modifier .padding(vertical = 12.dp) @@ -378,7 +400,8 @@ private fun AssistantMessageItem(message: ChatMessage) { alignment = Alignment.Center ) } - Box( + CopyableMessageBubble( + text = message.content, modifier = Modifier .padding(start = 8.dp, end = 16.dp) .defaultMinSize(minHeight = MIN_CHAT_HEIGHT) @@ -390,28 +413,8 @@ private fun AssistantMessageItem(message: ChatMessage) { ) ) .background(color = colorResource(R.color.bg_message_bubble)) - .combinedClickable( - onClick = {}, - onLongClick = { showMenu = true } - ) ) { MessageTextItem(message) - DropdownMenu( - expanded = showMenu, - onDismissRequest = { showMenu = false } - ) { - DropdownMenuItem( - text = { Text(stringResource(R.string.common_copy)) }, - onClick = { - scope.launch { - clipboard.setClipEntry( - ClipData.newPlainText(null, message.content).toClipEntry() - ) - } - showMenu = false - } - ) - } } } } @@ -419,17 +422,14 @@ private fun AssistantMessageItem(message: ChatMessage) { @Composable private fun UserMessageItem(message: ChatMessage) { - var showMenu by remember { mutableStateOf(false) } - val clipboard = LocalClipboard.current - val scope = rememberCoroutineScope() - Box( modifier = Modifier .padding(vertical = 12.dp) .fillMaxWidth(), contentAlignment = Alignment.CenterEnd ) { - Box( + CopyableMessageBubble( + text = message.content, modifier = Modifier .padding(start = 16.dp, end = 8.dp) .defaultMinSize(minHeight = MIN_CHAT_HEIGHT) @@ -441,28 +441,8 @@ private fun UserMessageItem(message: ChatMessage) { ) ) .background(color = colorResource(R.color.bg_message_bubble)) - .combinedClickable( - onClick = {}, - onLongClick = { showMenu = true } - ) ) { MessageTextItem(message) - DropdownMenu( - expanded = showMenu, - onDismissRequest = { showMenu = false } - ) { - DropdownMenuItem( - text = { Text(stringResource(R.string.common_copy)) }, - onClick = { - scope.launch { - clipboard.setClipEntry( - ClipData.newPlainText(null, message.content).toClipEntry() - ) - } - showMenu = false - } - ) - } } } }