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() {
|
||||
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.TextUtils
|
||||
import android.text.format.DateUtils
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
|
|
@ -293,6 +294,8 @@ fun Message(
|
|||
textSize = 16f
|
||||
typeface = ResourcesCompat.getFont(ctx, R.font.inter)
|
||||
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
|
||||
setTextColor(contentColor.toArgb())
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package chat.revolt.components.generic
|
|||
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.TextUtils
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import android.view.ViewGroup
|
||||
|
|
@ -24,13 +25,10 @@ import androidx.compose.ui.viewinterop.AndroidView
|
|||
import androidx.core.content.res.ResourcesCompat
|
||||
import chat.revolt.R
|
||||
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.MarkdownParser
|
||||
import chat.revolt.internals.markdown.MarkdownState
|
||||
import chat.revolt.internals.markdown.TimestampRule
|
||||
import chat.revolt.internals.markdown.UserMentionRule
|
||||
import chat.revolt.internals.markdown.addRevoltRules
|
||||
import chat.revolt.internals.markdown.createCodeRule
|
||||
import chat.revolt.internals.markdown.createInlineCodeRule
|
||||
import com.discord.simpleast.core.simple.SimpleMarkdownRules
|
||||
|
|
@ -58,12 +56,9 @@ fun UIMarkdown(
|
|||
LaunchedEffect(text) {
|
||||
val parser = MarkdownParser()
|
||||
.addRules(
|
||||
SimpleMarkdownRules.createEscapeRule(),
|
||||
UserMentionRule(),
|
||||
ChannelMentionRule(),
|
||||
CustomEmoteRule(),
|
||||
TimestampRule(),
|
||||
SimpleMarkdownRules.createEscapeRule()
|
||||
)
|
||||
.addRevoltRules()
|
||||
.addRules(
|
||||
createCodeRule(context, codeBlockColor.toArgb()),
|
||||
createInlineCodeRule(context, codeBlockColor.toArgb()),
|
||||
|
|
@ -102,6 +97,8 @@ fun UIMarkdown(
|
|||
setMaxLines(maxLines)
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, fontSize.value)
|
||||
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
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
|
||||
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import android.text.format.DateUtils
|
||||
import android.util.Log
|
||||
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>")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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> {
|
||||
return CodeRules.createInlineCodeRule(
|
||||
{ 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.ReplyManager
|
||||
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.MarkdownParser
|
||||
import chat.revolt.internals.markdown.MarkdownState
|
||||
import chat.revolt.internals.markdown.TimestampRule
|
||||
import chat.revolt.internals.markdown.UserMentionRule
|
||||
import chat.revolt.internals.markdown.addRevoltRules
|
||||
import chat.revolt.internals.markdown.createCodeRule
|
||||
import chat.revolt.internals.markdown.createInlineCodeRule
|
||||
import chat.revolt.sheets.ChannelInfoSheet
|
||||
|
|
@ -300,11 +297,8 @@ fun ChannelScreen(
|
|||
val parser = MarkdownParser()
|
||||
.addRules(
|
||||
SimpleMarkdownRules.createEscapeRule(),
|
||||
UserMentionRule(),
|
||||
ChannelMentionRule(),
|
||||
CustomEmoteRule(),
|
||||
TimestampRule(),
|
||||
)
|
||||
.addRevoltRules()
|
||||
.addRules(
|
||||
createCodeRule(context, codeBlockColor.toArgb()),
|
||||
createInlineCodeRule(context, codeBlockColor.toArgb()),
|
||||
|
|
@ -320,7 +314,11 @@ fun ChannelScreen(
|
|||
parser = parser,
|
||||
initialState = MarkdownState(0),
|
||||
renderContext = MarkdownContext(
|
||||
memberMap = mapOf(),
|
||||
memberMap = viewModel.activeChannel?.server?.let { serverId ->
|
||||
RevoltAPI.members.markdownMemberMapFor(
|
||||
serverId
|
||||
)
|
||||
} ?: mapOf(),
|
||||
userMap = RevoltAPI.userCache.toMap(),
|
||||
channelMap = RevoltAPI.channelCache.mapValues { ch ->
|
||||
ch.value.name ?: ch.value.id ?: "#DeletedChannel"
|
||||
|
|
|
|||
Loading…
Reference in New Issue