feat: mass and role mentions
Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
parent
a9e22bcb98
commit
62a9a6c022
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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<LabsAccessControlVariates>(
|
||||
|
|
@ -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>(
|
||||
MassMentionsVariates.Enabled
|
||||
)
|
||||
val massMentionsGranted: Boolean
|
||||
get() = when (massMentions) {
|
||||
is MassMentionsVariates.Enabled -> true
|
||||
is MassMentionsVariates.Disabled -> false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<AutocompleteSuggestion> {
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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<IntRange>
|
||||
): SequentialParser.ParsingResult {
|
||||
// TODO - Implement
|
||||
val result = SequentialParser.ParsingResultBuilder()
|
||||
val delegateIndices = RangesListBuilder()
|
||||
return result.withFurtherProcessing(delegateIndices.get())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M720,520L720,440L880,440L880,520L720,520ZM768,800L640,704L688,640L816,736L768,800ZM688,320L640,256L768,160L816,224L688,320ZM200,760L200,600L160,600Q127,600 103.5,576.5Q80,553 80,520L80,440Q80,407 103.5,383.5Q127,360 160,360L320,360L520,240L520,720L320,600L280,600L280,760L200,760ZM440,578L440,382L342,440L160,440Q160,440 160,440Q160,440 160,440L160,520Q160,520 160,520Q160,520 160,520L342,520L440,578ZM560,614L560,346Q587,370 603.5,404.5Q620,439 620,480Q620,521 603.5,555.5Q587,590 560,614ZM300,480L300,480L300,480Q300,480 300,480Q300,480 300,480L300,480Q300,480 300,480Q300,480 300,480L300,480L300,480Z"/>
|
||||
</vector>
|
||||
Loading…
Reference in New Issue