fix: expose community icon control on open relays (#2640)

Signed-off-by: npub1cl47vfhsqpqy9pwndphpm36vcp7vvz5h2js4qpqm5yewzj7nutkq7xyw8c <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Co-authored-by: npub1cl47vfhsqpqy9pwndphpm36vcp7vvz5h2js4qpqm5yewzj7nutkq7xyw8c <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
This commit is contained in:
Wes
2026-07-23 17:13:48 -06:00
committed by GitHub
parent 710ed9fff5
commit e341b09cb9
4 changed files with 82 additions and 12 deletions

View File

@@ -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 (
<section className="min-w-0" data-testid="settings-community-members">
<SettingsSectionHeader
title="Community access"
description="Manage how this community appears to members."
/>
<CommunityIconSettingsCard />
</section>
);
}
const normalizedInput = normalizeRelayPubkeyInput(pubkeyInput);
const canGrantAdmin = currentRole === "owner";
const canAdd =

View File

@@ -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;
});

View File

@@ -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({

View File

@@ -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 {