From 97c7fbfe80f2a207d054a06f34e447fb5bc241e0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Jul 2026 02:59:08 +0000 Subject: [PATCH] fix: compact git-card action buttons and dedupe issue/PR subject heading The NIP-34 status buttons (Mark merged / Close / Reopen) and the PR View-changes button used default Material3 sizing, which reads oversized inside a note card. They now use the same 32dp compact height, tighter content padding and labelMedium text as the app-recommendation buttons. Issue and PR cards also showed the title twice when the authoring client duplicated the subject tag as a leading markdown heading in the content (gitworkshop/ngit do this). The body renderer now strips that first heading when it matches the subject already rendered as the card title. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01USiovhenaijgVEFFzASgfy --- .../amethyst/ui/note/types/Git.kt | 29 ++++++++++++++--- .../ui/note/types/GitPullRequestChanges.kt | 20 ++++++++---- .../ui/note/types/GitStatusActions.kt | 32 ++++++++++++++----- 3 files changed, 63 insertions(+), 18 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt index 99fb0abf00..218f677add 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt @@ -261,12 +261,31 @@ private fun GitMetaRow( /** Shortens a 40-char git object id to its 7-char prefix for display. */ private fun String.shortCommit(): String = if (length > 7) take(7) else this +/** + * Some NIP-34 clients duplicate the subject tag as a leading markdown heading of the + * content ("# Title\n\n…"). The card already renders the subject as its own title, so + * when the first line is a heading matching [subject], drop it from the body. + */ +private fun stripDuplicatedSubject( + content: String, + subject: String?, +): String { + if (subject.isNullOrBlank()) return content + val trimmed = content.trimStart() + if (!trimmed.startsWith("#")) return content + val firstLineEnd = trimmed.indexOf('\n').let { if (it < 0) trimmed.length else it } + val heading = trimmed.substring(0, firstLineEnd).trimStart('#').trim() + if (!heading.equals(subject.trim(), ignoreCase = true)) return content + return trimmed.substring(firstLineEnd).trimStart() +} + /** * The shared markdown body used by patches, issues and pull requests: honors * the collapsed [makeItShort] preview for the logged-in user's own posts and * otherwise renders the full content with sensitivity warnings and uncited * hashtags. The subject, when present, is rendered separately as a title by the - * caller, so it is not inlined here. + * caller, so it is not inlined here — pass it as [renderedSubject] so a copy + * duplicated into the content as a leading heading is stripped. */ @Composable private fun GitMarkdownBody( @@ -277,8 +296,10 @@ private fun GitMarkdownBody( backgroundColor: MutableState, accountViewModel: AccountViewModel, nav: INav, + renderedSubject: String? = null, ) { - LoadDecryptedContent(note, accountViewModel) { body -> + LoadDecryptedContent(note, accountViewModel) { rawBody -> + val body = remember(rawBody, renderedSubject) { stripDuplicatedSubject(rawBody, renderedSubject) } val isAuthorTheLoggedUser = remember(note.event) { accountViewModel.isLoggedUser(note.author) } @@ -537,7 +558,7 @@ private fun RenderGitIssueEvent( Spacer(modifier = HalfDoubleVertSpacer) - GitMarkdownBody(note, makeItShort, canPreview, quotesLeft, backgroundColor, accountViewModel, nav) + GitMarkdownBody(note, makeItShort, canPreview, quotesLeft, backgroundColor, accountViewModel, nav, renderedSubject = subject) if (!makeItShort) { GitStatusActions(note, accountViewModel) @@ -669,7 +690,7 @@ private fun RenderGitPullRequestEvent( Spacer(modifier = HalfDoubleVertSpacer) - GitMarkdownBody(note, makeItShort, canPreview, quotesLeft, backgroundColor, accountViewModel, nav) + GitMarkdownBody(note, makeItShort, canPreview, quotesLeft, backgroundColor, accountViewModel, nav, renderedSubject = subject) if (!makeItShort) { GitPullRequestChanges(cloneUrls, currentCommit, mergeBase, accountViewModel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/GitPullRequestChanges.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/GitPullRequestChanges.kt index 6146391da6..71148c1c5d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/GitPullRequestChanges.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/GitPullRequestChanges.kt @@ -99,9 +99,13 @@ fun GitPullRequestChanges( when (val s = state) { ChangesState.Idle -> - FilledTonalButton(onClick = { load() }, modifier = Modifier.padding(top = 8.dp)) { - Icon(MaterialSymbols.Code, contentDescription = null, modifier = Modifier.size(18.dp)) - Text(stringRes(R.string.git_pr_view_changes), modifier = Modifier.padding(start = 6.dp)) + FilledTonalButton( + onClick = { load() }, + modifier = Modifier.padding(top = 8.dp).then(CompactButtonHeight), + contentPadding = CompactButtonPadding, + ) { + Icon(MaterialSymbols.Code, contentDescription = null, modifier = Modifier.size(16.dp)) + Text(stringRes(R.string.git_pr_view_changes), style = MaterialTheme.typography.labelMedium, modifier = Modifier.padding(start = 6.dp)) } ChangesState.Loading -> @@ -129,9 +133,13 @@ fun GitPullRequestChanges( } ChangesState.Failed -> - FilledTonalButton(onClick = { load() }, modifier = Modifier.padding(top = 8.dp)) { - Icon(MaterialSymbols.Refresh, contentDescription = null, modifier = Modifier.size(18.dp)) - Text(stringRes(R.string.git_pr_changes_retry), modifier = Modifier.padding(start = 6.dp)) + FilledTonalButton( + onClick = { load() }, + modifier = Modifier.padding(top = 8.dp).then(CompactButtonHeight), + contentPadding = CompactButtonPadding, + ) { + Icon(MaterialSymbols.Refresh, contentDescription = null, modifier = Modifier.size(16.dp)) + Text(stringRes(R.string.git_pr_changes_retry), style = MaterialTheme.typography.labelMedium, modifier = Modifier.padding(start = 6.dp)) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/GitStatusActions.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/GitStatusActions.kt index ea8cc7075a..5afc90e1e3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/GitStatusActions.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/GitStatusActions.kt @@ -22,6 +22,8 @@ package com.vitorpamplona.amethyst.ui.note.types import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.ButtonDefaults @@ -55,6 +57,10 @@ import com.vitorpamplona.quartz.nip34Git.status.GitStatusOpenEvent private enum class StatusTarget { OPEN, CLOSED, APPLIED } +/** Compact sizing shared by the small action buttons on git cards. */ +internal val CompactButtonHeight = Modifier.height(32.dp) +internal val CompactButtonPadding = PaddingValues(horizontal = 14.dp, vertical = 4.dp) + /** * NIP-34 status controls for an issue, patch or pull request. Visible only to the * people allowed to moderate the thread — the item's author and the repository @@ -85,23 +91,33 @@ fun GitStatusActions( verticalArrangement = Arrangement.spacedBy(8.dp), ) { if (closedOrApplied) { - FilledTonalButton(onClick = { sendStatus(accountViewModel, note, StatusTarget.OPEN) }) { - Icon(MaterialSymbols.RadioButtonChecked, contentDescription = null, modifier = Modifier.size(18.dp)) - Text(stringRes(R.string.git_status_reopen), modifier = Modifier.padding(start = 6.dp)) + FilledTonalButton( + onClick = { sendStatus(accountViewModel, note, StatusTarget.OPEN) }, + modifier = CompactButtonHeight, + contentPadding = CompactButtonPadding, + ) { + Icon(MaterialSymbols.RadioButtonChecked, contentDescription = null, modifier = Modifier.size(16.dp)) + Text(stringRes(R.string.git_status_reopen), style = MaterialTheme.typography.labelMedium, modifier = Modifier.padding(start = 6.dp)) } } else { if (isPatchOrPr) { - FilledTonalButton(onClick = { sendStatus(accountViewModel, note, StatusTarget.APPLIED) }) { - Icon(MaterialSymbols.Check, contentDescription = null, modifier = Modifier.size(18.dp)) - Text(stringRes(R.string.git_status_mark_merged), modifier = Modifier.padding(start = 6.dp)) + FilledTonalButton( + onClick = { sendStatus(accountViewModel, note, StatusTarget.APPLIED) }, + modifier = CompactButtonHeight, + contentPadding = CompactButtonPadding, + ) { + Icon(MaterialSymbols.Check, contentDescription = null, modifier = Modifier.size(16.dp)) + Text(stringRes(R.string.git_status_mark_merged), style = MaterialTheme.typography.labelMedium, modifier = Modifier.padding(start = 6.dp)) } } OutlinedButton( onClick = { sendStatus(accountViewModel, note, StatusTarget.CLOSED) }, colors = ButtonDefaults.outlinedButtonColors(contentColor = MaterialTheme.colorScheme.error), + modifier = CompactButtonHeight, + contentPadding = CompactButtonPadding, ) { - Icon(MaterialSymbols.Cancel, contentDescription = null, modifier = Modifier.size(18.dp)) - Text(stringRes(R.string.git_status_close), modifier = Modifier.padding(start = 6.dp)) + Icon(MaterialSymbols.Cancel, contentDescription = null, modifier = Modifier.size(16.dp)) + Text(stringRes(R.string.git_status_close), style = MaterialTheme.typography.labelMedium, modifier = Modifier.padding(start = 6.dp)) } } }