feat(desktop): Messages-style headers across every remaining screen

Rolled the compact header pattern out to the screens the earlier pass
missed. Each one now lives on the same padding/typography rhythm as
Messages: Row(fillMaxWidth, padding horizontal = 12, vertical = 8),
titleMedium for labels, IconButton size = 32dp with 20dp icons tinted
with colorScheme.primary.

- ThreadScreen: back button + "Thread" title. Was headlineMedium with
  bottom-only padding; now compact row.
- UserProfileScreen: back + "Profile" title on the left, Edit /
  Follow buttons kept on the right (they stay as text buttons — they
  represent destructive intent, not icon affordances).
- ArticleReaderScreen: back + "Article" title; zoom % label still
  surfaces next to the title when != 100%.
- ArticleEditorScreen: back IconButton + "Article" title (was a text
  "Back" OutlinedButton with no title). Save / Publish buttons kept
  on the right — same reasoning as UserProfileScreen.
- ChessScreen: back (conditional) + "Chess" / "Live Game" title;
  refresh + New Game converted from Button → IconButton so the
  header reads at the same scale as the rest.
- RelayDashboardScreen: had no header at all, just a PrimaryTabRow.
  Converted Monitor / Configure to FilterChip tabs-first (matching
  Feed / Reads) so the selected tab is the title.

FeedHeader relocation:
- Moved commons/ui/feed/FeedHeader.kt → desktopApp/ui/FeedHeader.kt.
  Only NotificationsScreen.kt referenced it, and dropping the import
  from that file was enough because it's now in the same package.
- Removed the empty commons/ui/feed/ directory.

