From 16030599d9ec6558bbec6699c9127901aff43e87 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 17:11:31 +0000 Subject: [PATCH 1/2] fix(concord): align top-bar height and simplify channel rows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Route the Concord community screen and hub through ShorterTopAppBar (50dp) instead of the raw Material3 TopAppBar (64dp default), so the top bar — and its 3-dot overflow menu — sits at the same height as the Buzz/relay-group community screen, which already uses the shorter bar. Also drop the recent-posters facepile from the Concord and Buzz channel rows, and move the last-message time up to the first line (next to the channel name), leaving the unread-message badge on the second line. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_017x22okBh6HbiCrJN3zf5xn --- .../ui/screen/loggedIn/buzz/BuzzImportRow.kt | 47 +++++++------------ .../concord/ConcordChannelListScreen.kt | 24 ++++------ .../concord/ConcordHomeScreen.kt | 4 +- 3 files changed, 29 insertions(+), 46 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzImportRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzImportRow.kt index 387a77b0eb..208b2b9623 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzImportRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzImportRow.kt @@ -58,26 +58,21 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUse import com.vitorpamplona.amethyst.ui.note.timeAgo import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.buzzTimelinePreviewSummary -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.ConcordAuthorFacepile import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.ConcordUnreadBadge import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupCardWarmupSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.newestTimelineNote -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.recentAuthorHexes import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.relayGroupChannelUnreadCountFlow import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip29RelayGroups.GroupId -/** How many recent-poster avatars a channel row's facepile shows at most. */ -private const val FACEPILE_MAX = 4 - /** A first screen's worth of recent messages to prefetch per visible card, so previews fill in. */ private const val CARD_WARMUP_LIMIT = 10 /** * One channel row in a Buzz workspace's community view: a channel the user is a member of (via - * kind-44100), rendered like the Concord server view — a colored monogram, the channel name with a - * recent-posters facepile, a preview of the last message (author + snippet, or the Buzz activity - * summary for system/diff/job rows), the relative time of that message, and an unread-count badge. + * kind-44100), rendered like the Concord server view — a colored monogram, the channel name with the + * last message's relative time, and below it a preview of the last message (author + snippet, or the + * Buzz activity summary for system/diff/job rows) with an unread-count badge. * Tapping the card opens the channel ([onOpen]); the row itself is a clean tap-to-open target — its * per-channel actions (Pin/Unpin, Add/Remove-from-Messages) live in the opened channel's/forum's * top-bar overflow, not on the row. A pinned channel still shows a pin marker here ([isStarred]). @@ -85,10 +80,10 @@ private const val CARD_WARMUP_LIMIT = 10 * Reused by the relay group-list screen where Buzz membership discovery is folded in. * * [showActivityPreview] gates the chat-activity machinery — the recent-message warmup, the - * last-message preview, the recent-posters facepile and the unread badge. Enable it for **chat** - * channels (whose content lives in [RelayGroupChannel.notes]); leave it off for **forum** channels, - * whose posts are threads (a separate store), so the row doesn't open a kind-9 chat subscription that - * would return nothing and drives a member-count summary instead. + * last-message preview and the unread badge. Enable it for **chat** channels (whose content lives in + * [RelayGroupChannel.notes]); leave it off for **forum** channels, whose posts are threads (a + * separate store), so the row doesn't open a kind-9 chat subscription that would return nothing and + * drives a member-count summary instead. */ @Composable fun BuzzImportRow( @@ -122,11 +117,10 @@ fun BuzzImportRow( val memberCount = channel.memberCount() val isPrivate = channel.isPrivate() - // The channel's own notes flow drives the preview/facepile so they update the moment a message - // folds in, independent of the metadata-scoped [observeChannel] above. Only collected for chat - // channels; a forum row shows a member-count summary with no facepile/unread. + // The channel's own notes flow drives the preview so it updates the moment a message folds in, + // independent of the metadata-scoped [observeChannel] above. Only collected for chat channels; a + // forum row shows a member-count summary with no unread. val lastNote: Note? - val faceAuthors: List val unread: Int if (showActivityPreview) { val notesState by channel @@ -134,14 +128,12 @@ fun BuzzImportRow( .notes.stateFlow .collectAsStateWithLifecycle() lastNote = remember(notesState) { channel.newestTimelineNote(account) } - faceAuthors = remember(notesState) { channel.recentAuthorHexes(account, FACEPILE_MAX) } unread = remember(groupId) { relayGroupChannelUnreadCountFlow(account, groupId) } .collectAsStateWithLifecycle(0) .value } else { lastNote = null - faceAuthors = emptyList() unread = 0 } val hasUnread = unread > 0 @@ -154,7 +146,6 @@ fun BuzzImportRow( isPrivate = isPrivate, memberCount = memberCount, lastNote = lastNote, - faceAuthors = faceAuthors, unread = unread, hasUnread = hasUnread, isStarred = isStarred, @@ -179,7 +170,6 @@ private fun BuzzImportRowContent( isPrivate: Boolean, memberCount: Int, lastNote: Note?, - faceAuthors: List, unread: Int, hasUnread: Boolean, isStarred: Boolean, @@ -192,8 +182,8 @@ private fun BuzzImportRowContent( BuzzImportAvatar(name = name, seed = seed) Spacer(Modifier.width(12.dp)) Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) { - // Line 1: a lock (private), the channel name, a pin marker (starred), and the recent- - // posters facepile pushed to the right. + // Line 1: a lock (private), the channel name, a pin marker (starred), and the last-message + // time pushed to the right. Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) { if (isPrivate) { Icon( @@ -219,13 +209,6 @@ private fun BuzzImportRowContent( modifier = Modifier.size(14.dp), ) } - ConcordAuthorFacepile(faceAuthors, accountViewModel) - } - // Line 2: the last-message preview, then the time + unread badge. - Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { - Box(Modifier.weight(1f)) { - BuzzChannelPreviewLine(lastNote, memberCount, accountViewModel) - } lastNote?.createdAt()?.let { ts -> Text( timeAgo(ts, LocalContext.current, prefix = ""), @@ -234,6 +217,12 @@ private fun BuzzImportRowContent( maxLines = 1, ) } + } + // Line 2: the last-message preview, then the unread-message badge. + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Box(Modifier.weight(1f)) { + BuzzChannelPreviewLine(lastNote, memberCount, accountViewModel) + } ConcordUnreadBadge(unread) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelListScreen.kt index cbda7206a3..532b172116 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelListScreen.kt @@ -46,7 +46,6 @@ import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton -import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -74,6 +73,7 @@ import com.vitorpamplona.amethyst.ui.components.util.setText import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route +import com.vitorpamplona.amethyst.ui.navigation.topbars.ShorterTopAppBar import com.vitorpamplona.amethyst.ui.note.timeAgo import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.datasource.ConcordChannelPreviewLoader @@ -223,7 +223,7 @@ fun ConcordChannelListScreen( Scaffold( topBar = { - TopAppBar( + ShorterTopAppBar( title = { Text(communityName, maxLines = 1) }, navigationIcon = { // Back arrow only when pushed from elsewhere; as a bottom-nav tab the bar takes its place. @@ -403,8 +403,6 @@ private fun ConcordChannelListRow( remember(communityId, channelKey) { concordChannelUnreadCountFlow(account, communityId, channelKey) } .collectAsStateWithLifecycle(0) val hasUnread = unread > 0 - // The recent posters' faces — recomputed as the channel's notes change (keyed on channelState). - val faceAuthors = remember(channelState) { channel.recentAuthorHexes(FACEPILE_MAX) } Row( Modifier @@ -421,7 +419,7 @@ private fun ConcordChannelListRow( tint = if (hasUnread) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant, ) Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) { - // Line 1: channel name + the recent-posters facepile pushed to the right. + // Line 1: channel name + the last-message time pushed to the right. Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { Text( channelName, @@ -431,13 +429,6 @@ private fun ConcordChannelListRow( maxLines = 1, overflow = TextOverflow.Ellipsis, ) - ConcordAuthorFacepile(faceAuthors, accountViewModel) - } - // Line 2: the last-message preview (or a live "typing…"), then the time + unread badge. - Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { - Box(Modifier.weight(1f)) { - ConcordChannelPreviewLine(lastNote, isVoice, typingAuthors, accountViewModel) - } lastNote?.createdAt()?.let { ts -> Text( timeAgo(ts, LocalContext.current, prefix = ""), @@ -446,6 +437,12 @@ private fun ConcordChannelListRow( maxLines = 1, ) } + } + // Line 2: the last-message preview (or a live "typing…"), then the unread-message badge. + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Box(Modifier.weight(1f)) { + ConcordChannelPreviewLine(lastNote, isVoice, typingAuthors, accountViewModel) + } ConcordUnreadBadge(unread) } } @@ -458,9 +455,6 @@ private fun ConcordChannelListRow( } } -/** How many recent-poster avatars a channel row's facepile shows at most. */ -private const val FACEPILE_MAX = 4 - /** * Bottom room the list leaves for the floating action button: a 56dp FAB + the Scaffold's 16dp margin * + slack, so the last row's overflow menu stays tappable instead of sitting under the FAB. Matches diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordHomeScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordHomeScreen.kt index d7db00fe83..4d24678e4b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordHomeScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordHomeScreen.kt @@ -39,7 +39,6 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -68,6 +67,7 @@ import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar import com.vitorpamplona.amethyst.ui.navigation.bottombars.FabBottomBarPadded import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route +import com.vitorpamplona.amethyst.ui.navigation.topbars.ShorterTopAppBar import com.vitorpamplona.amethyst.ui.note.timeAgo import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.datasource.ConcordChannelSubscription @@ -115,7 +115,7 @@ fun ConcordHomeScreen( Scaffold( topBar = { - TopAppBar( + ShorterTopAppBar( title = { Text(stringRes(R.string.concord_home_title)) }, navigationIcon = { // Back arrow only when this is a pushed screen (from the drawer / a deep link); From bde562b4a603dcdb13b4444cf28e3d7a12704ab3 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 18:11:59 +0000 Subject: [PATCH 2/2] chore(concord): remove now-unused facepile helpers Delete ConcordAuthorFacepile and the ConcordChannel/RelayGroupChannel recentAuthorHexes extensions, orphaned after the channel rows dropped the recent-posters facepile. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_017x22okBh6HbiCrJN3zf5xn --- .../publicChannels/concord/ConcordFacepile.kt | 60 ------------------- .../publicChannels/concord/ConcordUnread.kt | 20 ------- .../relayGroup/RelayGroupUnread.kt | 24 -------- 3 files changed, 104 deletions(-) delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordFacepile.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordFacepile.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordFacepile.kt deleted file mode 100644 index 53042dfa2e..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordFacepile.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * 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. - */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture -import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.quartz.nip01Core.core.HexKey - -/** - * A horizontal strip of the recent posters in a channel — the "who's here" cue that makes a busy - * channel feel alive. Each poster is drawn with the app's standard profile avatar - * ([ClickableUserPicture]), so it carries the same following badge (top-right) and trust-score tag - * (bottom-centre) shown everywhere else a user appears, instead of a bare cropped image. Laid out - * with a small gap rather than an overlapping stack so those badges stay readable; the newest poster - * is leftmost. Renders nothing for an empty [authorHexes], so callers can drop it in unconditionally. - */ -@Composable -fun ConcordAuthorFacepile( - authorHexes: List, - accountViewModel: AccountViewModel, - modifier: Modifier = Modifier, - avatarSize: Dp = 24.dp, - maxShown: Int = 4, -) { - if (authorHexes.isEmpty()) return - val shown = authorHexes.take(maxShown) - Row(modifier, horizontalArrangement = Arrangement.spacedBy(2.dp)) { - shown.forEach { hex -> - ClickableUserPicture( - baseUserHex = hex, - size = avatarSize, - accountViewModel = accountViewModel, - ) - } - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordUnread.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordUnread.kt index 5f5b6124ef..bf94968907 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordUnread.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordUnread.kt @@ -26,7 +26,6 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.quartz.concord.cord03Channels.ConcordChannelId -import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip22Comments.CommentEvent import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow @@ -135,22 +134,3 @@ private fun ConcordChannel.newMessagesSince( notes.count { _, note -> (note.createdAt() ?: 0L) > sinceSecs && isConcordTimelineMessage(note, account) } - -/** - * The pubkeys of the [limit] most-recent distinct posters in this channel, newest first — the - * facepile shown on a channel row. One O(notes) pass keeps each author's latest post time, so a - * chatty author counts once (at their newest message) rather than crowding out quieter voices. - */ -fun ConcordChannel.recentAuthorHexes(limit: Int): List { - val latestByAuthor = HashMap() - for (note in notes.values()) { - val author = note.author?.pubkeyHex ?: continue - val at = note.createdAt() ?: continue - val prev = latestByAuthor[author] - if (prev == null || at > prev) latestByAuthor[author] = at - } - return latestByAuthor.entries - .sortedByDescending { it.value } - .take(limit) - .map { it.key } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupUnread.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupUnread.kt index 11eae20dea..666dd002b0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupUnread.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupUnread.kt @@ -26,7 +26,6 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.isMinichatReply -import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip29RelayGroups.GroupId @@ -112,29 +111,6 @@ fun RelayGroupChannel.newestTimelineNote(account: Account): Note? = .sortedByDefaultFeedOrder() .firstOrNull() -/** - * The pubkeys of the [limit] most-recent distinct posters in this group, newest first — the facepile - * shown on a channel row. One O(notes) pass keeps each author's latest post time, so a chatty author - * counts once (at their newest message) rather than crowding out quieter voices. - */ -fun RelayGroupChannel.recentAuthorHexes( - account: Account, - limit: Int, -): List { - val latestByAuthor = HashMap() - for (note in notes.values()) { - if (!isRelayGroupTimelineMessage(note, account)) continue - val author = note.author?.pubkeyHex ?: continue - val at = note.createdAt() ?: continue - val prev = latestByAuthor[author] - if (prev == null || at > prev) latestByAuthor[author] = at - } - return latestByAuthor.entries - .sortedByDescending { it.value } - .take(limit) - .map { it.key } -} - /** Whether this group's message store holds any acceptable timeline message created after [sinceSecs]. */ private fun RelayGroupChannel.hasChatNewerThan( account: Account,