feat(desktop): tabs-first header for Home / Reads, compact Notifications header

Refactored the three feed screen headers so they all match the Messages
pattern — single compact row padded horizontal = 12, vertical = 8, no
oversized titles, no multi-row metadata.

FeedScreen (Home):
- Replaced the `headlineMedium` "Global Feed" / "Following Feed" title
  plus a redundant Global/Following chip row plus a separate relay-count
  meta-line plus a large "New Post" button with:
    [ Following | Global ]   [relays] [refresh] [+ compose]
  The selected tab IS the screen title. Relay count + followed-user
  count surface through a hover tooltip on the relays icon (desktop
  convention; info is preserved without taking a whole row).
- Relays icon click opens the picker when the account can edit, falls
  through to navigate-to-relays otherwise.

ReadsScreen:
- Same treatment: dropped the "Reads" label, lifted Following/Global
  chips to the left, single refresh icon on the right. "N relays
  connected" moved to the refresh button's content description.

commons/FeedHeader (used by NotificationsScreen):
- Dropped the RelayStatusIndicator (icon + count text + refresh button,
  took 3 slots). Now just titleMedium on the left + a single refresh
  IconButton on the right with relay count in its content description,
  matching the Messages "titleMedium + icon buttons" rhythm.
- Internal padding(horizontal = 12, vertical = 8) baked in so callers
  don't need a trailing Spacer.

