From c6cd473bd513e29ebf8e1e0e8b736143ff775a71 Mon Sep 17 00:00:00 2001 From: Infi Date: Sat, 28 Jun 2025 03:11:07 +0200 Subject: [PATCH] feat: support interactions Signed-off-by: Infi --- .../java/chat/revolt/api/schemas/Messages.kt | 7 + .../chat/revolt/composables/chat/Message.kt | 15 +- .../chat/revolt/sheets/ReactionInfoSheet.kt | 139 ++++++++++-------- 3 files changed, 96 insertions(+), 65 deletions(-) diff --git a/app/src/main/java/chat/revolt/api/schemas/Messages.kt b/app/src/main/java/chat/revolt/api/schemas/Messages.kt index 8eb7ddbe..da20b3a1 100644 --- a/app/src/main/java/chat/revolt/api/schemas/Messages.kt +++ b/app/src/main/java/chat/revolt/api/schemas/Messages.kt @@ -21,6 +21,7 @@ data class Message( val masquerade: Masquerade? = null, val system: SystemInfo? = null, val webhook: WebHook? = null, + val interactions: InteractionsDescription? = null, val type: String? = null, // this is _only_ used for websocket events! val tail: Boolean? = null // this is used to determine if the message is the last in a message group ) { @@ -116,4 +117,10 @@ data class SystemInfo( data class WebHook( val avatar: String? = null, val name: String? = null, +) + +@Serializable +data class InteractionsDescription( + val reactions: List? = null, + @SerialName("restrict_reactions") val restrictReactions: Boolean? = null ) \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/composables/chat/Message.kt b/app/src/main/java/chat/revolt/composables/chat/Message.kt index cb572a3f..11192df6 100644 --- a/app/src/main/java/chat/revolt/composables/chat/Message.kt +++ b/app/src/main/java/chat/revolt/composables/chat/Message.kt @@ -518,16 +518,25 @@ fun Message( } } } - } - if ((message.reactions?.size ?: 0) > 0) { + val reactionsAndInteractions = remember(message.reactions) { + message.reactions.orEmpty().toMutableMap().also { + message.interactions?.reactions?.forEach { reaction -> + if (!it.containsKey(reaction)) { + it[reaction] = listOf() + } + } + } + } + + if (reactionsAndInteractions.isNotEmpty()) { Spacer(modifier = Modifier.height(8.dp)) FlowRow( horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { - message.reactions?.forEach { reaction -> + reactionsAndInteractions.forEach { reaction -> Reaction( reaction.key, reaction.value, onClick = { hasOwn -> diff --git a/app/src/main/java/chat/revolt/sheets/ReactionInfoSheet.kt b/app/src/main/java/chat/revolt/sheets/ReactionInfoSheet.kt index 017f1361..f5c4d7a3 100644 --- a/app/src/main/java/chat/revolt/sheets/ReactionInfoSheet.kt +++ b/app/src/main/java/chat/revolt/sheets/ReactionInfoSheet.kt @@ -14,12 +14,13 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ScrollableTabRow +import androidx.compose.material3.PrimaryScrollableTabRow import androidx.compose.material3.Tab import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -63,7 +64,19 @@ fun ReactionInfoSheet(messageId: String, emoji: String, onDismiss: () -> Unit) { val message = RevoltAPI.messageCache[messageId] ?: return val channel = RevoltAPI.channelCache[message.channel] ?: return val reactions = message.reactions - val reactionEmoji = reactions?.keys?.toList() + val interactions = message.interactions?.reactions ?: emptyList() + val reactionEmoji = + (reactions?.keys?.toList() ?: emptyList()) + .plus(interactions) + .distinct() + .filterNot { it.isEmpty() } + .sortedBy { + if (it.isUlid()) { + RevoltAPI.emojiCache[it]?.name ?: it.codePointAt(0).toString() + } else { + it.codePointAt(0).toString() + } + } val context = LocalContext.current val scope = rememberCoroutineScope() @@ -81,60 +94,62 @@ fun ReactionInfoSheet(messageId: String, emoji: String, onDismiss: () -> Unit) { var selectedReactionIndex by remember( messageId, emoji - ) { mutableIntStateOf(reactionEmoji?.indexOfFirst { it == emoji } ?: 0) } + ) { mutableIntStateOf(reactionEmoji.indexOfFirst { it == emoji }) } - if (selectedReactionIndex >= (reactionEmoji?.size ?: 0)) { + if (selectedReactionIndex >= reactionEmoji.size || selectedReactionIndex < 0) { selectedReactionIndex = 0 } - if (reactionEmoji?.isEmpty() == true) { + if (reactionEmoji.isEmpty()) { onDismiss() } LazyColumn { stickyHeader(key = "tabs") { - ScrollableTabRow( - selectedTabIndex = selectedReactionIndex, - modifier = Modifier.fillMaxWidth(), - containerColor = MaterialTheme.colorScheme.surfaceContainerLow, - divider = {} - ) { - reactionEmoji?.forEachIndexed { index, emoji -> - Tab( - text = { - if (emoji.isUlid()) { - Row(verticalAlignment = Alignment.CenterVertically) { - RemoteImage( - url = "$REVOLT_FILES/emojis/${emoji}", - description = null, - modifier = Modifier.size(16.dp) - ) - Spacer(Modifier.size(6.dp)) + if (reactionEmoji.isNotEmpty() && selectedReactionIndex < reactionEmoji.size) { + PrimaryScrollableTabRow( + selectedTabIndex = selectedReactionIndex, + modifier = Modifier.fillMaxWidth(), + containerColor = MaterialTheme.colorScheme.surfaceContainerLow, + divider = {} + ) { + reactionEmoji.forEachIndexed { index, emoji -> + Tab( + text = { + if (emoji.isUlid()) { + Row(verticalAlignment = Alignment.CenterVertically) { + RemoteImage( + url = "$REVOLT_FILES/emojis/${emoji}", + description = null, + modifier = Modifier.size(16.dp) + ) + Spacer(Modifier.size(6.dp)) + Text( + "${reactions?.get(emoji)?.size ?: 0}", + style = LocalTextStyle.current.copy(fontFeatureSettings = "tnum") + ) + } + } else { Text( - "${reactions[emoji]?.size ?: 0}", + "$emoji ${reactions?.get(emoji)?.size ?: 0}", style = LocalTextStyle.current.copy(fontFeatureSettings = "tnum") ) } - } else { - Text( - "$emoji ${reactions[emoji]?.size ?: 0}", - style = LocalTextStyle.current.copy(fontFeatureSettings = "tnum") - ) - } - }, - selected = selectedReactionIndex == index, - onClick = { selectedReactionIndex = index } - ) + }, + selected = selectedReactionIndex == index, + onClick = { selectedReactionIndex = index } + ) + } } + HorizontalDivider() } - HorizontalDivider() } - if (reactionEmoji?.isNotEmpty() == true) { - item("info") { + item("info") { + if (reactionEmoji.isNotEmpty() == true) { val current = reactionEmoji[selectedReactionIndex] - // Code related to enabling of experimental features + // val interactionSource = remember { MutableInteractionSource() } val canBeUsedForTapCountIncrement = remember(selectedReactionIndex) { @@ -197,7 +212,7 @@ fun ReactionInfoSheet(messageId: String, emoji: String, onDismiss: () -> Unit) { } ) } - // End of code related to enabling of experimental features + // Column( verticalArrangement = Arrangement.spacedBy(16.dp), @@ -297,35 +312,35 @@ fun ReactionInfoSheet(messageId: String, emoji: String, onDismiss: () -> Unit) { HorizontalDivider() } } + } - val reactionsForEmoji = reactions[reactionEmoji[selectedReactionIndex]] - items(reactionsForEmoji?.size ?: 0) { index -> - val reaction = reactionsForEmoji?.get(index) ?: return@items - val userOrNull = RevoltAPI.userCache[reaction] - val user = userOrNull ?: User.getPlaceholder(reaction) - val member = if (channel.server != null && user.id != null) { - RevoltAPI.members.getMember(channel.server, user.id) - } else { - null - } + val reactionsForEmoji = + reactions?.get(reactionEmoji[selectedReactionIndex]) ?: emptyList() + items(items = reactionsForEmoji) { reaction -> + val userOrNull = RevoltAPI.userCache[reaction] + val user = userOrNull ?: User.getPlaceholder(reaction) + val member = if (channel.server != null && user.id != null) { + RevoltAPI.members.getMember(channel.server, user.id) + } else { + null + } - LaunchedEffect(reaction) { - if (reaction !in RevoltAPI.userCache) { - try { - RevoltAPI.userCache[reaction] = fetchUser(reaction) - } catch (e: Exception) { - // too bad! - } + LaunchedEffect(reaction) { + if (reaction !in RevoltAPI.userCache) { + try { + RevoltAPI.userCache[reaction] = fetchUser(reaction) + } catch (e: Exception) { + // too bad! } } - - MemberListItem( - member = member, - user = user, - serverId = channel.server, - userId = reaction, - ) } + + MemberListItem( + member = member, + user = user, + serverId = channel.server, + userId = reaction, + ) } item("bottom") {