Merge pull request #3544 from vitorpamplona/claude/pr-note-button-sizing-732r64

Clean up Git card styling and fix duplicated subject in content
This commit is contained in:
Vitor Pamplona
2026-07-12 21:28:11 -04:00
committed by GitHub
3 changed files with 64 additions and 33 deletions
@@ -21,12 +21,10 @@
package com.vitorpamplona.amethyst.ui.note.types
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
@@ -85,7 +83,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.repoHasFetchableClo
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.Font12SP
import com.vitorpamplona.amethyst.ui.theme.HalfDoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.amethyst.ui.theme.Size16dp
import com.vitorpamplona.amethyst.ui.theme.Size5dp
@@ -93,7 +90,6 @@ import com.vitorpamplona.amethyst.ui.theme.Size8dp
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.grayText
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags
import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent
import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent
@@ -102,9 +98,7 @@ import com.vitorpamplona.quartz.nip34Git.pr.GitPullRequestEvent
import com.vitorpamplona.quartz.nip34Git.pr.GitPullRequestUpdateEvent
import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent
private val CardShape = QuoteBorder
private val ChipShape = RoundedCornerShape(8.dp)
private val CardPadding = PaddingValues(start = Size10dp, top = Size10dp, end = Size10dp, bottom = Size5dp)
private val HeaderSpacing = Arrangement.spacedBy(Size8dp)
private val LinkRowSpacing = Arrangement.spacedBy(Size8dp)
@@ -113,15 +107,7 @@ private fun GitCardContainer(
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
) {
val border = MaterialTheme.colorScheme.subtleBorder
Column(
modifier =
modifier
.fillMaxWidth()
.clip(CardShape)
.border(1.dp, border, CardShape)
.padding(CardPadding),
) {
Column(modifier = modifier.fillMaxWidth()) {
content()
}
}
@@ -261,12 +247,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 +282,10 @@ private fun GitMarkdownBody(
backgroundColor: MutableState<Color>,
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 +544,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 +676,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)
@@ -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))
}
}
}
@@ -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))
}
}
}