feat: reaction info sheet

Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
Infi 2023-12-07 23:40:16 +01:00
parent 8b1dc84237
commit 7b011e3206
7 changed files with 274 additions and 2 deletions

View File

@ -7,6 +7,7 @@ sealed class Action {
data class SwitchChannel(val channelId: String) : Action()
data class LinkInfo(val url: String) : Action()
data class EmoteInfo(val emoteId: String) : Action()
data class MessageReactionInfo(val messageId: String, val emoji: String) : Action()
data class TopNavigate(val route: String) : Action()
data class ChatNavigate(val route: String) : Action()
}

View File

@ -71,6 +71,8 @@ import chat.revolt.api.routes.channel.unreact
import chat.revolt.api.routes.microservices.january.asJanuaryProxyUrl
import chat.revolt.api.schemas.AutumnResource
import chat.revolt.api.schemas.User
import chat.revolt.callbacks.Action
import chat.revolt.callbacks.ActionChannel
import chat.revolt.components.generic.UserAvatar
import chat.revolt.components.generic.UserAvatarWidthPlaceholder
import chat.revolt.internals.markdown.LongClickableSpan
@ -488,7 +490,14 @@ fun Message(
}
}
) {
scope.launch {
ActionChannel.send(
Action.MessageReactionInfo(
message.id!!,
reaction.key
)
)
}
}
}
}

View File

