feat: support interactions

Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
Infi 2025-06-28 03:11:07 +02:00
parent a8065151af
commit c6cd473bd5
3 changed files with 96 additions and 65 deletions

View File

@ -21,6 +21,7 @@ data class Message(
val masquerade: Masquerade? = null, val masquerade: Masquerade? = null,
val system: SystemInfo? = null, val system: SystemInfo? = null,
val webhook: WebHook? = null, val webhook: WebHook? = null,
val interactions: InteractionsDescription? = null,
val type: String? = null, // this is _only_ used for websocket events! 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 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( data class WebHook(
val avatar: String? = null, val avatar: String? = null,
val name: String? = null, val name: String? = null,
)
@Serializable
data class InteractionsDescription(
val reactions: List<String>? = null,
@SerialName("restrict_reactions") val restrictReactions: Boolean? = null
) )

View File

@ -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)) Spacer(modifier = Modifier.height(8.dp))
FlowRow( FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
message.reactions?.forEach { reaction -> reactionsAndInteractions.forEach { reaction ->
Reaction( Reaction(
reaction.key, reaction.value, reaction.key, reaction.value,
onClick = { hasOwn -> onClick = { hasOwn ->

View File

@ -14,12 +14,13 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ScrollableTabRow import androidx.compose.material3.PrimaryScrollableTabRow
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
@ -63,7 +64,19 @@ fun ReactionInfoSheet(messageId: String, emoji: String, onDismiss: () -> Unit) {
val message = RevoltAPI.messageCache[messageId] ?: return val message = RevoltAPI.messageCache[messageId] ?: return
val channel = RevoltAPI.channelCache[message.channel] ?: return val channel = RevoltAPI.channelCache[message.channel] ?: return
val reactions = message.reactions 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 context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@ -81,60 +94,62 @@ fun ReactionInfoSheet(messageId: String, emoji: String, onDismiss: () -> Unit) {
var selectedReactionIndex by remember( var selectedReactionIndex by remember(
messageId, messageId,
emoji 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 selectedReactionIndex = 0
} }
if (reactionEmoji?.isEmpty() == true) { if (reactionEmoji.isEmpty()) {
onDismiss() onDismiss()
} }
LazyColumn { LazyColumn {
stickyHeader(key = "tabs") { stickyHeader(key = "tabs") {
ScrollableTabRow( if (reactionEmoji.isNotEmpty() && selectedReactionIndex < reactionEmoji.size) {
selectedTabIndex = selectedReactionIndex, PrimaryScrollableTabRow(
modifier = Modifier.fillMaxWidth(), selectedTabIndex = selectedReactionIndex,
containerColor = MaterialTheme.colorScheme.surfaceContainerLow, modifier = Modifier.fillMaxWidth(),
divider = {} containerColor = MaterialTheme.colorScheme.surfaceContainerLow,
) { divider = {}
reactionEmoji?.forEachIndexed { index, emoji -> ) {
Tab( reactionEmoji.forEachIndexed { index, emoji ->
text = { Tab(
if (emoji.isUlid()) { text = {
Row(verticalAlignment = Alignment.CenterVertically) { if (emoji.isUlid()) {
RemoteImage( Row(verticalAlignment = Alignment.CenterVertically) {
url = "$REVOLT_FILES/emojis/${emoji}", RemoteImage(
description = null, url = "$REVOLT_FILES/emojis/${emoji}",
modifier = Modifier.size(16.dp) description = null,
) modifier = Modifier.size(16.dp)
Spacer(Modifier.size(6.dp)) )
Spacer(Modifier.size(6.dp))
Text(
"${reactions?.get(emoji)?.size ?: 0}",
style = LocalTextStyle.current.copy(fontFeatureSettings = "tnum")
)
}
} else {
Text( Text(
"${reactions[emoji]?.size ?: 0}", "$emoji ${reactions?.get(emoji)?.size ?: 0}",
style = LocalTextStyle.current.copy(fontFeatureSettings = "tnum") style = LocalTextStyle.current.copy(fontFeatureSettings = "tnum")
) )
} }
} else { },
Text( selected = selectedReactionIndex == index,
"$emoji ${reactions[emoji]?.size ?: 0}", onClick = { selectedReactionIndex = index }
style = LocalTextStyle.current.copy(fontFeatureSettings = "tnum") )
) }
}
},
selected = selectedReactionIndex == index,
onClick = { selectedReactionIndex = index }
)
} }
HorizontalDivider()
} }
HorizontalDivider()
} }
if (reactionEmoji?.isNotEmpty() == true) { item("info") {
item("info") { if (reactionEmoji.isNotEmpty() == true) {
val current = reactionEmoji[selectedReactionIndex] val current = reactionEmoji[selectedReactionIndex]
// Code related to enabling of experimental features // <editor-fold desc="Code related to enabling of experimental features">
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
val canBeUsedForTapCountIncrement = val canBeUsedForTapCountIncrement =
remember(selectedReactionIndex) { remember(selectedReactionIndex) {
@ -197,7 +212,7 @@ fun ReactionInfoSheet(messageId: String, emoji: String, onDismiss: () -> Unit) {
} }
) )
} }
// End of code related to enabling of experimental features // </editor-fold>
Column( Column(
verticalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp),
@ -297,35 +312,35 @@ fun ReactionInfoSheet(messageId: String, emoji: String, onDismiss: () -> Unit) {
HorizontalDivider() HorizontalDivider()
} }
} }
}
val reactionsForEmoji = reactions[reactionEmoji[selectedReactionIndex]] val reactionsForEmoji =
items(reactionsForEmoji?.size ?: 0) { index -> reactions?.get(reactionEmoji[selectedReactionIndex]) ?: emptyList()
val reaction = reactionsForEmoji?.get(index) ?: return@items items(items = reactionsForEmoji) { reaction ->
val userOrNull = RevoltAPI.userCache[reaction] val userOrNull = RevoltAPI.userCache[reaction]
val user = userOrNull ?: User.getPlaceholder(reaction) val user = userOrNull ?: User.getPlaceholder(reaction)
val member = if (channel.server != null && user.id != null) { val member = if (channel.server != null && user.id != null) {
RevoltAPI.members.getMember(channel.server, user.id) RevoltAPI.members.getMember(channel.server, user.id)
} else { } else {
null null
} }
LaunchedEffect(reaction) { LaunchedEffect(reaction) {
if (reaction !in RevoltAPI.userCache) { if (reaction !in RevoltAPI.userCache) {
try { try {
RevoltAPI.userCache[reaction] = fetchUser(reaction) RevoltAPI.userCache[reaction] = fetchUser(reaction)
} catch (e: Exception) { } catch (e: Exception) {
// too bad! // too bad!
}
} }
} }
MemberListItem(
member = member,
user = user,
serverId = channel.server,
userId = reaction,
)
} }
MemberListItem(
member = member,
user = user,
serverId = channel.server,
userId = reaction,
)
} }
item("bottom") { item("bottom") {