From 87732e8f06e2890115fea23ad3e957c6d693fed6 Mon Sep 17 00:00:00 2001 From: Niladri Das Date: Thu, 28 May 2026 15:18:47 +0530 Subject: [PATCH] [ui] refine account previews --- .../bniladridas/diff/ui/MarkdownPreview.kt | 47 +++++++++++++++++ .../diff/ui/components/DiffComponents.kt | 7 +-- .../bniladridas/diff/ui/screens/DiffApp.kt | 50 +++++++------------ .../diff/ui/MarkdownPreviewTest.kt | 44 ++++++++++++++++ 4 files changed, 112 insertions(+), 36 deletions(-) create mode 100644 app/src/main/java/com/bniladridas/diff/ui/MarkdownPreview.kt create mode 100644 app/src/test/java/com/bniladridas/diff/ui/MarkdownPreviewTest.kt diff --git a/app/src/main/java/com/bniladridas/diff/ui/MarkdownPreview.kt b/app/src/main/java/com/bniladridas/diff/ui/MarkdownPreview.kt new file mode 100644 index 0000000..5c71823 --- /dev/null +++ b/app/src/main/java/com/bniladridas/diff/ui/MarkdownPreview.kt @@ -0,0 +1,47 @@ +package com.bniladridas.diff.ui + +private val markdownImagePattern = Regex("""!\[([^\]]*)]\([^)]+\)""") +private val markdownLinkPattern = Regex("""\[([^\]]+)]\(https?://[^)]+\)""") +private val htmlImagePattern = Regex("""]*>""", RegexOption.IGNORE_CASE) +private val htmlAltPattern = Regex("""\balt=["']([^"']*)["']""", RegexOption.IGNORE_CASE) +private val bareUrlPattern = Regex("""https?://[^\s)>\]]+""") + +internal fun markdownBodyPreview( + rawBody: String, + emptyText: String, +): String { + if (rawBody.isBlank()) return emptyText + + return rawBody + .replace(htmlImagePattern) { match -> + htmlAltPattern.find(match.value)?.groups?.get(1)?.value?.cleanBadgeLabel()?.let { "badge: $it" }.orEmpty() + } + .replace(markdownImagePattern) { match -> + match.groups[1]?.value?.cleanBadgeLabel()?.let { "badge: $it" }.orEmpty() + } + .replace(markdownLinkPattern) { match -> + match.groups[1]?.value?.trim().orEmpty() + } + .replace(bareUrlPattern) { match -> + match.value.displayUrl() + } + .lines() + .map { it.trim() } + .filter { it.isNotBlank() } + .joinToString("\n") + .ifBlank { emptyText } +} + +private fun String.cleanBadgeLabel(): String? { + val cleaned = trim() + .removeSuffix(" badge") + .removeSuffix(" Badge") + .takeIf { it.isNotBlank() && it != "-" } + + return cleaned +} + +private fun String.displayUrl(): String { + val withoutScheme = removePrefix("https://").removePrefix("http://") + return withoutScheme.substringBefore('/').ifBlank { "link" } +} diff --git a/app/src/main/java/com/bniladridas/diff/ui/components/DiffComponents.kt b/app/src/main/java/com/bniladridas/diff/ui/components/DiffComponents.kt index 2410084..f270bfd 100644 --- a/app/src/main/java/com/bniladridas/diff/ui/components/DiffComponents.kt +++ b/app/src/main/java/com/bniladridas/diff/ui/components/DiffComponents.kt @@ -48,6 +48,7 @@ import com.bniladridas.diff.model.RepoTreeItem import com.bniladridas.diff.model.TimelineEvent import com.bniladridas.diff.model.WorkspaceTab import com.bniladridas.diff.ui.formatDate +import com.bniladridas.diff.ui.markdownBodyPreview import com.bniladridas.diff.ui.shortSha import com.bniladridas.diff.ui.theme.BrandOrange import com.bniladridas.diff.ui.theme.BrandOrangeSoft @@ -456,7 +457,7 @@ fun CommentCard( ) } Text( - comment.body.ifBlank { "No comment body." }, + markdownBodyPreview(comment.body, "No comment body."), color = bodyText(), style = MaterialTheme.typography.bodyMedium, maxLines = 10, @@ -513,7 +514,7 @@ fun ReviewCard(review: PullReview) { Text(review.author, color = strongText(), fontWeight = FontWeight.SemiBold) Text(formatDate(review.submittedAt), color = mutedText(), style = MaterialTheme.typography.labelSmall) if (review.body.isNotBlank()) { - Text(review.body, color = bodyText(), maxLines = 3, overflow = TextOverflow.Ellipsis) + Text(markdownBodyPreview(review.body, ""), color = bodyText(), maxLines = 3, overflow = TextOverflow.Ellipsis) } } Tag(review.state.lowercase(), reviewStateColor(review.state), reviewStateFill(review.state)) @@ -575,7 +576,7 @@ fun TimelineEventCard(event: TimelineEvent) { ) event.body?.let { body -> Text( - body, + markdownBodyPreview(body, ""), color = bodyText(), style = MaterialTheme.typography.bodySmall, maxLines = 4, diff --git a/app/src/main/java/com/bniladridas/diff/ui/screens/DiffApp.kt b/app/src/main/java/com/bniladridas/diff/ui/screens/DiffApp.kt index 8497743..81f429d 100644 --- a/app/src/main/java/com/bniladridas/diff/ui/screens/DiffApp.kt +++ b/app/src/main/java/com/bniladridas/diff/ui/screens/DiffApp.kt @@ -3061,7 +3061,7 @@ private fun AccountDialog( Surface( shape = RoundedCornerShape(10.dp), color = MaterialTheme.colorScheme.background, - border = BorderStroke(1.dp, DiffLine), + border = BorderStroke(1.dp, appOutline()), ) { Column( modifier = Modifier @@ -3075,13 +3075,13 @@ private fun AccountDialog( Column(Modifier.weight(1f)) { Text( "Account", - color = Color.White, + color = appStrong(), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.SemiBold, ) Text( login?.let { "Connected as $it" } ?: "Use a GitHub token for authenticated reads.", - color = TextMuted, + color = appMuted(), style = MaterialTheme.typography.bodySmall, ) } @@ -3094,26 +3094,18 @@ private fun AccountDialog( modifier = Modifier.fillMaxWidth(), singleLine = true, visualTransformation = PasswordVisualTransformation(), - textStyle = MaterialTheme.typography.bodyMedium.copy(color = Color.White), - colors = OutlinedTextFieldDefaults.colors( - focusedBorderColor = BrandOrange.copy(alpha = 0.44f), - unfocusedBorderColor = DiffLine, - focusedLabelColor = BrandOrange, - unfocusedLabelColor = TextMuted, - cursorColor = BrandOrange, - focusedTextColor = Color.White, - unfocusedTextColor = Color.White, - ), + textStyle = MaterialTheme.typography.bodyMedium.copy(color = appStrong()), + colors = accountTextFieldColors(), ) Text( "Fine-grained tokens should include repository read and write access for review, branch, and file edit actions.", - color = TextMuted, + color = appMuted(), style = MaterialTheme.typography.bodySmall, ) status?.let { Text( it, - color = if (login != null) BrandOrange else TextMuted, + color = if (login != null) BrandOrange else appMuted(), style = MaterialTheme.typography.labelSmall, ) } @@ -3147,16 +3139,8 @@ private fun AccountDialog( modifier = Modifier.fillMaxWidth(), singleLine = true, visualTransformation = PasswordVisualTransformation(), - textStyle = MaterialTheme.typography.bodyMedium.copy(color = Color.White), - colors = OutlinedTextFieldDefaults.colors( - focusedBorderColor = BrandOrange.copy(alpha = 0.44f), - unfocusedBorderColor = DiffLine, - focusedLabelColor = BrandOrange, - unfocusedLabelColor = TextMuted, - cursorColor = BrandOrange, - focusedTextColor = Color.White, - unfocusedTextColor = Color.White, - ), + textStyle = MaterialTheme.typography.bodyMedium.copy(color = appStrong()), + colors = accountTextFieldColors(), ) Row(horizontalArrangement = Arrangement.spacedBy(7.dp)) { FilterButton( @@ -3176,7 +3160,7 @@ private fun AccountDialog( SectionTitle("Supabase Sync") Text( "Matches the web user_preferences row for default repo, recent repos, and saved pulls. Add diff://auth/callback to Supabase redirect URLs.", - color = TextMuted, + color = appMuted(), style = MaterialTheme.typography.bodySmall, ) OutlinedTextField( @@ -3186,7 +3170,7 @@ private fun AccountDialog( modifier = Modifier.fillMaxWidth(), singleLine = true, visualTransformation = PasswordVisualTransformation(), - textStyle = MaterialTheme.typography.bodyMedium.copy(color = Color.White), + textStyle = MaterialTheme.typography.bodyMedium.copy(color = appStrong()), colors = accountTextFieldColors(), ) OutlinedTextField( @@ -3196,7 +3180,7 @@ private fun AccountDialog( modifier = Modifier.fillMaxWidth(), singleLine = true, visualTransformation = PasswordVisualTransformation(), - textStyle = MaterialTheme.typography.bodyMedium.copy(color = Color.White), + textStyle = MaterialTheme.typography.bodyMedium.copy(color = appStrong()), colors = accountTextFieldColors(), ) OutlinedTextField( @@ -3206,7 +3190,7 @@ private fun AccountDialog( modifier = Modifier.fillMaxWidth(), singleLine = true, visualTransformation = PasswordVisualTransformation(), - textStyle = MaterialTheme.typography.bodyMedium.copy(color = Color.White), + textStyle = MaterialTheme.typography.bodyMedium.copy(color = appStrong()), colors = accountTextFieldColors(), ) OutlinedTextField( @@ -3215,7 +3199,7 @@ private fun AccountDialog( label = { Text("Supabase access token") }, modifier = Modifier.fillMaxWidth(), singleLine = true, - textStyle = MaterialTheme.typography.bodyMedium.copy(color = Color.White), + textStyle = MaterialTheme.typography.bodyMedium.copy(color = appStrong()), colors = accountTextFieldColors(), ) OutlinedTextField( @@ -3224,7 +3208,7 @@ private fun AccountDialog( label = { Text("Supabase refresh token") }, modifier = Modifier.fillMaxWidth(), singleLine = true, - textStyle = MaterialTheme.typography.bodyMedium.copy(color = Color.White), + textStyle = MaterialTheme.typography.bodyMedium.copy(color = appStrong()), colors = accountTextFieldColors(), ) val canSync = draftSupabaseUrl.isNotBlank() && @@ -3297,7 +3281,7 @@ private fun AccountDialog( is LoadState.Failed -> syncState.message else -> if (supabaseConfig.isComplete) "Sync ready" else "Sync not configured" }, - color = TextMuted, + color = appMuted(), style = MaterialTheme.typography.labelSmall, modifier = Modifier.weight(1f), ) @@ -3310,7 +3294,7 @@ private fun AccountDialog( ) Text( "Keeps token, clears saved pulls and repo history", - color = TextMuted, + color = appMuted(), style = MaterialTheme.typography.labelSmall, modifier = Modifier.weight(1f), ) diff --git a/app/src/test/java/com/bniladridas/diff/ui/MarkdownPreviewTest.kt b/app/src/test/java/com/bniladridas/diff/ui/MarkdownPreviewTest.kt new file mode 100644 index 0000000..ba5c2b4 --- /dev/null +++ b/app/src/test/java/com/bniladridas/diff/ui/MarkdownPreviewTest.kt @@ -0,0 +1,44 @@ +package com.bniladridas.diff.ui + +import org.junit.Assert.assertEquals +import org.junit.Test + +class MarkdownPreviewTest { + @Test + fun `hides markdown image urls behind badge labels`() { + val body = """ + ![CI badge](https://img.shields.io/badge/ci-passing-brightgreen) + Ready for review. + """.trimIndent() + + assertEquals( + "badge: CI\nReady for review.", + markdownBodyPreview(body, "empty"), + ) + } + + @Test + fun `uses markdown link text instead of raw urls`() { + val body = "See [release notes](https://github.com/example/repo/releases/tag/v1) and https://github.com/example/repo." + + assertEquals( + "See release notes and github.com", + markdownBodyPreview(body, "empty"), + ) + } + + @Test + fun `hides html image badges behind alt labels`() { + val body = """Status badge""" + + assertEquals( + "badge: Status", + markdownBodyPreview(body, "empty"), + ) + } + + @Test + fun `keeps blank previews calm`() { + assertEquals("No comment body.", markdownBodyPreview(" ", "No comment body.")) + } +}