diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 1c558ec8..837f728f 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -22,6 +22,10 @@ dependencies { implementation(libs.androidx.lifecycle.compose) implementation(libs.androidx.navigation.compose) implementation(libs.androidx.paging.compose) + implementation(libs.compose.richtext.markdown) + implementation(libs.compose.richtext.material) + implementation(libs.compose.richtext.ui) + implementation(libs.copydown) implementation(libs.dagger.hilt.android) implementation(libs.sqldelight.extensions.coroutines) implementation(libs.kotlinx.serialization.json) 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 a32cfc4c..e2bc2756 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 @@ -1,6 +1,5 @@ package dev.msfjarvis.claw.android.ui -import android.text.Html import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.lazy.rememberLazyListState @@ -29,10 +28,13 @@ import com.google.accompanist.insets.ProvideWindowInsets import com.google.accompanist.insets.navigationBarsPadding import com.google.accompanist.insets.statusBarsPadding import com.google.accompanist.systemuicontroller.rememberSystemUiController +import com.halilibo.richtext.markdown.Markdown +import com.halilibo.richtext.ui.material.MaterialRichText import dev.msfjarvis.claw.android.viewmodel.ClawViewModel import dev.msfjarvis.claw.common.comments.CommentsPage import dev.msfjarvis.claw.common.theme.LobstersTheme import dev.msfjarvis.claw.common.urllauncher.UrlLauncher +import io.github.furstenheim.CopyDown private const val ScrollDelta = 50 @@ -42,6 +44,7 @@ fun LobstersApp( viewModel: ClawViewModel = viewModel(), urlLauncher: UrlLauncher, ) { + val copydown = remember { CopyDown() } val systemUiController = rememberSystemUiController() val scaffoldState = rememberScaffoldState() val listState = rememberLazyListState() @@ -102,7 +105,10 @@ fun LobstersApp( CommentsPage( postId = requireNotNull(backStackEntry.arguments?.getString("postId")), getDetails = viewModel::getPostComments, - parseHtml = { source -> Html.fromHtml(source).toString().trim() }, + renderMarkdown = { source, modifier -> + val markdown = copydown.convert(source) + MaterialRichText(modifier = modifier) { Markdown(markdown) } + }, paddingValues = paddingValues, ) } diff --git a/common/src/commonMain/kotlin/dev/msfjarvis/claw/common/comments/CommentEntry.kt b/common/src/commonMain/kotlin/dev/msfjarvis/claw/common/comments/CommentEntry.kt index 197acecc..5d4f0999 100644 --- a/common/src/commonMain/kotlin/dev/msfjarvis/claw/common/comments/CommentEntry.kt +++ b/common/src/commonMain/kotlin/dev/msfjarvis/claw/common/comments/CommentEntry.kt @@ -4,16 +4,14 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.material.Divider import androidx.compose.material.Surface -import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -43,15 +41,15 @@ fun CommentsHeader( @Composable fun CommentEntry( comment: Comment, - parseHtml: (String) -> String, + renderMarkdown: @Composable (comment: String, modifier: Modifier) -> Unit, ) { val indentLevel = comment.indentLevel.toInt() - 1 val startPadding = ((10 * indentLevel) + 16).dp - val text = parseHtml(comment.comment) Divider(color = Color.Gray.copy(0.4f)) - Row(modifier = Modifier.height(IntrinsicSize.Min)) { - CommentTreeColors(indentLevel = indentLevel) + Row(modifier = Modifier.wrapContentHeight()) { + // Don't work without IntrinsicSize, which we cannot use with the Android markdown implementation + // CommentTreeColors(indentLevel = indentLevel) Column( modifier = Modifier.padding(start = startPadding, end = 8.dp, top = 4.dp, bottom = 4.dp) ) { @@ -60,7 +58,7 @@ fun CommentEntry( avatarUrl = "https://lobste.rs/${comment.user.avatarUrl}", contentDescription = "Submitted by ${comment.user.username}", ) - Text(text = text, modifier = Modifier.padding(top = 8.dp)) + renderMarkdown(comment.comment, Modifier.padding(top = 8.dp)) } } } diff --git a/common/src/commonMain/kotlin/dev/msfjarvis/claw/common/comments/Comments.kt b/common/src/commonMain/kotlin/dev/msfjarvis/claw/common/comments/Comments.kt index dd6ea428..feb0da24 100644 --- a/common/src/commonMain/kotlin/dev/msfjarvis/claw/common/comments/Comments.kt +++ b/common/src/commonMain/kotlin/dev/msfjarvis/claw/common/comments/Comments.kt @@ -28,7 +28,7 @@ import dev.msfjarvis.lobsters.ui.comments.NetworkState @Composable private fun CommentsPageInternal( details: LobstersPostDetails, - parseHtml: (String) -> String, + renderMarkdown: @Composable (comment: String, modifier: Modifier) -> Unit, bottomPadding: Dp, ) { LazyColumn(Modifier.padding(bottom = bottomPadding)) { @@ -36,7 +36,7 @@ private fun CommentsPageInternal( item { Spacer(modifier = Modifier.height(8.dp)) } - items(details.comments) { item -> CommentEntry(item, parseHtml) } + items(details.comments) { item -> CommentEntry(item, renderMarkdown) } item { Divider(color = Color.Gray.copy(0.4f)) } } @@ -47,7 +47,7 @@ private fun CommentsPageInternal( fun CommentsPage( postId: String, getDetails: suspend (String) -> LobstersPostDetails, - parseHtml: (String) -> String, + renderMarkdown: @Composable (comment: String, modifier: Modifier) -> Unit, paddingValues: PaddingValues, ) { var postDetails: NetworkState by remember { mutableStateOf(NetworkState.Loading) } @@ -58,7 +58,7 @@ fun CommentsPage( is NetworkState.Success<*> -> { CommentsPageInternal( (postDetails as NetworkState.Success).data, - parseHtml, + renderMarkdown, paddingValues.calculateBottomPadding(), ) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b8c9ffc0..f4e6d5b4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,6 +3,7 @@ accompanist = "0.19.0" aurora = "0.0.54-SNAPSHOT" coroutines = "1.5.2" hilt = "2.39" +richtext = "0.8.1" serialization = "1.3.0" sqldelight = "1.5.1" @@ -37,6 +38,11 @@ dagger-hilt-android = { module = "com.google.dagger:hilt-android", version.ref = dagger-hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" } dagger-hilt-core = { module = "com.google.dagger:hilt-core", version.ref = "hilt" } +compose-richtext-ui = { module = "com.halilibo.compose-richtext:richtext-ui", version.ref = "richtext" } +compose-richtext-material = { module = "com.halilibo.compose-richtext:richtext-ui-material", version.ref = "richtext" } +compose-richtext-markdown = { module = "com.halilibo.compose-richtext:richtext-commonmark", version.ref = "richtext" } +copydown = "io.github.furstenheim:copy_down:1.0" + multiplatform-paging = "dev.msfjarvis.paging:multiplatform-paging:0.4.5-SNAPSHOT" retrofit-lib = "com.squareup.retrofit2:retrofit:2.9.0"