From a89aaed9faf5e571e5783fe2e7f9b04e9531f82d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 14:10:11 +0000 Subject: [PATCH] feat(buzz): create channels/forums from section-label "+" instead of a FAB MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On a Buzz community the FAB (which created a channel) is replaced by a "+" on the Channels label — matching how Direct Messages already offers a "+" — and a Forums label gains its own "+" that starts the create flow pre-set to a forum channel. Both section headers now always render so the "+" is reachable even before any channel loads; the collapse chevron is shown only when the section is non-empty. The create route gains an isForum flag (threaded onto RelayGroupCreateScreen → RelayGroupMetadataViewModel.isForum) so the Forums "+" opens the create screen on a forum, the Channels "+" on a chat channel. The vanilla NIP-29 relay (a flat directory with no sections) keeps its FAB. The stale "accept the invite in the browser" empty text is dropped — an empty community is now a starting point. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_016MNVEKhaAu4vQRZnXv3rfG --- .../amethyst/ui/navigation/AppNavigation.kt | 1 + .../amethyst/ui/navigation/routes/Routes.kt | 3 + .../relayGroup/RelayGroupChannelListScreen.kt | 88 +++++++++++++------ .../relayGroup/RelayGroupMetadataScreen.kt | 7 +- amethyst/src/main/res/values/strings.xml | 1 + 5 files changed, 73 insertions(+), 27 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index 14faf2d41c..d520612fcb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -790,6 +790,7 @@ fun BuildNavigation( composableFromEndArgs { RelayGroupCreateScreen( relayUrl = it.relayUrl, + isForum = it.isForum, accountViewModel = accountViewModel, nav = nav, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt index 57b5af66e8..4392da65e9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt @@ -741,6 +741,9 @@ sealed class Route { @Serializable data class RelayGroupCreate( val relayUrl: String, + // Buzz only: start the create flow on a `forum` channel (threaded posts) instead of a + // `stream` (chat) one — set by the community screen's per-section "+" buttons. + val isForum: Boolean = false, ) : Route() @Serializable data class RelayGroupEdit( 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 aa20a8aea0..b9236d68f4 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 @@ -338,13 +338,17 @@ fun RelayGroupChannelListScreen( } }, floatingActionButton = { - FloatingActionButton(onClick = { nav.nav(Route.RelayGroupCreate(relay.url)) }, shape = CircleShape) { - Icon( - symbol = MaterialSymbols.Add, - contentDescription = - stringRes(if (isBuzz) R.string.buzz_channel_create_title else R.string.relay_group_create_title), - modifier = Modifier.size(24.dp), - ) + // A Buzz community creates channels/forums from the per-section "+" in their labels (like + // Direct Messages), so no FAB there. A vanilla NIP-29 relay is a flat directory with no + // sections, so it keeps the FAB to create a group. + if (!isBuzz) { + FloatingActionButton(onClick = { nav.nav(Route.RelayGroupCreate(relay.url)) }, shape = CircleShape) { + Icon( + symbol = MaterialSymbols.Add, + contentDescription = stringRes(R.string.relay_group_create_title), + modifier = Modifier.size(24.dp), + ) + } } }, ) { padding -> @@ -378,9 +382,11 @@ fun RelayGroupChannelListScreen( // overlays content by design, so clearing it is the list's job. As contentPadding (not a // modifier) so rows scroll *through* that strip and only come to rest clear of it; the // modifier form would shrink the viewport and leave the FAB floating over dead space. + // Only the vanilla NIP-29 path has a FAB now; a Buzz community creates from its section + // headers, so it needs no bottom clearance. LazyColumn( modifier = Modifier.padding(padding), - contentPadding = PaddingValues(bottom = FAB_CLEARANCE), + contentPadding = PaddingValues(bottom = if (isBuzz) 0.dp else FAB_CLEARANCE), ) { if (showTorHint) { item(key = "tor-hint") { @@ -394,16 +400,15 @@ fun RelayGroupChannelListScreen( } if (isBuzz) { + // While the membership fetch is still running and nothing has loaded, show a + // "Loading…" line. The old "you're not a member — accept the invite in the browser" + // empty text is gone: the section labels below now each carry a "+" to create a + // channel/forum, so an empty community is a starting point, not a dead end. val noChannelsYet = buzzChatChannels.isEmpty() && buzzForumChannels.isEmpty() - if (noChannelsYet) { - item(key = "buzz-no-channels") { + if (noChannelsYet && buzzStatus is BuzzRelayImportViewModel.Status.Loading) { + item(key = "buzz-loading") { Text( - text = - if (buzzStatus is BuzzRelayImportViewModel.Status.Loading) { - stringRes(R.string.buzz_import_loading) - } else { - stringRes(R.string.buzz_import_empty_body) - }, + text = stringRes(R.string.buzz_import_loading), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.fillMaxWidth().padding(24.dp), @@ -411,17 +416,24 @@ fun RelayGroupChannelListScreen( } } - // -- CHANNELS -- (Add-all now lives in the community's top-bar overflow menu) - if (buzzChatChannels.isNotEmpty()) { + // -- CHANNELS -- The label carries a "+" to create a channel (the community's FAB + // moved here, like Direct Messages). Add-all lives in the top-bar overflow menu. + // The header always shows so the "+" is available even before any channel loads; + // the collapse toggle is offered only when there's something to collapse. + run { val channelsCollapsed = "channels" in collapsedSections item(key = "sec-channels") { RelayGroupSectionHeader( title = stringRes(R.string.relay_group_section_channels), collapsed = channelsCollapsed, - onToggle = { toggleSection("channels") }, - ) + onToggle = if (buzzChatChannels.isNotEmpty()) ({ toggleSection("channels") }) else null, + ) { + SectionAddButton(stringRes(R.string.buzz_channel_create_title)) { + nav.nav(Route.RelayGroupCreate(relay.url)) + } + } } - if (!channelsCollapsed) { + if (buzzChatChannels.isNotEmpty() && !channelsCollapsed) { itemsIndexed(buzzChatChannels, key = { _, it -> "chat-${it.id}" }) { index, groupId -> RowHairline(index) BuzzImportRow( @@ -438,17 +450,22 @@ fun RelayGroupChannelListScreen( } } - // -- FORUMS -- - if (buzzForumChannels.isNotEmpty()) { + // -- FORUMS -- Same treatment: an always-visible label with a "+" that starts the + // create flow on a forum channel (threaded posts) instead of a chat one. + run { val forumsCollapsed = "forums" in collapsedSections item(key = "sec-forums") { RelayGroupSectionHeader( title = stringRes(R.string.relay_group_section_forums), collapsed = forumsCollapsed, - onToggle = { toggleSection("forums") }, - ) + onToggle = if (buzzForumChannels.isNotEmpty()) ({ toggleSection("forums") }) else null, + ) { + SectionAddButton(stringRes(R.string.buzz_forum_create_title)) { + nav.nav(Route.RelayGroupCreate(relay.url, isForum = true)) + } + } } - if (!forumsCollapsed) { + if (buzzForumChannels.isNotEmpty() && !forumsCollapsed) { itemsIndexed(buzzForumChannels, key = { _, it -> "forum-${it.id}" }) { index, groupId -> RowHairline(index) BuzzImportRow( @@ -652,6 +669,25 @@ private fun RelayGroupSectionHeader( } } +/** + * The trailing "+" for a section label (Channels / Forums), matching the Direct Messages header's + * New-message icon: a primary-tinted Add glyph that creates a new item of that section's type. + */ +@Composable +private fun SectionAddButton( + contentDescription: String, + onClick: () -> Unit, +) { + IconButton(onClick = onClick) { + Icon( + symbol = MaterialSymbols.Add, + contentDescription = contentDescription, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(22.dp), + ) + } +} + /** * 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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataScreen.kt index c71641b669..5f2c62d1d5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataScreen.kt @@ -97,10 +97,15 @@ fun RelayGroupCreateScreen( relayUrl: String, accountViewModel: AccountViewModel, nav: INav, + // Buzz only: pre-select the `forum` channel type (the Forums section's "+" routes here with true). + isForum: Boolean = false, ) { val relay = remember(relayUrl) { RelayUrlNormalizer.normalizeOrNull(relayUrl) } ?: return val viewModel: RelayGroupMetadataViewModel = viewModel(key = "RelayGroupCreate:$relayUrl") - LaunchedEffect(relay) { viewModel.initCreate(accountViewModel, relay) } + LaunchedEffect(relay) { + viewModel.initCreate(accountViewModel, relay) + if (isForum) viewModel.isForum = true + } // A group only works if the relay actually runs NIP-29 (otherwise it stores our 9007/9002 as // ordinary events, never emits metadata/roster, and the "group" is a dead hex id). Gate creation diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 8cef251ec7..75d6b194a8 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -2570,6 +2570,7 @@ Create a group New channel + New forum Private channel Hidden from the channel list and invite-only. Off means anyone on this relay can find and join it. Forum channel