add reply

This commit is contained in:
2026-07-14 16:29:29 +07:00
parent fbccb5ecd2
commit 32cfa4042c
4 changed files with 90 additions and 58 deletions

View File

@@ -27,7 +27,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.boundsInWindow
@@ -41,6 +40,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import rust.nostr.sdk.EventId
import rust.nostr.sdk.PublicKey
import rust.nostr.sdk.UnsignedEvent
import su.reya.coop.URL_REGEX
@@ -49,8 +49,9 @@ import su.reya.coop.isImageUrl
import su.reya.coop.removeImageUrls
@Immutable
data class MessageUiModel(
val id: String,
data class MessageModel(
val id: EventId,
val author: PublicKey,
val annotatedContent: AnnotatedString,
val images: List<String>,
val timestamp: String,
@@ -58,18 +59,13 @@ data class MessageUiModel(
)
@Composable
fun rememberMessageUiModel(
event: UnsignedEvent,
currentUserPublicKey: PublicKey?,
contentColor: Color
): MessageUiModel {
return remember(event, currentUserPublicKey, contentColor) {
fun rememberMessageModel(event: UnsignedEvent, currentUser: PublicKey? = null): MessageModel {
return remember(event, currentUser) {
val id = event.ensureId().id()!!
val isMine = currentUser == event.author()
val content = event.content()
val images = URL_REGEX.findAll(content)
.map { it.value }
.filter { it.isImageUrl() }
.toList()
val images = URL_REGEX.findAll(content).map { it.value }.filter { it.isImageUrl() }.toList()
val cleanedContent = content.removeImageUrls()
val annotatedString = buildAnnotatedString {
@@ -82,7 +78,6 @@ fun rememberMessageUiModel(
url = url,
styles = TextLinkStyles(
style = SpanStyle(
color = contentColor,
textDecoration = TextDecoration.Underline,
fontWeight = FontWeight.Medium
)
@@ -96,19 +91,20 @@ fun rememberMessageUiModel(
append(cleanedContent.substring(lastIndex))
}
MessageUiModel(
id = event.id()?.toHex() ?: event.hashCode().toString(),
MessageModel(
id = id,
author = event.author(),
annotatedContent = annotatedString,
images = images,
timestamp = event.createdAt().formatAsTime(),
isMine = event.author() == currentUserPublicKey
isMine = isMine,
)
}
}
@Composable
fun ChatMessage(
model: MessageUiModel,
model: MessageModel,
modifier: Modifier = Modifier,
onLongClick: (Rect) -> Unit = {}
) {

View File

@@ -43,7 +43,6 @@ import androidx.compose.material3.DropdownMenuGroup
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
@@ -84,7 +83,6 @@ 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
@@ -97,7 +95,7 @@ import su.reya.coop.LocalSnackbarHostState
import su.reya.coop.Room
import su.reya.coop.RoomUiState
import su.reya.coop.Screen
import su.reya.coop.formatAsGroupHeader
import su.reya.coop.formatAsGroup
import su.reya.coop.shared.Avatar
import su.reya.coop.uiStateFlow
import su.reya.coop.viewmodel.AccountViewModel
@@ -138,19 +136,20 @@ fun ChatScreen(
return
}
val loading = viewModel.loading
val newOtherMessages = viewModel.newOtherMessages
val requireScreening = viewModel.requireScreening
val messages = viewModel.messages
val groupedMessages =
remember { derivedStateOf { messages.groupBy { it.createdAt().formatAsGroup() } } }
val roomState by (room as Room).uiStateFlow(profileCache, currentUser?.publicKey)
.collectAsStateWithLifecycle(RoomUiState())
var text by remember { mutableStateOf("") }
var selectedMessage by remember { mutableStateOf<Pair<MessageUiModel, Rect>?>(null) }
val loading = viewModel.loading
val newOtherMessages = viewModel.newOtherMessages
val requireScreening = viewModel.requireScreening
val messages = viewModel.messages
val groupedMessages =
remember { derivedStateOf { messages.groupBy { it.createdAt().formatAsGroupHeader() } } }
var selectedMessage by remember { mutableStateOf<Pair<MessageModel, Rect>?>(null) }
var replyingTo by remember { mutableStateOf<MessageModel?>(null) }
val blurAmount by animateDpAsState(
targetValue = if (selectedMessage != null) 8.dp else 0.dp,
@@ -267,9 +266,6 @@ fun ChatScreen(
room?.let { ScreenerCard(accountViewModel, it) }
}
val mineColor = MaterialTheme.colorScheme.onPrimaryContainer
val otherColor = MaterialTheme.colorScheme.onSurface
when (messages.isNotEmpty()) {
true -> {
LazyColumn(
@@ -283,24 +279,19 @@ fun ChatScreen(
groupedMessages.value.forEach { (dateHeader, messagesInGroup) ->
items(
items = messagesInGroup,
key = { it.id()?.toHex() ?: it.hashCode().toString() }
) { event ->
val isMine = currentUser?.publicKey == event.author()
val uiModel = rememberMessageUiModel(
event = event,
currentUserPublicKey = currentUser?.publicKey,
contentColor = if (isMine) mineColor else otherColor
)
val isHighlighted =
selectedMessage?.first?.id == uiModel.id
key = { it.ensureId().id()?.toHex()!! }
) {
val model =
rememberMessageModel(it, currentUser?.publicKey)
ChatMessage(
model = uiModel,
model = model,
modifier = Modifier.graphicsLayer {
alpha = if (isHighlighted) 0f else 1f
alpha =
if (selectedMessage?.first?.id == model.id) 0f else 1f
},
onLongClick = { rect ->
selectedMessage = uiModel to rect
selectedMessage = model to rect
}
)
}
@@ -373,6 +364,9 @@ fun ChatScreen(
}
else -> {
replyingTo?.let {
ReplyBox(it) { replyingTo = null }
}
ChatInput(
value = text,
onValueChange = { text = it },
@@ -471,6 +465,10 @@ fun ChatScreen(
}
}
"Reply" -> {
replyingTo = model
}
else -> {}
}
selectedMessage = null
@@ -481,13 +479,54 @@ fun ChatScreen(
}
}
@Composable
private fun ReplyBox(model: MessageModel, onDismiss: () -> Unit) {
val profileCache = LocalProfileCache.current
val profileFlow = remember(model) { profileCache.getMetadata(model.author) }
val profile by profileFlow.collectAsStateWithLifecycle()
Box(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
) {
Surface(
modifier = Modifier
.fillMaxWidth()
.clickable { onDismiss() },
color = MaterialTheme.colorScheme.tertiaryContainer,
shape = RoundedCornerShape(16.dp),
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
) {
Text(
text = "Replying to ${profile?.name}",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onTertiaryContainer.copy(
alpha = 0.6f
),
)
Text(
text = model.annotatedContent.toString().ifBlank {
if (model.images.isNotEmpty()) "[Image]" else ""
},
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onTertiaryContainer,
)
}
}
}
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@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),
"Copy" to Res.drawable.ic_copy,
"Reply" to Res.drawable.ic_reply
)
DropdownMenuGroup(
@@ -497,7 +536,7 @@ private fun ContextMenu(onAction: (String) -> Unit) {
) {
val itemCount = menuItems.size
menuItems.forEachIndexed { index, (label, icon, hasDivider) ->
menuItems.forEachIndexed { index, (label, icon) ->
DropdownMenuItem(
shapes = MenuDefaults.itemShape(index, itemCount),
colors = MenuDefaults.selectableItemVibrantColors(),
@@ -511,11 +550,6 @@ private fun ContextMenu(onAction: (String) -> Unit) {
checked = false,
onCheckedChange = { _ -> onAction(label) },
)
if (hasDivider) {
HorizontalDivider(
modifier = Modifier.padding(vertical = 4.dp),
)
}
}
}
}