feat: use an actual markdown rendering library
it even highlights code blocks
This commit is contained in:
parent
5116357e37
commit
a43f68b33e
|
|
@ -13,7 +13,6 @@
|
|||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
<option value="$PROJECT_DIR$/markdown" />
|
||||
</set>
|
||||
</option>
|
||||
</GradleProjectSettings>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
<component name="FrameworkDetectionExcludesConfiguration">
|
||||
<file type="web" url="file://$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
|
|
|
|||
|
|
@ -163,7 +163,8 @@ dependencies {
|
|||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.2'
|
||||
|
||||
// Markdown
|
||||
implementation project(':markdown')
|
||||
implementation "com.github.discord:SimpleAST:2.7.0"
|
||||
implementation "androidx.appcompat:appcompat:1.7.0-alpha02"
|
||||
}
|
||||
|
||||
kapt {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package chat.revolt.components.chat
|
||||
|
||||
import android.net.Uri
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.TextUtils
|
||||
import android.widget.Toast
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.compose.foundation.*
|
||||
|
|
@ -9,11 +11,15 @@ import androidx.compose.material3.*
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import chat.revolt.R
|
||||
import chat.revolt.api.REVOLT_FILES
|
||||
import chat.revolt.api.RevoltAPI
|
||||
import chat.revolt.api.asJanuaryProxyUrl
|
||||
|
|
@ -22,7 +28,6 @@ import chat.revolt.api.internals.WebCompat
|
|||
import chat.revolt.api.schemas.AutumnResource
|
||||
import chat.revolt.components.generic.UserAvatar
|
||||
import chat.revolt.components.generic.UserAvatarWidthPlaceholder
|
||||
import chat.revolt.markdown.Markdown
|
||||
import chat.revolt.api.schemas.Message as MessageSchema
|
||||
|
||||
fun viewAttachmentInBrowser(ctx: android.content.Context, attachment: AutumnResource) {
|
||||
|
|
@ -49,10 +54,12 @@ fun formatLongAsTime(time: Long): String {
|
|||
fun Message(
|
||||
message: MessageSchema,
|
||||
truncate: Boolean = false,
|
||||
parse: (MessageSchema) -> SpannableStringBuilder = { SpannableStringBuilder(it.content) },
|
||||
onMessageContextMenu: () -> Unit = {},
|
||||
) {
|
||||
val author = RevoltAPI.userCache[message.author] ?: return CircularProgressIndicator()
|
||||
val context = LocalContext.current
|
||||
val contentColor = LocalContentColor.current
|
||||
|
||||
Column {
|
||||
if (message.tail == false) {
|
||||
|
|
@ -134,11 +141,17 @@ fun Message(
|
|||
message.content?.let {
|
||||
if (message.content.isBlank()) return@let // if only an attachment is sent
|
||||
|
||||
Text(
|
||||
text = Markdown.annotate(it),
|
||||
maxLines = if (truncate) 1 else Int.MAX_VALUE,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
AndroidView(factory = { ctx ->
|
||||
androidx.appcompat.widget.AppCompatTextView(ctx).apply {
|
||||
text = parse(message)
|
||||
maxLines = if (truncate) 1 else Int.MAX_VALUE
|
||||
ellipsize = TextUtils.TruncateAt.END
|
||||
textSize = 16f
|
||||
typeface = ResourcesCompat.getFont(ctx, R.font.inter)
|
||||
|
||||
setTextColor(contentColor.toArgb())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
message.attachments?.let {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,118 @@
|
|||
package chat.revolt.components.generic
|
||||
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
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.UserMentionRule
|
||||
import chat.revolt.internals.markdown.createCodeRule
|
||||
import chat.revolt.internals.markdown.createInlineCodeRule
|
||||
import com.discord.simpleast.core.simple.SimpleMarkdownRules
|
||||
import com.discord.simpleast.core.simple.SimpleRenderer
|
||||
|
||||
/**
|
||||
* A Markdown rendering component for Markdown embedded in UI (e.g. in a button).
|
||||
* @param text The text to render.
|
||||
* @param fontSize The font size to use.
|
||||
* @param modifier The modifier to apply to the rendered text. Will be applied to AndroidView and thus subject to AndroidView's limitations.
|
||||
* @param maxLines The maximum number of lines to display. Text will always be ellipsized on overflow. Defaults to [Int.MAX_VALUE].
|
||||
*/
|
||||
@Composable
|
||||
fun UIMarkdown(
|
||||
text: String,
|
||||
fontSize: TextUnit,
|
||||
modifier: Modifier = Modifier,
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val foregroundColor = LocalContentColor.current
|
||||
val codeBlockColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
||||
val spannableStringBuilder = remember { mutableStateOf(SpannableStringBuilder()) }
|
||||
|
||||
LaunchedEffect(text) {
|
||||
val parser = MarkdownParser()
|
||||
.addRules(
|
||||
SimpleMarkdownRules.createEscapeRule(),
|
||||
UserMentionRule(),
|
||||
ChannelMentionRule(),
|
||||
CustomEmoteRule(),
|
||||
)
|
||||
.addRules(
|
||||
createCodeRule(context, codeBlockColor.toArgb()),
|
||||
createInlineCodeRule(context, codeBlockColor.toArgb()),
|
||||
)
|
||||
.addRules(
|
||||
SimpleMarkdownRules.createSimpleMarkdownRules(
|
||||
includeEscapeRule = false
|
||||
)
|
||||
)
|
||||
|
||||
spannableStringBuilder.value = SimpleRenderer.render(
|
||||
source = text,
|
||||
parser = parser,
|
||||
initialState = MarkdownState(0),
|
||||
renderContext = MarkdownContext(
|
||||
memberMap = mapOf(),
|
||||
userMap = RevoltAPI.userCache.toMap(),
|
||||
channelMap = RevoltAPI.channelCache.mapValues { ch ->
|
||||
ch.value.name ?: ch.value.id!!
|
||||
},
|
||||
emojiMap = RevoltAPI.emojiCache,
|
||||
serverId = null
|
||||
)
|
||||
)
|
||||
|
||||
Log.d("Markdown", "Rendered: ${spannableStringBuilder.value}")
|
||||
}
|
||||
|
||||
AndroidView(
|
||||
factory = {
|
||||
androidx.appcompat.widget.AppCompatTextView(it).apply {
|
||||
ellipsize = TextUtils.TruncateAt.END
|
||||
typeface = ResourcesCompat.getFont(it, R.font.inter)
|
||||
|
||||
setTextColor(foregroundColor.toArgb())
|
||||
setMaxLines(maxLines)
|
||||
setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, fontSize.value)
|
||||
|
||||
setText(spannableStringBuilder.value)
|
||||
}
|
||||
},
|
||||
modifier = modifier,
|
||||
update = {
|
||||
it.text = spannableStringBuilder.value
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun UIMarkdownPreview() {
|
||||
// Will not render in side preview but will render on device
|
||||
UIMarkdown(
|
||||
text = "Hello, **world**!",
|
||||
fontSize = 16.sp,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
package chat.revolt.internals.markdown
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.RectF
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import android.text.style.LeadingMarginSpan
|
||||
import android.text.style.LineBackgroundSpan
|
||||
import androidx.annotation.ColorInt
|
||||
import com.discord.simpleast.core.node.Node
|
||||
|
||||
// Attribution:
|
||||
// https://github.com/discord/SimpleAST/blob/567b61c51056cbdec39e839100690c576c26a4c6/app/src/main/java/com/discord/simpleast/sample/spans/BlockBackgroundNode.kt
|
||||
// LICENSED UNDER THE APACHE LICENSE, VERSION 2.0
|
||||
// Adapted for Revolt.
|
||||
|
||||
/**
|
||||
* Creates a block background for code sections.
|
||||
*/
|
||||
class BlockBackgroundNode<R>(
|
||||
private val quoteDepth: Int,
|
||||
private val fillColor: Int = Color.DKGRAY,
|
||||
private val strokeColor: Int = Color.BLACK,
|
||||
vararg children: Node<R>,
|
||||
) : Node.Parent<R>(*children) {
|
||||
|
||||
override fun render(builder: SpannableStringBuilder, renderContext: R) {
|
||||
// Ensure the block we want to append starts on a newline.
|
||||
ensureEndsWithNewline(builder)
|
||||
|
||||
val codeStartIndex = builder.length
|
||||
super.render(builder, renderContext)
|
||||
// BlockBackgroundSpan requires this to function
|
||||
ensureEndsWithNewline(builder)
|
||||
|
||||
val backgroundSpan = BlockBackgroundSpan(
|
||||
fillColor, strokeColor,
|
||||
strokeWidth = 2,
|
||||
strokeRadius = 15,
|
||||
leftMargin = 40 * quoteDepth
|
||||
)
|
||||
builder.setSpan(
|
||||
backgroundSpan,
|
||||
codeStartIndex,
|
||||
builder.length,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
|
||||
// Apply a leading margin to all lines in the block.
|
||||
val leadingMarginSpan = LeadingMarginSpan.Standard(15)
|
||||
builder.setSpan(
|
||||
leadingMarginSpan,
|
||||
codeStartIndex,
|
||||
builder.length,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
|
||||
private fun ensureEndsWithNewline(builder: SpannableStringBuilder) {
|
||||
if (builder.isNotEmpty()) {
|
||||
val lastChar = CharArray(6)
|
||||
builder.getChars(builder.length - 1, builder.length, lastChar, 0)
|
||||
if (lastChar[0] != '\n') {
|
||||
builder.append('\n')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the position of the paragraph on the screen and draws the desired background.
|
||||
*/
|
||||
class BlockBackgroundSpan(
|
||||
@ColorInt fillColor: Int,
|
||||
@ColorInt strokeColor: Int,
|
||||
strokeWidth: Int,
|
||||
strokeRadius: Int,
|
||||
val leftMargin: Int
|
||||
) : LineBackgroundSpan {
|
||||
|
||||
private val fillPaint = Paint().apply {
|
||||
this.style = Paint.Style.FILL
|
||||
this.color = fillColor
|
||||
}
|
||||
|
||||
private val strokePaint = Paint().apply {
|
||||
this.style = Paint.Style.STROKE
|
||||
this.color = strokeColor
|
||||
this.strokeWidth = strokeWidth.toFloat()
|
||||
this.isAntiAlias = true
|
||||
}
|
||||
|
||||
private val rect = RectF()
|
||||
private val radius = strokeRadius.toFloat()
|
||||
|
||||
fun draw(canvas: Canvas) {
|
||||
canvas.drawRoundRect(rect, radius, radius, fillPaint)
|
||||
canvas.drawRoundRect(rect, radius, radius, strokePaint)
|
||||
}
|
||||
|
||||
override fun drawBackground(
|
||||
canvas: Canvas,
|
||||
paint: Paint,
|
||||
left: Int,
|
||||
right: Int,
|
||||
top: Int,
|
||||
baseline: Int,
|
||||
bottom: Int,
|
||||
text: CharSequence,
|
||||
start: Int,
|
||||
end: Int,
|
||||
lnum: Int
|
||||
) {
|
||||
if (text !is Spanned) return
|
||||
|
||||
if (text.getSpanStart(this) == start) {
|
||||
rect.left = left.toFloat() + leftMargin
|
||||
rect.top = top.toFloat()
|
||||
}
|
||||
|
||||
if (text.getSpanEnd(this) == end) {
|
||||
rect.right = right.toFloat()
|
||||
rect.bottom = bottom.toFloat()
|
||||
draw(canvas)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,20 @@
|
|||
package chat.revolt.internals.markdown
|
||||
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
||||
import chat.revolt.api.schemas.Emoji
|
||||
import chat.revolt.api.schemas.User
|
||||
import com.discord.simpleast.core.node.Node
|
||||
import com.discord.simpleast.core.parser.Parser
|
||||
|
||||
typealias MarkdownParser = Parser<MarkdownContext, Node<MarkdownContext>, MarkdownState>
|
||||
|
||||
data class MarkdownState(val currentQuoteDepth: Int) {
|
||||
fun newQuoteDepth(depth: Int): MarkdownState = MarkdownState(depth)
|
||||
}
|
||||
|
||||
data class MarkdownContext(
|
||||
val memberMap: SnapshotStateMap<String, String>,
|
||||
val userMap: SnapshotStateMap<String, User>,
|
||||
val channelMap: SnapshotStateMap<String, String>,
|
||||
val serverId: String,
|
||||
val memberMap: Map<String, String>,
|
||||
val userMap: Map<String, User>,
|
||||
val channelMap: Map<String, String>,
|
||||
val emojiMap: Map<String, Emoji>,
|
||||
val serverId: String?
|
||||
)
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package chat.revolt.internals.markdown
|
||||
|
||||
import android.text.SpannableStringBuilder
|
||||
import com.discord.simpleast.core.node.Node
|
||||
|
||||
class UserMentionNode(private val userId: String) : Node<MarkdownContext>() {
|
||||
override fun render(builder: SpannableStringBuilder, renderContext: MarkdownContext) {
|
||||
builder.append(
|
||||
renderContext.memberMap[userId]?.let { "@$it" }
|
||||
?: renderContext.userMap[userId]?.let { "@${it.username}" }
|
||||
?: "<@${userId}>"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class ChannelMentionNode(private val channelId: String) : Node<MarkdownContext>() {
|
||||
override fun render(builder: SpannableStringBuilder, renderContext: MarkdownContext) {
|
||||
builder.append(
|
||||
renderContext.channelMap[channelId]?.let { "#$it" }
|
||||
?: "<#${channelId}>"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class CustomEmoteNode(private val emoteId: String) : Node<MarkdownContext>() {
|
||||
override fun render(builder: SpannableStringBuilder, renderContext: MarkdownContext) {
|
||||
builder.append(
|
||||
renderContext.emojiMap[emoteId]?.let { ":${it.name}:" }
|
||||
?: ":${emoteId}:"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
package chat.revolt.internals.markdown
|
||||
|
||||
import android.content.Context
|
||||
import android.text.style.BackgroundColorSpan
|
||||
import android.text.style.TextAppearanceSpan
|
||||
import chat.revolt.R
|
||||
import com.discord.simpleast.code.CodeRules
|
||||
import com.discord.simpleast.code.CodeStyleProviders
|
||||
import com.discord.simpleast.core.node.Node
|
||||
import com.discord.simpleast.core.node.StyleNode
|
||||
import com.discord.simpleast.core.parser.ParseSpec
|
||||
import com.discord.simpleast.core.parser.Parser
|
||||
import com.discord.simpleast.core.parser.Rule
|
||||
import java.util.regex.Matcher
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class UserMentionRule<S> :
|
||||
Rule<MarkdownContext, UserMentionNode, S>(Pattern.compile("^<@([0-9A-Z]{26})>")) {
|
||||
override fun parse(
|
||||
matcher: Matcher,
|
||||
parser: Parser<MarkdownContext, in UserMentionNode, S>,
|
||||
state: S
|
||||
): ParseSpec<MarkdownContext, S> {
|
||||
return ParseSpec.createTerminal(UserMentionNode(matcher.group(1)!!), state)
|
||||
}
|
||||
}
|
||||
|
||||
class ChannelMentionRule<S> :
|
||||
Rule<MarkdownContext, ChannelMentionNode, S>(Pattern.compile("^<#([0-9A-Z]{26})>")) {
|
||||
override fun parse(
|
||||
matcher: Matcher,
|
||||
parser: Parser<MarkdownContext, in ChannelMentionNode, S>,
|
||||
state: S
|
||||
): ParseSpec<MarkdownContext, S> {
|
||||
return ParseSpec.createTerminal(ChannelMentionNode(matcher.group(1)!!), state)
|
||||
}
|
||||
}
|
||||
|
||||
class CustomEmoteRule<S> :
|
||||
Rule<MarkdownContext, CustomEmoteNode, S>(Pattern.compile("^:([0-9A-Z]{26}):")) {
|
||||
override fun parse(
|
||||
matcher: Matcher,
|
||||
parser: Parser<MarkdownContext, in CustomEmoteNode, S>,
|
||||
state: S
|
||||
): ParseSpec<MarkdownContext, S> {
|
||||
return ParseSpec.createTerminal(CustomEmoteNode(matcher.group(1)!!), state)
|
||||
}
|
||||
}
|
||||
|
||||
fun <RC, S> createInlineCodeRule(context: Context, backgroundColor: Int): Rule<RC, Node<RC>, S> {
|
||||
return CodeRules.createInlineCodeRule(
|
||||
{ listOf(TextAppearanceSpan(context, R.style.Code_TextAppearance)) },
|
||||
{ listOf(BackgroundColorSpan(backgroundColor)) }
|
||||
)
|
||||
}
|
||||
|
||||
fun <RC, S : MarkdownState> createCodeRule(
|
||||
context: Context,
|
||||
backgroundColor: Int
|
||||
): Rule<RC, Node<RC>, S> {
|
||||
val codeStyleProviders = CodeStyleProviders<RC>(
|
||||
defaultStyleProvider = { listOf(TextAppearanceSpan(context, R.style.Code_TextAppearance)) },
|
||||
commentStyleProvider = {
|
||||
listOf(
|
||||
TextAppearanceSpan(
|
||||
context,
|
||||
R.style.Code_TextAppearance_Comment
|
||||
)
|
||||
)
|
||||
},
|
||||
literalStyleProvider = {
|
||||
listOf(
|
||||
TextAppearanceSpan(
|
||||
context,
|
||||
R.style.Code_TextAppearance_Literal
|
||||
)
|
||||
)
|
||||
},
|
||||
keywordStyleProvider = {
|
||||
listOf(
|
||||
TextAppearanceSpan(
|
||||
context,
|
||||
R.style.Code_TextAppearance_Keyword
|
||||
)
|
||||
)
|
||||
},
|
||||
identifierStyleProvider = {
|
||||
listOf(
|
||||
TextAppearanceSpan(
|
||||
context,
|
||||
R.style.Code_TextAppearance_Identifier
|
||||
)
|
||||
)
|
||||
},
|
||||
typesStyleProvider = {
|
||||
listOf(
|
||||
TextAppearanceSpan(
|
||||
context,
|
||||
R.style.Code_TextAppearance_Types
|
||||
)
|
||||
)
|
||||
},
|
||||
genericsStyleProvider = {
|
||||
listOf(
|
||||
TextAppearanceSpan(
|
||||
context,
|
||||
R.style.Code_TextAppearance_Generics
|
||||
)
|
||||
)
|
||||
},
|
||||
paramsStyleProvider = {
|
||||
listOf(
|
||||
TextAppearanceSpan(
|
||||
context,
|
||||
R.style.Code_TextAppearance_Params
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
||||
val languageMap = CodeRules.createCodeLanguageMap<RC, S>(codeStyleProviders)
|
||||
|
||||
return CodeRules.createCodeRule(
|
||||
codeStyleProviders.defaultStyleProvider,
|
||||
languageMap
|
||||
) { codeNode, block, state ->
|
||||
if (!block) {
|
||||
StyleNode<RC, Any>(listOf(BackgroundColorSpan(backgroundColor)))
|
||||
.apply { addChild(codeNode) }
|
||||
} else {
|
||||
BlockBackgroundNode(
|
||||
state.currentQuoteDepth,
|
||||
backgroundColor,
|
||||
backgroundColor,
|
||||
codeNode
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -28,7 +28,6 @@ import chat.revolt.api.routes.user.blockUser
|
|||
import chat.revolt.api.schemas.ContentReportReason
|
||||
import chat.revolt.components.chat.Message
|
||||
import chat.revolt.components.generic.FormTextField
|
||||
import chat.revolt.markdown.Markdown
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
enum class ReportingState {
|
||||
|
|
@ -112,7 +111,7 @@ fun ReportMessageDialog(
|
|||
if (messageIsBridged) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = Markdown.annotate(stringResource(id = R.string.report_message_bridge_notice)),
|
||||
text = stringResource(id = R.string.report_message_bridge_notice),
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import androidx.compose.runtime.*
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
|
|
@ -55,6 +56,16 @@ import chat.revolt.components.screens.chat.AttachmentManager
|
|||
import chat.revolt.components.screens.chat.ChannelIcon
|
||||
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.UserMentionRule
|
||||
import chat.revolt.internals.markdown.createCodeRule
|
||||
import chat.revolt.internals.markdown.createInlineCodeRule
|
||||
import com.discord.simpleast.core.simple.SimpleMarkdownRules
|
||||
import com.discord.simpleast.core.simple.SimpleRenderer
|
||||
import io.ktor.http.*
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
|
|
@ -401,6 +412,8 @@ fun ChannelScreen(
|
|||
val lazyListState = rememberLazyListState()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val codeBlockColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
||||
|
||||
val pickFileLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.OpenMultipleDocuments()
|
||||
) { uriList ->
|
||||
|
|
@ -520,7 +533,39 @@ fun ChannelScreen(
|
|||
items = viewModel.renderableMessages,
|
||||
key = { it.id!! }
|
||||
) { message ->
|
||||
Message(message) {
|
||||
Message(message, parse = {
|
||||
val parser = MarkdownParser()
|
||||
.addRules(
|
||||
SimpleMarkdownRules.createEscapeRule(),
|
||||
UserMentionRule(),
|
||||
ChannelMentionRule(),
|
||||
CustomEmoteRule(),
|
||||
)
|
||||
.addRules(
|
||||
createCodeRule(context, codeBlockColor.toArgb()),
|
||||
createInlineCodeRule(context, codeBlockColor.toArgb()),
|
||||
)
|
||||
.addRules(
|
||||
SimpleMarkdownRules.createSimpleMarkdownRules(
|
||||
includeEscapeRule = false
|
||||
)
|
||||
)
|
||||
|
||||
SimpleRenderer.render(
|
||||
source = it.content ?: "",
|
||||
parser = parser,
|
||||
initialState = MarkdownState(0),
|
||||
renderContext = MarkdownContext(
|
||||
memberMap = mapOf(),
|
||||
userMap = RevoltAPI.userCache.toMap(),
|
||||
channelMap = RevoltAPI.channelCache.mapValues { ch ->
|
||||
ch.value.name ?: ch.value.id!!
|
||||
},
|
||||
emojiMap = RevoltAPI.emojiCache,
|
||||
serverId = channel.server ?: "",
|
||||
)
|
||||
)
|
||||
}) {
|
||||
navController.navigate("message/${message.id}/menu")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<font-family xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<font
|
||||
app:font="@font/inter_thin"
|
||||
app:fontStyle="normal"
|
||||
app:fontWeight="100" />
|
||||
<font
|
||||
app:font="@font/inter_extralight"
|
||||
app:fontStyle="normal"
|
||||
app:fontWeight="200" />
|
||||
<font
|
||||
app:font="@font/inter_light"
|
||||
app:fontStyle="normal"
|
||||
app:fontWeight="300" />
|
||||
<font
|
||||
app:font="@font/inter_regular"
|
||||
app:fontStyle="normal"
|
||||
app:fontWeight="400" />
|
||||
<font
|
||||
app:font="@font/inter_medium"
|
||||
app:fontStyle="normal"
|
||||
app:fontWeight="500" />
|
||||
<font
|
||||
app:font="@font/inter_semibold"
|
||||
app:fontStyle="normal"
|
||||
app:fontWeight="600" />
|
||||
<font
|
||||
app:font="@font/inter_bold"
|
||||
app:fontStyle="normal"
|
||||
app:fontWeight="700" />
|
||||
<font
|
||||
app:font="@font/inter_extrabold"
|
||||
app:fontStyle="normal"
|
||||
app:fontWeight="800" />
|
||||
<font
|
||||
app:font="@font/inter_black"
|
||||
app:fontStyle="normal"
|
||||
app:fontWeight="900" />
|
||||
</font-family>
|
||||
Binary file not shown.
|
|
@ -155,7 +155,7 @@
|
|||
|
||||
<string name="report_message">Thank you for taking the time to report this message. Please provide a reason for reporting this message.</string>
|
||||
<string name="report_message_preview">Selected message:</string>
|
||||
<string name="report_message_bridge_notice">**Note:** This message may have been sent from another platform. It is recommended to also report the message on the platform it was sent from.</string>
|
||||
<string name="report_message_bridge_notice">Note: This message may have been sent from another platform. It is recommended to also report the message on the platform it was sent from.</string>
|
||||
<string name="report_server">Thank you for taking the time to report this server. Please provide a reason for reporting this server.</string>
|
||||
<string name="report_server_preview">Selected server:</string>
|
||||
<string name="report_user">Thank you for taking the time to report this user. Please provide a reason for reporting this user.</string>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="Code.TextAppearance" parent="">
|
||||
<item name="android:textSize">14sp</item>
|
||||
<item name="android:textColor">#FFFFFF</item>
|
||||
<item name="android:fontFamily">@font/jetbrainsmono_regular</item>
|
||||
</style>
|
||||
|
||||
<style name="Code.TextAppearance.Comment" parent="">
|
||||
<item name="android:textColor">#607d8b</item>
|
||||
</style>
|
||||
|
||||
<style name="Code.TextAppearance.Literal" parent="">
|
||||
<item name="android:textColor">#e91e63</item>
|
||||
</style>
|
||||
|
||||
<style name="Code.TextAppearance.Keyword" parent="">
|
||||
<item name="android:textColor">#2196f3</item>
|
||||
<item name="android:textStyle">bold</item>
|
||||
</style>
|
||||
|
||||
<style name="Code.TextAppearance.Identifier" parent="">
|
||||
<item name="android:textColor">#cddc39</item>
|
||||
</style>
|
||||
|
||||
<style name="Code.TextAppearance.Types" parent="">
|
||||
<item name="android:textColor">#00e676</item>
|
||||
</style>
|
||||
|
||||
<style name="Code.TextAppearance.Generics" parent="">
|
||||
<item name="android:textColor">#ff5722</item>
|
||||
</style>
|
||||
|
||||
<style name="Code.TextAppearance.Params" parent="">
|
||||
<item name="android:textColor">#009688</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="Theme.Revolt" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
<style name="Theme.Revolt" parent="Theme.AppCompat.DayNight.NoActionBar" />
|
||||
</resources>
|
||||
|
|
@ -1 +0,0 @@
|
|||
/build
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
plugins {
|
||||
id 'com.android.library'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
}
|
||||
|
||||
android {
|
||||
namespace 'chat.revolt.markdown'
|
||||
compileSdk 33
|
||||
|
||||
defaultConfig {
|
||||
minSdk 23
|
||||
targetSdk 33
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles "consumer-rules.pro"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
buildFeatures {
|
||||
compose true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion compose_version
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.core:core-ktx:1.9.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'com.google.android.material:material:1.8.0'
|
||||
|
||||
implementation platform("androidx.compose:compose-bom:$compose_bom_version")
|
||||
implementation "androidx.compose.ui:ui"
|
||||
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
package chat.revolt.markdown
|
||||
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
|
||||
object Markdown {
|
||||
fun <Ctx> annotateInContext(text: String, context: Ctx): AnnotatedString {
|
||||
// TODO this is all placeholder code
|
||||
val boldRegex = Regex("\\*\\*(.*?)\\*\\*")
|
||||
return buildAnnotatedString {
|
||||
append(text)
|
||||
|
||||
boldRegex.findAll(text).forEach { match ->
|
||||
addStyle(
|
||||
style = SpanStyle(fontWeight = FontWeight.Bold),
|
||||
start = match.groups[1]!!.range.first,
|
||||
end = match.groups[1]!!.range.last + 1
|
||||
)
|
||||
}
|
||||
|
||||
toAnnotatedString()
|
||||
}
|
||||
}
|
||||
|
||||
fun annotate(text: String): AnnotatedString = annotateInContext(text, Unit)
|
||||
}
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
package chat.revolt.markdown.ast
|
||||
|
||||
/*
|
||||
* SPECS:
|
||||
* - Unist, the universal syntax tree @ https://github.com/syntax-tree/unist
|
||||
* - Mdast, the markdown syntax tree @ https://github.com/syntax-tree/mdast
|
||||
*/
|
||||
|
||||
open class Literal(
|
||||
/**
|
||||
* The value of a node.
|
||||
*/
|
||||
open val value: String = ""
|
||||
) : Node()
|
||||
|
||||
open class Parent(
|
||||
/**
|
||||
* List representing the children of a node.
|
||||
*/
|
||||
val children: List<Node> = emptyList()
|
||||
) : Node()
|
||||
|
||||
open class Root(
|
||||
override val type: String = "root"
|
||||
) : Parent()
|
||||
|
||||
open class Paragraph(
|
||||
override val type: String = "paragraph"
|
||||
) : Parent()
|
||||
|
||||
open class Text(
|
||||
override val type: String = "text",
|
||||
override val value: String = ""
|
||||
) : Literal()
|
||||
|
||||
open class Heading(
|
||||
override val type: String = "heading",
|
||||
val depth: Int = 1
|
||||
) : Parent()
|
||||
|
||||
open class ThematicBreak(
|
||||
override val type: String = "thematicBreak"
|
||||
) : Node()
|
||||
|
||||
open class Blockquote(
|
||||
override val type: String = "blockquote"
|
||||
) : Parent()
|
||||
|
||||
open class MdList(
|
||||
override val type: String = "list",
|
||||
val ordered: Boolean = false,
|
||||
val start: Int = 1,
|
||||
val spread: Boolean = false
|
||||
) : Parent()
|
||||
|
||||
open class ListItem(
|
||||
override val type: String = "listItem",
|
||||
val spread: Boolean = false
|
||||
) : Parent()
|
||||
|
||||
open class Code(
|
||||
override val type: String = "code",
|
||||
val lang: String? = null,
|
||||
val meta: String? = null
|
||||
) : Literal()
|
||||
|
||||
open class Emphasis(
|
||||
override val type: String = "emphasis"
|
||||
) : Parent()
|
||||
|
||||
open class Strong(
|
||||
override val type: String = "strong"
|
||||
) : Parent()
|
||||
|
||||
open class Delete(
|
||||
override val type: String = "delete"
|
||||
) : Parent()
|
||||
|
||||
open class InlineCode(
|
||||
override val type: String = "inlineCode"
|
||||
) : Literal()
|
||||
|
||||
open class Break(
|
||||
override val type: String = "break"
|
||||
) : Node()
|
||||
|
||||
open class Link(
|
||||
override val type: String = "link",
|
||||
val title: String? = null,
|
||||
val url: String = ""
|
||||
) : Parent()
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
package chat.revolt.markdown.ast
|
||||
|
||||
/*
|
||||
* SPECS:
|
||||
* - Unist, the universal syntax tree @ https://github.com/syntax-tree/unist
|
||||
* - Mdast, the markdown syntax tree @ https://github.com/syntax-tree/mdast
|
||||
*/
|
||||
|
||||
class Point(
|
||||
/**
|
||||
* Line number in the document, starting at 1.
|
||||
*/
|
||||
val line: Int = 1,
|
||||
|
||||
/**
|
||||
* Column on line in the document, starting at 1.
|
||||
*/
|
||||
val column: Int = 1,
|
||||
|
||||
/**
|
||||
* Character offset in the document, starting at 0.
|
||||
*/
|
||||
val offset: Int = 0
|
||||
)
|
||||
|
||||
class Position(
|
||||
/**
|
||||
* Place of the first character of the parsed source region.
|
||||
*/
|
||||
val start: Point = Point(),
|
||||
|
||||
/**
|
||||
* Place of the first character after the parsed source region.
|
||||
*/
|
||||
val end: Point = Point(),
|
||||
|
||||
/**
|
||||
* Start column at each index (plus start line) in the source region,
|
||||
* for elements that span multiple lines.
|
||||
*/
|
||||
val indent: List<Int> = emptyList()
|
||||
)
|
||||
|
||||
open class Node(
|
||||
/**
|
||||
* The variant of the node.
|
||||
*/
|
||||
open val type: String = "",
|
||||
|
||||
/**
|
||||
* Information from the ecosystem.
|
||||
*/
|
||||
val data: Map<String, Any> = emptyMap(),
|
||||
|
||||
/**
|
||||
* Location of the node in a source document.
|
||||
* Must not be present if a node is generated.
|
||||
*/
|
||||
val position: Position = Position()
|
||||
)
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
package chat.revolt.markdown.parser
|
||||
|
||||
class Parser {
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
package chat.revolt.markdown
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
||||
|
|
@ -10,8 +10,8 @@ dependencyResolutionManagement {
|
|||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven { url "https://jitpack.io" }
|
||||
}
|
||||
}
|
||||
rootProject.name = "Revolt"
|
||||
include ':app'
|
||||
include ':markdown'
|
||||
|
|
|
|||
Loading…
Reference in New Issue