diff --git a/app/src/main/java/chat/revolt/components/chat/InReplyTo.kt b/app/src/main/java/chat/revolt/components/chat/InReplyTo.kt index 21809eac..40970fbb 100644 --- a/app/src/main/java/chat/revolt/components/chat/InReplyTo.kt +++ b/app/src/main/java/chat/revolt/components/chat/InReplyTo.kt @@ -33,9 +33,9 @@ import chat.revolt.api.routes.channel.fetchSingleMessage import chat.revolt.api.schemas.User import chat.revolt.api.settings.Experiments import chat.revolt.components.generic.UserAvatar -import chat.revolt.components.markdown.jbm.JBM -import chat.revolt.components.markdown.jbm.JBMRenderer -import chat.revolt.components.markdown.jbm.LocalJBMarkdownTreeState +import chat.revolt.markdown.jbm.JBM +import chat.revolt.markdown.jbm.JBMRenderer +import chat.revolt.markdown.jbm.LocalJBMarkdownTreeState @OptIn(JBM::class) @Composable 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 1e77a9dd..5728bace 100644 --- a/app/src/main/java/chat/revolt/components/chat/Message.kt +++ b/app/src/main/java/chat/revolt/components/chat/Message.kt @@ -78,9 +78,9 @@ import chat.revolt.components.generic.UserAvatar import chat.revolt.components.generic.UserAvatarWidthPlaceholder import chat.revolt.components.markdown.LocalMarkdownTreeConfig import chat.revolt.components.markdown.RichMarkdown -import chat.revolt.components.markdown.jbm.JBM -import chat.revolt.components.markdown.jbm.JBMRenderer -import chat.revolt.components.markdown.jbm.LocalJBMarkdownTreeState +import chat.revolt.markdown.jbm.JBM +import chat.revolt.markdown.jbm.JBMRenderer +import chat.revolt.markdown.jbm.LocalJBMarkdownTreeState import chat.revolt.internals.text.Gigamoji import kotlinx.coroutines.launch import chat.revolt.api.schemas.Message as MessageSchema diff --git a/app/src/main/java/chat/revolt/components/markdown/jbm/JBMApi.kt b/app/src/main/java/chat/revolt/markdown/jbm/JBMApi.kt similarity index 68% rename from app/src/main/java/chat/revolt/components/markdown/jbm/JBMApi.kt rename to app/src/main/java/chat/revolt/markdown/jbm/JBMApi.kt index bdc15282..4ce186b1 100644 --- a/app/src/main/java/chat/revolt/components/markdown/jbm/JBMApi.kt +++ b/app/src/main/java/chat/revolt/markdown/jbm/JBMApi.kt @@ -1,7 +1,6 @@ -package chat.revolt.components.markdown.jbm +package chat.revolt.markdown.jbm import org.intellij.markdown.ast.ASTNode -import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor import org.intellij.markdown.parser.MarkdownParser @RequiresOptIn(message = "This API is experimental and has many TODOs.") @@ -12,6 +11,6 @@ annotation class JBM @JBM object JBMApi { fun parse(src: String): ASTNode { - return MarkdownParser(GFMFlavourDescriptor()).buildMarkdownTreeFromString(src) + return MarkdownParser(RSMFlavourDescriptor()).buildMarkdownTreeFromString(src) } } \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/components/markdown/jbm/JBMRenderer.kt b/app/src/main/java/chat/revolt/markdown/jbm/JBMRenderer.kt similarity index 86% rename from app/src/main/java/chat/revolt/components/markdown/jbm/JBMRenderer.kt rename to app/src/main/java/chat/revolt/markdown/jbm/JBMRenderer.kt index 72c09e70..2ada5010 100644 --- a/app/src/main/java/chat/revolt/components/markdown/jbm/JBMRenderer.kt +++ b/app/src/main/java/chat/revolt/markdown/jbm/JBMRenderer.kt @@ -1,4 +1,4 @@ -package chat.revolt.components.markdown.jbm +package chat.revolt.markdown.jbm import android.content.Intent import android.content.res.Configuration @@ -67,6 +67,7 @@ import chat.revolt.R import chat.revolt.activities.InviteActivity import chat.revolt.api.REVOLT_FILES import chat.revolt.api.RevoltAPI +import chat.revolt.api.internals.isUlid import chat.revolt.api.routes.custom.fetchEmoji import chat.revolt.api.schemas.isInviteUri import chat.revolt.api.settings.LoadedSettings @@ -177,6 +178,86 @@ private fun annotateText( append(source) } + RSMElementTypes.USER_MENTION -> { + val contents = node.getTextInNode(sourceText).toString() + val userId = contents.removeSurrounding("<@", ">") + if (userId == contents || !userId.isUlid()) { + // Invalid user mention. Append as if it were regular text. + for (child in node.children) { + append(annotateText(state, child)) + } + } else { + // Now we're getting somewhere + pushStringAnnotation( + tag = JBMAnnotations.UserMention.tag, + annotation = userId + ) + pushStyle( + SpanStyle( + color = state.colors.clickable, + background = state.colors.clickableBackground + ) + ) + val member = state.currentServer?.let { serverId -> + RevoltAPI.members.getMember(serverId, userId) + } + val mentionDisplay = member?.nickname?.let { nick -> "@$nick" } + ?: RevoltAPI.userCache[userId]?.username?.let { username -> "@$username" } + ?: "<@$userId>" + append(mentionDisplay) + pop() + pop() + } + } + + RSMElementTypes.CHANNEL_MENTION -> { + val contents = node.getTextInNode(sourceText).toString() + val channelId = contents.removeSurrounding("<#", ">") + if (channelId == contents || !channelId.isUlid()) { + // Invalid channel mention. Append as if it were regular text. + for (child in node.children) { + append(annotateText(state, child)) + } + } else { + // Now we're getting somewhere + pushStringAnnotation( + tag = JBMAnnotations.ChannelMention.tag, + annotation = channelId + ) + pushStyle( + SpanStyle( + color = state.colors.clickable, + background = state.colors.clickableBackground + ) + ) + val channel = RevoltAPI.channelCache[channelId] + val mentionDisplay = channel?.name?.let { name -> "#$name" } + ?: "<#$channelId>" + append(mentionDisplay) + pop() + pop() + } + } + + RSMElementTypes.CUSTOM_EMOTE -> { + val contents = node.getTextInNode(sourceText).toString() + val emoteId = contents.removeSurrounding(":", ":") + if (emoteId == contents || !emoteId.isUlid()) { + // Invalid custom emote. Append as if it were regular text. + for (child in node.children) { + append(annotateText(state, child)) + } + } else { + // Now we're getting somewhere + pushStringAnnotation( + tag = JBMAnnotations.CustomEmote.tag, + annotation = emoteId + ) + appendInlineContent(JBMAnnotations.CustomEmote.tag, emoteId) + pop() + } + } + MarkdownTokenTypes.ATX_HEADER -> { // Do not need to do anything } @@ -443,6 +524,36 @@ private fun JBMText(node: ASTNode, modifier: Modifier) { return@handler true } + + JBMAnnotations.UserMention.tag -> { + scope.launch { + ActionChannel.send( + Action.OpenUserSheet( + item, + mdState.currentServer + ) + ) + } + return@handler true + } + + JBMAnnotations.ChannelMention.tag -> { + scope.launch { + ActionChannel.send( + Action.SwitchChannel(item) + ) + } + return@handler true + } + + JBMAnnotations.CustomEmote.tag -> { + scope.launch { + ActionChannel.send( + Action.EmoteInfo(item) + ) + } + return@handler true + } } } } @@ -464,47 +575,6 @@ private fun JBMText(node: ASTNode, modifier: Modifier) { return@handler true } - - annotatedText.getStringAnnotations( - tag = Annotations.UserMention.tag, - start = offset, - end = offset - ).firstOrNull()?.let { annotation -> - scope.launch { - ActionChannel.send( - Action.OpenUserSheet( - annotation.item, - mdState.currentServer - ) - ) - } - - return@handler true - } - - annotatedText.getStringAnnotations( - tag = Annotations.ChannelMention.tag, - start = offset, - end = offset - ).firstOrNull()?.let { annotation -> - scope.launch { - ActionChannel.send(Action.SwitchChannel(annotation.item)) - } - - return@handler true - } - - annotatedText.getStringAnnotations( - tag = Annotations.CustomEmote.tag, - start = offset, - end = offset - ).firstOrNull()?.let { annotation -> - scope.launch { - ActionChannel.send(Action.EmoteInfo(annotation.item)) - } - - return@handler true - } } return@handler false diff --git a/app/src/main/java/chat/revolt/markdown/jbm/RSMElementTypes.kt b/app/src/main/java/chat/revolt/markdown/jbm/RSMElementTypes.kt new file mode 100644 index 00000000..2ca4be1a --- /dev/null +++ b/app/src/main/java/chat/revolt/markdown/jbm/RSMElementTypes.kt @@ -0,0 +1,18 @@ +package chat.revolt.markdown.jbm + +import org.intellij.markdown.IElementType +import org.intellij.markdown.MarkdownElementType + +object RSMElementTypes { + @JvmField + val USER_MENTION: IElementType = MarkdownElementType("USER_MENTION") + + @JvmField + val CHANNEL_MENTION: IElementType = MarkdownElementType("CHANNEL_MENTION") + + @JvmField + val CUSTOM_EMOTE: IElementType = MarkdownElementType("EMOJI") + + @JvmField + val TIMESTAMP: IElementType = MarkdownElementType("TIMESTAMP") +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/markdown/jbm/RSMFlavourDescriptor.kt b/app/src/main/java/chat/revolt/markdown/jbm/RSMFlavourDescriptor.kt new file mode 100644 index 00000000..0a7d6ec4 --- /dev/null +++ b/app/src/main/java/chat/revolt/markdown/jbm/RSMFlavourDescriptor.kt @@ -0,0 +1,38 @@ +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.UserMentionParser +import org.intellij.markdown.MarkdownTokenTypes +import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor +import org.intellij.markdown.flavours.gfm.GFMTokenTypes +import org.intellij.markdown.flavours.gfm.StrikeThroughDelimiterParser +import org.intellij.markdown.parser.sequentialparsers.EmphasisLikeParser +import org.intellij.markdown.parser.sequentialparsers.SequentialParser +import org.intellij.markdown.parser.sequentialparsers.SequentialParserManager +import org.intellij.markdown.parser.sequentialparsers.impl.AutolinkParser +import org.intellij.markdown.parser.sequentialparsers.impl.BacktickParser +import org.intellij.markdown.parser.sequentialparsers.impl.EmphStrongDelimiterParser +import org.intellij.markdown.parser.sequentialparsers.impl.ImageParser +import org.intellij.markdown.parser.sequentialparsers.impl.InlineLinkParser +import org.intellij.markdown.parser.sequentialparsers.impl.MathParser +import org.intellij.markdown.parser.sequentialparsers.impl.ReferenceLinkParser + +class RSMFlavourDescriptor : GFMFlavourDescriptor() { + override val sequentialParserManager = object : SequentialParserManager() { + override fun getParserSequence(): List { + return listOf( + UserMentionParser(), + ChannelMentionParser(), + CustomEmoteParser(), + AutolinkParser(listOf(MarkdownTokenTypes.AUTOLINK, GFMTokenTypes.GFM_AUTOLINK)), + BacktickParser(), + MathParser(), + ImageParser(), + InlineLinkParser(), + ReferenceLinkParser(), + EmphasisLikeParser(EmphStrongDelimiterParser(), StrikeThroughDelimiterParser()) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/markdown/jbm/sequentialparsers/ChannelMentionParser.kt b/app/src/main/java/chat/revolt/markdown/jbm/sequentialparsers/ChannelMentionParser.kt new file mode 100644 index 00000000..b9a1ed14 --- /dev/null +++ b/app/src/main/java/chat/revolt/markdown/jbm/sequentialparsers/ChannelMentionParser.kt @@ -0,0 +1,40 @@ +package chat.revolt.markdown.jbm.sequentialparsers + +import chat.revolt.markdown.jbm.RSMElementTypes +import org.intellij.markdown.MarkdownTokenTypes +import org.intellij.markdown.parser.sequentialparsers.RangesListBuilder +import org.intellij.markdown.parser.sequentialparsers.SequentialParser +import org.intellij.markdown.parser.sequentialparsers.TokensCache + +class ChannelMentionParser : SequentialParser { + override fun parse( + tokens: TokensCache, + rangesToGlue: List + ): SequentialParser.ParsingResult { + val result = SequentialParser.ParsingResultBuilder() + val delegateIndices = RangesListBuilder() + var iterator: TokensCache.Iterator = tokens.RangesListIterator(rangesToGlue) + + while (iterator.type != null) { + if (iterator.type == MarkdownTokenTypes.LT && iterator.charLookup(1) == '#') { + val start = iterator.index + while (iterator.type != MarkdownTokenTypes.GT && iterator.type != null) { + iterator = iterator.advance() + } + if (iterator.type == MarkdownTokenTypes.GT) { + result.withNode( + SequentialParser.Node( + start..iterator.index + 1, + RSMElementTypes.USER_MENTION + ) + ) + } + } else { + delegateIndices.put(iterator.index) + } + iterator = iterator.advance() + } + + return result.withFurtherProcessing(delegateIndices.get()) + } +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/markdown/jbm/sequentialparsers/CustomEmoteParser.kt b/app/src/main/java/chat/revolt/markdown/jbm/sequentialparsers/CustomEmoteParser.kt new file mode 100644 index 00000000..95ac1eb8 --- /dev/null +++ b/app/src/main/java/chat/revolt/markdown/jbm/sequentialparsers/CustomEmoteParser.kt @@ -0,0 +1,52 @@ +package chat.revolt.markdown.jbm.sequentialparsers + +import chat.revolt.markdown.jbm.RSMElementTypes +import org.intellij.markdown.MarkdownTokenTypes +import org.intellij.markdown.parser.sequentialparsers.RangesListBuilder +import org.intellij.markdown.parser.sequentialparsers.SequentialParser +import org.intellij.markdown.parser.sequentialparsers.TokensCache + +class CustomEmoteParser : SequentialParser { + override fun parse( + tokens: TokensCache, + rangesToGlue: List + ): SequentialParser.ParsingResult { + val result = SequentialParser.ParsingResultBuilder() + val delegateIndices = RangesListBuilder() + var iterator: TokensCache.Iterator = tokens.RangesListIterator(rangesToGlue) + + while (iterator.type != null) { + if (iterator.type == MarkdownTokenTypes.COLON) { + + val endIterator = findNextColon(iterator.advance()) + + if (endIterator != null) { + result.withNode( + SequentialParser.Node( + iterator.index..endIterator.index + 1, + RSMElementTypes.CUSTOM_EMOTE + ) + ) + iterator = endIterator.advance() + continue + } + } + delegateIndices.put(iterator.index) + iterator = iterator.advance() + } + + return result.withFurtherProcessing(delegateIndices.get()) + } + + private fun findNextColon(it: TokensCache.Iterator): TokensCache.Iterator? { + var iterator = it + while (iterator.type != null) { + if (iterator.type == MarkdownTokenTypes.COLON) { + return iterator + } + + iterator = iterator.advance() + } + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/markdown/jbm/sequentialparsers/UserMentionParser.kt b/app/src/main/java/chat/revolt/markdown/jbm/sequentialparsers/UserMentionParser.kt new file mode 100644 index 00000000..7788b795 --- /dev/null +++ b/app/src/main/java/chat/revolt/markdown/jbm/sequentialparsers/UserMentionParser.kt @@ -0,0 +1,40 @@ +package chat.revolt.markdown.jbm.sequentialparsers + +import chat.revolt.markdown.jbm.RSMElementTypes +import org.intellij.markdown.MarkdownTokenTypes +import org.intellij.markdown.parser.sequentialparsers.RangesListBuilder +import org.intellij.markdown.parser.sequentialparsers.SequentialParser +import org.intellij.markdown.parser.sequentialparsers.TokensCache + +class UserMentionParser : SequentialParser { + override fun parse( + tokens: TokensCache, + rangesToGlue: List + ): SequentialParser.ParsingResult { + val result = SequentialParser.ParsingResultBuilder() + val delegateIndices = RangesListBuilder() + var iterator: TokensCache.Iterator = tokens.RangesListIterator(rangesToGlue) + + while (iterator.type != null) { + if (iterator.type == MarkdownTokenTypes.LT && iterator.charLookup(1) == '@') { + val start = iterator.index + while (iterator.type != MarkdownTokenTypes.GT && iterator.type != null) { + iterator = iterator.advance() + } + if (iterator.type == MarkdownTokenTypes.GT) { + result.withNode( + SequentialParser.Node( + start..iterator.index + 1, + RSMElementTypes.USER_MENTION + ) + ) + } + } else { + delegateIndices.put(iterator.index) + } + iterator = iterator.advance() + } + + return result.withFurtherProcessing(delegateIndices.get()) + } +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/screens/labs/ui/sandbox/JBMSandbox.kt b/app/src/main/java/chat/revolt/screens/labs/ui/sandbox/JBMSandbox.kt index 0c7dd7b1..6684d2d2 100644 --- a/app/src/main/java/chat/revolt/screens/labs/ui/sandbox/JBMSandbox.kt +++ b/app/src/main/java/chat/revolt/screens/labs/ui/sandbox/JBMSandbox.kt @@ -19,9 +19,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.navigation.NavController -import chat.revolt.components.markdown.jbm.JBM -import chat.revolt.components.markdown.jbm.JBMRenderer -import chat.revolt.components.markdown.jbm.LocalJBMarkdownTreeState +import chat.revolt.markdown.jbm.JBM +import chat.revolt.markdown.jbm.JBMRenderer +import chat.revolt.markdown.jbm.LocalJBMarkdownTreeState import chat.revolt.settings.dsl.SettingsPage @OptIn(JBM::class)