https://claude.ai/code/session_01NufduPfZvYQVYwLkbCjCUo
This commit is contained in:
Claude
2026-04-24 14:23:32 +00:00
parent 8fb16bc7a3
commit b198385849
4 changed files with 134 additions and 189 deletions
@@ -20,12 +20,11 @@
*/
package com.vitorpamplona.amethyst.desktop.ui
import androidx.compose.foundation.clickable
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.TooltipArea
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@@ -33,15 +32,14 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.FilterChip
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
@@ -489,7 +487,6 @@ fun FeedScreen(
onDispose { subId?.let { coordinator.releaseInteractions(it) } }
}
@OptIn(ExperimentalLayoutApi::class)
Box(modifier = Modifier.fillMaxSize()) {
Column(modifier = Modifier.fillMaxSize()) {
// Header with compose button
@@ -629,9 +626,15 @@ fun FeedScreen(
}
/**
* Feed header with title, mode selector, relay count, and compose button.
* Feed header \u2014 Messages-style single row: Following/Global tabs on the left,
* compact icon buttons on the right (relays, refresh, compose). The selected
* tab IS the title; no redundant "Global Feed" / "Following Feed" label.
*
* Relay count and followed-users count are surfaced via a hover tooltip on
* the relays icon (desktop convention \u2014 the at-a-glance info is preserved
* without stealing header real estate).
*/
@OptIn(ExperimentalLayoutApi::class)
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun FeedHeader(
feedMode: FeedMode,
@@ -644,90 +647,96 @@ private fun FeedHeader(
onNavigateToRelays: () -> Unit = {},
onOpenRelayPicker: () -> Unit = {},
) {
FlowRow(
modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp),
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Column {
FlowRow(
verticalArrangement = Arrangement.Center,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(
if (feedMode == FeedMode.GLOBAL) "Global Feed" else "Following Feed",
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onBackground,
// Tabs \u2014 the selected one is the screen title.
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
if (account != null) {
FilterChip(
selected = feedMode == FeedMode.FOLLOWING,
onClick = { onFeedModeChange(FeedMode.FOLLOWING) },
label = { Text("Following") },
)
if (account != null) {
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
FilterChip(
selected = feedMode == FeedMode.GLOBAL,
onClick = { onFeedModeChange(FeedMode.GLOBAL) },
label = { Text("Global") },
)
FilterChip(
selected = feedMode == FeedMode.FOLLOWING,
onClick = { onFeedModeChange(FeedMode.FOLLOWING) },
label = { Text("Following") },
)
}
}
}
Spacer(Modifier.height(4.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
"${feedRelays.size} relays",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.primary,
modifier =
Modifier.clickable { onNavigateToRelays() },
)
if (feedMode == FeedMode.FOLLOWING) {
Text(
" \u2022 $followedUsersCount followed",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
Spacer(Modifier.width(8.dp))
if (account != null && !account.isReadOnly) {
IconButton(
onClick = onOpenRelayPicker,
modifier = Modifier.size(24.dp),
) {
Icon(
MaterialSymbols.Dns,
contentDescription = "Edit Feed Relays",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(18.dp),
)
}
Spacer(Modifier.width(4.dp))
}
IconButton(
onClick = onRefresh,
modifier = Modifier.size(24.dp),
) {
Icon(
MaterialSymbols.Refresh,
contentDescription = "Refresh",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(18.dp),
)
}
}
FilterChip(
selected = feedMode == FeedMode.GLOBAL,
onClick = { onFeedModeChange(FeedMode.GLOBAL) },
label = { Text("Global") },
)
}
Button(
onClick = onCompose,
enabled = account != null && !account.isReadOnly,
) {
Icon(MaterialSymbols.Add, "New Post", Modifier.size(18.dp))
Spacer(Modifier.width(8.dp))
Text("New Post")
// Actions \u2014 compact icon buttons at the same scale as the Messages header.
Row(verticalAlignment = Alignment.CenterVertically) {
val relaysTooltip =
buildString {
append("${feedRelays.size} relays")
if (feedMode == FeedMode.FOLLOWING) {
append(" \u2022 $followedUsersCount followed")
}
}
TooltipArea(
tooltip = {
Surface(
shape = MaterialTheme.shapes.small,
color = MaterialTheme.colorScheme.surfaceContainerHigh,
tonalElevation = 2.dp,
shadowElevation = 4.dp,
) {
Text(
relaysTooltip,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
style = MaterialTheme.typography.bodySmall,
)
}
},
) {
IconButton(
onClick = {
if (account != null && !account.isReadOnly) {
onOpenRelayPicker()
} else {
onNavigateToRelays()
}
},
modifier = Modifier.size(32.dp),
) {
Icon(
MaterialSymbols.Dns,
contentDescription = relaysTooltip,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(20.dp),
)
}
}
IconButton(
onClick = onRefresh,
modifier = Modifier.size(32.dp),
) {
Icon(
MaterialSymbols.Refresh,
contentDescription = "Refresh",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(20.dp),
)
}
if (account != null && !account.isReadOnly) {
IconButton(
onClick = onCompose,
modifier = Modifier.size(32.dp),
) {
Icon(
MaterialSymbols.Add,
contentDescription = "New Post",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(20.dp),
)
}
}
}
}
}
@@ -245,8 +245,6 @@ fun NotificationsScreen(
onRefresh = { relayManager.connect() },
)
Spacer(Modifier.height(16.dp))
if (connectedRelays.isEmpty()) {
LoadingState("Connecting to relays...")
} else if (notifications.isEmpty() && !initialLoadComplete) {
@@ -23,8 +23,6 @@ package com.vitorpamplona.amethyst.desktop.ui
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@@ -32,7 +30,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Card
@@ -290,70 +287,45 @@ fun ReadsScreen(
}
}
@OptIn(ExperimentalLayoutApi::class)
Column(modifier = Modifier.fillMaxSize()) {
// Header — wraps on narrow columns
FlowRow(
// Header — Messages-style: tabs left, refresh right. The selected tab
// (Following / Global) acts as the screen title, so no separate label.
Row(
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Column {
FlowRow(
verticalArrangement = Arrangement.Center,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(
"Reads",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onBackground,
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
if (account != null) {
FilterChip(
selected = feedMode == FeedMode.FOLLOWING,
onClick = { feedMode = FeedMode.FOLLOWING },
label = { Text("Following") },
)
// Feed mode selector
if (account != null) {
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
FilterChip(
selected = feedMode == FeedMode.GLOBAL,
onClick = { feedMode = FeedMode.GLOBAL },
label = { Text("Global") },
)
FilterChip(
selected = feedMode == FeedMode.FOLLOWING,
onClick = { feedMode = FeedMode.FOLLOWING },
label = { Text("Following") },
)
}
}
}
FilterChip(
selected = feedMode == FeedMode.GLOBAL,
onClick = { feedMode = FeedMode.GLOBAL },
label = { Text("Global") },
)
}
Spacer(Modifier.height(4.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
"${connectedRelays.size} relays connected",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Spacer(Modifier.width(8.dp))
IconButton(
onClick = { relayManager.connect() },
modifier = Modifier.size(24.dp),
) {
Icon(
MaterialSymbols.Refresh,
contentDescription = "Refresh",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(18.dp),
)
}
}
IconButton(
onClick = { relayManager.connect() },
modifier = Modifier.size(32.dp),
) {
Icon(
MaterialSymbols.Refresh,
contentDescription = "Refresh (${connectedRelays.size} relays connected)",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(20.dp),
)
}
}
Spacer(Modifier.height(8.dp))
when {
connectedRelays.isEmpty() -> {
LoadingState("Connecting to relays...")