From b5c57500b1fb30d4cd6dc1caf3e971982a3795df Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Wed, 28 Jun 2023 10:58:39 +0530 Subject: [PATCH] feat: revert back to old text pipeline Things have been entirely too slow on the lobste.rs front and I don't care much to continue pushing for it. Fixes #382 Fixes #383 --- CHANGELOG.md | 1 - android/build.gradle.kts | 1 + .../msfjarvis/claw/android/MainActivity.kt | 3 + .../android/injection/HTMLConverterModule.kt | 20 ++ .../msfjarvis/claw/android/ui/LobstersApp.kt | 3 + .../claw/android/ui/util/HTMLConverterImpl.kt | 21 ++ common/build.gradle.kts | 3 + .../claw/common/comments/CommentEntry.kt | 11 +- .../claw/common/comments/CommentNode.kt | 5 + .../claw/common/comments/Comments.kt | 5 + .../claw/common/comments/HTMLConverter.kt | 15 ++ .../dev/msfjarvis/claw/common/theme/Theme.kt | 5 +- .../dev/msfjarvis/claw/common/ui/HTMLText.kt | 220 ------------------ .../dev/msfjarvis/claw/common/ui/Markdown.kt | 72 ++++++ .../msfjarvis/claw/common/user/UserProfile.kt | 10 +- gradle/libs.versions.toml | 5 + 16 files changed, 171 insertions(+), 229 deletions(-) create mode 100644 android/src/main/kotlin/dev/msfjarvis/claw/android/injection/HTMLConverterModule.kt create mode 100644 android/src/main/kotlin/dev/msfjarvis/claw/android/ui/util/HTMLConverterImpl.kt create mode 100644 common/src/main/kotlin/dev/msfjarvis/claw/common/comments/HTMLConverter.kt delete mode 100644 common/src/main/kotlin/dev/msfjarvis/claw/common/ui/HTMLText.kt create mode 100644 common/src/main/kotlin/dev/msfjarvis/claw/common/ui/Markdown.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bb54af5..e9128aa9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -* Comments and user profile text rendering was overhauled, there should be no visual changes * Added another workaround for native library loading crash ## [1.29.0] - 2023-06-08 diff --git a/android/build.gradle.kts b/android/build.gradle.kts index b92661cf..1302115c 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -70,6 +70,7 @@ dependencies { implementation(libs.androidx.profileinstaller) implementation(libs.androidx.work.runtime.ktx) implementation(libs.coil) + implementation(libs.copydown) implementation(libs.crux) implementation(libs.dagger) implementation(libs.jsoup) diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/MainActivity.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/MainActivity.kt index 54d5e930..8d555323 100644 --- a/android/src/main/kotlin/dev/msfjarvis/claw/android/MainActivity.kt +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/MainActivity.kt @@ -18,6 +18,7 @@ import androidx.core.view.WindowCompat import com.deliveryhero.whetstone.Whetstone import com.deliveryhero.whetstone.activity.ContributesActivityInjector import dev.msfjarvis.claw.android.ui.LobstersApp +import dev.msfjarvis.claw.common.comments.HTMLConverter import dev.msfjarvis.claw.common.urllauncher.UrlLauncher import javax.inject.Inject @@ -25,6 +26,7 @@ import javax.inject.Inject class MainActivity : ComponentActivity() { @Inject lateinit var urlLauncher: UrlLauncher + @Inject lateinit var htmlConverter: HTMLConverter private var webUri: String? = null @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) @@ -38,6 +40,7 @@ class MainActivity : ComponentActivity() { LobstersApp( urlLauncher = urlLauncher, + htmlConverter = htmlConverter, windowSizeClass = windowSizeClass, setWebUri = { url -> webUri = url }, ) diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/injection/HTMLConverterModule.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/injection/HTMLConverterModule.kt new file mode 100644 index 00000000..470f3780 --- /dev/null +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/injection/HTMLConverterModule.kt @@ -0,0 +1,20 @@ +/* + * Copyright © 2021-2023 Harsh Shandilya. + * Use of this source code is governed by an MIT-style + * license that can be found in the LICENSE file or at + * https://opensource.org/licenses/MIT. + */ +package dev.msfjarvis.claw.android.injection + +import com.deliveryhero.whetstone.app.ApplicationScope +import com.squareup.anvil.annotations.ContributesTo +import dagger.Binds +import dagger.Module +import dev.msfjarvis.claw.android.ui.util.HTMLConverterImpl +import dev.msfjarvis.claw.common.comments.HTMLConverter + +@Module +@ContributesTo(ApplicationScope::class) +interface HTMLConverterModule { + @Binds fun HTMLConverterImpl.bind(): HTMLConverter +} diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/LobstersApp.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/LobstersApp.kt index 602c3c31..2db43202 100644 --- a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/LobstersApp.kt +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/LobstersApp.kt @@ -64,6 +64,7 @@ import dev.msfjarvis.claw.android.ui.navigation.Destinations import dev.msfjarvis.claw.android.viewmodel.ClawViewModel import dev.msfjarvis.claw.api.LobstersApi import dev.msfjarvis.claw.common.comments.CommentsPage +import dev.msfjarvis.claw.common.comments.HTMLConverter import dev.msfjarvis.claw.common.theme.LobstersTheme import dev.msfjarvis.claw.common.ui.decorations.ClawAppBar import dev.msfjarvis.claw.common.urllauncher.UrlLauncher @@ -76,6 +77,7 @@ import kotlinx.coroutines.launch @Composable fun LobstersApp( urlLauncher: UrlLauncher, + htmlConverter: HTMLConverter, windowSizeClass: WindowSizeClass, setWebUri: (String?) -> Unit, modifier: Modifier = Modifier, @@ -236,6 +238,7 @@ fun LobstersApp( CommentsPage( postId = postId, postActions = postActions, + htmlConverter = htmlConverter, getSeenComments = viewModel::getSeenComments, markSeenComments = viewModel::markSeenComments, ) diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/util/HTMLConverterImpl.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/util/HTMLConverterImpl.kt new file mode 100644 index 00000000..c202e2e0 --- /dev/null +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/util/HTMLConverterImpl.kt @@ -0,0 +1,21 @@ +/* + * Copyright © 2023 Harsh Shandilya. + * Use of this source code is governed by an MIT-style + * license that can be found in the LICENSE file or at + * https://opensource.org/licenses/MIT. + */ +package dev.msfjarvis.claw.android.ui.util + +import androidx.compose.runtime.Stable +import dev.msfjarvis.claw.common.comments.HTMLConverter +import io.github.furstenheim.CopyDown +import javax.inject.Inject + +@Stable +class HTMLConverterImpl @Inject constructor() : HTMLConverter { + private val copydown = CopyDown() + + override fun convertHTMLToMarkdown(html: String): String { + return copydown.convert(html) + } +} diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 2fdf4a07..f77e8fb8 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -45,6 +45,9 @@ dependencies { implementation(libs.androidx.compose.runtime) implementation(libs.androidx.compose.ui.text) implementation(libs.coil.compose) + implementation(libs.compose.richtext.markdown) + implementation(libs.compose.richtext.material3) + implementation(libs.compose.richtext.ui) implementation(libs.kotlinx.collections.immutable) implementation(libs.kotlinx.coroutines.core) implementation(libs.napier) diff --git a/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/CommentEntry.kt b/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/CommentEntry.kt index c1f9d18f..57a48b14 100644 --- a/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/CommentEntry.kt +++ b/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/CommentEntry.kt @@ -41,8 +41,8 @@ import dev.msfjarvis.claw.common.posts.PostActions import dev.msfjarvis.claw.common.posts.PostTitle import dev.msfjarvis.claw.common.posts.Submitter import dev.msfjarvis.claw.common.posts.TagRow -import dev.msfjarvis.claw.common.ui.HTMLText import dev.msfjarvis.claw.common.ui.NetworkImage +import dev.msfjarvis.claw.common.ui.ThemedRichText import dev.msfjarvis.claw.model.LinkMetadata import dev.msfjarvis.claw.model.LobstersPostDetails import java.time.Instant @@ -53,6 +53,7 @@ import kotlinx.collections.immutable.toImmutableList internal fun CommentsHeader( postDetails: LobstersPostDetails, postActions: PostActions, + htmlConverter: HTMLConverter, modifier: Modifier = Modifier, ) { val uriHandler = LocalUriHandler.current @@ -83,7 +84,7 @@ internal fun CommentsHeader( } if (postDetails.description.isNotBlank()) { - HTMLText(postDetails.description) + ThemedRichText(htmlConverter.convertHTMLToMarkdown(postDetails.description)) Spacer(Modifier.height(4.dp)) } Submitter( @@ -133,6 +134,7 @@ private val CommentEntryPadding = 16f.dp @Composable internal fun CommentEntry( commentNode: CommentNode, + htmlConverter: HTMLConverter, toggleExpanded: (CommentNode) -> Unit, modifier: Modifier = Modifier, ) { @@ -167,7 +169,10 @@ internal fun CommentEntry( Modifier.clickable { uriHandler.openUri("https://lobste.rs/u/${comment.user.username}") }, ) if (commentNode.isExpanded) { - HTMLText(text = comment.comment, modifier = Modifier.padding(top = 8.dp)) + ThemedRichText( + text = htmlConverter.convertHTMLToMarkdown(comment.comment), + modifier = Modifier.padding(top = 8.dp) + ) } } } diff --git a/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/CommentNode.kt b/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/CommentNode.kt index cebb4a10..439abc2b 100644 --- a/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/CommentNode.kt +++ b/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/CommentNode.kt @@ -68,11 +68,13 @@ internal fun findTopMostParent(node: CommentNode): CommentNode { internal fun LazyListScope.nodes( nodes: List, + htmlConverter: HTMLConverter, toggleExpanded: (CommentNode) -> Unit, ) { nodes.forEach { node -> node( node = node, + htmlConverter = htmlConverter, toggleExpanded = toggleExpanded, ) } @@ -80,6 +82,7 @@ internal fun LazyListScope.nodes( private fun LazyListScope.node( node: CommentNode, + htmlConverter: HTMLConverter, toggleExpanded: (CommentNode) -> Unit, ) { // Skip the node if neither the node nor its parent is expanded @@ -89,6 +92,7 @@ private fun LazyListScope.node( item { CommentEntry( commentNode = node, + htmlConverter = htmlConverter, toggleExpanded = toggleExpanded, ) Divider() @@ -96,6 +100,7 @@ private fun LazyListScope.node( if (node.children.isNotEmpty()) { nodes( node.children, + htmlConverter = htmlConverter, toggleExpanded = toggleExpanded, ) } diff --git a/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/Comments.kt b/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/Comments.kt index 119cee39..5da029b1 100644 --- a/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/Comments.kt +++ b/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/Comments.kt @@ -41,6 +41,7 @@ import dev.msfjarvis.claw.model.LobstersPostDetails private fun CommentsPageInternal( details: LobstersPostDetails, postActions: PostActions, + htmlConverter: HTMLConverter, commentState: PostComments?, markSeenComments: (String, List) -> Unit, modifier: Modifier = Modifier, @@ -54,6 +55,7 @@ private fun CommentsPageInternal( CommentsHeader( postDetails = details, postActions = postActions, + htmlConverter = htmlConverter, ) } @@ -68,6 +70,7 @@ private fun CommentsPageInternal( nodes( nodes = commentNodes, + htmlConverter = htmlConverter, toggleExpanded = { node -> val newNode = setExpanded(node, !node.isExpanded) val parent = findTopMostParent(newNode) @@ -99,6 +102,7 @@ private fun CommentsPageInternal( fun CommentsPage( postId: String, postActions: PostActions, + htmlConverter: HTMLConverter, getSeenComments: suspend (String) -> PostComments?, markSeenComments: (String, List) -> Unit, modifier: Modifier = Modifier, @@ -119,6 +123,7 @@ fun CommentsPage( CommentsPageInternal( details = (postDetails as Success).data, postActions = postActions, + htmlConverter = htmlConverter, commentState = commentState, markSeenComments = markSeenComments, modifier = modifier.fillMaxSize(), diff --git a/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/HTMLConverter.kt b/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/HTMLConverter.kt new file mode 100644 index 00000000..6ce5d1d6 --- /dev/null +++ b/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/HTMLConverter.kt @@ -0,0 +1,15 @@ +/* + * Copyright © 2023 Harsh Shandilya. + * Use of this source code is governed by an MIT-style + * license that can be found in the LICENSE file or at + * https://opensource.org/licenses/MIT. + */ +package dev.msfjarvis.claw.common.comments + +import androidx.compose.runtime.Stable + +/** Defines a contract to convert strings of HTML to Markdown. */ +@Stable +fun interface HTMLConverter { + fun convertHTMLToMarkdown(html: String): String +} diff --git a/common/src/main/kotlin/dev/msfjarvis/claw/common/theme/Theme.kt b/common/src/main/kotlin/dev/msfjarvis/claw/common/theme/Theme.kt index 5eef7c64..3ee211b6 100644 --- a/common/src/main/kotlin/dev/msfjarvis/claw/common/theme/Theme.kt +++ b/common/src/main/kotlin/dev/msfjarvis/claw/common/theme/Theme.kt @@ -17,6 +17,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.ProvidedValue import androidx.compose.ui.platform.LocalContext +import com.halilibo.richtext.ui.material3.SetupMaterial3RichText private val LightThemeColors = lightColorScheme( @@ -97,6 +98,8 @@ fun LobstersTheme( else -> if (darkTheme) DarkThemeColors else LightThemeColors } CompositionLocalProvider(*providedValues) { - MaterialTheme(colorScheme = colorScheme, typography = AppTypography, content = content) + MaterialTheme(colorScheme = colorScheme, typography = AppTypography) { + SetupMaterial3RichText { content() } + } } } diff --git a/common/src/main/kotlin/dev/msfjarvis/claw/common/ui/HTMLText.kt b/common/src/main/kotlin/dev/msfjarvis/claw/common/ui/HTMLText.kt deleted file mode 100644 index a0325d15..00000000 --- a/common/src/main/kotlin/dev/msfjarvis/claw/common/ui/HTMLText.kt +++ /dev/null @@ -1,220 +0,0 @@ -/* - * Copyright © 2023 Harsh Shandilya. - * Use of this source code is governed by an MIT-style - * license that can be found in the LICENSE file or at - * https://opensource.org/licenses/MIT. - */ -package dev.msfjarvis.claw.common.ui - -import android.graphics.Typeface -import android.text.Spanned -import android.text.style.BulletSpan -import android.text.style.ForegroundColorSpan -import android.text.style.QuoteSpan -import android.text.style.RelativeSizeSpan -import android.text.style.StrikethroughSpan -import android.text.style.StyleSpan -import android.text.style.SubscriptSpan -import android.text.style.SuperscriptSpan -import android.text.style.URLSpan -import android.text.style.UnderlineSpan -import androidx.compose.foundation.background -import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.foundation.layout.Box -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.TextLayoutResult -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.BaselineShift -import androidx.compose.ui.text.style.TextDecoration -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.TextUnit -import androidx.compose.ui.unit.sp -import androidx.core.text.HtmlCompat -import dev.msfjarvis.claw.common.theme.LobstersTheme - -private const val URL_TAG = "url_tag" - -@Composable -fun HTMLText( - text: String, - modifier: Modifier = Modifier, - style: TextStyle = MaterialTheme.typography.bodyLarge, - softWrap: Boolean = true, - overflow: TextOverflow = TextOverflow.Clip, - maxLines: Int = Int.MAX_VALUE, - onTextLayout: (TextLayoutResult) -> Unit = {}, - fontSize: TextUnit = 14.sp, - flags: Int = HtmlCompat.FROM_HTML_MODE_COMPACT, - customSpannedHandler: ((Spanned) -> AnnotatedString)? = null -) { - val content = text.asHTML(fontSize, flags, customSpannedHandler) - val uriHandler = LocalUriHandler.current - ClickableText( - modifier = modifier, - text = content, - style = style, - softWrap = softWrap, - overflow = overflow, - maxLines = maxLines, - onTextLayout = onTextLayout, - onClick = { offset -> - content.getStringAnnotations(URL_TAG, offset, offset).firstOrNull()?.let { stringAnnotation -> - uriHandler.openUri(stringAnnotation.item) - } - } - ) -} - -@Composable -private fun String.asHTML( - fontSize: TextUnit, - flags: Int, - customSpannedHandler: ((Spanned) -> AnnotatedString)? = null -) = buildAnnotatedString { - val spanned = HtmlCompat.fromHtml(this@asHTML, flags) - val spans = spanned.getSpans(0, spanned.length, Any::class.java) - - if (customSpannedHandler != null) { - append(customSpannedHandler(spanned)) - } else { - append(spanned.toString()) - } - - spans - .filter { it !is BulletSpan } - .forEach { span -> - val start = spanned.getSpanStart(span) - val end = spanned.getSpanEnd(span) - when (span) { - is RelativeSizeSpan -> span.spanStyle(fontSize) - is StyleSpan -> span.spanStyle() - is UnderlineSpan -> span.spanStyle() - is ForegroundColorSpan -> span.spanStyle() - is StrikethroughSpan -> span.spanStyle() - is SuperscriptSpan -> span.spanStyle() - is SubscriptSpan -> span.spanStyle() - is QuoteSpan -> span.spanStyle() - is URLSpan -> { - addStringAnnotation(tag = URL_TAG, annotation = span.url, start = start, end = end) - span.spanStyle() - } - else -> { - null - } - }?.let { spanStyle -> addStyle(spanStyle, start, end) } - } -} - -@Suppress("UnusedReceiverParameter") -@Composable -private fun URLSpan.spanStyle(): SpanStyle = - SpanStyle( - background = MaterialTheme.colorScheme.surfaceVariant, - color = MaterialTheme.colorScheme.onSurface, - fontWeight = FontWeight.Bold, - textDecoration = TextDecoration.Underline, - ) - -@Suppress("UnusedReceiverParameter") -private fun UnderlineSpan.spanStyle(): SpanStyle = - SpanStyle(textDecoration = TextDecoration.Underline) - -private fun ForegroundColorSpan.spanStyle(): SpanStyle = SpanStyle(color = Color(foregroundColor)) - -@Suppress("UnusedReceiverParameter") -private fun StrikethroughSpan.spanStyle(): SpanStyle = - SpanStyle(textDecoration = TextDecoration.LineThrough) - -private fun RelativeSizeSpan.spanStyle(fontSize: TextUnit): SpanStyle = - SpanStyle(fontSize = (fontSize.value * sizeChange).sp) - -@Suppress("UnusedReceiverParameter") -private fun QuoteSpan.spanStyle(): SpanStyle = SpanStyle(fontStyle = FontStyle.Italic) - -private fun StyleSpan.spanStyle(): SpanStyle? = - when (style) { - Typeface.BOLD -> SpanStyle(fontWeight = FontWeight.Bold) - Typeface.ITALIC -> SpanStyle(fontStyle = FontStyle.Italic) - Typeface.BOLD_ITALIC -> - SpanStyle( - fontWeight = FontWeight.Bold, - fontStyle = FontStyle.Italic, - ) - else -> null - } - -@Suppress("UnusedReceiverParameter") -private fun SubscriptSpan.spanStyle(): SpanStyle = - SpanStyle(baselineShift = BaselineShift.Subscript) - -@Suppress("UnusedReceiverParameter") -private fun SuperscriptSpan.spanStyle(): SpanStyle = - SpanStyle(baselineShift = BaselineShift.Superscript) - -@Composable -private fun ClickableText( - text: AnnotatedString, - modifier: Modifier = Modifier, - style: TextStyle = TextStyle.Default, - softWrap: Boolean = true, - overflow: TextOverflow = TextOverflow.Clip, - maxLines: Int = Int.MAX_VALUE, - onTextLayout: (TextLayoutResult) -> Unit = {}, - onClick: (Int) -> Unit -) { - val layoutResult = remember { mutableStateOf(null) } - val pressIndicator = - Modifier.pointerInput(onClick) { - detectTapGestures { pos -> - layoutResult.value?.let { layoutResult -> onClick(layoutResult.getOffsetForPosition(pos)) } - } - } - Text( - text = text, - modifier = modifier.then(pressIndicator), - style = style, - softWrap = softWrap, - overflow = overflow, - maxLines = maxLines, - onTextLayout = { - layoutResult.value = it - onTextLayout(it) - } - ) -} - -@Preview -@Composable -fun HTMLTextPreview() { - LobstersTheme { - Box(modifier = Modifier.background(MaterialTheme.colorScheme.background)) { - HTMLText( - text = - """ -

