diff --git a/app/src/main/java/chat/revolt/callbacks/ActionChannel.kt b/app/src/main/java/chat/revolt/callbacks/ActionChannel.kt index 75844a88..127158e6 100644 --- a/app/src/main/java/chat/revolt/callbacks/ActionChannel.kt +++ b/app/src/main/java/chat/revolt/callbacks/ActionChannel.kt @@ -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() } diff --git a/app/src/main/java/chat/revolt/components/chat/Message.kt b/app/src/main/java/chat/revolt/components/chat/Message.kt index 470a5573..7cc59be6 100644 --- a/app/src/main/java/chat/revolt/components/chat/Message.kt +++ b/app/src/main/java/chat/revolt/components/chat/Message.kt @@ -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 + ) + ) + } } } } diff --git a/app/src/main/java/chat/revolt/components/chat/Reaction.kt b/app/src/main/java/chat/revolt/components/chat/Reaction.kt index 76d46bb6..cc5b7a3b 100644 --- a/app/src/main/java/chat/revolt/components/chat/Reaction.kt +++ b/app/src/main/java/chat/revolt/components/chat/Reaction.kt @@ -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) ) diff --git a/app/src/main/java/chat/revolt/internals/EmojiImpl.kt b/app/src/main/java/chat/revolt/internals/EmojiImpl.kt index 5b0f0f51..47465c1c 100644 --- a/app/src/main/java/chat/revolt/internals/EmojiImpl.kt +++ b/app/src/main/java/chat/revolt/internals/EmojiImpl.kt @@ -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) } diff --git a/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt b/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt index d40ea521..50336fd6 100644 --- a/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt +++ b/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt @@ -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() diff --git a/app/src/main/java/chat/revolt/sheets/ReactionInfoSheet.kt b/app/src/main/java/chat/revolt/sheets/ReactionInfoSheet.kt new file mode 100644 index 00000000..bfe7614b --- /dev/null +++ b/app/src/main/java/chat/revolt/sheets/ReactionInfoSheet.kt @@ -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() } + + 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)) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e107af65..7de97d98 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -229,6 +229,7 @@ from %1$s from a private server + A default emoji that you can use anywhere Channel description There hasn\'t been a description set for this channel yet.