feat(git): New Issue is a full screen; fix square FAB shape

- Converts the New Issue composer from an AlertDialog to a dedicated screen
  (new Route.GitRepositoryNewIssue + GitNewIssueScreen with its own top bar
  and a Create action). The Issues FAB now navigates to it.
- The extended FAB rendered square because the app theme sets shapes.large
  (the extended-FAB default shape) to 0.dp; pin an explicit RoundedCornerShape.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
This commit is contained in:
Claude
2026-06-29 22:06:02 +00:00
parent 3e0c688b8c
commit 2f9162a96f
4 changed files with 109 additions and 62 deletions
@@ -134,6 +134,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.FollowPack
import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.list.FollowPacksScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.GeoHashPostScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.GeoHashScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.GitNewIssueScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.GitRepositoryCodeScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.GitRepositoryIssuesScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.GitRepositoryPullsScreen
@@ -496,6 +497,7 @@ fun BuildNavigation(
composableFromEndArgs<Route.GitRepositoryCode> { GitRepositoryCodeScreen(Address(it.kind, it.pubKeyHex, it.dTag), accountViewModel, nav) }
composableFromEndArgs<Route.GitRepositoryIssues> { GitRepositoryIssuesScreen(Address(it.kind, it.pubKeyHex, it.dTag), accountViewModel, nav) }
composableFromEndArgs<Route.GitRepositoryPulls> { GitRepositoryPullsScreen(Address(it.kind, it.pubKeyHex, it.dTag), accountViewModel, nav) }
composableFromEndArgs<Route.GitRepositoryNewIssue> { GitNewIssueScreen(Address(it.kind, it.pubKeyHex, it.dTag), accountViewModel, nav) }
composableFromEndArgs<Route.FollowPack> { FollowPackFeedScreen(Address(it.kind, it.pubKeyHex, it.dTag), accountViewModel, nav) }
composableFromEndArgs<Route.Room> { ChatroomScreen(it.toKey(), it.message, it.attachment, it.replyId, it.draftId, it.expiresDays, accountViewModel, nav) }
@@ -559,6 +559,18 @@ sealed class Route {
)
}
@Serializable data class GitRepositoryNewIssue(
val kind: Int,
val pubKeyHex: HexKey,
val dTag: String,
) : Route() {
constructor(address: Address) : this(
kind = address.kind,
pubKeyHex = address.pubKeyHex,
dTag = address.dTag,
)
}
@Serializable data class FollowPack(
val kind: Int,
val pubKeyHex: HexKey,
@@ -22,9 +22,14 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.material3.AlertDialog
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
@@ -33,78 +38,109 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.ui.layouts.LocalDisappearingScaffoldPadding
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.topbars.ShorterTopAppBar
import com.vitorpamplona.amethyst.ui.navigation.topbars.TitleIconModifier
import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent
import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent
/**
* A minimal "New issue" composer: a subject and a markdown body. On submit it
* builds a NIP-34 [GitIssueEvent] addressed to [repoNote], signs it with the
* account signer and broadcasts it. The repository owner is notified via the `a`
* tag the builder adds automatically.
*/
@Composable
fun GitNewIssueDialog(
fun GitNewIssueScreen(
address: Address,
accountViewModel: AccountViewModel,
nav: INav,
) {
LoadAddressableNote(address, accountViewModel) { note ->
note?.let { GitNewIssueForm(it, accountViewModel, nav) }
}
}
/**
* Full-screen "New issue" composer: a subject and a markdown body. On submit it builds a
* NIP-34 [GitIssueEvent] addressed to [repoNote], signs it with the account signer and
* broadcasts it, then returns to the issues list.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun GitNewIssueForm(
repoNote: AddressableNote,
accountViewModel: AccountViewModel,
onDismiss: () -> Unit,
nav: INav,
) {
var subject by rememberSaveable { mutableStateOf("") }
var body by rememberSaveable { mutableStateOf("") }
var labels by rememberSaveable { mutableStateOf("") }
AlertDialog(
onDismissRequest = onDismiss,
icon = { Icon(MaterialSymbols.Description, contentDescription = null, modifier = Modifier.size(24.dp)) },
title = { Text(stringRes(R.string.git_new_issue_title)) },
text = {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(
value = subject,
onValueChange = { subject = it },
label = { Text(stringRes(R.string.git_new_issue_subject)) },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
)
OutlinedTextField(
value = body,
onValueChange = { body = it },
label = { Text(stringRes(R.string.git_new_issue_body)) },
minLines = 4,
modifier = Modifier.fillMaxWidth(),
)
OutlinedTextField(
value = labels,
onValueChange = { labels = it },
label = { Text(stringRes(R.string.git_new_issue_labels)) },
placeholder = { Text(stringRes(R.string.git_new_issue_labels_hint)) },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
)
}
},
confirmButton = {
TextButton(
enabled = subject.isNotBlank(),
onClick = {
sendGitIssue(accountViewModel, repoNote, subject.trim(), body.trim(), parseLabels(labels))
onDismiss()
DisappearingScaffold(
isInvertedLayout = false,
topBar = {
ShorterTopAppBar(
title = { Text(stringRes(R.string.git_new_issue_title)) },
navigationIcon = {
Row(TitleIconModifier, verticalAlignment = Alignment.CenterVertically) {
IconButton(onClick = nav::popBack) { ArrowBackIcon() }
}
},
) {
Text(stringRes(R.string.git_new_issue_create))
}
actions = {
TextButton(
enabled = subject.isNotBlank(),
onClick = {
sendGitIssue(accountViewModel, repoNote, subject.trim(), body.trim(), parseLabels(labels))
nav.popBack()
},
) {
Text(stringRes(R.string.git_new_issue_create))
}
},
)
},
dismissButton = {
TextButton(onClick = onDismiss) { Text(stringRes(R.string.git_new_issue_cancel)) }
},
)
accountViewModel = accountViewModel,
) {
Column(
modifier =
Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(LocalDisappearingScaffoldPadding.current)
.padding(horizontal = 16.dp, vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
OutlinedTextField(
value = subject,
onValueChange = { subject = it },
label = { Text(stringRes(R.string.git_new_issue_subject)) },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
)
OutlinedTextField(
value = body,
onValueChange = { body = it },
label = { Text(stringRes(R.string.git_new_issue_body)) },
minLines = 6,
modifier = Modifier.fillMaxWidth(),
)
OutlinedTextField(
value = labels,
onValueChange = { labels = it },
label = { Text(stringRes(R.string.git_new_issue_labels)) },
placeholder = { Text(stringRes(R.string.git_new_issue_labels_hint)) },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
)
}
}
}
/** Splits a free-text label field on commas/whitespace, stripping any leading `#`. */
@@ -412,8 +412,6 @@ private fun GitRepositoryIssues(
val event by observeNoteEvent<GitRepositoryEvent>(note, accountViewModel)
RepoContentSubscription(note, event, accountViewModel)
var showNewIssue by rememberSaveable(note.idHex) { mutableStateOf(false) }
StatusFeedScreen(
persistKey = note.idHex + "GitRepoIssuesStatus",
event = event,
@@ -422,12 +420,8 @@ private fun GitRepositoryIssues(
closedViewModel = closedViewModel,
accountViewModel = accountViewModel,
nav = nav,
floatingButton = if (event != null) ({ NewIssueFab { showNewIssue = true } }) else null,
floatingButton = if (event != null) ({ NewIssueFab { nav.nav(Route.GitRepositoryNewIssue(note.address)) } }) else null,
)
if (showNewIssue && event != null) {
GitNewIssueDialog(repoNote = note, accountViewModel = accountViewModel, onDismiss = { showNewIssue = false })
}
}
@Composable
@@ -720,8 +714,11 @@ private fun StatusFilterChips(
@Composable
private fun NewIssueFab(onClick: () -> Unit) {
// The app theme overrides shapes.large (the extended-FAB default) to 0.dp, which makes the
// FAB square — so pin an explicit rounded shape here.
ExtendedFloatingActionButton(
onClick = onClick,
shape = RoundedCornerShape(16.dp),
icon = { Icon(MaterialSymbols.Add, contentDescription = null, modifier = Modifier.size(20.dp)) },
text = { Text(stringRes(R.string.git_new_issue_button)) },
)