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
|
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) {
|
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
|
* Message will mention all users who are online and can see the channel.
|
||||||
MentionsOnline(1 shl 2)
|
*
|
||||||
|
* This cannot be true if [MentionsEveryone] is true.
|
||||||
|
*
|
||||||
|
* > **Cannot be set on send**
|
||||||
|
*/
|
||||||
|
MentionsOnline(1 shl 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
operator fun Int.plus(other: MessageFlag): Int {
|
operator fun Int.plus(other: MessageFlag): Int {
|
||||||
|
|
@ -20,6 +40,6 @@ fun Int.hasMessageFlag(flag: MessageFlag): Boolean {
|
||||||
return this and flag.value == flag.value
|
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)
|
return this != null && this.hasMessageFlag(flag)
|
||||||
}
|
}
|
||||||
|
|
@ -42,8 +42,14 @@ enum class PermissionBit(val value: Long) {
|
||||||
DeafenMembers(1L shl 34),
|
DeafenMembers(1L shl 34),
|
||||||
MoveMembers(1L shl 35),
|
MoveMembers(1L shl 35),
|
||||||
|
|
||||||
|
// % 1 bit reserved
|
||||||
|
|
||||||
|
// * Channel permissions cont.
|
||||||
|
MentionEveryone(1L shl 37),
|
||||||
|
MentionRoles(1L shl 38),
|
||||||
|
|
||||||
// * Misc. permissions
|
// * Misc. permissions
|
||||||
// % Bits 36 to 52: free area
|
// % Bits 38 to 52: free area
|
||||||
// % Bits 53 to 64: do not use
|
// % Bits 53 to 64: do not use
|
||||||
|
|
||||||
// * Grant all permissions
|
// * Grant all permissions
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,11 @@ data class Message(
|
||||||
val system: SystemInfo? = null,
|
val system: SystemInfo? = null,
|
||||||
val webhook: WebHook? = null,
|
val webhook: WebHook? = null,
|
||||||
val interactions: InteractionsDescription? = 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 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
|
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()
|
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 {
|
object FeatureFlags {
|
||||||
@FeatureFlag("LabsAccessControl")
|
@FeatureFlag("LabsAccessControl")
|
||||||
var labsAccessControl by mutableStateOf<LabsAccessControlVariates>(
|
var labsAccessControl by mutableStateOf<LabsAccessControlVariates>(
|
||||||
|
|
@ -81,4 +94,14 @@ object FeatureFlags {
|
||||||
is UserCardsVariates.Enabled -> true
|
is UserCardsVariates.Enabled -> true
|
||||||
is UserCardsVariates.Restricted -> (userCards as UserCardsVariates.Restricted).predicate()
|
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.REVOLT_FILES
|
||||||
import chat.revolt.api.RevoltAPI
|
import chat.revolt.api.RevoltAPI
|
||||||
import chat.revolt.api.internals.BrushCompat
|
import chat.revolt.api.internals.BrushCompat
|
||||||
|
import chat.revolt.api.internals.MessageFlag
|
||||||
import chat.revolt.api.internals.Roles
|
import chat.revolt.api.internals.Roles
|
||||||
import chat.revolt.api.internals.SpecialUsers
|
import chat.revolt.api.internals.SpecialUsers
|
||||||
import chat.revolt.api.internals.ULID
|
import chat.revolt.api.internals.ULID
|
||||||
|
import chat.revolt.api.internals.has
|
||||||
import chat.revolt.api.internals.solidColor
|
import chat.revolt.api.internals.solidColor
|
||||||
import chat.revolt.api.routes.channel.react
|
import chat.revolt.api.routes.channel.react
|
||||||
import chat.revolt.api.routes.channel.unreact
|
import chat.revolt.api.routes.channel.unreact
|
||||||
|
|
@ -262,7 +264,11 @@ fun Message(
|
||||||
} else {
|
} else {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.then(
|
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(
|
Modifier.background(
|
||||||
MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,10 @@ sealed class AutocompleteSuggestion {
|
||||||
val id: String,
|
val id: String,
|
||||||
val query: String
|
val query: String
|
||||||
) : AutocompleteSuggestion()
|
) : AutocompleteSuggestion()
|
||||||
|
|
||||||
|
data class MassMention(
|
||||||
|
val content: String
|
||||||
|
) : AutocompleteSuggestion()
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
|
@ -289,6 +293,7 @@ fun MessageField(
|
||||||
is AutocompleteSuggestion.Channel -> item.channel.id!!
|
is AutocompleteSuggestion.Channel -> item.channel.id!!
|
||||||
is AutocompleteSuggestion.Emoji -> item.shortcode
|
is AutocompleteSuggestion.Emoji -> item.shortcode
|
||||||
is AutocompleteSuggestion.Role -> item.id
|
is AutocompleteSuggestion.Role -> item.id
|
||||||
|
is AutocompleteSuggestion.MassMention -> item.content
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
when (val item = autocompleteSuggestions[it]) {
|
when (val item = autocompleteSuggestions[it]) {
|
||||||
|
|
@ -453,6 +458,35 @@ fun MessageField(
|
||||||
modifier = Modifier.animateItem()
|
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
|
package chat.revolt.internals
|
||||||
|
|
||||||
import chat.revolt.api.RevoltAPI
|
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.schemas.ChannelType
|
||||||
|
import chat.revolt.api.settings.FeatureFlags
|
||||||
import chat.revolt.composables.chat.AutocompleteSuggestion
|
import chat.revolt.composables.chat.AutocompleteSuggestion
|
||||||
|
|
||||||
object Autocomplete {
|
object Autocomplete {
|
||||||
|
|
@ -44,6 +48,18 @@ object Autocomplete {
|
||||||
): List<AutocompleteSuggestion> {
|
): List<AutocompleteSuggestion> {
|
||||||
val channel = RevoltAPI.channelCache[channelId] ?: return emptyList()
|
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) {
|
return when (channel.channelType) {
|
||||||
ChannelType.DirectMessage -> {
|
ChannelType.DirectMessage -> {
|
||||||
val otherUser = channel.recipients?.find { it != RevoltAPI.selfId }
|
val otherUser = channel.recipients?.find { it != RevoltAPI.selfId }
|
||||||
|
|
@ -102,7 +118,9 @@ object Autocomplete {
|
||||||
if (serverId == null) return emptyList()
|
if (serverId == null) return emptyList()
|
||||||
if (query.length < 2) 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)
|
val byNickname = RevoltAPI.members.filterNamesFor(serverId, query)
|
||||||
.map { m -> m to RevoltAPI.userCache[m.id?.user] }.filter { (_, u) ->
|
.map { m -> m to RevoltAPI.userCache[m.id?.user] }.filter { (_, u) ->
|
||||||
u != null
|
u != null
|
||||||
|
|
@ -156,7 +174,9 @@ object Autocomplete {
|
||||||
}
|
}
|
||||||
|
|
||||||
null -> emptyList()
|
null -> emptyList()
|
||||||
}
|
} + if (selfPermissions has PermissionBit.MentionEveryone && FeatureFlags.massMentionsGranted) massMentionSuggestions.map { mention ->
|
||||||
|
AutocompleteSuggestion.MassMention(mention)
|
||||||
|
} else listOf()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun channel(
|
fun channel(
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,9 @@ object RSMElementTypes {
|
||||||
@JvmField
|
@JvmField
|
||||||
val ROLE_MENTION: IElementType = MarkdownElementType("ROLE_MENTION")
|
val ROLE_MENTION: IElementType = MarkdownElementType("ROLE_MENTION")
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val MASS_MENTION: IElementType = MarkdownElementType("MASS_MENTION")
|
||||||
|
|
||||||
@JvmField
|
@JvmField
|
||||||
val CUSTOM_EMOTE: IElementType = MarkdownElementType("EMOJI")
|
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.ChannelMentionParser
|
||||||
import chat.revolt.markdown.jbm.sequentialparsers.CustomEmoteParser
|
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.RoleMentionParser
|
||||||
import chat.revolt.markdown.jbm.sequentialparsers.UserMentionParser
|
import chat.revolt.markdown.jbm.sequentialparsers.UserMentionParser
|
||||||
import org.intellij.markdown.MarkdownTokenTypes
|
import org.intellij.markdown.MarkdownTokenTypes
|
||||||
|
|
@ -26,6 +27,7 @@ class RSMFlavourDescriptor : GFMFlavourDescriptor() {
|
||||||
UserMentionParser(),
|
UserMentionParser(),
|
||||||
ChannelMentionParser(),
|
ChannelMentionParser(),
|
||||||
RoleMentionParser(),
|
RoleMentionParser(),
|
||||||
|
MassMentionParser(),
|
||||||
CustomEmoteParser(),
|
CustomEmoteParser(),
|
||||||
AutolinkParser(listOf(MarkdownTokenTypes.AUTOLINK, GFMTokenTypes.GFM_AUTOLINK)),
|
AutolinkParser(listOf(MarkdownTokenTypes.AUTOLINK, GFMTokenTypes.GFM_AUTOLINK)),
|
||||||
BacktickParser(),
|
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