Heading

-

This is a paragraph body

-
This is a code block
-      

This is an inline code block

-

This is a blockquote

-

This is a link

-

Image

- """ - .trimIndent() - ) - } - } -} diff --git a/common/src/main/kotlin/dev/msfjarvis/claw/common/ui/Markdown.kt b/common/src/main/kotlin/dev/msfjarvis/claw/common/ui/Markdown.kt new file mode 100644 index 00000000..f1a6b59f --- /dev/null +++ b/common/src/main/kotlin/dev/msfjarvis/claw/common/ui/Markdown.kt @@ -0,0 +1,72 @@ +/* + * Copyright © 2023 Harsh Shandilya. + * Use of this source code is governed by an MIT-style + * license that can be found in the LICENSE file or at + * https://opensource.org/licenses/MIT. + */ +package dev.msfjarvis.claw.common.ui + +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import com.halilibo.richtext.markdown.Markdown +import com.halilibo.richtext.ui.RichText +import com.halilibo.richtext.ui.RichTextStyle +import com.halilibo.richtext.ui.string.RichTextStringStyle +import dev.msfjarvis.claw.common.theme.LobstersTheme +import dev.msfjarvis.claw.common.ui.preview.ThemePreviews + +@Composable +internal fun ThemedRichText( + text: String, + modifier: Modifier = Modifier, +) { + val linkStyle = + SpanStyle( + background = MaterialTheme.colorScheme.surfaceVariant, + color = MaterialTheme.colorScheme.onSurface, + fontWeight = FontWeight.Bold, + textDecoration = TextDecoration.Underline, + ) + val stringStyle = RichTextStringStyle.Default.copy(linkStyle = linkStyle) + CompositionLocalProvider( + LocalTextStyle provides MaterialTheme.typography.bodyLarge, + LocalContentColor provides MaterialTheme.colorScheme.onBackground, + ) { + RichText( + modifier = modifier, + style = RichTextStyle.Default.copy(stringStyle = stringStyle), + ) { + Markdown(text) + } + } +} + +@ThemePreviews +@Composable +internal fun ThemedRichTextPreview() { + val text = + """ + ### Heading + This is a paragraph body + + ``` + This is a code block + ``` + + This is an `inline code block` + + [This is a link](https://github.com/msfjarvis/compose-lobsters) + + ![Image](https://avatars.githubusercontent.com/u/13348378?v=4) + """ + .trimIndent() + + LobstersTheme { ThemedRichText(text = text) } +} diff --git a/common/src/main/kotlin/dev/msfjarvis/claw/common/user/UserProfile.kt b/common/src/main/kotlin/dev/msfjarvis/claw/common/user/UserProfile.kt index 47b8e815..aae8cebf 100644 --- a/common/src/main/kotlin/dev/msfjarvis/claw/common/user/UserProfile.kt +++ b/common/src/main/kotlin/dev/msfjarvis/claw/common/user/UserProfile.kt @@ -29,10 +29,10 @@ import dev.msfjarvis.claw.common.NetworkState import dev.msfjarvis.claw.common.NetworkState.Error import dev.msfjarvis.claw.common.NetworkState.Loading import dev.msfjarvis.claw.common.NetworkState.Success -import dev.msfjarvis.claw.common.ui.HTMLText import dev.msfjarvis.claw.common.ui.NetworkError import dev.msfjarvis.claw.common.ui.NetworkImage import dev.msfjarvis.claw.common.ui.ProgressBar +import dev.msfjarvis.claw.common.ui.ThemedRichText import dev.msfjarvis.claw.model.User @Suppress("UNCHECKED_CAST") @@ -99,10 +99,12 @@ private fun UserProfileInternal( text = user.username, style = MaterialTheme.typography.displaySmall, ) - HTMLText(text = user.about) + ThemedRichText( + text = user.about, + ) user.invitedBy?.let { invitedBy -> - HTMLText( - text = """Invited by ${invitedBy}""", + ThemedRichText( + text = "Invited by [${invitedBy}](https://lobste.rs/u/${user.invitedBy})", ) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f38a015d..26f69795 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,6 +9,7 @@ dagger = "2.46.1" junit = "5.9.3" kotlin = "1.8.10" retrofit = "2.9.0" +richtext = "0.16.0" sentry-sdk = "6.24.0" serialization = "1.5.1" sqldelight = "2.0.0-rc02" @@ -56,6 +57,10 @@ build-vcu = "nl.littlerobots.version-catalog-update:nl.littlerobots.version-cata build-versions = "com.github.ben-manes:gradle-versions-plugin:0.47.0" coil = { module = "io.coil-kt:coil", version.ref = "coil" } coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" } +compose-richtext-markdown = { module = "com.halilibo.compose-richtext:richtext-commonmark", version.ref = "richtext" } +compose-richtext-material3 = { module = "com.halilibo.compose-richtext:richtext-ui-material3", version.ref = "richtext" } +compose-richtext-ui = { module = "com.halilibo.compose-richtext:richtext-ui", version.ref = "richtext" } +copydown = "io.github.furstenheim:copy_down:1.1" crux = "com.chimbori.crux:crux:3.12.0" dagger = { module = "com.google.dagger:dagger", version.ref = "dagger" } dagger-compiler = { module = "com.google.dagger:dagger-compiler", version.ref = "dagger" }