From 62a9a6c022698546daa08bacda9d535660421026 Mon Sep 17 00:00:00 2001 From: Infi Date: Wed, 2 Jul 2025 02:53:46 +0200 Subject: [PATCH] feat: mass and role mentions Signed-off-by: Infi --- .../chat/revolt/api/internals/MessageFlag.kt | 36 ++++++++++++++----- .../chat/revolt/api/internals/Permissions.kt | 8 ++++- .../java/chat/revolt/api/schemas/Messages.kt | 5 +++ .../chat/revolt/api/settings/FeatureFlags.kt | 23 ++++++++++++ .../chat/revolt/composables/chat/Message.kt | 8 ++++- .../revolt/composables/chat/MessageField.kt | 34 ++++++++++++++++++ .../chat/revolt/internals/Autocomplete.kt | 24 +++++++++++-- .../revolt/markdown/jbm/RSMElementTypes.kt | 3 ++ .../markdown/jbm/RSMFlavourDescriptor.kt | 2 ++ .../sequentialparsers/MassMentionParser.kt | 17 +++++++++ .../main/res/drawable/icn_campaign_24dp.xml | 10 ++++++ 11 files changed, 158 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/chat/revolt/markdown/jbm/sequentialparsers/MassMentionParser.kt create mode 100644 app/src/main/res/drawable/icn_campaign_24dp.xml diff --git a/app/src/main/java/chat/revolt/api/internals/MessageFlag.kt b/app/src/main/java/chat/revolt/api/internals/MessageFlag.kt index 299b8e31..d4f04445 100644 --- a/app/src/main/java/chat/revolt/api/internals/MessageFlag.kt +++ b/app/src/main/java/chat/revolt/api/internals/MessageFlag.kt @@ -1,15 +1,35 @@ package chat.revolt.api.internals +/** + * Flags for messages that can be set to modify their behavior. + * + * See [Reference](https://docs.rs/revolt-models/latest/revolt_models/v0/enum.MessageFlags.html) for + * values + * + * `shl 0` is not used. + * + * @property value The integer value representing the flag. + */ enum class MessageFlag(val value: Int) { - // Message will not send push / desktop notifications - SuppressNotifications(1 shl 0), + /** + * Message will not send push / desktop notifications. + */ + SuppressNotifications(1 shl 1), - // Message will mention all users who can see the channel - MentionsEveryone(1 shl 1), + /** + * Message will mention all users who can see the channel + * > **Cannot be set on send** + */ + MentionsEveryone(1 shl 2), - // Message will mention all users who are online and can see the channel. - // This cannot be true if MentionsEveryone is true - MentionsOnline(1 shl 2) + /** + * Message will mention all users who are online and can see the channel. + * + * This cannot be true if [MentionsEveryone] is true. + * + * > **Cannot be set on send** + */ + MentionsOnline(1 shl 3) } operator fun Int.plus(other: MessageFlag): Int { @@ -20,6 +40,6 @@ fun Int.hasMessageFlag(flag: MessageFlag): Boolean { return this and flag.value == flag.value } -infix fun Int?.hasMessageFlag(flag: MessageFlag): Boolean { +infix fun Int?.has(flag: MessageFlag): Boolean { return this != null && this.hasMessageFlag(flag) } \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/api/internals/Permissions.kt b/app/src/main/java/chat/revolt/api/internals/Permissions.kt index a7e0cc7b..bda00b30 100644 --- a/app/src/main/java/chat/revolt/api/internals/Permissions.kt +++ b/app/src/main/java/chat/revolt/api/internals/Permissions.kt @@ -42,8 +42,14 @@ enum class PermissionBit(val value: Long) { DeafenMembers(1L shl 34), MoveMembers(1L shl 35), + // % 1 bit reserved + + // * Channel permissions cont. + MentionEveryone(1L shl 37), + MentionRoles(1L shl 38), + // * Misc. permissions - // % Bits 36 to 52: free area + // % Bits 38 to 52: free area // % Bits 53 to 64: do not use // * Grant all permissions diff --git a/app/src/main/java/chat/revolt/api/schemas/Messages.kt b/app/src/main/java/chat/revolt/api/schemas/Messages.kt index da20b3a1..a311ce6d 100644 --- a/app/src/main/java/chat/revolt/api/schemas/Messages.kt +++ b/app/src/main/java/chat/revolt/api/schemas/Messages.kt @@ -22,6 +22,11 @@ data class Message( val system: SystemInfo? = null, val webhook: WebHook? = null, val interactions: InteractionsDescription? = null, + val pinned: Boolean? = null, + /** + * See [chat.revolt.api.internals.MessageFlag] + */ + val flags: Int? = 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 ) { diff --git a/app/src/main/java/chat/revolt/api/settings/FeatureFlags.kt b/app/src/main/java/chat/revolt/api/settings/FeatureFlags.kt index 8cc879d8..3a22d769 100644 --- a/app/src/main/java/chat/revolt/api/settings/FeatureFlags.kt +++ b/app/src/main/java/chat/revolt/api/settings/FeatureFlags.kt @@ -43,6 +43,19 @@ sealed class UserCardsVariates { data class Restricted(val predicate: () -> Boolean) : UserCardsVariates() } +@FeatureFlag("MassMentions") +sealed class MassMentionsVariates { + @Treatment( + "Enable mass mentions and role mentions for all users" + ) + object Enabled : MassMentionsVariates() + + @Treatment( + "Disable mass mentions and role mentions for all users" + ) + object Disabled : MassMentionsVariates() +} + object FeatureFlags { @FeatureFlag("LabsAccessControl") var labsAccessControl by mutableStateOf( @@ -81,4 +94,14 @@ object FeatureFlags { is UserCardsVariates.Enabled -> true is UserCardsVariates.Restricted -> (userCards as UserCardsVariates.Restricted).predicate() } + + @FeatureFlag("MassMentions") + var massMentions by mutableStateOf( + MassMentionsVariates.Enabled + ) + val massMentionsGranted: Boolean + get() = when (massMentions) { + is MassMentionsVariates.Enabled -> true + is MassMentionsVariates.Disabled -> false + } } diff --git a/app/src/main/java/chat/revolt/composables/chat/Message.kt b/app/src/main/java/chat/revolt/composables/chat/Message.kt index 11192df6..83665575 100644 --- a/app/src/main/java/chat/revolt/composables/chat/Message.kt +++ b/app/src/main/java/chat/revolt/composables/chat/Message.kt @@ -64,9 +64,11 @@ import chat.revolt.activities.media.VideoViewActivity import chat.revolt.api.REVOLT_FILES import chat.revolt.api.RevoltAPI import chat.revolt.api.internals.BrushCompat +import chat.revolt.api.internals.MessageFlag import chat.revolt.api.internals.Roles import chat.revolt.api.internals.SpecialUsers import chat.revolt.api.internals.ULID +import chat.revolt.api.internals.has import chat.revolt.api.internals.solidColor import chat.revolt.api.routes.channel.react import chat.revolt.api.routes.channel.unreact @@ -262,7 +264,11 @@ fun Message( } else { Column( modifier = Modifier.then( - if ((message.mentions?.contains(RevoltAPI.selfId) == true) || mentionsSelfRole) { + if ((message.mentions?.contains(RevoltAPI.selfId) == true) + || mentionsSelfRole + || message.flags has MessageFlag.MentionsOnline + || message.flags has MessageFlag.MentionsEveryone + ) { Modifier.background( MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) ) diff --git a/app/src/main/java/chat/revolt/composables/chat/MessageField.kt b/app/src/main/java/chat/revolt/composables/chat/MessageField.kt index 605a32c3..2c44a11f 100644 --- a/app/src/main/java/chat/revolt/composables/chat/MessageField.kt +++ b/app/src/main/java/chat/revolt/composables/chat/MessageField.kt @@ -135,6 +135,10 @@ sealed class AutocompleteSuggestion { val id: String, val query: String ) : AutocompleteSuggestion() + + data class MassMention( + val content: String + ) : AutocompleteSuggestion() } @OptIn(ExperimentalFoundationApi::class) @@ -289,6 +293,7 @@ fun MessageField( is AutocompleteSuggestion.Channel -> item.channel.id!! is AutocompleteSuggestion.Emoji -> item.shortcode is AutocompleteSuggestion.Role -> item.id + is AutocompleteSuggestion.MassMention -> item.content } }) { when (val item = autocompleteSuggestions[it]) { @@ -453,6 +458,35 @@ fun MessageField( modifier = Modifier.animateItem() ) } + + is AutocompleteSuggestion.MassMention -> { + SuggestionChip( + onClick = { + textFieldState.edit { + val lastWordStartsAt = + textFieldState.text + .substring(0, textFieldState.selection.max) + .lastWordStartsAt() + replace( + if (lastWordStartsAt == -1) 0 else (lastWordStartsAt + 1), + textFieldState.selection.max, + "@${item.content} " + ) + } + }, + label = { Text("@${item.content}") }, + icon = { + Icon( + painter = painterResource(R.drawable.icn_campaign_24dp), + contentDescription = null, + modifier = Modifier + .size(SuggestionChipDefaults.IconSize) + .align(Alignment.CenterHorizontally) + ) + }, + modifier = Modifier.animateItem() + ) + } } } } diff --git a/app/src/main/java/chat/revolt/internals/Autocomplete.kt b/app/src/main/java/chat/revolt/internals/Autocomplete.kt index 7844e0b8..9fa2fa3d 100644 --- a/app/src/main/java/chat/revolt/internals/Autocomplete.kt +++ b/app/src/main/java/chat/revolt/internals/Autocomplete.kt @@ -1,7 +1,11 @@ package chat.revolt.internals import chat.revolt.api.RevoltAPI +import chat.revolt.api.internals.PermissionBit +import chat.revolt.api.internals.Roles +import chat.revolt.api.internals.has import chat.revolt.api.schemas.ChannelType +import chat.revolt.api.settings.FeatureFlags import chat.revolt.composables.chat.AutocompleteSuggestion object Autocomplete { @@ -44,6 +48,18 @@ object Autocomplete { ): List { val channel = RevoltAPI.channelCache[channelId] ?: return emptyList() + val member = serverId?.let { RevoltAPI.members.getMember(serverId, RevoltAPI.selfId ?: "") } + val massMentionSuggestions = listOf("everyone", "online") + .filter { it.startsWith(query, ignoreCase = true) } + + val selfPermissions = RevoltAPI.channelCache[channelId]?.let { ch -> + Roles.permissionFor( + ch, + RevoltAPI.userCache[RevoltAPI.selfId], + member + ) + } + return when (channel.channelType) { ChannelType.DirectMessage -> { val otherUser = channel.recipients?.find { it != RevoltAPI.selfId } @@ -102,7 +118,9 @@ object Autocomplete { if (serverId == null) return emptyList() if (query.length < 2) return emptyList() - val roles = RevoltAPI.serverCache[serverId]?.roles ?: emptyMap() + val roles = + if (selfPermissions has PermissionBit.MentionRoles && FeatureFlags.massMentionsGranted) RevoltAPI.serverCache[serverId]?.roles + ?: emptyMap() else emptyMap() val byNickname = RevoltAPI.members.filterNamesFor(serverId, query) .map { m -> m to RevoltAPI.userCache[m.id?.user] }.filter { (_, u) -> u != null @@ -156,7 +174,9 @@ object Autocomplete { } null -> emptyList() - } + } + if (selfPermissions has PermissionBit.MentionEveryone && FeatureFlags.massMentionsGranted) massMentionSuggestions.map { mention -> + AutocompleteSuggestion.MassMention(mention) + } else listOf() } fun channel( diff --git a/app/src/main/java/chat/revolt/markdown/jbm/RSMElementTypes.kt b/app/src/main/java/chat/revolt/markdown/jbm/RSMElementTypes.kt index c3683ff1..72c57355 100644 --- a/app/src/main/java/chat/revolt/markdown/jbm/RSMElementTypes.kt +++ b/app/src/main/java/chat/revolt/markdown/jbm/RSMElementTypes.kt @@ -12,6 +12,9 @@ object RSMElementTypes { @JvmField val ROLE_MENTION: IElementType = MarkdownElementType("ROLE_MENTION") + + @JvmField + val MASS_MENTION: IElementType = MarkdownElementType("MASS_MENTION") @JvmField val CUSTOM_EMOTE: IElementType = MarkdownElementType("EMOJI") diff --git a/app/src/main/java/chat/revolt/markdown/jbm/RSMFlavourDescriptor.kt b/app/src/main/java/chat/revolt/markdown/jbm/RSMFlavourDescriptor.kt index 0defc7e2..b3f51b0b 100644 --- a/app/src/main/java/chat/revolt/markdown/jbm/RSMFlavourDescriptor.kt +++ b/app/src/main/java/chat/revolt/markdown/jbm/RSMFlavourDescriptor.kt @@ -2,6 +2,7 @@ package chat.revolt.markdown.jbm import chat.revolt.markdown.jbm.sequentialparsers.ChannelMentionParser import chat.revolt.markdown.jbm.sequentialparsers.CustomEmoteParser +import chat.revolt.markdown.jbm.sequentialparsers.MassMentionParser import chat.revolt.markdown.jbm.sequentialparsers.RoleMentionParser import chat.revolt.markdown.jbm.sequentialparsers.UserMentionParser import org.intellij.markdown.MarkdownTokenTypes @@ -26,6 +27,7 @@ class RSMFlavourDescriptor : GFMFlavourDescriptor() { UserMentionParser(), ChannelMentionParser(), RoleMentionParser(), + MassMentionParser(), CustomEmoteParser(), AutolinkParser(listOf(MarkdownTokenTypes.AUTOLINK, GFMTokenTypes.GFM_AUTOLINK)), BacktickParser(), diff --git a/app/src/main/java/chat/revolt/markdown/jbm/sequentialparsers/MassMentionParser.kt b/app/src/main/java/chat/revolt/markdown/jbm/sequentialparsers/MassMentionParser.kt new file mode 100644 index 00000000..37944adf --- /dev/null +++ b/app/src/main/java/chat/revolt/markdown/jbm/sequentialparsers/MassMentionParser.kt @@ -0,0 +1,17 @@ +package chat.revolt.markdown.jbm.sequentialparsers + +import org.intellij.markdown.parser.sequentialparsers.RangesListBuilder +import org.intellij.markdown.parser.sequentialparsers.SequentialParser +import org.intellij.markdown.parser.sequentialparsers.TokensCache + +class MassMentionParser : SequentialParser { + override fun parse( + tokens: TokensCache, + rangesToGlue: List + ): SequentialParser.ParsingResult { + // TODO - Implement + val result = SequentialParser.ParsingResultBuilder() + val delegateIndices = RangesListBuilder() + return result.withFurtherProcessing(delegateIndices.get()) + } +} diff --git a/app/src/main/res/drawable/icn_campaign_24dp.xml b/app/src/main/res/drawable/icn_campaign_24dp.xml new file mode 100644 index 00000000..ebd9dc27 --- /dev/null +++ b/app/src/main/res/drawable/icn_campaign_24dp.xml @@ -0,0 +1,10 @@ + + +