feat: reaction rendering
Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
parent
555385646e
commit
8b1dc84237
|
|
@ -3,6 +3,10 @@ package chat.revolt.api.internals
|
||||||
import kotlin.experimental.and
|
import kotlin.experimental.and
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
fun String.isUlid(): Boolean {
|
||||||
|
return "[0-9A-HJKMNP-TV-Z]{26}".toRegex().matches(this)
|
||||||
|
}
|
||||||
|
|
||||||
object ULID {
|
object ULID {
|
||||||
private const val entropy = 10
|
private const val entropy = 10
|
||||||
private const val len = 26
|
private const val len = 26
|
||||||
|
|
@ -47,64 +51,64 @@ object ULID {
|
||||||
chars[11] =
|
chars[11] =
|
||||||
b32chars[
|
b32chars[
|
||||||
(
|
(
|
||||||
entropy[0].toInt() shl 2 or (entropy[1].toShort() and 0xff).toInt()
|
entropy[0].toInt() shl 2 or (entropy[1].toShort() and 0xff).toInt()
|
||||||
.ushr(6) and 0x1f
|
.ushr(6) and 0x1f
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
chars[12] = b32chars[((entropy[1].toShort() and 0xff).toInt().ushr(1) and 0x1f)]
|
chars[12] = b32chars[((entropy[1].toShort() and 0xff).toInt().ushr(1) and 0x1f)]
|
||||||
chars[13] =
|
chars[13] =
|
||||||
b32chars[
|
b32chars[
|
||||||
(
|
(
|
||||||
entropy[1].toInt() shl 4 or (entropy[2].toShort() and 0xff).toInt()
|
entropy[1].toInt() shl 4 or (entropy[2].toShort() and 0xff).toInt()
|
||||||
.ushr(4) and 0x1f
|
.ushr(4) and 0x1f
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
chars[14] =
|
chars[14] =
|
||||||
b32chars[
|
b32chars[
|
||||||
(
|
(
|
||||||
entropy[2].toInt() shl 5 or (entropy[3].toShort() and 0xff).toInt()
|
entropy[2].toInt() shl 5 or (entropy[3].toShort() and 0xff).toInt()
|
||||||
.ushr(7) and 0x1f
|
.ushr(7) and 0x1f
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
chars[15] = b32chars[((entropy[3].toShort() and 0xff).toInt().ushr(2) and 0x1f)]
|
chars[15] = b32chars[((entropy[3].toShort() and 0xff).toInt().ushr(2) and 0x1f)]
|
||||||
chars[16] =
|
chars[16] =
|
||||||
b32chars[
|
b32chars[
|
||||||
(
|
(
|
||||||
entropy[3].toInt() shl 3 or (entropy[4].toShort() and 0xff).toInt()
|
entropy[3].toInt() shl 3 or (entropy[4].toShort() and 0xff).toInt()
|
||||||
.ushr(5) and 0x1f
|
.ushr(5) and 0x1f
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
chars[17] = b32chars[(entropy[4].toInt() and 0x1f)]
|
chars[17] = b32chars[(entropy[4].toInt() and 0x1f)]
|
||||||
chars[18] = b32chars[(entropy[5].toShort() and 0xff).toInt().ushr(3)]
|
chars[18] = b32chars[(entropy[5].toShort() and 0xff).toInt().ushr(3)]
|
||||||
chars[19] =
|
chars[19] =
|
||||||
b32chars[
|
b32chars[
|
||||||
(
|
(
|
||||||
entropy[5].toInt() shl 2 or (entropy[6].toShort() and 0xff).toInt()
|
entropy[5].toInt() shl 2 or (entropy[6].toShort() and 0xff).toInt()
|
||||||
.ushr(6) and 0x1f
|
.ushr(6) and 0x1f
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
chars[20] = b32chars[((entropy[6].toShort() and 0xff).toInt().ushr(1) and 0x1f)]
|
chars[20] = b32chars[((entropy[6].toShort() and 0xff).toInt().ushr(1) and 0x1f)]
|
||||||
chars[21] =
|
chars[21] =
|
||||||
b32chars[
|
b32chars[
|
||||||
(
|
(
|
||||||
entropy[6].toInt() shl 4 or (entropy[7].toShort() and 0xff).toInt()
|
entropy[6].toInt() shl 4 or (entropy[7].toShort() and 0xff).toInt()
|
||||||
.ushr(4) and 0x1f
|
.ushr(4) and 0x1f
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
chars[22] =
|
chars[22] =
|
||||||
b32chars[
|
b32chars[
|
||||||
(
|
(
|
||||||
entropy[7].toInt() shl 5 or (entropy[8].toShort() and 0xff).toInt()
|
entropy[7].toInt() shl 5 or (entropy[8].toShort() and 0xff).toInt()
|
||||||
.ushr(7) and 0x1f
|
.ushr(7) and 0x1f
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
chars[23] = b32chars[((entropy[8].toShort() and 0xff).toInt().ushr(2) and 0x1f)]
|
chars[23] = b32chars[((entropy[8].toShort() and 0xff).toInt().ushr(2) and 0x1f)]
|
||||||
chars[24] =
|
chars[24] =
|
||||||
b32chars[
|
b32chars[
|
||||||
(
|
(
|
||||||
entropy[8].toInt() shl 3 or (entropy[9].toShort() and 0xff).toInt()
|
entropy[8].toInt() shl 3 or (entropy[9].toShort() and 0xff).toInt()
|
||||||
.ushr(5) and 0x1f
|
.ushr(5) and 0x1f
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
chars[25] = b32chars[(entropy[9].toInt() and 0x1f)]
|
chars[25] = b32chars[(entropy[9].toInt() and 0x1f)]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import chat.revolt.api.realtime.frames.receivable.ChannelStopTypingFrame
|
||||||
import chat.revolt.api.realtime.frames.receivable.ChannelUpdateFrame
|
import chat.revolt.api.realtime.frames.receivable.ChannelUpdateFrame
|
||||||
import chat.revolt.api.realtime.frames.receivable.MessageAppendFrame
|
import chat.revolt.api.realtime.frames.receivable.MessageAppendFrame
|
||||||
import chat.revolt.api.realtime.frames.receivable.MessageFrame
|
import chat.revolt.api.realtime.frames.receivable.MessageFrame
|
||||||
|
import chat.revolt.api.realtime.frames.receivable.MessageReactFrame
|
||||||
import chat.revolt.api.realtime.frames.receivable.MessageUpdateFrame
|
import chat.revolt.api.realtime.frames.receivable.MessageUpdateFrame
|
||||||
import chat.revolt.api.realtime.frames.receivable.PongFrame
|
import chat.revolt.api.realtime.frames.receivable.PongFrame
|
||||||
import chat.revolt.api.realtime.frames.receivable.ReadyFrame
|
import chat.revolt.api.realtime.frames.receivable.ReadyFrame
|
||||||
|
|
@ -255,6 +256,69 @@ object RealtimeSocket {
|
||||||
RevoltAPI.wsFrameChannel.send(messageUpdateFrame)
|
RevoltAPI.wsFrameChannel.send(messageUpdateFrame)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"MessageReact" -> {
|
||||||
|
val messageReactFrame =
|
||||||
|
RevoltJson.decodeFromString(MessageReactFrame.serializer(), rawFrame)
|
||||||
|
Log.d(
|
||||||
|
"RealtimeSocket",
|
||||||
|
"Received message react frame for ${messageReactFrame.id}."
|
||||||
|
)
|
||||||
|
|
||||||
|
val oldMessage = RevoltAPI.messageCache[messageReactFrame.id]
|
||||||
|
if (oldMessage == null) {
|
||||||
|
Log.d(
|
||||||
|
"RealtimeSocket",
|
||||||
|
"Message ${messageReactFrame.id} not found in cache. Will not update."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val reactions = oldMessage.reactions?.toMutableMap() ?: mutableMapOf()
|
||||||
|
val forEmoji =
|
||||||
|
reactions[messageReactFrame.emoji_id]?.toMutableList() ?: mutableListOf()
|
||||||
|
forEmoji.add(messageReactFrame.user_id)
|
||||||
|
reactions[messageReactFrame.emoji_id] = forEmoji
|
||||||
|
|
||||||
|
RevoltAPI.messageCache[messageReactFrame.id] =
|
||||||
|
oldMessage.copy(reactions = reactions)
|
||||||
|
|
||||||
|
RevoltAPI.wsFrameChannel.send(messageReactFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
"MessageUnreact" -> {
|
||||||
|
val messageUnreactFrame =
|
||||||
|
RevoltJson.decodeFromString(MessageReactFrame.serializer(), rawFrame)
|
||||||
|
Log.d(
|
||||||
|
"RealtimeSocket",
|
||||||
|
"Received message unreact frame for ${messageUnreactFrame.id}."
|
||||||
|
)
|
||||||
|
|
||||||
|
val oldMessage = RevoltAPI.messageCache[messageUnreactFrame.id]
|
||||||
|
if (oldMessage == null) {
|
||||||
|
Log.d(
|
||||||
|
"RealtimeSocket",
|
||||||
|
"Message ${messageUnreactFrame.id} not found in cache. Will not update."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val reactions = oldMessage.reactions?.toMutableMap() ?: mutableMapOf()
|
||||||
|
val forEmoji =
|
||||||
|
reactions[messageUnreactFrame.emoji_id]?.toMutableList() ?: mutableListOf()
|
||||||
|
forEmoji.remove(messageUnreactFrame.user_id)
|
||||||
|
|
||||||
|
if (forEmoji.isEmpty()) {
|
||||||
|
reactions.remove(messageUnreactFrame.emoji_id)
|
||||||
|
} else {
|
||||||
|
reactions[messageUnreactFrame.emoji_id] = forEmoji
|
||||||
|
}
|
||||||
|
|
||||||
|
RevoltAPI.messageCache[messageUnreactFrame.id] =
|
||||||
|
oldMessage.copy(reactions = reactions)
|
||||||
|
|
||||||
|
RevoltAPI.wsFrameChannel.send(messageUnreactFrame)
|
||||||
|
}
|
||||||
|
|
||||||
"UserUpdate" -> {
|
"UserUpdate" -> {
|
||||||
val userUpdateFrame =
|
val userUpdateFrame =
|
||||||
RevoltJson.decodeFromString(UserUpdateFrame.serializer(), rawFrame)
|
RevoltJson.decodeFromString(UserUpdateFrame.serializer(), rawFrame)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package chat.revolt.api.routes.channel
|
||||||
|
|
||||||
|
import chat.revolt.api.RevoltHttp
|
||||||
|
import io.ktor.client.request.delete
|
||||||
|
import io.ktor.client.request.put
|
||||||
|
|
||||||
|
suspend fun react(channelId: String, messageId: String, emoji: String) {
|
||||||
|
RevoltHttp.put("/channels/$channelId/messages/$messageId/reactions/$emoji")
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun unreact(channelId: String, messageId: String, emoji: String) {
|
||||||
|
RevoltHttp.delete("/channels/$channelId/messages/$messageId/reactions/$emoji")
|
||||||
|
}
|
||||||
|
|
@ -20,8 +20,11 @@ import androidx.browser.customtabs.CustomTabsIntent
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
|
@ -39,6 +42,7 @@ import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.key
|
import androidx.compose.runtime.key
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
|
@ -62,12 +66,15 @@ import chat.revolt.api.internals.SpecialUsers
|
||||||
import chat.revolt.api.internals.ULID
|
import chat.revolt.api.internals.ULID
|
||||||
import chat.revolt.api.internals.WebCompat
|
import chat.revolt.api.internals.WebCompat
|
||||||
import chat.revolt.api.internals.solidColor
|
import chat.revolt.api.internals.solidColor
|
||||||
|
import chat.revolt.api.routes.channel.react
|
||||||
|
import chat.revolt.api.routes.channel.unreact
|
||||||
import chat.revolt.api.routes.microservices.january.asJanuaryProxyUrl
|
import chat.revolt.api.routes.microservices.january.asJanuaryProxyUrl
|
||||||
import chat.revolt.api.schemas.AutumnResource
|
import chat.revolt.api.schemas.AutumnResource
|
||||||
import chat.revolt.api.schemas.User
|
import chat.revolt.api.schemas.User
|
||||||
import chat.revolt.components.generic.UserAvatar
|
import chat.revolt.components.generic.UserAvatar
|
||||||
import chat.revolt.components.generic.UserAvatarWidthPlaceholder
|
import chat.revolt.components.generic.UserAvatarWidthPlaceholder
|
||||||
import chat.revolt.internals.markdown.LongClickableSpan
|
import chat.revolt.internals.markdown.LongClickableSpan
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import chat.revolt.api.schemas.Message as MessageSchema
|
import chat.revolt.api.schemas.Message as MessageSchema
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -155,7 +162,7 @@ fun formatLongAsTime(time: Long): String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun Message(
|
fun Message(
|
||||||
message: MessageSchema,
|
message: MessageSchema,
|
||||||
|
|
@ -171,6 +178,8 @@ fun Message(
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val contentColor = LocalContentColor.current
|
val contentColor = LocalContentColor.current
|
||||||
|
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
val attachmentView = rememberLauncherForActivityResult(
|
val attachmentView = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.StartActivityForResult(),
|
contract = ActivityResultContracts.StartActivityForResult(),
|
||||||
onResult = {
|
onResult = {
|
||||||
|
|
@ -450,6 +459,40 @@ fun Message(
|
||||||
})
|
})
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((message.reactions?.size ?: 0) > 0) {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
FlowRow(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
message.reactions?.forEach { reaction ->
|
||||||
|
Reaction(reaction.key, reaction.value,
|
||||||
|
onClick = { hasOwn ->
|
||||||
|
scope.launch {
|
||||||
|
if (hasOwn) {
|
||||||
|
unreact(
|
||||||
|
message.channel!!,
|
||||||
|
message.id!!,
|
||||||
|
reaction.key
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
react(
|
||||||
|
message.channel!!,
|
||||||
|
message.id!!,
|
||||||
|
reaction.key
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,157 @@
|
||||||
|
package chat.revolt.components.chat
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.animation.animateColorAsState
|
||||||
|
import androidx.compose.animation.slideInVertically
|
||||||
|
import androidx.compose.animation.slideOutVertically
|
||||||
|
import androidx.compose.animation.togetherWith
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.text.PlatformTextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import chat.revolt.api.REVOLT_FILES
|
||||||
|
import chat.revolt.api.RevoltAPI
|
||||||
|
import chat.revolt.api.internals.isUlid
|
||||||
|
import chat.revolt.components.generic.RemoteImage
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun Reaction(
|
||||||
|
emoji: String,
|
||||||
|
members: List<String>,
|
||||||
|
onClick: (Boolean) -> Unit,
|
||||||
|
onLongClick: () -> Unit
|
||||||
|
) {
|
||||||
|
val hasOwn = members.contains(RevoltAPI.selfId)
|
||||||
|
|
||||||
|
val background by animateColorAsState(
|
||||||
|
targetValue = if (hasOwn) {
|
||||||
|
MaterialTheme.colorScheme.primary
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
||||||
|
},
|
||||||
|
label = "Reaction background"
|
||||||
|
)
|
||||||
|
val foreground by animateColorAsState(
|
||||||
|
targetValue = if (hasOwn) {
|
||||||
|
MaterialTheme.colorScheme.onPrimary
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.onSurface
|
||||||
|
},
|
||||||
|
label = "Reaction foreground"
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(MaterialTheme.shapes.small)
|
||||||
|
.background(background)
|
||||||
|
.combinedClickable(
|
||||||
|
onClick = { onClick(hasOwn) },
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
)
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
CompositionLocalProvider(LocalContentColor provides foreground) {
|
||||||
|
if (emoji.isUlid()) {
|
||||||
|
RemoteImage(
|
||||||
|
url = "$REVOLT_FILES/emojis/${emoji}/emoji.gif?max_side=64",
|
||||||
|
description = null,
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Box(modifier = Modifier.size(16.dp), contentAlignment = Alignment.Center) {
|
||||||
|
Text(
|
||||||
|
text = emoji,
|
||||||
|
style = MaterialTheme.typography.bodyLarge.copy(
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
platformStyle = PlatformTextStyle(
|
||||||
|
includeFontPadding = false
|
||||||
|
)
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
|
||||||
|
members.size.let { number ->
|
||||||
|
number.toString()
|
||||||
|
.mapIndexed { index, c ->
|
||||||
|
ReactionDigit(
|
||||||
|
digitChar = c,
|
||||||
|
fullNumber = number,
|
||||||
|
place = index
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.forEach {
|
||||||
|
AnimatedContent(
|
||||||
|
targetState = it,
|
||||||
|
transitionSpec = {
|
||||||
|
if (targetState > initialState) {
|
||||||
|
slideInVertically { -it } togetherWith slideOutVertically { it }
|
||||||
|
} else {
|
||||||
|
slideInVertically { it } togetherWith slideOutVertically { -it }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label = "Reaction count",
|
||||||
|
) { target ->
|
||||||
|
Text(
|
||||||
|
text = target.digitChar.toString(),
|
||||||
|
style = MaterialTheme.typography.bodyLarge.copy(
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
fontFeatureSettings = "tnum"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ReactionDigit(val digitChar: Char, val fullNumber: Int, val place: Int) {
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
return when (other) {
|
||||||
|
is ReactionDigit -> digitChar == other.digitChar
|
||||||
|
else -> super.equals(other)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = digitChar.hashCode()
|
||||||
|
result = 31 * result + fullNumber
|
||||||
|
result = 31 * result + place
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun ReactionDigit.compareTo(other: ReactionDigit): Int {
|
||||||
|
return fullNumber.compareTo(other.fullNumber)
|
||||||
|
}
|
||||||
|
|
@ -26,6 +26,8 @@ import chat.revolt.api.realtime.frames.receivable.ChannelStopTypingFrame
|
||||||
import chat.revolt.api.realtime.frames.receivable.MessageAppendFrame
|
import chat.revolt.api.realtime.frames.receivable.MessageAppendFrame
|
||||||
import chat.revolt.api.realtime.frames.receivable.MessageDeleteFrame
|
import chat.revolt.api.realtime.frames.receivable.MessageDeleteFrame
|
||||||
import chat.revolt.api.realtime.frames.receivable.MessageFrame
|
import chat.revolt.api.realtime.frames.receivable.MessageFrame
|
||||||
|
import chat.revolt.api.realtime.frames.receivable.MessageReactFrame
|
||||||
|
import chat.revolt.api.realtime.frames.receivable.MessageUnreactFrame
|
||||||
import chat.revolt.api.realtime.frames.receivable.MessageUpdateFrame
|
import chat.revolt.api.realtime.frames.receivable.MessageUpdateFrame
|
||||||
import chat.revolt.api.routes.channel.SendMessageReply
|
import chat.revolt.api.routes.channel.SendMessageReply
|
||||||
import chat.revolt.api.routes.channel.ackChannel
|
import chat.revolt.api.routes.channel.ackChannel
|
||||||
|
|
@ -353,6 +355,46 @@ class ChannelScreenViewModel : ViewModel() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is MessageReactFrame -> {
|
||||||
|
if (it.channel_id != activeChannel?.id) return@onEach
|
||||||
|
|
||||||
|
val hasMessage = renderableMessages.any { currentMsg ->
|
||||||
|
currentMsg.id == it.id
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasMessage) return@onEach
|
||||||
|
|
||||||
|
regroupMessages(
|
||||||
|
renderableMessages.map { currentMsg ->
|
||||||
|
if (currentMsg.id == it.id) {
|
||||||
|
RevoltAPI.messageCache[it.id] ?: currentMsg
|
||||||
|
} else {
|
||||||
|
currentMsg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is MessageUnreactFrame -> {
|
||||||
|
if (it.channel_id != activeChannel?.id) return@onEach
|
||||||
|
|
||||||
|
val hasMessage = renderableMessages.any { currentMsg ->
|
||||||
|
currentMsg.id == it.id
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasMessage) return@onEach
|
||||||
|
|
||||||
|
regroupMessages(
|
||||||
|
renderableMessages.map { currentMsg ->
|
||||||
|
if (currentMsg.id == it.id) {
|
||||||
|
RevoltAPI.messageCache[it.id] ?: currentMsg
|
||||||
|
} else {
|
||||||
|
currentMsg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
is MessageDeleteFrame -> {
|
is MessageDeleteFrame -> {
|
||||||
if (it.channel != activeChannel?.id) return@onEach
|
if (it.channel != activeChannel?.id) return@onEach
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue