add concord reactions and edits

This commit is contained in:
2026-09-15 15:17:14 +07:00
parent 63ef074e8f
commit 5b72b07ca8
9 changed files with 349 additions and 63 deletions
@@ -229,7 +229,8 @@ fun ChatMessage(
if (model.reactions.isNotEmpty()) {
MessageReactions(
reactions = model.reactions,
emojis = model.reactions.map { it.emoji },
total = model.reactions.sumOf { it.authors.size },
modifier = Modifier.offset(y = 12.dp)
)
}
@@ -247,13 +248,14 @@ fun ChatMessage(
}
}
/** The reaction chips under a bubble, shared by DMs and Channels. */
@Composable
private fun MessageReactions(
reactions: List<ReactionGroup>,
fun MessageReactions(
emojis: List<String>,
total: Int,
modifier: Modifier = Modifier
) {
val totalCount = reactions.sumOf { it.authors.size }
val displayEmojis = reactions.take(3).map { it.emoji }
val displayEmojis = emojis.take(3)
Row(
modifier = modifier,
@@ -274,7 +276,7 @@ private fun MessageReactions(
}
}
}
if (totalCount > 2) {
if (total > 2) {
Surface(
modifier = Modifier.size(24.dp),
color = MaterialTheme.colorScheme.surface,
@@ -282,7 +284,7 @@ private fun MessageReactions(
) {
Box(contentAlignment = Alignment.Center) {
Text(
text = totalCount.toString(),
text = total.toString(),
style = MaterialTheme.typography.labelSmall,
fontSize = 10.sp,
)
@@ -650,7 +650,7 @@ private fun ReplyPreview(
}
@Composable
private fun ReactionToolbar(
fun ReactionToolbar(
onReaction: (String) -> Unit
) {
val reactionEmojis = listOf("👍", "❤️", "😂", "😮", "😢", "😡", "🎉")
@@ -1,5 +1,7 @@
package su.reya.coop.screens.communities
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -28,6 +30,7 @@ import androidx.compose.material3.ScaffoldDefaults
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
@@ -56,6 +59,8 @@ import su.reya.coop.dayLabel
import su.reya.coop.sanitizeName
import su.reya.coop.screens.chat.ChatInput
import su.reya.coop.screens.chat.DateSeparator
import su.reya.coop.screens.chat.MessageReactions
import su.reya.coop.screens.chat.ReactionToolbar
import su.reya.coop.shared.Avatar
import su.reya.coop.short
import su.reya.coop.timeLabel
@@ -81,6 +86,12 @@ fun ChannelScreen(viewModel: ChannelScreenViewModel) {
var text by remember { mutableStateOf("") }
/** The message whose action row is open, if any. */
var activeMessageId by remember { mutableStateOf<String?>(null) }
/** The message an Edit is being written for, if any. Its text lives in [text]. */
var editing by remember { mutableStateOf<ConcordMessage?>(null) }
val grouped by remember {
derivedStateOf {
viewModel.messages
@@ -193,6 +204,18 @@ fun ChannelScreen(viewModel: ChannelScreenViewModel) {
ChannelMessageRow(
message = message,
isMine = message.author == currentUser?.publicKey?.toHex(),
active = activeMessageId == message.idHex,
onDismissActions = { activeMessageId = null },
onLongPress = { activeMessageId = message.idHex },
onReaction = { emoji ->
viewModel.sendReaction(message, emoji)
activeMessageId = null
},
onEdit = {
editing = message
text = message.content
activeMessageId = null
},
)
}
item(key = "day:$day") { DateSeparator(day) }
@@ -211,13 +234,33 @@ fun ChannelScreen(viewModel: ChannelScreenViewModel) {
color = MaterialTheme.colorScheme.outline,
)
} else {
if (editing != null) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = "Editing a message",
modifier = Modifier.weight(1f),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary,
)
TextButton(onClick = { editing = null; text = "" }) { Text("Cancel") }
}
}
// No attachments and no dictation in v1, so the input shows neither: a button
// that does nothing would be worse than its absence.
ChatInput(
value = text,
onValueChange = { text = it },
onSend = {
viewModel.sendMessage(text)
val target = editing
if (target == null) viewModel.sendMessage(text)
else viewModel.editMessage(target.idHex, text)
editing = null
text = ""
},
)
@@ -229,7 +272,15 @@ fun ChannelScreen(viewModel: ChannelScreenViewModel) {
}
@Composable
private fun ChannelMessageRow(message: ConcordMessage, isMine: Boolean) {
private fun ChannelMessageRow(
message: ConcordMessage,
isMine: Boolean,
active: Boolean,
onDismissActions: () -> Unit,
onLongPress: () -> Unit,
onReaction: (String) -> Unit,
onEdit: () -> Unit,
) {
val (pubkey, profile) = rememberAuthor(message.author)
val name = profile?.name?.sanitizeName()?.takeIf { it.isNotBlank() }
?: pubkey?.short()
@@ -242,7 +293,16 @@ private fun ChannelMessageRow(message: ConcordMessage, isMine: Boolean) {
) {
Avatar(picture = profile?.picture, description = name, size = 36.dp)
Spacer(modifier = Modifier.size(10.dp))
Column(modifier = Modifier.weight(1f)) {
Column(
modifier = Modifier
.weight(1f)
.combinedClickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = onDismissActions,
onLongClick = onLongPress,
)
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = if (isMine) "$name (you)" else name,
@@ -258,12 +318,36 @@ private fun ChannelMessageRow(message: ConcordMessage, isMine: Boolean) {
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.outline,
)
if (message.edited) {
Spacer(modifier = Modifier.size(4.dp))
Text(
text = "(edited)",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.outline,
)
}
}
Text(
text = message.content,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
)
if (message.reactions.isNotEmpty()) {
MessageReactions(
emojis = message.reactions.map { it.emoji },
total = message.reactions.sumOf { it.authors.size },
modifier = Modifier.padding(top = 4.dp),
)
}
if (active) {
Column(modifier = Modifier.padding(top = 8.dp)) {
ReactionToolbar(onReaction = onReaction)
// Only the author may edit, so only they are offered it.
if (isMine) {
TextButton(onClick = onEdit) { Text("Edit") }
}
}
}
}
}
}