From 97ff9715426df5761e534c22f6baa8a6753ea5e1 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 14:36:42 +0000 Subject: [PATCH] feat(buzz): move per-row channel actions into the opened screen's top bar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The community list rows (channels, forums, DMs) each carried a 3-dot overflow — Pin/Unpin and Add/Remove-from-Messages. Those are gone; each row is now a clean tap-to-open target and its actions live in the top-bar overflow of the screen it opens: - Channels & DMs open RelayGroupChatScreen (RelayGroupTopBar): add Pin/Unpin (Buzz, non-DM) alongside the existing Messages toggle. A DM has no kind-10009 entry, so its Messages toggle drives the DM-specific hide/unhide (kind-41012 / re-open) read from the per-viewer 30622 snapshot; the DM overflow is always shown so that action stays reachable. - Forums open RelayGroupThreadsScreen: give its top bar an overflow with Pin/Unpin and Add/Remove-from-Messages. Shared BuzzPinDropdownItem / RelayGroupMessagesDropdownItem keep the two bars consistent. AccountViewModel gains hideBuzzDm/unhideBuzzDm wrappers. The community-list top-bar overflow (Add people, Invite, Agent Console, Add all) is unchanged. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_016MNVEKhaAu4vQRZnXv3rfG --- .../ui/screen/loggedIn/AccountViewModel.kt | 16 +++ .../ui/screen/loggedIn/buzz/BuzzImportRow.kt | 94 +--------------- .../relayGroup/BuzzChannelMenuItems.kt | 104 ++++++++++++++++++ .../relayGroup/RelayGroupChannelListScreen.kt | 55 ++------- .../relayGroup/RelayGroupThreadsScreen.kt | 23 ++++ .../relayGroup/RelayGroupTopBar.kt | 37 ++++++- 6 files changed, 190 insertions(+), 139 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/BuzzChannelMenuItems.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index b105993fc6..363a703d06 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -1708,6 +1708,22 @@ class AccountViewModel( */ fun acceptChannelInvite(channel: RelayGroupChannel) = addRelayGroupToMessages(channel) + /** + * Hide a Buzz DM from Messages (kind-41012). DM-specific — a DM has no kind-10009 entry; the relay + * republishes my per-viewer 30622 hidden snapshot, dropping it from the inbox until I re-open it. + */ + fun hideBuzzDm(channel: RelayGroupChannel) = launchSigner { account.hideBuzzDm(channel) } + + /** + * Bring a hidden Buzz DM back to Messages: Buzz has no "unhide", so re-open the conversation with + * the same [participants] (a kind-41010 resolving to the same canonical channel), which drops it + * from the 30622 hidden snapshot. + */ + fun unhideBuzzDm( + relay: NormalizedRelayUrl, + participants: List, + ) = launchSigner { account.openBuzzDm(relay, participants) } + /** * Keep the channel off Messages without touching membership. Local and reversible — I stay in the * roster and can still open and post; [leaveChannelInvite] is the one that actually removes me. 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 a0d45791da..387a77b0eb 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 @@ -32,16 +32,11 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -83,9 +78,9 @@ private const val CARD_WARMUP_LIMIT = 10 * 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. - * Tapping the card opens the channel ([onOpen]); the trailing overflow (3-dot) menu holds the - * per-channel actions — Pin/Unpin and the Add/Remove-from-Messages toggle ([isAdded] says which half - * to show, and it must come from the live kind-10009 list) — so the row stays clean. + * 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]). * * Reused by the relay group-list screen where Buzz membership discovery is folded in. * @@ -98,13 +93,9 @@ private const val CARD_WARMUP_LIMIT = 10 @Composable fun BuzzImportRow( groupId: GroupId, - isAdded: Boolean, - onAdd: () -> Unit, - onRemove: () -> Unit, accountViewModel: AccountViewModel, onOpen: (() -> Unit)? = null, isStarred: Boolean = false, - onToggleStar: (() -> Unit)? = null, showActivityPreview: Boolean = true, ) { val account = accountViewModel.account @@ -166,11 +157,7 @@ fun BuzzImportRow( faceAuthors = faceAuthors, unread = unread, hasUnread = hasUnread, - isAdded = isAdded, isStarred = isStarred, - onToggleStar = onToggleStar, - onAdd = onAdd, - onRemove = onRemove, accountViewModel = accountViewModel, ) } @@ -195,15 +182,11 @@ private fun BuzzImportRowContent( faceAuthors: List, unread: Int, hasUnread: Boolean, - isAdded: Boolean, isStarred: Boolean, - onToggleStar: (() -> Unit)?, - onAdd: () -> Unit, - onRemove: () -> Unit, accountViewModel: AccountViewModel, ) { Row( - modifier = Modifier.padding(start = 12.dp, top = 10.dp, bottom = 10.dp, end = 4.dp), + modifier = Modifier.padding(start = 12.dp, top = 10.dp, bottom = 10.dp, end = 16.dp), verticalAlignment = Alignment.CenterVertically, ) { BuzzImportAvatar(name = name, seed = seed) @@ -254,13 +237,6 @@ private fun BuzzImportRowContent( ConcordUnreadBadge(unread) } } - BuzzChannelRowMenu( - isAdded = isAdded, - onAdd = onAdd, - onRemove = onRemove, - isStarred = isStarred, - onToggleStar = onToggleStar, - ) } } @@ -308,68 +284,6 @@ private fun BuzzChannelPreviewLine( ) } -/** - * The per-channel overflow (3-dot) menu: Pin/Unpin and Add-to-my-list. Moved off the row itself so a - * channel card reads as a clean Concord-style row, with its actions one tap behind the kebab. - */ -@Composable -private fun BuzzChannelRowMenu( - isAdded: Boolean, - onAdd: () -> Unit, - onRemove: () -> Unit, - isStarred: Boolean, - onToggleStar: (() -> Unit)?, -) { - var expanded by remember { mutableStateOf(false) } - Box { - IconButton(onClick = { expanded = true }) { - Icon( - symbol = MaterialSymbols.MoreVert, - contentDescription = stringRes(R.string.more_options), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(20.dp), - ) - } - DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { - if (onToggleStar != null) { - DropdownMenuItem( - leadingIcon = { - Icon( - symbol = MaterialSymbols.PushPin, - contentDescription = null, - tint = if (isStarred) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(20.dp), - ) - }, - text = { Text(stringRes(if (isStarred) R.string.buzz_unpin else R.string.buzz_pin)) }, - onClick = { - expanded = false - onToggleStar() - }, - ) - } - // A toggle, not a one-way "Added" badge: a channel already on the kind-10009 list offers - // the way back off it. Neither half touches the relay roster, so the channel stays in this - // list (and readable) either way — only whether it shows on Messages changes. - DropdownMenuItem( - leadingIcon = { - Icon( - symbol = if (isAdded) MaterialSymbols.VisibilityOff else MaterialSymbols.Add, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(20.dp), - ) - }, - text = { Text(stringRes(if (isAdded) R.string.remove_from_messages else R.string.add_to_messages)) }, - onClick = { - expanded = false - if (isAdded) onRemove() else onAdd() - }, - ) - } - } -} - /** A round monogram whose color is derived deterministically from the channel id. */ @Composable private fun BuzzImportAvatar( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/BuzzChannelMenuItems.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/BuzzChannelMenuItems.kt new file mode 100644 index 0000000000..6240aca33a --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/BuzzChannelMenuItems.kt @@ -0,0 +1,104 @@ +/* + * 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.relayGroup + +import androidx.compose.foundation.layout.size +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +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.model.buzz.BuzzChannelStars +import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.quartz.nip29RelayGroups.GroupId + +/** + * Pin/Unpin a Buzz channel (a device-local favorite — [BuzzChannelStars]). Moved off the per-channel + * list row into the opened channel's/forum's top-bar overflow, so the list row stays a clean + * tap-to-open target. Reads the live starred set so the label + icon reflect the current state. + */ +@Composable +fun BuzzPinDropdownItem( + groupId: GroupId, + closeMenu: () -> Unit, +) { + val starred by BuzzChannelStars.flow.collectAsStateWithLifecycle() + val isStarred = groupId.id in starred + DropdownMenuItem( + leadingIcon = { + Icon( + symbol = MaterialSymbols.PushPin, + contentDescription = null, + tint = if (isStarred) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(20.dp), + ) + }, + text = { Text(stringRes(if (isStarred) R.string.buzz_unpin else R.string.buzz_pin)) }, + onClick = { + closeMenu() + BuzzChannelStars.toggle(groupId.id) + }, + ) +} + +/** + * Add/Remove this relay-group [channel] from my kind-10009 list (whether it shows in Messages). A + * reversible toggle that never touches my relay membership — same split as "Leave" — so it stays + * readable either way. Reads the live kind-10009 list so it flips as the change lands. Moved off the + * per-channel list row into the opened screen's top-bar overflow. + */ +@Composable +fun RelayGroupMessagesDropdownItem( + channel: RelayGroupChannel, + accountViewModel: AccountViewModel, + closeMenu: () -> Unit, +) { + val joinedGroupIds by accountViewModel.account.relayGroupList.liveRelayGroupIds + .collectAsStateWithLifecycle() + val onMyList = channel.groupId in joinedGroupIds + DropdownMenuItem( + leadingIcon = { + Icon( + symbol = if (onMyList) MaterialSymbols.VisibilityOff else MaterialSymbols.Add, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(20.dp), + ) + }, + text = { Text(stringRes(if (onMyList) R.string.remove_from_messages else R.string.add_to_messages)) }, + onClick = { + closeMenu() + if (onMyList) { + accountViewModel.removeRelayGroupFromMessages(channel) + } else { + accountViewModel.addRelayGroupToMessages(channel) + } + }, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt index bcaedd0096..a1cc027c72 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt @@ -36,8 +36,6 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.HorizontalDivider @@ -463,13 +461,9 @@ fun RelayGroupChannelListScreen( RowHairline(index) BuzzImportRow( groupId = groupId, - isAdded = groupId.id in buzzAdded, - onAdd = { buzzVm.add(groupId) }, - onRemove = { buzzVm.remove(groupId) }, accountViewModel = accountViewModel, onOpen = { nav.nav(Route.RelayGroup(groupId.id, relay.url)) }, isStarred = groupId.id in starred, - onToggleStar = { BuzzChannelStars.toggle(groupId.id) }, ) } } @@ -501,15 +495,11 @@ fun RelayGroupChannelListScreen( RowHairline(index) BuzzImportRow( groupId = groupId, - isAdded = groupId.id in buzzAdded, - onAdd = { buzzVm.add(groupId) }, - onRemove = { buzzVm.remove(groupId) }, accountViewModel = accountViewModel, // A forum channel's primary content is its threads (kind-45001 posts), not a // kind-9 chat, so open the forum/threads view directly instead of the chat. onOpen = { nav.nav(Route.RelayGroupThreads(groupId.id, relay.url)) }, isStarred = groupId.id in starred, - onToggleStar = { BuzzChannelStars.toggle(groupId.id) }, // Forum posts live in a separate thread store, not the chat notes the // activity preview reads — so don't warm a kind-9 sub that returns nothing. showActivityPreview = false, @@ -548,7 +538,6 @@ fun RelayGroupChannelListScreen( row = row, myPubkey = myPubkey, isHidden = false, - onToggleMessages = { dmVm.removeFromMessages(row) }, accountViewModel = accountViewModel, nav = nav, ) { @@ -580,7 +569,6 @@ fun RelayGroupChannelListScreen( row = row, myPubkey = myPubkey, isHidden = true, - onToggleMessages = { dmVm.addToMessages(row) }, accountViewModel = accountViewModel, nav = nav, ) { @@ -721,25 +709,23 @@ private fun SectionAddButton( /** * One inline Direct-Message conversation row inside the community view: the counterpart's avatar + - * name (or a "+N" cluster label for a group DM), a preview of the last message, a compact - * last-activity time, and an overflow holding the Add/Remove-from-Messages toggle. The channel's - * recent content is warmed while the row is visible so the preview fills in ahead of a tap. Tapping - * opens the DM as its relay-group chat. + * name (or a "+N" cluster label for a group DM), a preview of the last message, and a compact + * last-activity time. The channel's recent content is warmed while the row is visible so the preview + * fills in ahead of a tap. Tapping opens the DM as its relay-group chat; the Add/Remove-from-Messages + * (hide/unhide) action lives in that chat screen's top-bar overflow, not on this row. * - * [isHidden] renders the row faded and flips the overflow to "Add to Messages" — a hidden DM is a - * live conversation the viewer merely parked, so it stays openable and reversible. + * [isHidden] renders the row faded — a hidden DM is a live conversation the viewer merely parked, so + * it stays openable and reversible from the opened conversation. */ @Composable private fun BuzzDmInlineRow( row: BuzzDmListViewModel.DmRow, myPubkey: HexKey, isHidden: Boolean, - onToggleMessages: () -> Unit, accountViewModel: AccountViewModel, nav: INav, onClick: () -> Unit, ) { - var menuOpen by remember { mutableStateOf(false) } val others = row.others.ifEmpty { listOf(myPubkey) } val leadHex = others.first() val leadUser = remember(leadHex) { LocalCache.getOrCreateUser(leadHex) } @@ -768,7 +754,7 @@ private fun BuzzDmInlineRow( Modifier .fillMaxWidth() .clickable(onClick = onClick) - .padding(start = 16.dp, end = 4.dp, top = 10.dp, bottom = 10.dp) + .padding(start = 16.dp, end = 16.dp, top = 10.dp, bottom = 10.dp) .alpha(if (isHidden) 0.55f else 1f), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp), @@ -797,33 +783,6 @@ private fun BuzzDmInlineRow( color = MaterialTheme.colorScheme.onSurfaceVariant, ) } - Box { - IconButton(onClick = { menuOpen = true }) { - Icon( - symbol = MaterialSymbols.MoreVert, - contentDescription = stringRes(R.string.more_options), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(20.dp), - ) - } - DropdownMenu(expanded = menuOpen, onDismissRequest = { menuOpen = false }) { - DropdownMenuItem( - leadingIcon = { - Icon( - symbol = if (isHidden) MaterialSymbols.Add else MaterialSymbols.VisibilityOff, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(20.dp), - ) - }, - text = { Text(stringRes(if (isHidden) R.string.add_to_messages else R.string.remove_from_messages)) }, - onClick = { - menuOpen = false - onToggleMessages() - }, - ) - } - } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupThreadsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupThreadsScreen.kt index e015e48a7b..85f0725a78 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupThreadsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupThreadsScreen.kt @@ -34,15 +34,19 @@ import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.DropdownMenu import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -166,6 +170,25 @@ private fun RelayGroupThreads( ) } }, + actions = { + // The forum's per-item actions, moved off the community-list row into this screen's + // top-bar overflow: Pin/Unpin and the Add/Remove-from-Messages toggle. Buzz-only, + // which every forum channel is. + if (isBuzz) { + var menuOpen by remember { mutableStateOf(false) } + IconButton(onClick = { menuOpen = true }) { + Icon( + symbol = MaterialSymbols.MoreVert, + contentDescription = stringRes(R.string.more_options), + modifier = Modifier.size(22.dp), + ) + } + DropdownMenu(expanded = menuOpen, onDismissRequest = { menuOpen = false }) { + BuzzPinDropdownItem(channel.groupId) { menuOpen = false } + RelayGroupMessagesDropdownItem(channel, accountViewModel) { menuOpen = false } + } + } + }, popBack = nav::popBack, ) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupTopBar.kt index 47a19bea25..afb2682855 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupTopBar.kt @@ -53,6 +53,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle 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.model.buzz.BuzzDmRegistry import com.vitorpamplona.amethyst.commons.model.buzz.BuzzRelayDialect import com.vitorpamplona.amethyst.commons.model.buzz.BuzzWorkspaceStates import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel @@ -117,6 +118,9 @@ fun RelayGroupTopBar( // My kind-10009 list, live: drives the Add/Remove-from-Messages toggle in the overflow below. val joinedGroupIds by accountViewModel.account.relayGroupList.liveRelayGroupIds .collectAsStateWithLifecycle() + // A Buzz DM is hidden via its own per-viewer 30622 snapshot, not the kind-10009 list, so the + // Messages toggle branches on this for DMs. + val hiddenDms by BuzzDmRegistry.hidden.collectAsStateWithLifecycle() // Read once here (nav.canPop() is @Composable) so the post-action navigation can pop from a menu // callback — leaving a group shouldn't strand the user on the screen of a group they left. val canPop = nav.canPop() @@ -237,7 +241,9 @@ fun RelayGroupTopBar( // Buzz `t=stream` channel it is always empty (forum posts live in `t=forum` channels, which // the relay's channel list already surfaces in their own section), so it read as a broken // feature on every chat. Demoted to the overflow, where the frequency of use actually is. - if (!isDm || naddr != null || showMembershipActions) { + // A Buzz DM always gets the overflow too — its hide/unhide (below) is the DM row's old + // action, and it must be reachable even where the membership actions aren't offered. + if (!isDm || naddr != null || showMembershipActions || (isDm && isBuzzRelay)) { IconButton(onClick = { menuOpen = true }) { Icon( symbol = MaterialSymbols.MoreVert, @@ -246,6 +252,33 @@ fun RelayGroupTopBar( ) } DropdownMenu(expanded = menuOpen, onDismissRequest = { menuOpen = false }) { + // Pin/Unpin moved here off the community-list row. A local favorite, so it's offered + // for any Buzz channel/forum regardless of membership; DMs are never pinned. + if (isBuzzRelay && !isDm) { + BuzzPinDropdownItem(channel.groupId) { menuOpen = false } + } + // A DM's Add/Remove-from-Messages, moved off the DM list row. It rides the per-viewer + // 30622 hide snapshot (kind-41012 hide / re-open), not the kind-10009 list, and is + // shown regardless of the membership gate below. + if (isBuzzRelay && isDm) { + val dmHidden = channel.groupId.id in (hiddenDms[myPubkey] ?: emptySet()) + DropdownMenuItem( + text = { Text(stringRes(if (dmHidden) R.string.add_to_messages else R.string.remove_from_messages)) }, + onClick = { + menuOpen = false + if (dmHidden) { + val participants = + channel.event + ?.buzzParticipants() + ?.filter { it != myPubkey } + .orEmpty() + accountViewModel.unhideBuzzDm(channel.groupId.relayUrl, participants.ifEmpty { listOf(myPubkey) }) + } else { + accountViewModel.hideBuzzDm(channel) + } + }, + ) + } if (!isDm) { DropdownMenuItem( text = { Text(stringRes(R.string.relay_group_threads_title)) }, @@ -308,6 +341,8 @@ fun RelayGroupTopBar( // Two distinct actions, never conflated: the Messages toggle adds/drops the group // on my kind-10009 list but keeps my relay membership either way; "Leave" sends // the kind-9022 that actually removes me. Same split as the channel-invite card. + // (A DM's Messages toggle is the hide/unhide item above — it rides a different + // mechanism and must show even when these membership actions don't.) // // Reads the live kind-10009 list rather than assuming the group is on it: this // bar also opens for channels reached from the workspace browse (a Buzz relay