From 5ea82929cb28814c027c0640996870f9594d4722 Mon Sep 17 00:00:00 2001 From: npub1cl47vfhsqpqy9pwndphpm36vcp7vvz5h2js4qpqm5yewzj7nutkq7xyw8c Date: Thu, 23 Jul 2026 15:41:31 -0700 Subject: [PATCH] fix: expose community icon control on open relays Co-authored-by: npub1cl47vfhsqpqy9pwndphpm36vcp7vvz5h2js4qpqm5yewzj7nutkq7xyw8c Signed-off-by: npub1cl47vfhsqpqy9pwndphpm36vcp7vvz5h2js4qpqm5yewzj7nutkq7xyw8c --- .../ui/CommunityMembersSettingsCard.tsx | 25 +++++++++-- .../src/features/settings/ui/SettingsView.tsx | 15 +++---- desktop/src/shared/api/relayMembers.test.mjs | 45 +++++++++++++++++++ desktop/src/shared/api/relayMembers.ts | 9 ++++ 4 files changed, 82 insertions(+), 12 deletions(-) diff --git a/desktop/src/features/community-members/ui/CommunityMembersSettingsCard.tsx b/desktop/src/features/community-members/ui/CommunityMembersSettingsCard.tsx index 4572ad0017..a170c85891 100644 --- a/desktop/src/features/community-members/ui/CommunityMembersSettingsCard.tsx +++ b/desktop/src/features/community-members/ui/CommunityMembersSettingsCard.tsx @@ -12,12 +12,13 @@ import { nip19 } from "nostr-tools"; import * as React from "react"; import { toast } from "sonner"; +import { canEditCommunityProfile } from "@/shared/api/relayMembers"; import { useUsersBatchQuery } from "@/features/profile/hooks"; import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar"; import { useAddRelayMemberMutation, useChangeRelayMemberRoleMutation, - useMyRelayMembershipQuery, + useMyRelayMembershipLookupQuery, useRelayMembersQuery, useRemoveRelayMemberMutation, } from "@/features/community-members/hooks"; @@ -275,9 +276,13 @@ export function CommunityMembersSettingsCard({ }: { currentPubkey?: string; }) { - const myMembershipQuery = useMyRelayMembershipQuery(); - const currentRole = myMembershipQuery.data?.role ?? null; + const myMembershipQuery = useMyRelayMembershipLookupQuery(); + const currentRole = myMembershipQuery.data?.membership?.role ?? null; const canManageRelay = currentRole === "owner" || currentRole === "admin"; + // Open relays do not advertise NIP-43, so they cannot expose an admin role + // snapshot. Keep the icon editor available there and let the relay enforce + // the authoritative owner/admin check on kind 9033. + const canEditIcon = canEditCommunityProfile(myMembershipQuery.data); const membersQuery = useRelayMembersQuery(canManageRelay); const members = React.useMemo( () => membersQuery.data ?? [], @@ -323,10 +328,22 @@ export function CommunityMembersSettingsCard({ ); } - if (!canManageRelay || !currentRole) { + if (!canEditIcon) { return null; } + if (!canManageRelay || !currentRole) { + return ( +
+ + +
+ ); + } + const normalizedInput = normalizeRelayPubkeyInput(pubkeyInput); const canGrantAdmin = currentRole === "owner"; const canAdd = diff --git a/desktop/src/features/settings/ui/SettingsView.tsx b/desktop/src/features/settings/ui/SettingsView.tsx index c70b3411b6..90634e01f4 100644 --- a/desktop/src/features/settings/ui/SettingsView.tsx +++ b/desktop/src/features/settings/ui/SettingsView.tsx @@ -3,7 +3,10 @@ import { getVersion } from "@tauri-apps/api/app"; import { AlertCircle, ArrowLeft, LoaderCircle, RefreshCw } from "lucide-react"; import { useMyRelayMembershipLookupQuery } from "@/features/community-members/hooks"; -import { shouldWarnMissingMembershipSnapshot } from "@/shared/api/relayMembers"; +import { + canEditCommunityProfile, + shouldWarnMissingMembershipSnapshot, +} from "@/shared/api/relayMembers"; import { getFeature } from "@/shared/features/manifest"; import { resolveEnabled, @@ -126,8 +129,6 @@ export function SettingsView({ const myMembershipQuery = useMyRelayMembershipLookupQuery(); const featureState = useFeatureSnapshot(); const visibleSections = React.useMemo(() => { - const membership = myMembershipQuery.data?.membership; - return settingsSections.filter((s) => { // Feature gate check. Manifest is preview-only — if the gate id is in // the manifest, it's preview and needs an opt-in; if it's not, it's @@ -138,12 +139,10 @@ export function SettingsView({ return false; } } - // Community members requires admin/owner role + // Closed relays require a discovered admin/owner role. Open relays have + // no NIP-43 snapshot, so expose only the relay-authorized profile editor. if (s.value === "community-members") { - return ( - membership != null && - (membership.role === "owner" || membership.role === "admin") - ); + return canEditCommunityProfile(myMembershipQuery.data); } return true; }); diff --git a/desktop/src/shared/api/relayMembers.test.mjs b/desktop/src/shared/api/relayMembers.test.mjs index b4773fe4d6..0f54f9b098 100644 --- a/desktop/src/shared/api/relayMembers.test.mjs +++ b/desktop/src/shared/api/relayMembers.test.mjs @@ -2,6 +2,7 @@ import assert from "node:assert/strict"; import test from "node:test"; import { + canEditCommunityProfile, loadRelayMembershipLookup, shouldWarnMissingMembershipSnapshot, } from "./relayMembers.ts"; @@ -35,6 +36,50 @@ test("membership relays request their membership snapshot", async () => { assert.equal(lookup.membership?.role, "admin"); }); +test("community profile editing is visible on open relays", () => { + assert.equal( + canEditCommunityProfile({ + snapshotFound: false, + membershipRequired: false, + membership: null, + }), + true, + ); +}); + +test("community profile editing is visible to closed-relay admins and owners", () => { + for (const role of ["admin", "owner"]) { + assert.equal( + canEditCommunityProfile({ + snapshotFound: true, + membershipRequired: true, + membership: { pubkey: "a".repeat(64), role }, + }), + true, + ); + } +}); + +test("community profile editing stays hidden while loading and from closed-relay non-admins", () => { + assert.equal(canEditCommunityProfile(undefined), false); + assert.equal( + canEditCommunityProfile({ + snapshotFound: true, + membershipRequired: true, + membership: { pubkey: "a".repeat(64), role: "member" }, + }), + false, + ); + assert.equal( + canEditCommunityProfile({ + snapshotFound: true, + membershipRequired: true, + membership: null, + }), + false, + ); +}); + test("missing snapshot warns when the relay requires membership", () => { assert.equal( shouldWarnMissingMembershipSnapshot({ diff --git a/desktop/src/shared/api/relayMembers.ts b/desktop/src/shared/api/relayMembers.ts index b0d6e5675e..d449d4fc7e 100644 --- a/desktop/src/shared/api/relayMembers.ts +++ b/desktop/src/shared/api/relayMembers.ts @@ -38,6 +38,15 @@ export type RelayMembershipLookup = { membership: RelayMember | null; }; +export function canEditCommunityProfile( + lookup: RelayMembershipLookup | undefined, +): boolean { + const role = lookup?.membership?.role; + return ( + lookup?.membershipRequired === false || role === "owner" || role === "admin" + ); +} + export function shouldWarnMissingMembershipSnapshot( lookup: RelayMembershipLookup | undefined, ): boolean {