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 SwitchChannel(val channelId: String) : Action()
data class LinkInfo(val url: String) : Action() data class LinkInfo(val url: String) : Action()
data class EmoteInfo(val emoteId: 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 TopNavigate(val route: String) : Action()
data class ChatNavigate(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.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.callbacks.Action
import chat.revolt.callbacks.ActionChannel
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
@ -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) { CompositionLocalProvider(LocalContentColor provides foreground) {
if (emoji.isUlid()) { if (emoji.isUlid()) {
RemoteImage( RemoteImage(
url = "$REVOLT_FILES/emojis/${emoji}/emoji.gif?max_side=64", url = "$REVOLT_FILES/emojis/${emoji}/emoji.gif",
description = null, description = null,
modifier = Modifier.size(16.dp) modifier = Modifier.size(16.dp)
) )

View File

@ -269,6 +269,16 @@ class EmojiImpl {
}.flatten().toList() }.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 { init {
metadata = initMetadata(RevoltApplication.instance.applicationContext) 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.ChangelogSheet
import chat.revolt.sheets.EmoteInfoSheet import chat.revolt.sheets.EmoteInfoSheet
import chat.revolt.sheets.LinkInfoSheet import chat.revolt.sheets.LinkInfoSheet
import chat.revolt.sheets.ReactionInfoSheet
import chat.revolt.sheets.ServerContextSheet import chat.revolt.sheets.ServerContextSheet
import chat.revolt.sheets.StatusSheet import chat.revolt.sheets.StatusSheet
import chat.revolt.sheets.UserInfoSheet import chat.revolt.sheets.UserInfoSheet
@ -327,6 +328,10 @@ fun ChatRouterScreen(
var showEmoteInfoSheet by remember { mutableStateOf(false) } var showEmoteInfoSheet by remember { mutableStateOf(false) }
var emoteInfoSheetTarget by remember { mutableStateOf("") } 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) } var useTabletAwareUI by remember { mutableStateOf(false) }
val toggleDrawerLambda = remember { val toggleDrawerLambda = remember {
@ -441,6 +446,12 @@ fun ChatRouterScreen(
showEmoteInfoSheet = true showEmoteInfoSheet = true
} }
is Action.MessageReactionInfo -> {
reactionInfoSheetMessageId = action.messageId
reactionInfoSheetEmoji = action.emoji
showReactionInfoSheet = true
}
is Action.TopNavigate -> { is Action.TopNavigate -> {
topNav.navigate(action.route) 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( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .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">from %1$s</string>
<string name="emote_info_from_server_unknown">from a private server</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">Channel description</string>
<string name="channel_info_sheet_description_empty">There hasn\'t been a description set for this channel yet.</string> <string name="channel_info_sheet_description_empty">There hasn\'t been a description set for this channel yet.</string>