From 5cfda2950d53e9e4c6f0b856bee92eaacb279fae Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Tue, 14 Jul 2026 17:21:29 +0700 Subject: [PATCH] render reply --- .../su/reya/coop/screens/chat/ChatMessage.kt | 9 +- .../su/reya/coop/screens/chat/ChatScreen.kt | 86 ++++++++++++++++--- 2 files changed, 79 insertions(+), 16 deletions(-) diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/chat/ChatMessage.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/chat/ChatMessage.kt index 8c3000f..50e7c99 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/chat/ChatMessage.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/chat/ChatMessage.kt @@ -55,7 +55,8 @@ data class MessageModel( val annotatedContent: AnnotatedString, val images: List, val timestamp: String, - val isMine: Boolean + val isMine: Boolean, + val replyEventIds: List ) @Composable @@ -64,6 +65,7 @@ fun rememberMessageModel(event: UnsignedEvent, currentUser: PublicKey? = null): val id = event.ensureId().id()!! val isMine = currentUser == event.author() val content = event.content() + val replyEventIds = event.tags().eventIds() val images = URL_REGEX.findAll(content).map { it.value }.filter { it.isImageUrl() }.toList() val cleanedContent = content.removeImageUrls() @@ -98,6 +100,7 @@ fun rememberMessageModel(event: UnsignedEvent, currentUser: PublicKey? = null): images = images, timestamp = event.createdAt().formatAsTime(), isMine = isMine, + replyEventIds = replyEventIds ) } } @@ -142,14 +145,14 @@ fun ChatMessage( } ), horizontalAlignment = if (model.isMine) Alignment.End else Alignment.Start, - verticalArrangement = Arrangement.spacedBy(8.dp) + verticalArrangement = Arrangement.spacedBy(4.dp) ) { if (model.annotatedContent.isNotBlank()) { Surface( + modifier = Modifier.widthIn(max = 280.dp), color = containerColor, contentColor = contentColor, shape = bubbleShape, - modifier = Modifier.widthIn(max = 280.dp) ) { Text( text = model.annotatedContent, 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 985f645..1e7fc78 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 @@ -29,6 +29,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.union import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState @@ -89,6 +90,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.compose.resources.painterResource +import rust.nostr.sdk.UnsignedEvent import su.reya.coop.LocalNavigator import su.reya.coop.LocalProfileCache import su.reya.coop.LocalSnackbarHostState @@ -272,28 +274,42 @@ fun ChatScreen( modifier = Modifier .weight(1f) .fillMaxWidth(), - contentPadding = PaddingValues(16.dp), - reverseLayout = true, state = listState, + reverseLayout = true, + contentPadding = PaddingValues(16.dp), ) { groupedMessages.value.forEach { (dateHeader, messagesInGroup) -> items( items = messagesInGroup, key = { it.ensureId().id()?.toHex()!! } - ) { + ) { event -> val model = - rememberMessageModel(it, currentUser?.publicKey) + rememberMessageModel(event, currentUser?.publicKey) - ChatMessage( - model = model, - modifier = Modifier.graphicsLayer { - alpha = - if (selectedMessage?.first?.id == model.id) 0f else 1f - }, - onLongClick = { rect -> - selectedMessage = model to rect + val replyPreview = + remember(model.replyEventIds, messages.size) { + model.replyEventIds.firstOrNull() + ?.let { replyId -> + messages.find { it.id() == replyId } + } } - ) + + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + replyPreview?.let { ReplyPreview(it, model.isMine) } + ChatMessage( + model = model, + modifier = Modifier.graphicsLayer { + alpha = + if (selectedMessage?.first?.id == model.id) 0f else 1f + }, + onLongClick = { rect -> + selectedMessage = model to rect + } + ) + } } item { DateSeparator(dateHeader) @@ -521,6 +537,50 @@ private fun ReplyBox(model: MessageModel, onDismiss: () -> Unit) { } } +@Composable +private fun ReplyPreview(event: UnsignedEvent, isMine: Boolean = false) { + val profileCache = LocalProfileCache.current + val profileFlow = remember(event) { profileCache.getMetadata(event.author()) } + val profile by profileFlow.collectAsStateWithLifecycle() + + val bubbleShape = if (isMine) { + RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp, bottomStart = 20.dp, bottomEnd = 4.dp) + } else { + RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp, bottomStart = 4.dp, bottomEnd = 20.dp) + } + + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = if (isMine) Alignment.CenterEnd else Alignment.CenterStart + ) { + Surface( + modifier = Modifier.widthIn(max = 280.dp), + color = MaterialTheme.colorScheme.tertiaryContainer, + shape = bubbleShape, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + ) { + Text( + text = profile?.name ?: "Unknown", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onTertiaryContainer.copy( + alpha = 0.6f + ), + ) + Text( + text = event.content(), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onTertiaryContainer, + maxLines = 1, + ) + } + } + } +} + @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable private fun ContextMenu(onAction: (String) -> Unit) {