feat: support links in markdown
Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
parent
92a21296fe
commit
a2381fa005
|
|
@ -29,4 +29,13 @@ class Members {
|
||||||
fun clear() {
|
fun clear() {
|
||||||
memberCache.clear()
|
memberCache.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a Map of userId to server-nickname for the given serverId.
|
||||||
|
*/
|
||||||
|
fun markdownMemberMapFor(serverId: String): Map<String, String> {
|
||||||
|
return memberCache[serverId]?.mapNotNull { (userId, member) ->
|
||||||
|
member.nickname?.let { userId to member.nickname }
|
||||||
|
}?.toMap() ?: emptyMap()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ import android.net.Uri
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
|
import android.text.method.LinkMovementMethod
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
|
@ -293,6 +294,8 @@ fun Message(
|
||||||
textSize = 16f
|
textSize = 16f
|
||||||
typeface = ResourcesCompat.getFont(ctx, R.font.inter)
|
typeface = ResourcesCompat.getFont(ctx, R.font.inter)
|
||||||
|
|
||||||
|
movementMethod = LinkMovementMethod.getInstance()
|
||||||
|
|
||||||
setTextColor(contentColor.toArgb())
|
setTextColor(contentColor.toArgb())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package chat.revolt.components.generic
|
||||||
|
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
|
import android.text.method.LinkMovementMethod
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
|
@ -24,13 +25,10 @@ import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.core.content.res.ResourcesCompat
|
import androidx.core.content.res.ResourcesCompat
|
||||||
import chat.revolt.R
|
import chat.revolt.R
|
||||||
import chat.revolt.api.RevoltAPI
|
import chat.revolt.api.RevoltAPI
|
||||||
import chat.revolt.internals.markdown.ChannelMentionRule
|
|
||||||
import chat.revolt.internals.markdown.CustomEmoteRule
|
|
||||||
import chat.revolt.internals.markdown.MarkdownContext
|
import chat.revolt.internals.markdown.MarkdownContext
|
||||||
import chat.revolt.internals.markdown.MarkdownParser
|
import chat.revolt.internals.markdown.MarkdownParser
|
||||||
import chat.revolt.internals.markdown.MarkdownState
|
import chat.revolt.internals.markdown.MarkdownState
|
||||||
import chat.revolt.internals.markdown.TimestampRule
|
import chat.revolt.internals.markdown.addRevoltRules
|
||||||
import chat.revolt.internals.markdown.UserMentionRule
|
|
||||||
import chat.revolt.internals.markdown.createCodeRule
|
import chat.revolt.internals.markdown.createCodeRule
|
||||||
import chat.revolt.internals.markdown.createInlineCodeRule
|
import chat.revolt.internals.markdown.createInlineCodeRule
|
||||||
import com.discord.simpleast.core.simple.SimpleMarkdownRules
|
import com.discord.simpleast.core.simple.SimpleMarkdownRules
|
||||||
|
|
@ -58,12 +56,9 @@ fun UIMarkdown(
|
||||||
LaunchedEffect(text) {
|
LaunchedEffect(text) {
|
||||||
val parser = MarkdownParser()
|
val parser = MarkdownParser()
|
||||||
.addRules(
|
.addRules(
|
||||||
SimpleMarkdownRules.createEscapeRule(),
|
SimpleMarkdownRules.createEscapeRule()
|
||||||
UserMentionRule(),
|
|
||||||
ChannelMentionRule(),
|
|
||||||
CustomEmoteRule(),
|
|
||||||
TimestampRule(),
|
|
||||||
)
|
)
|
||||||
|
.addRevoltRules()
|
||||||
.addRules(
|
.addRules(
|
||||||
createCodeRule(context, codeBlockColor.toArgb()),
|
createCodeRule(context, codeBlockColor.toArgb()),
|
||||||
createInlineCodeRule(context, codeBlockColor.toArgb()),
|
createInlineCodeRule(context, codeBlockColor.toArgb()),
|
||||||
|
|
@ -102,6 +97,8 @@ fun UIMarkdown(
|
||||||
setMaxLines(maxLines)
|
setMaxLines(maxLines)
|
||||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, fontSize.value)
|
setTextSize(TypedValue.COMPLEX_UNIT_SP, fontSize.value)
|
||||||
|
|
||||||
|
movementMethod = LinkMovementMethod.getInstance()
|
||||||
|
|
||||||
layoutParams = ViewGroup.LayoutParams(
|
layoutParams = ViewGroup.LayoutParams(
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package chat.revolt.internals.markdown
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import android.text.TextPaint
|
||||||
|
import android.text.style.ClickableSpan
|
||||||
|
import android.view.View
|
||||||
|
import androidx.browser.customtabs.CustomTabsIntent
|
||||||
|
|
||||||
|
class LinkSpan(private val url: String) : ClickableSpan() {
|
||||||
|
override fun onClick(widget: View) {
|
||||||
|
val customTab = CustomTabsIntent.Builder()
|
||||||
|
.setShowTitle(true)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
customTab.launchUrl(widget.context, Uri.parse(url))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateDrawState(ds: TextPaint) {
|
||||||
|
ds.color = ds.linkColor
|
||||||
|
ds.isUnderlineText = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package chat.revolt.internals.markdown
|
package chat.revolt.internals.markdown
|
||||||
|
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
|
import android.text.Spanned
|
||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.discord.simpleast.core.node.Node
|
import com.discord.simpleast.core.node.Node
|
||||||
|
|
@ -101,4 +102,16 @@ class TimestampNode(private val timestamp: Long, private val modifier: String? =
|
||||||
builder.append("<invalid timestamp>")
|
builder.append("<invalid timestamp>")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LinkNode(val content: String, val url: String = content) : Node<MarkdownContext>() {
|
||||||
|
override fun render(builder: SpannableStringBuilder, renderContext: MarkdownContext) {
|
||||||
|
builder.append(content)
|
||||||
|
builder.setSpan(
|
||||||
|
LinkSpan(url),
|
||||||
|
builder.length - content.length,
|
||||||
|
builder.length,
|
||||||
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -67,6 +67,42 @@ class TimestampRule<S> :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const val RE_LINK =
|
||||||
|
"<?https?://(www\\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\\.[a-z]{2,4}\\b([-a-zA-Z0-9@:%_+.~#?&/=]*)>?"
|
||||||
|
|
||||||
|
class LinkRule<S> : Rule<MarkdownContext, Node<MarkdownContext>, S>(
|
||||||
|
Pattern.compile("^$RE_LINK")
|
||||||
|
) {
|
||||||
|
override fun parse(
|
||||||
|
matcher: Matcher,
|
||||||
|
parser: Parser<MarkdownContext, in Node<MarkdownContext>, S>,
|
||||||
|
state: S
|
||||||
|
): ParseSpec<MarkdownContext, S> {
|
||||||
|
val url = matcher.group(0)!!.trimStart('<').trimEnd('>')
|
||||||
|
return ParseSpec.createTerminal(
|
||||||
|
LinkNode(url),
|
||||||
|
state
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NamedLinkRule<S> : Rule<MarkdownContext, Node<MarkdownContext>, S>(
|
||||||
|
Pattern.compile("^\\[([^]]+)]\\(($RE_LINK)\\)")
|
||||||
|
) {
|
||||||
|
override fun parse(
|
||||||
|
matcher: Matcher,
|
||||||
|
parser: Parser<MarkdownContext, in Node<MarkdownContext>, S>,
|
||||||
|
state: S
|
||||||
|
): ParseSpec<MarkdownContext, S> {
|
||||||
|
val content = matcher.group(1)!!
|
||||||
|
val url = matcher.group(2)!!.trimStart('<').trimEnd('>')
|
||||||
|
return ParseSpec.createTerminal(
|
||||||
|
LinkNode(content, url),
|
||||||
|
state
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun <RC, S> createInlineCodeRule(context: Context, backgroundColor: Int): Rule<RC, Node<RC>, S> {
|
fun <RC, S> createInlineCodeRule(context: Context, backgroundColor: Int): Rule<RC, Node<RC>, S> {
|
||||||
return CodeRules.createInlineCodeRule(
|
return CodeRules.createInlineCodeRule(
|
||||||
{ listOf(TextAppearanceSpan(context, R.style.Code_TextAppearance)) },
|
{ listOf(TextAppearanceSpan(context, R.style.Code_TextAppearance)) },
|
||||||
|
|
@ -155,4 +191,15 @@ fun <RC> createCodeRule(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun MarkdownParser.addRevoltRules(): MarkdownParser {
|
||||||
|
return addRules(
|
||||||
|
UserMentionRule(),
|
||||||
|
ChannelMentionRule(),
|
||||||
|
CustomEmoteRule(),
|
||||||
|
TimestampRule(),
|
||||||
|
NamedLinkRule(),
|
||||||
|
LinkRule(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -77,13 +77,10 @@ import chat.revolt.components.screens.chat.AttachmentManager
|
||||||
import chat.revolt.components.screens.chat.ChannelHeader
|
import chat.revolt.components.screens.chat.ChannelHeader
|
||||||
import chat.revolt.components.screens.chat.ReplyManager
|
import chat.revolt.components.screens.chat.ReplyManager
|
||||||
import chat.revolt.components.screens.chat.TypingIndicator
|
import chat.revolt.components.screens.chat.TypingIndicator
|
||||||
import chat.revolt.internals.markdown.ChannelMentionRule
|
|
||||||
import chat.revolt.internals.markdown.CustomEmoteRule
|
|
||||||
import chat.revolt.internals.markdown.MarkdownContext
|
import chat.revolt.internals.markdown.MarkdownContext
|
||||||
import chat.revolt.internals.markdown.MarkdownParser
|
import chat.revolt.internals.markdown.MarkdownParser
|
||||||
import chat.revolt.internals.markdown.MarkdownState
|
import chat.revolt.internals.markdown.MarkdownState
|
||||||
import chat.revolt.internals.markdown.TimestampRule
|
import chat.revolt.internals.markdown.addRevoltRules
|
||||||
import chat.revolt.internals.markdown.UserMentionRule
|
|
||||||
import chat.revolt.internals.markdown.createCodeRule
|
import chat.revolt.internals.markdown.createCodeRule
|
||||||
import chat.revolt.internals.markdown.createInlineCodeRule
|
import chat.revolt.internals.markdown.createInlineCodeRule
|
||||||
import chat.revolt.sheets.ChannelInfoSheet
|
import chat.revolt.sheets.ChannelInfoSheet
|
||||||
|
|
@ -300,11 +297,8 @@ fun ChannelScreen(
|
||||||
val parser = MarkdownParser()
|
val parser = MarkdownParser()
|
||||||
.addRules(
|
.addRules(
|
||||||
SimpleMarkdownRules.createEscapeRule(),
|
SimpleMarkdownRules.createEscapeRule(),
|
||||||
UserMentionRule(),
|
|
||||||
ChannelMentionRule(),
|
|
||||||
CustomEmoteRule(),
|
|
||||||
TimestampRule(),
|
|
||||||
)
|
)
|
||||||
|
.addRevoltRules()
|
||||||
.addRules(
|
.addRules(
|
||||||
createCodeRule(context, codeBlockColor.toArgb()),
|
createCodeRule(context, codeBlockColor.toArgb()),
|
||||||
createInlineCodeRule(context, codeBlockColor.toArgb()),
|
createInlineCodeRule(context, codeBlockColor.toArgb()),
|
||||||
|
|
@ -320,7 +314,11 @@ fun ChannelScreen(
|
||||||
parser = parser,
|
parser = parser,
|
||||||
initialState = MarkdownState(0),
|
initialState = MarkdownState(0),
|
||||||
renderContext = MarkdownContext(
|
renderContext = MarkdownContext(
|
||||||
memberMap = mapOf(),
|
memberMap = viewModel.activeChannel?.server?.let { serverId ->
|
||||||
|
RevoltAPI.members.markdownMemberMapFor(
|
||||||
|
serverId
|
||||||
|
)
|
||||||
|
} ?: mapOf(),
|
||||||
userMap = RevoltAPI.userCache.toMap(),
|
userMap = RevoltAPI.userCache.toMap(),
|
||||||
channelMap = RevoltAPI.channelCache.mapValues { ch ->
|
channelMap = RevoltAPI.channelCache.mapValues { ch ->
|
||||||
ch.value.name ?: ch.value.id ?: "#DeletedChannel"
|
ch.value.name ?: ch.value.id ?: "#DeletedChannel"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue