From 66b65eb152e9790d3768c017374b9fd164d2b30a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Jan 2026 17:30:42 +0000 Subject: [PATCH] feat: Add NIP-58 Badge Award renderers (kind 8) Implement feed and detail renderers for Badge Award events that: - Fetch and display badge metadata (thumbnail, name) - Show recipient count in feed view ("Awarded to n people") - Display full recipient list with usernames in detail view - Link badge thumbnail and name to the badge event (kind 30009) - Support award comments in detail view Technical details: - Parse badge address from "a" tag (30009:pubkey:identifier format) - Fetch badge event reactively using eventStore.replaceable - Use AddressPointer from nostr-tools/nip19 for type safety - Register kind 8 in both feed and detail renderer registries Also updates all "badge definition" terminology to just "badge" in code comments for consistency with user-facing strings. --- .../nostr/kinds/BadgeAwardDetailRenderer.tsx | 139 ++++++++++++++++++ .../nostr/kinds/BadgeAwardRenderer.tsx | 106 +++++++++++++ src/components/nostr/kinds/index.tsx | 4 + src/lib/nip58-helpers.ts | 6 +- 4 files changed, 252 insertions(+), 3 deletions(-) create mode 100644 src/components/nostr/kinds/BadgeAwardDetailRenderer.tsx create mode 100644 src/components/nostr/kinds/BadgeAwardRenderer.tsx diff --git a/src/components/nostr/kinds/BadgeAwardDetailRenderer.tsx b/src/components/nostr/kinds/BadgeAwardDetailRenderer.tsx new file mode 100644 index 0000000..b6efdd0 --- /dev/null +++ b/src/components/nostr/kinds/BadgeAwardDetailRenderer.tsx @@ -0,0 +1,139 @@ +import { NostrEvent } from "@/types/nostr"; +import { getAwardBadgeAddress, getAwardedPubkeys } from "@/lib/nip58-helpers"; +import { use$ } from "applesauce-react/hooks"; +import eventStore from "@/services/event-store"; +import { AddressPointer } from "nostr-tools/nip19"; +import { + getBadgeName, + getBadgeIdentifier, + getBadgeImageUrl, +} from "@/lib/nip58-helpers"; +import { UserName } from "../UserName"; +import { Award } from "lucide-react"; +import { ClickableEventTitle } from "./BaseEventRenderer"; + +interface BadgeAwardDetailRendererProps { + event: NostrEvent; +} + +/** + * Parse an address pointer from an a tag value + * Format: "kind:pubkey:identifier" + */ +function parseAddress(aTagValue: string): AddressPointer | null { + const parts = aTagValue.split(":"); + if (parts.length !== 3) return null; + + const kind = parseInt(parts[0], 10); + const pubkey = parts[1]; + const identifier = parts[2]; + + if (isNaN(kind) || !pubkey || identifier === undefined) return null; + + return { kind, pubkey, identifier }; +} + +/** + * Detail renderer for Kind 8 - Badge Award (NIP-58) + * Shows badge information and list of recipients + */ +export function BadgeAwardDetailRenderer({ + event, +}: BadgeAwardDetailRendererProps) { + const badgeAddress = getAwardBadgeAddress(event); + const awardedPubkeys = getAwardedPubkeys(event); + + // Parse the badge address (30009:pubkey:identifier) + const coordinate = badgeAddress ? parseAddress(badgeAddress) : null; + + // Fetch the badge event + const badgeEvent = use$( + () => + coordinate + ? eventStore.replaceable( + coordinate.kind, + coordinate.pubkey, + coordinate.identifier, + ) + : undefined, + [coordinate?.kind, coordinate?.pubkey, coordinate?.identifier], + ); + + // Get badge metadata + const badgeName = badgeEvent ? getBadgeName(badgeEvent) : null; + const badgeIdentifier = badgeEvent ? getBadgeIdentifier(badgeEvent) : null; + const badgeImageUrl = badgeEvent ? getBadgeImageUrl(badgeEvent) : null; + + const displayTitle = badgeName || badgeIdentifier || "Badge"; + + return ( +
+ {/* Badge Header */} +
+ {/* Badge Image */} + {badgeImageUrl ? ( + {displayTitle} + ) : ( +
+ +
+ )} + + {/* Badge Title */} +
+ {badgeEvent ? ( + + {displayTitle} + + ) : ( +

{displayTitle}

+ )} +

Badge Award

+
+
+ + {/* Metadata */} +
+

Issued By

+ +
+ + {/* Recipients List */} + {awardedPubkeys.length > 0 && ( +
+

+ Awarded to {awardedPubkeys.length}{" "} + {awardedPubkeys.length === 1 ? "Person" : "People"} +

+
+ {awardedPubkeys.map((pubkey) => ( +
+ + +
+ ))} +
+
+ )} + + {/* Award Comment */} + {event.content && ( +
+

Comment

+

{event.content}

+
+ )} +
+ ); +} diff --git a/src/components/nostr/kinds/BadgeAwardRenderer.tsx b/src/components/nostr/kinds/BadgeAwardRenderer.tsx new file mode 100644 index 0000000..b787170 --- /dev/null +++ b/src/components/nostr/kinds/BadgeAwardRenderer.tsx @@ -0,0 +1,106 @@ +import { + BaseEventContainer, + BaseEventProps, + ClickableEventTitle, +} from "./BaseEventRenderer"; +import { getAwardBadgeAddress, getAwardedPubkeys } from "@/lib/nip58-helpers"; +import { use$ } from "applesauce-react/hooks"; +import eventStore from "@/services/event-store"; +import { AddressPointer } from "nostr-tools/nip19"; +import { + getBadgeName, + getBadgeIdentifier, + getBadgeImageUrl, +} from "@/lib/nip58-helpers"; +import { Award } from "lucide-react"; + +/** + * Parse an address pointer from an a tag value + * Format: "kind:pubkey:identifier" + */ +function parseAddress(aTagValue: string): AddressPointer | null { + const parts = aTagValue.split(":"); + if (parts.length !== 3) return null; + + const kind = parseInt(parts[0], 10); + const pubkey = parts[1]; + const identifier = parts[2]; + + if (isNaN(kind) || !pubkey || identifier === undefined) return null; + + return { kind, pubkey, identifier }; +} + +/** + * Renderer for Kind 8 - Badge Award (NIP-58) + * Shows badge thumbnail, name, and number of recipients + */ +export function BadgeAwardRenderer({ event }: BaseEventProps) { + const badgeAddress = getAwardBadgeAddress(event); + const awardedPubkeys = getAwardedPubkeys(event); + + // Parse the badge address (30009:pubkey:identifier) + const coordinate = badgeAddress ? parseAddress(badgeAddress) : null; + + // Fetch the badge event + const badgeEvent = use$( + () => + coordinate + ? eventStore.replaceable( + coordinate.kind, + coordinate.pubkey, + coordinate.identifier, + ) + : undefined, + [coordinate?.kind, coordinate?.pubkey, coordinate?.identifier], + ); + + // Get badge metadata + const badgeName = badgeEvent ? getBadgeName(badgeEvent) : null; + const badgeIdentifier = badgeEvent ? getBadgeIdentifier(badgeEvent) : null; + const badgeImageUrl = badgeEvent ? getBadgeImageUrl(badgeEvent) : null; + + const displayTitle = badgeName || badgeIdentifier || "Badge"; + const recipientCount = awardedPubkeys.length; + + return ( + +
+ {/* Badge Thumbnail */} + {badgeImageUrl ? ( + {displayTitle} + ) : ( +
+ +
+ )} + + {/* Badge Info */} +
+ {badgeEvent ? ( + + {displayTitle} + + ) : ( + + {displayTitle} + + )} + +

+ Awarded to {recipientCount}{" "} + {recipientCount === 1 ? "person" : "people"} +

+
+
+
+ ); +} diff --git a/src/components/nostr/kinds/index.tsx b/src/components/nostr/kinds/index.tsx index f3de435..5fabfc0 100644 --- a/src/components/nostr/kinds/index.tsx +++ b/src/components/nostr/kinds/index.tsx @@ -144,6 +144,8 @@ import { P2pOrderRenderer } from "./P2pOrderRenderer"; import { P2pOrderDetailRenderer } from "./P2pOrderDetailRenderer"; import { BadgeDefinitionRenderer } from "./BadgeDefinitionRenderer"; import { BadgeDefinitionDetailRenderer } from "./BadgeDefinitionDetailRenderer"; +import { BadgeAwardRenderer } from "./BadgeAwardRenderer"; +import { BadgeAwardDetailRenderer } from "./BadgeAwardDetailRenderer"; /** * Registry of kind-specific renderers @@ -155,6 +157,7 @@ const kindRenderers: Record> = { 3: Kind3Renderer, // Contact List 6: RepostRenderer, // Repost 7: Kind7Renderer, // Reaction + 8: BadgeAwardRenderer, // Badge Award (NIP-58) 9: Kind9Renderer, // Chat Message (NIP-C7) 11: Kind1Renderer, // Public Thread Reply (NIP-10) 16: RepostRenderer, // Generic Repost @@ -268,6 +271,7 @@ const detailRenderers: Record< > = { 0: Kind0DetailRenderer, // Profile Metadata Detail 3: Kind3DetailView, // Contact List Detail + 8: BadgeAwardDetailRenderer, // Badge Award Detail (NIP-58) 777: SpellDetailRenderer, // Spell Detail 1337: Kind1337DetailRenderer, // Code Snippet Detail (NIP-C0) 1617: PatchDetailRenderer, // Patch Detail (NIP-34) diff --git a/src/lib/nip58-helpers.ts b/src/lib/nip58-helpers.ts index f89b5d7..1b36102 100644 --- a/src/lib/nip58-helpers.ts +++ b/src/lib/nip58-helpers.ts @@ -9,7 +9,7 @@ import type { NostrEvent } from "nostr-tools"; */ /** - * Get the unique identifier for a badge definition (d tag) + * Get the unique identifier for a badge (d tag) */ export function getBadgeIdentifier(event: NostrEvent): string | undefined { return getTagValue(event, "d"); @@ -78,14 +78,14 @@ export function getBadgeImageUrl(event: NostrEvent): string | null { /** * Get all pubkeys awarded this badge (from kind 8 award events) - * Note: This should be called on award events (kind 8), not badge definitions + * Note: This should be called on award events (kind 8), not badges (kind 30009) */ export function getAwardedPubkeys(awardEvent: NostrEvent): string[] { return getTagValues(awardEvent, "p"); } /** - * Get the badge definition address referenced by an award event (kind 8) + * Get the badge address referenced by an award event (kind 8) * @returns The "a" tag value (e.g., "30009:pubkey:identifier") */ export function getAwardBadgeAddress(