From 34fa103dbd6eaa70df083a441943b13e1d6d3e25 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 14:26:23 +0000 Subject: [PATCH] feat(buzz): gate the channel/forum "+" to community members Only a Buzz community member may create a channel (the relay makes the creator its owner; a non-member's kind-9007 is rejected), so the section-label "+" now shows only when the viewer is in the community's NIP-43 roster. Reactive: the relay-signed roster snapshot (kind 13534, republished on every membership change) can arrive after first composition, so BuzzCommunityMembership is re-read whenever one lands. The section labels themselves still render for everyone; only the "+" is gated. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_016MNVEKhaAu4vQRZnXv3rfG --- .../relayGroup/RelayGroupChannelListScreen.kt | 55 +++++++++++++++---- 1 file changed, 43 insertions(+), 12 deletions(-) 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 b9236d68f4..bcaedd0096 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 @@ -109,6 +109,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl import com.vitorpamplona.quartz.nip29RelayGroups.GroupId import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMetadataEvent +import com.vitorpamplona.quartz.nip43RelayMembers.list.RelayMembershipListEvent import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -208,6 +209,24 @@ fun RelayGroupChannelListScreen( // (kind-44100) so browsing the relay lists the channels you already belong to — each addable to // your kind-10009 list (so it then shows in Messages / Relay Groups). Same screen, one Browse. val isBuzz = BuzzRelayDialect.isBuzz(relay) || relayInfo.software?.contains("buzz", ignoreCase = true) == true + + // Who may create a channel/forum here — the gate for the section "+" buttons. A Buzz relay lets + // any community member create one (the creator becomes its owner); a non-member's 9007 is + // rejected, so we only offer the "+" to members of this community's NIP-43 roster. Reactive: the + // relay-signed roster snapshot (kind 13534, republished after every membership change) may land + // after first composition, so re-read [BuzzCommunityMembership] whenever one does. + val myPubkey = accountViewModel.account.signer.pubKey + val canCreateChannels by produceState( + initialValue = BuzzCommunityMembership.isMember(relay, myPubkey), + relay, + myPubkey, + ) { + value = BuzzCommunityMembership.isMember(relay, myPubkey) + LocalCache + .observeEvents(Filter(kinds = listOf(RelayMembershipListEvent.KIND))) + .collect { value = BuzzCommunityMembership.isMember(relay, myPubkey) } + } + val buzzVm: BuzzRelayImportViewModel = viewModel(key = "BuzzImport-${relay.url}") LaunchedEffect(relay, isBuzz) { if (isBuzz) buzzVm.bind(accountViewModel.account, relay.url) } val buzzChannels by buzzVm.channels.collectAsStateWithLifecycle() @@ -352,7 +371,6 @@ fun RelayGroupChannelListScreen( } }, ) { padding -> - val myPubkey = accountViewModel.userProfile().pubkeyHex // A Buzz relay is a community, so always render its sectioned list (Channels, Forums, Direct // Messages, Agent Console) even before anything loads, rather than the generic "empty" text. if (channels.isEmpty() && !isBuzz) { @@ -419,7 +437,8 @@ fun RelayGroupChannelListScreen( // -- 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. + // the collapse toggle is offered only when there's something to collapse. The "+" + // is offered only to community members, who are the ones the relay lets create. run { val channelsCollapsed = "channels" in collapsedSections item(key = "sec-channels") { @@ -427,11 +446,17 @@ fun RelayGroupChannelListScreen( title = stringRes(R.string.relay_group_section_channels), collapsed = channelsCollapsed, onToggle = if (buzzChatChannels.isNotEmpty()) ({ toggleSection("channels") }) else null, - ) { - SectionAddButton(stringRes(R.string.buzz_channel_create_title)) { - nav.nav(Route.RelayGroupCreate(relay.url)) - } - } + trailing = + if (canCreateChannels) { + { + SectionAddButton(stringRes(R.string.buzz_channel_create_title)) { + nav.nav(Route.RelayGroupCreate(relay.url)) + } + } + } else { + null + }, + ) } if (buzzChatChannels.isNotEmpty() && !channelsCollapsed) { itemsIndexed(buzzChatChannels, key = { _, it -> "chat-${it.id}" }) { index, groupId -> @@ -459,11 +484,17 @@ fun RelayGroupChannelListScreen( title = stringRes(R.string.relay_group_section_forums), collapsed = forumsCollapsed, onToggle = if (buzzForumChannels.isNotEmpty()) ({ toggleSection("forums") }) else null, - ) { - SectionAddButton(stringRes(R.string.buzz_forum_create_title)) { - nav.nav(Route.RelayGroupCreate(relay.url, isForum = true)) - } - } + trailing = + if (canCreateChannels) { + { + SectionAddButton(stringRes(R.string.buzz_forum_create_title)) { + nav.nav(Route.RelayGroupCreate(relay.url, isForum = true)) + } + } + } else { + null + }, + ) } if (buzzForumChannels.isNotEmpty() && !forumsCollapsed) { itemsIndexed(buzzForumChannels, key = { _, it -> "forum-${it.id}" }) { index, groupId ->