@ -75,7 +75,7 @@ fun Reaction(
CompositionLocalProvider(LocalContentColor provides foreground) {
if (emoji.isUlid()) {
RemoteImage(
url = "$REVOLT_FILES/emojis/${emoji}/emoji.gif?max_side=64",
url = "$REVOLT_FILES/emojis/${emoji}/emoji.gif",
description = null,
modifier = Modifier.size(16.dp)
)

View File

@ -269,6 +269,16 @@ class EmojiImpl {
}.flatten().toList()
}
fun unicodeAsShortcode(unicode: String): String? {
return metadata.asSequence().mapNotNull { group ->
group.emoji.find { emoji ->
emoji.base.joinToString("") { String(Character.toChars(it.toInt())) } == unicode
}
}.firstOrNull().let { emoji ->
emoji?.shortcodes?.firstOrNull()
}
}
init {
metadata = initMetadata(RevoltApplication.instance.applicationContext)
}

View File

@ -103,6 +103,7 @@ import chat.revolt.sheets.AddServerSheet
import chat.revolt.sheets.ChangelogSheet
import chat.revolt.sheets.EmoteInfoSheet
import chat.revolt.sheets.LinkInfoSheet
import chat.revolt.sheets.ReactionInfoSheet
import chat.revolt.sheets.ServerContextSheet
import chat.revolt.sheets.StatusSheet
import chat.revolt.sheets.UserInfoSheet
@ -327,6 +328,10 @@ fun ChatRouterScreen(
var showEmoteInfoSheet by remember { mutableStateOf(false) }
var emoteInfoSheetTarget by remember { mutableStateOf("") }
var showReactionInfoSheet by remember { mutableStateOf(false) }
var reactionInfoSheetMessageId by remember { mutableStateOf("") }
var reactionInfoSheetEmoji by remember { mutableStateOf("") }
var useTabletAwareUI by remember { mutableStateOf(false) }
val toggleDrawerLambda = remember {
@ -441,6 +446,12 @@ fun ChatRouterScreen(
showEmoteInfoSheet = true
}
is Action.MessageReactionInfo -> {
reactionInfoSheetMessageId = action.messageId
reactionInfoSheetEmoji = action.emoji
showReactionInfoSheet = true
}
is Action.TopNavigate -> {
topNav.navigate(action.route)
}
@ -676,6 +687,25 @@ fun ChatRouterScreen(
}
}
if (showReactionInfoSheet) {
val reactionInfoSheetState = rememberModalBottomSheetState()
ModalBottomSheet(
sheetState = reactionInfoSheetState,
onDismissRequest = {
showReactionInfoSheet = false
}
) {
ReactionInfoSheet(
messageId = reactionInfoSheetMessageId,
emoji = reactionInfoSheetEmoji,
onDismiss = {
showReactionInfoSheet = false
}
)
}
}
Column(
modifier = Modifier
.fillMaxWidth()

View File

@ -0,0 +1,221 @@
package chat.revolt.sheets
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Divider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ScrollableTabRow
import androidx.compose.material3.Tab
import androidx.compose.material3.Text
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.stringResource
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.R
import chat.revolt.api.REVOLT_FILES
import chat.revolt.api.RevoltAPI
import chat.revolt.api.internals.MessageProcessor
import chat.revolt.api.internals.isUlid
import chat.revolt.api.routes.custom.fetchEmoji
import chat.revolt.api.schemas.Emoji
import chat.revolt.api.schemas.User
import chat.revolt.components.generic.RemoteImage
@OptIn(ExperimentalFoundationApi::class)
@Composable
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 extendedEmojiInfo = remember(emoji) { mutableStateListOf<Emoji>() }
LaunchedEffect(reactionEmoji) {
reactionEmoji?.forEach {
if (it.isUlid()) {
extendedEmojiInfo.add(RevoltAPI.emojiCache[it] ?: fetchEmoji(it))
}
}
}
var selectedReactionIndex by remember(
messageId,
emoji
) { mutableIntStateOf(reactionEmoji?.indexOfFirst { it == emoji } ?: 0) }
if (selectedReactionIndex >= (reactionEmoji?.size ?: 0)) {
selectedReactionIndex = 0
}
if (reactionEmoji?.isEmpty() == true) {
onDismiss()
}
LazyColumn {
stickyHeader(key = "tabs") {
ScrollableTabRow(
selectedTabIndex = selectedReactionIndex,
modifier = Modifier.fillMaxWidth(),
divider = {}
) {
reactionEmoji?.forEachIndexed { index, emoji ->
Tab(
text = {
if (emoji.isUlid()) {
Row(verticalAlignment = Alignment.CenterVertically) {
RemoteImage(
url = "$REVOLT_FILES/emojis/${emoji}/emoji.gif",
description = null,
modifier = Modifier.size(16.dp)
)
Spacer(Modifier.size(4.dp))
Text("${reactions[emoji]?.size ?: 0}")
}
} else {
Text("$emoji ${reactions[emoji]?.size ?: 0}")
}
},
selected = selectedReactionIndex == index,
onClick = { selectedReactionIndex = index }
)
}
}
Divider()
}
if (reactionEmoji?.isNotEmpty() == true) {
item("info") {
val current = reactionEmoji[selectedReactionIndex]
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(16.dp)
.clip(MaterialTheme.shapes.medium)
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(0.dp))
) {
if (current.isUlid()) {
val cached = extendedEmojiInfo.find { it.id == current }
RemoteImage(
url = "$REVOLT_FILES/emojis/$current/emoji.gif",
description = cached?.name,
contentScale = ContentScale.Fit,
modifier = Modifier
.padding(16.dp)
.size(32.dp)
)
} else {
Box(
modifier = Modifier
.padding(16.dp)
.size(32.dp),
contentAlignment = Alignment.Center
) {
Text(
text = current,
style = MaterialTheme.typography.bodyLarge.copy(
fontSize = 28.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
platformStyle = PlatformTextStyle(
includeFontPadding = false
)
),
modifier = Modifier
.size(64.dp)
)
}
}
Column(
modifier = Modifier.padding(
top = 16.dp,
start = 0.dp,
end = 16.dp,
bottom = 16.dp
)
) {
if (current.isUlid()) {
val cached = extendedEmojiInfo.find { it.id == current }
Text(
text = ":${cached?.name ?: current}:",
fontWeight = FontWeight.Bold,
letterSpacing = 1.15.sp
)
} else {
Text(
text = MessageProcessor.emoji.unicodeAsShortcode(current)
?: current,
fontWeight = FontWeight.Bold,
letterSpacing = 1.15.sp
)
}
Text(
text = if (current.isUlid()) {
val cached = extendedEmojiInfo.find { it.id == current }
if (cached?.parent != null) {
when (cached.parent.type) {
"Server" -> RevoltAPI.serverCache[cached.parent.id]?.name?.let {
stringResource(
id = R.string.emote_info_from_server,
it
)
}
?: stringResource(id = R.string.emote_info_from_server_unknown)
else -> stringResource(id = R.string.emote_info_from_server_unknown)
}
} else {
stringResource(id = R.string.emote_info_from_server_unknown)
}
} else {
stringResource(id = R.string.emote_info_from_unicode)
}
)
}
}
}
val reactionsForEmoji = reactions[reactionEmoji[selectedReactionIndex]]
items(reactionsForEmoji?.size ?: 0) { index ->
val reaction = reactionsForEmoji?.get(index) ?: return@items
val user = RevoltAPI.userCache[reaction] ?: User.getPlaceholder(reaction)
val member = if (channel.server != null && user.id != null) {
RevoltAPI.members.getMember(channel.server, user.id)
} else {
null
}
MemberListMemberUser(user = user, member = member, serverId = channel.server) {}
}
}
item("bottom") {
Spacer(Modifier.size(16.dp))
}
}
}

View File

@ -229,6 +229,7 @@
<string name="emote_info_from_server">from %1$s</string>
<string name="emote_info_from_server_unknown">from a private server</string>
<string name="emote_info_from_unicode">A default emoji that you can use anywhere</string>
<string name="channel_info_sheet_description">Channel description</string>
<string name="channel_info_sheet_description_empty">There hasn\'t been a description set for this channel yet.</string>