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 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<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))
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 ->

View File

@ -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
// <editor-fold desc="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
// </editor-fold>
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") {