Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 ?? [],
Expand Down Expand Up @@ -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 =
Expand Down
15 changes: 7 additions & 8 deletions desktop/src/features/settings/ui/SettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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;
});
Expand Down
45 changes: 45 additions & 0 deletions desktop/src/shared/api/relayMembers.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import assert from "node:assert/strict";
import test from "node:test";

import {
canEditCommunityProfile,
loadRelayMembershipLookup,
shouldWarnMissingMembershipSnapshot,
} from "./relayMembers.ts";
Expand Down Expand Up @@ -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({
Expand Down
9 changes: 9 additions & 0 deletions desktop/src/shared/api/relayMembers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading