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 = """
+ 
+ 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 = """
"""
+
+ assertEquals(
+ "badge: Status",
+ markdownBodyPreview(body, "empty"),
+ )
+ }
+
+ @Test
+ fun `keeps blank previews calm`() {
+ assertEquals("No comment body.", markdownBodyPreview(" ", "No comment body."))
+ }
+}