From f6e68f31d4f7a9d13c817d8cfd1f6236a4dc29f4 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 22:22:11 +0000 Subject: [PATCH] feat: name the host relay on Buzz notification cards A Buzz/NIP-29 group id is only unique within its host relay, so a message notification titled just "#general" doesn't say which #general it came from. The Notifications card already draws a RelayGroupChannelHeader naming the channel; add the relay next to it, mirroring what a Concord message gets on the same cards (ConcordCommunityPill naming its parent community) and what the Messages row already does for these groups. Extracts the relay chip out of ChatroomHeaderCompose into a shared RelayNameChip so the Messages row and the Notifications feed render the same chip, the way ConcordCommunityPill is already shared between them. The chip taps through to the relay's channel list; the name/avatar still open the room. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_018DsoLSb42C3CdjMceJW2Pq --- .../ui/note/types/RelayGroupMessage.kt | 21 ++++- .../relayGroup/RelayNameChip.kt | 85 +++++++++++++++++++ .../chats/rooms/ChatroomHeaderCompose.kt | 44 +--------- 3 files changed, 104 insertions(+), 46 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayNameChip.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayGroupMessage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayGroupMessage.kt index 1f6001a5e9..29bc9e0a01 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayGroupMessage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayGroupMessage.kt @@ -49,13 +49,16 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayNameChip import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.replyModifier import com.vitorpamplona.quartz.buzz.workspace.buzzParticipants import com.vitorpamplona.quartz.buzz.workspace.isBuzzDm import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl import com.vitorpamplona.quartz.nipC7Chats.ChatEvent /** @@ -100,7 +103,14 @@ fun RenderRelayGroupMessage( } } -/** A compact, tappable header naming the Buzz group (or DM participant) a message belongs to. */ +/** + * A compact, tappable header naming the Buzz group (or DM participant) a message belongs to, plus a + * [RelayNameChip] naming its host relay — the same pairing the Messages row uses, and the NIP-29 + * analog of the [com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.ConcordCommunityPill] + * a Concord message wears on these same cards. A group id is only unique within its host relay, so + * without the relay a notification from `#general` doesn't say *which* `#general` it came from. The + * chip taps through to the relay's channel list, while the name/avatar open the room itself. + */ @Composable fun RelayGroupChannelHeader( channel: RelayGroupChannel, @@ -129,16 +139,21 @@ fun RelayGroupChannelHeader( ) if (dmOther != null) { - RelayGroupDmName(dmOther, channel, accountViewModel, Modifier.weight(1f)) + RelayGroupDmName(dmOther, channel, accountViewModel, Modifier.weight(1f, fill = false)) } else { Text( text = channel.toBestDisplayName(), fontWeight = FontWeight.Bold, maxLines = 1, overflow = TextOverflow.Ellipsis, - modifier = Modifier.weight(1f), + modifier = Modifier.weight(1f, fill = false), ) } + + RelayNameChip( + label = channel.groupId.relayUrl.displayUrl(), + onClick = { nav.nav(Route.RelayGroupServer(channel.groupId.relayUrl.url)) }, + ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayNameChip.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayNameChip.kt new file mode 100644 index 0000000000..e83a560314 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayNameChip.kt @@ -0,0 +1,85 @@ +/* + * 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.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.icons.symbols.Icon +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.ui.theme.ChatLabelMaxWidth + +/** + * A tappable chip naming the relay a NIP-29 / Buzz group lives on. A group id is only unique within + * its host relay, so the relay is part of the room's identity — the same `#general` can exist on two + * Buzz servers and only this chip tells them apart. + * + * Wears the same highlighted wash as [com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.ConcordCommunityPill] + * so every "which server / community does this room belong to" chip reads the same way. Unlike the + * muted note-header [com.vitorpamplona.amethyst.commons.ui.note.HeaderPill] (PoW/OTS/location + * markers), this one is a first-class navigation entry point, so it keeps the stronger + * `secondaryContainer` highlight. Shared by the Messages row and the Notifications feed so a Buzz + * message reads the same wherever it surfaces; the width is capped at [ChatLabelMaxWidth] with a + * middle ellipsis so a long host is truncated instead of crowding the channel name out. + */ +@Composable +fun RelayNameChip( + label: String, + onClick: () -> Unit, +) { + Surface( + shape = RoundedCornerShape(6.dp), + color = MaterialTheme.colorScheme.secondaryContainer, + modifier = Modifier.widthIn(max = ChatLabelMaxWidth).clickable(onClick = onClick), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(3.dp), + modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp), + ) { + Icon( + symbol = MaterialSymbols.Dns, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSecondaryContainer, + modifier = Modifier.size(11.dp), + ) + Text( + text = label, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSecondaryContainer, + maxLines = 1, + overflow = TextOverflow.MiddleEllipsis, + ) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt index 9440d667d8..a28bdff3c6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt @@ -20,22 +20,16 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer -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.shape.RoundedCornerShape import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -98,6 +92,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concor import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.concordCommunityHasUnreadFlow import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.rememberConcordImageModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.LoadEphemeralChatChannel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayNameChip import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.relayGroupChannelLastReadRoute import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.relayGroupServerHasUnreadFlow import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.dal.ConcordServerRoomNote @@ -746,43 +741,6 @@ private fun ConcordServerRoomCompose( ) } -/** - * A tappable chip naming the server/community a Messages row belongs to. Unlike the muted - * note-header [HeaderPill] (PoW/OTS/location markers), this one is a first-class navigation entry - * point, so it keeps the stronger `secondaryContainer` highlight. - */ -@Composable -private fun RelayNameChip( - label: String, - onClick: () -> Unit, -) { - Surface( - shape = RoundedCornerShape(6.dp), - color = MaterialTheme.colorScheme.secondaryContainer, - modifier = Modifier.widthIn(max = ChatLabelMaxWidth).clickable(onClick = onClick), - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(3.dp), - modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp), - ) { - Icon( - symbol = MaterialSymbols.Dns, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSecondaryContainer, - modifier = Modifier.size(11.dp), - ) - Text( - text = label, - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSecondaryContainer, - maxLines = 1, - overflow = TextOverflow.MiddleEllipsis, - ) - } - } -} - /** * Renders a Messages row title as the channel name followed by a muted [HeaderPill] naming the room * type (Public Chat, Marmot Group, ...). The pill mirrors the Concord community chip so every group