https://claude.ai/code/session_01NufduPfZvYQVYwLkbCjCUo
This commit is contained in:
Claude
2026-04-24 14:38:43 +00:00
parent b198385849
commit ff12b6abbe
8 changed files with 92 additions and 49 deletions
@@ -31,6 +31,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
@@ -38,7 +39,6 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@@ -179,42 +179,49 @@ fun ChessScreen(
var showNewGameDialog by remember { mutableStateOf(false) } var showNewGameDialog by remember { mutableStateOf(false) }
Column(modifier = Modifier.fillMaxSize()) { Column(modifier = Modifier.fillMaxSize()) {
// Header // Header — Messages-style: compact row, titleMedium title, icon-only actions
Row( Row(
modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp), modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
if (selectedGameId != null) { if (selectedGameId != null) {
IconButton(onClick = { viewModel.selectGame(null) }) { IconButton(onClick = { viewModel.selectGame(null) }, modifier = Modifier.size(32.dp)) {
Icon(MaterialSymbols.AutoMirrored.ArrowBack, "Back to list") Icon(
MaterialSymbols.AutoMirrored.ArrowBack,
contentDescription = "Back to list",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(20.dp),
)
} }
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
} }
Text( Text(
if (selectedGameId != null) "Live Game" else "Chess", if (selectedGameId != null) "Live Game" else "Chess",
style = MaterialTheme.typography.headlineMedium, style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onBackground, color = MaterialTheme.colorScheme.onBackground,
) )
} }
if (selectedGameId == null) { if (selectedGameId == null) {
Row( Row(verticalAlignment = Alignment.CenterVertically) {
horizontalArrangement = Arrangement.spacedBy(8.dp), IconButton(onClick = { viewModel.forceRefresh() }, modifier = Modifier.size(32.dp)) {
verticalAlignment = Alignment.CenterVertically, Icon(
) { MaterialSymbols.Refresh,
// Refresh button contentDescription = "Refresh",
IconButton(onClick = { viewModel.forceRefresh() }) { tint = MaterialTheme.colorScheme.primary,
Icon(MaterialSymbols.Refresh, "Refresh") modifier = Modifier.size(20.dp),
)
} }
// New Game button
if (!account.isReadOnly) { if (!account.isReadOnly) {
Button(onClick = { showNewGameDialog = true }) { IconButton(onClick = { showNewGameDialog = true }, modifier = Modifier.size(32.dp)) {
Icon(MaterialSymbols.Add, "New Game") Icon(
Spacer(Modifier.width(8.dp)) MaterialSymbols.Add,
Text("New Game") contentDescription = "New Game",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(20.dp),
)
} }
} }
} }
@@ -29,12 +29,15 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -63,6 +66,8 @@ import com.vitorpamplona.amethyst.commons.compose.editor.MarkdownEditorState
import com.vitorpamplona.amethyst.commons.compose.editor.MarkdownToolbar import com.vitorpamplona.amethyst.commons.compose.editor.MarkdownToolbar
import com.vitorpamplona.amethyst.commons.compose.editor.MetadataPanel import com.vitorpamplona.amethyst.commons.compose.editor.MetadataPanel
import com.vitorpamplona.amethyst.commons.compose.markdown.RenderMarkdown import com.vitorpamplona.amethyst.commons.compose.markdown.RenderMarkdown
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.model.nip23LongContent.LongFormPublishAction import com.vitorpamplona.amethyst.commons.model.nip23LongContent.LongFormPublishAction
import com.vitorpamplona.amethyst.desktop.account.AccountState import com.vitorpamplona.amethyst.desktop.account.AccountState
import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager
@@ -227,24 +232,41 @@ fun ArticleEditorScreen(
} }
}, },
) { ) {
// Top bar // Header — Messages-style: back + titleMedium on left, actions on right
Row( Row(
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp), modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
OutlinedButton(onClick = onBack) { Row(verticalAlignment = Alignment.CenterVertically) {
Text("Back") IconButton(onClick = onBack, modifier = Modifier.size(32.dp)) {
Icon(
MaterialSymbols.AutoMirrored.ArrowBack,
contentDescription = "Back",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(20.dp),
)
}
Spacer(Modifier.width(8.dp))
Text(
"Article",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onBackground,
)
} }
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
saveMessage?.let { saveMessage?.let {
Text( Text(
it, it,
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.align(Alignment.CenterVertically),
) )
} }
// Save/Publish stay as text buttons — they're primary destructive
// actions, not affordances you'd reduce to an icon.
OutlinedButton(onClick = { saveDraft() }) { OutlinedButton(onClick = { saveDraft() }) {
Text("Save") Text("Save")
} }
@@ -404,24 +404,25 @@ fun ArticleReaderScreen(
} }
}, },
) { ) {
// Top bar: back + bookmark placeholder // Header — Messages-style: compact row, titleMedium title
Row( Row(
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp), modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
IconButton(onClick = onBack) { IconButton(onClick = onBack, modifier = Modifier.size(32.dp)) {
Icon( Icon(
MaterialSymbols.AutoMirrored.ArrowBack, MaterialSymbols.AutoMirrored.ArrowBack,
contentDescription = "Back", contentDescription = "Back",
modifier = Modifier.size(24.dp), tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(20.dp),
) )
} }
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
Text( Text(
"Article", "Article",
style = MaterialTheme.typography.headlineMedium, style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onBackground, color = MaterialTheme.colorScheme.onBackground,
) )
if (zoomLevel != 1.0f) { if (zoomLevel != 1.0f) {
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.commons.ui.feed package com.vitorpamplona.amethyst.desktop.ui
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@@ -56,7 +56,6 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.rememberMaterialSymbolPa
import com.vitorpamplona.amethyst.commons.state.EventCollectionState import com.vitorpamplona.amethyst.commons.state.EventCollectionState
import com.vitorpamplona.amethyst.commons.ui.components.EmptyState import com.vitorpamplona.amethyst.commons.ui.components.EmptyState
import com.vitorpamplona.amethyst.commons.ui.components.LoadingState import com.vitorpamplona.amethyst.commons.ui.components.LoadingState
import com.vitorpamplona.amethyst.commons.ui.feed.FeedHeader
import com.vitorpamplona.amethyst.commons.util.toTimeAgo import com.vitorpamplona.amethyst.commons.util.toTimeAgo
import com.vitorpamplona.amethyst.desktop.account.AccountState import com.vitorpamplona.amethyst.desktop.account.AccountState
import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache
@@ -198,22 +198,23 @@ fun ThreadScreen(
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
Column(modifier = Modifier.fillMaxSize()) { Column(modifier = Modifier.fillMaxSize()) {
// Header with back button // Header — Messages-style: compact row with back + titleMedium
Row( Row(
modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp), modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
IconButton(onClick = onBack) { IconButton(onClick = onBack, modifier = Modifier.size(32.dp)) {
Icon( Icon(
MaterialSymbols.AutoMirrored.ArrowBack, MaterialSymbols.AutoMirrored.ArrowBack,
contentDescription = "Back", contentDescription = "Back",
modifier = Modifier.size(24.dp), tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(20.dp),
) )
} }
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
Text( Text(
"Thread", "Thread",
style = MaterialTheme.typography.headlineMedium, style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onBackground, color = MaterialTheme.colorScheme.onBackground,
) )
} }
@@ -458,21 +458,26 @@ fun UserProfileScreen(
) )
} }
// Header with back button // Header — Messages-style: compact row, titleMedium title
item(key = "header") { item(key = "header") {
Row( Row(
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp), modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
IconButton(onClick = onBack) { IconButton(onClick = onBack, modifier = Modifier.size(32.dp)) {
Icon(MaterialSymbols.AutoMirrored.ArrowBack, "Back") Icon(
MaterialSymbols.AutoMirrored.ArrowBack,
contentDescription = "Back",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(20.dp),
)
} }
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
Text( Text(
"Profile", "Profile",
style = MaterialTheme.typography.headlineMedium, style = MaterialTheme.typography.titleMedium,
) )
} }
@@ -20,11 +20,13 @@
*/ */
package com.vitorpamplona.amethyst.desktop.ui.relay package com.vitorpamplona.amethyst.desktop.ui.relay
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.PrimaryTabRow import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Tab import androidx.compose.material3.FilterChip
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@@ -34,7 +36,9 @@ import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.model.nip65RelayList.Nip65RelayListState import com.vitorpamplona.amethyst.commons.model.nip65RelayList.Nip65RelayListState
import com.vitorpamplona.amethyst.desktop.model.DesktopAccountRelays import com.vitorpamplona.amethyst.desktop.model.DesktopAccountRelays
import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager
@@ -50,7 +54,6 @@ enum class DashboardTab(
CONFIGURE("Configure"), CONFIGURE("Configure"),
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun RelayDashboardScreen( fun RelayDashboardScreen(
relayManager: DesktopRelayConnectionManager, relayManager: DesktopRelayConnectionManager,
@@ -79,12 +82,17 @@ fun RelayDashboardScreen(
} }
Column(modifier = modifier.fillMaxSize()) { Column(modifier = modifier.fillMaxSize()) {
PrimaryTabRow(selectedTabIndex = DashboardTab.entries.indexOf(selectedTab)) { // Header — Messages-style tabs-first: selected chip acts as the screen title.
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
DashboardTab.entries.forEach { tab -> DashboardTab.entries.forEach { tab ->
Tab( FilterChip(
selected = selectedTab == tab, selected = selectedTab == tab,
onClick = { selectedTab = tab }, onClick = { selectedTab = tab },
text = { Text(tab.label) }, label = { Text(tab.label) },
) )
} }
} }