Implement NIP-43 relay access metadata (#104)

* feat: add NIP-43 relay access metadata support

Add feed and detail rendering for kind 13534 (Relay Members) events,
and enable all NIP-43 kind constants for relay access management.

- Add RelayMembersRenderer with feed and detail views
- Enable kind constants: 13534, 28934, 28935, 28936
- Use Shield icon to represent relay access control
- Extract members from NIP-43's "member" tags (not standard "p" tags)

* feat: add renderers for NIP-43 Add/Remove User events

- Change kind 13534 icon from Shield to Users for consistency
- Add feed and detail renderers for kind 8000 (Add User)
- Add feed and detail renderers for kind 8001 (Remove User)
- Both show the affected pubkey using PubkeyListFull component

* fix: show username in Add/Remove User feed renderers

Display the actual username (via UserName component) in kind 8000/8001
feed views instead of just generic text.

* refactor: simplify NIP-43 renderers to follow codebase patterns

- Use PubkeyListPreview in RelayMembersRenderer feed view (shows actual
  users instead of just count, matching FollowSetRenderer pattern)
- Remove redundant icon props from detail renderers (PubkeyListFull
  has sensible defaults)
- Simplify variable naming and reduce comments
- No functional changes, just cleaner code

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Alejandro
2026-01-15 12:26:37 +01:00
committed by GitHub
parent 1ce784561a
commit 571e7a0d14
4 changed files with 243 additions and 28 deletions

View File

@@ -0,0 +1,67 @@
import { Users } from "lucide-react";
import { getTagValues } from "@/lib/nostr-utils";
import {
BaseEventProps,
BaseEventContainer,
ClickableEventTitle,
} from "./BaseEventRenderer";
import { PubkeyListPreview, PubkeyListFull } from "../lists";
import type { NostrEvent } from "@/types/nostr";
/**
* Kind 13534 Renderer - Relay Members (Feed View)
* NIP-43 relay membership list using "member" tags
*/
export function RelayMembersRenderer({ event }: BaseEventProps) {
const members = getTagValues(event, "member");
return (
<BaseEventContainer event={event}>
<div className="flex flex-col gap-2">
<ClickableEventTitle
event={event}
className="flex items-center gap-1.5 text-sm font-medium"
>
<Users className="size-4 text-muted-foreground" />
<span>Relay Members</span>
</ClickableEventTitle>
{members.length === 0 ? (
<div className="text-xs text-muted-foreground italic">
Empty membership list
</div>
) : (
<PubkeyListPreview
pubkeys={members}
previewLimit={3}
label="members"
/>
)}
</div>
</BaseEventContainer>
);
}
/**
* Kind 13534 Detail View - Full relay membership list
*/
export function RelayMembersDetailRenderer({ event }: { event: NostrEvent }) {
const members = getTagValues(event, "member");
return (
<div className="flex flex-col gap-6 p-4">
<div className="flex items-center gap-2">
<Users className="size-6 text-muted-foreground" />
<span className="text-lg font-semibold">Relay Members</span>
</div>
{members.length > 0 ? (
<PubkeyListFull pubkeys={members} label="Members" />
) : (
<div className="text-sm text-muted-foreground italic">
Empty membership list
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,132 @@
import { UserPlus, UserMinus } from "lucide-react";
import { getTagValue } from "applesauce-core/helpers";
import {
BaseEventProps,
BaseEventContainer,
ClickableEventTitle,
} from "./BaseEventRenderer";
import { PubkeyListFull } from "../lists";
import { UserName } from "../UserName";
import type { NostrEvent } from "@/types/nostr";
/**
* Kind 8000 Renderer - Add User (Feed View)
* NIP-43 event published when a member is added to a relay
*/
export function AddUserRenderer({ event }: BaseEventProps) {
const pubkey = getTagValue(event, "p");
if (!pubkey) {
return (
<BaseEventContainer event={event}>
<div className="text-xs text-muted-foreground italic">
Invalid event (missing pubkey)
</div>
</BaseEventContainer>
);
}
return (
<BaseEventContainer event={event}>
<div className="flex flex-col gap-2">
<ClickableEventTitle
event={event}
className="flex items-center gap-1.5 text-sm font-medium"
>
<UserPlus className="size-4 text-muted-foreground" />
<span>User Added</span>
</ClickableEventTitle>
<div className="text-xs">
<UserName pubkey={pubkey} className="text-xs" />
<span className="text-muted-foreground"> added to relay</span>
</div>
</div>
</BaseEventContainer>
);
}
/**
* Kind 8000 Detail View
*/
export function AddUserDetailRenderer({ event }: { event: NostrEvent }) {
const pubkey = getTagValue(event, "p");
return (
<div className="flex flex-col gap-6 p-4">
<div className="flex items-center gap-2">
<UserPlus className="size-6 text-muted-foreground" />
<span className="text-lg font-semibold">User Added</span>
</div>
{pubkey ? (
<PubkeyListFull pubkeys={[pubkey]} label="Added Member" />
) : (
<div className="text-sm text-muted-foreground italic">
Invalid event (missing pubkey)
</div>
)}
</div>
);
}
/**
* Kind 8001 Renderer - Remove User (Feed View)
* NIP-43 event published when a member is removed from a relay
*/
export function RemoveUserRenderer({ event }: BaseEventProps) {
const pubkey = getTagValue(event, "p");
if (!pubkey) {
return (
<BaseEventContainer event={event}>
<div className="text-xs text-muted-foreground italic">
Invalid event (missing pubkey)
</div>
</BaseEventContainer>
);
}
return (
<BaseEventContainer event={event}>
<div className="flex flex-col gap-2">
<ClickableEventTitle
event={event}
className="flex items-center gap-1.5 text-sm font-medium"
>
<UserMinus className="size-4 text-muted-foreground" />
<span>User Removed</span>
</ClickableEventTitle>
<div className="text-xs">
<UserName pubkey={pubkey} className="text-xs" />
<span className="text-muted-foreground"> removed from relay</span>
</div>
</div>
</BaseEventContainer>
);
}
/**
* Kind 8001 Detail View
*/
export function RemoveUserDetailRenderer({ event }: { event: NostrEvent }) {
const pubkey = getTagValue(event, "p");
return (
<div className="flex flex-col gap-6 p-4">
<div className="flex items-center gap-2">
<UserMinus className="size-6 text-muted-foreground" />
<span className="text-lg font-semibold">User Removed</span>
</div>
{pubkey ? (
<PubkeyListFull pubkeys={[pubkey]} label="Removed Member" />
) : (
<div className="text-sm text-muted-foreground italic">
Invalid event (missing pubkey)
</div>
)}
</div>
);
}

View File

@@ -67,6 +67,16 @@ import { ZapstoreAppSetDetailRenderer } from "./ZapstoreAppSetDetailRenderer";
import { ZapstoreReleaseRenderer } from "./ZapstoreReleaseRenderer";
import { ZapstoreReleaseDetailRenderer } from "./ZapstoreReleaseDetailRenderer";
import { GroupMetadataRenderer } from "./GroupMetadataRenderer";
import {
RelayMembersRenderer,
RelayMembersDetailRenderer,
} from "./RelayMembersRenderer";
import {
AddUserRenderer,
AddUserDetailRenderer,
RemoveUserRenderer,
RemoveUserDetailRenderer,
} from "./RelayUserChangeRenderer";
// NIP-51 List Renderers
import { MuteListRenderer, MuteListDetailRenderer } from "./MuteListRenderer";
import { PinListRenderer, PinListDetailRenderer } from "./PinListRenderer";
@@ -159,6 +169,8 @@ const kindRenderers: Record<number, React.ComponentType<BaseEventProps>> = {
1621: IssueRenderer, // Issue (NIP-34)
9735: Kind9735Renderer, // Zap Receipt
9802: Kind9802Renderer, // Highlight
8000: AddUserRenderer, // Add User (NIP-43)
8001: RemoveUserRenderer, // Remove User (NIP-43)
777: SpellRenderer, // Spell (Grimoire)
10000: MuteListRenderer, // Mute List (NIP-51)
10001: PinListRenderer, // Pin List (NIP-51)
@@ -178,6 +190,7 @@ const kindRenderers: Record<number, React.ComponentType<BaseEventProps>> = {
10101: WikiAuthorsRenderer, // Good Wiki Authors (NIP-51)
10102: WikiRelaysRenderer, // Good Wiki Relays (NIP-51)
10317: Kind10317Renderer, // User Grasp List (NIP-34)
13534: RelayMembersRenderer, // Relay Members (NIP-43)
30000: FollowSetRenderer, // Follow Sets (NIP-51)
30002: GenericRelayListRenderer, // Relay Sets (NIP-51)
30003: BookmarkSetRenderer, // Bookmark Sets (NIP-51)
@@ -255,6 +268,8 @@ const detailRenderers: Record<
1618: PullRequestDetailRenderer, // Pull Request Detail (NIP-34)
1621: IssueDetailRenderer, // Issue Detail (NIP-34)
9802: Kind9802DetailRenderer, // Highlight Detail
8000: AddUserDetailRenderer, // Add User Detail (NIP-43)
8001: RemoveUserDetailRenderer, // Remove User Detail (NIP-43)
10000: MuteListDetailRenderer, // Mute List Detail (NIP-51)
10001: PinListDetailRenderer, // Pin List Detail (NIP-51)
10002: Kind10002DetailRenderer, // Relay List Detail (NIP-65)
@@ -268,6 +283,7 @@ const detailRenderers: Record<
10101: WikiAuthorsDetailRenderer, // Good Wiki Authors Detail (NIP-51)
10102: WikiRelaysDetailRenderer, // Good Wiki Relays Detail (NIP-51)
10317: Kind10317DetailRenderer, // User Grasp List Detail (NIP-34)
13534: RelayMembersDetailRenderer, // Relay Members Detail (NIP-43)
30000: FollowSetDetailRenderer, // Follow Sets Detail (NIP-51)
30003: BookmarkSetDetailRenderer, // Bookmark Sets Detail (NIP-51)
30004: ArticleCurationSetDetailRenderer, // Article Curation Sets Detail (NIP-51)

View File

@@ -887,13 +887,13 @@ export const EVENT_KINDS: Record<number | string, EventKind> = {
nip: "47",
icon: Wallet,
},
// 13534: {
// kind: 13534,
// name: "Membership Lists",
// description: "Membership Lists",
// nip: "43",
// icon: Users,
// },
13534: {
kind: 13534,
name: "Relay Members",
description: "Relay membership list",
nip: "43",
icon: Users,
},
// 17375: {
// kind: 17375,
// name: "Cashu Wallet Event",
@@ -950,27 +950,27 @@ export const EVENT_KINDS: Record<number | string, EventKind> = {
nip: "98",
icon: Key,
},
// 28934: {
// kind: 28934,
// name: "Join Request",
// description: "Join Request",
// nip: "43",
// icon: UserPlus,
// },
// 28935: {
// kind: 28935,
// name: "Invite Request",
// description: "Invite Request",
// nip: "43",
// icon: Mail,
// },
// 28936: {
// kind: 28936,
// name: "Leave Request",
// description: "Leave Request",
// nip: "43",
// icon: UserMinus,
// },
28934: {
kind: 28934,
name: "Join Request",
description: "Relay join request with invite code",
nip: "43",
icon: UserPlus,
},
28935: {
kind: 28935,
name: "Invite Request",
description: "Request invite code from relay",
nip: "43",
icon: Mail,
},
28936: {
kind: 28936,
name: "Leave Request",
description: "Request to revoke relay access",
nip: "43",
icon: UserMinus,
},
// Group Control
9000: {