From 1519e30e5c52f722b9fcfb545479a7d114a10cd6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Dec 2025 12:35:23 +0000 Subject: [PATCH] feat: add repository state (kind 30618) visualization Implement renderers for NIP-34 repository state announcements: - Add helper functions in nip34-helpers.ts to parse HEAD refs, branches, and tags - Create RepositoryStateRenderer for compact feed view showing push notifications - Create RepositoryStateDetailRenderer for detailed view with all refs - Register both renderers in the kind registry Feed view shows: "pushed to in " Detail view shows: HEAD info, all branches, all tags with copyable commit hashes --- .../kinds/RepositoryStateDetailRenderer.tsx | 143 ++++++++++++++++++ .../nostr/kinds/RepositoryStateRenderer.tsx | 51 +++++++ src/components/nostr/kinds/index.tsx | 4 + src/lib/nip34-helpers.ts | 87 +++++++++++ 4 files changed, 285 insertions(+) create mode 100644 src/components/nostr/kinds/RepositoryStateDetailRenderer.tsx create mode 100644 src/components/nostr/kinds/RepositoryStateRenderer.tsx diff --git a/src/components/nostr/kinds/RepositoryStateDetailRenderer.tsx b/src/components/nostr/kinds/RepositoryStateDetailRenderer.tsx new file mode 100644 index 0000000..a3e17dc --- /dev/null +++ b/src/components/nostr/kinds/RepositoryStateDetailRenderer.tsx @@ -0,0 +1,143 @@ +import { useMemo } from "react"; +import { GitBranch, GitCommit, Tag, Copy, CopyCheck } from "lucide-react"; +import { useCopy } from "@/hooks/useCopy"; +import type { NostrEvent } from "@/types/nostr"; +import { + getRepositoryIdentifier, + getRepositoryStateHead, + parseHeadBranch, + getRepositoryStateHeadCommit, + getRepositoryStateBranches, + getRepositoryStateTags, +} from "@/lib/nip34-helpers"; + +/** + * Detail renderer for Kind 30618 - Repository State + * Displays full repository state with all refs, branches, and tags + */ +export function RepositoryStateDetailRenderer({ event }: { event: NostrEvent }) { + const repoId = useMemo(() => getRepositoryIdentifier(event), [event]); + const headRef = useMemo(() => getRepositoryStateHead(event), [event]); + const branch = useMemo(() => parseHeadBranch(headRef), [event, headRef]); + const commitHash = useMemo(() => getRepositoryStateHeadCommit(event), [event]); + const branches = useMemo(() => getRepositoryStateBranches(event), [event]); + const tags = useMemo(() => getRepositoryStateTags(event), [event]); + + const displayName = repoId || "Repository"; + + return ( +
+ {/* Repository Header */} +
+ {/* Name */} +

{displayName}

+ + {/* HEAD Info */} + {branch && commitHash && ( +
+
+ + HEAD +
+
+
+ Branch: + {branch} +
+
+ Commit: + +
+
+
+ )} +
+ + {/* Branches Section */} + {branches.length > 0 && ( +
+

+ + Branches +

+
    + {branches.map(({ name, hash }) => ( +
  • + + {name} + + +
  • + ))} +
+
+ )} + + {/* Tags Section */} + {tags.length > 0 && ( +
+

+ + Tags +

+
    + {tags.map(({ name, hash }) => ( +
  • + + {name} + + +
  • + ))} +
+
+ )} + + {/* Raw HEAD Reference */} + {headRef && ( +
+

+ HEAD Reference +

+ + {headRef} + +
+ )} +
+ ); +} + +/** + * Component to display a commit hash with copy button + */ +function CommitHashItem({ hash }: { hash: string }) { + const { copy, copied } = useCopy(); + const shortHash = hash.substring(0, 8); + + return ( +
+ + {shortHash} + + +
+ ); +} diff --git a/src/components/nostr/kinds/RepositoryStateRenderer.tsx b/src/components/nostr/kinds/RepositoryStateRenderer.tsx new file mode 100644 index 0000000..71cf5f1 --- /dev/null +++ b/src/components/nostr/kinds/RepositoryStateRenderer.tsx @@ -0,0 +1,51 @@ +import { + BaseEventContainer, + type BaseEventProps, + ClickableEventTitle, +} from "./BaseEventRenderer"; +import { GitCommit } from "lucide-react"; +import { + getRepositoryIdentifier, + getRepositoryStateHeadCommit, + parseHeadBranch, + getRepositoryStateHead, +} from "@/lib/nip34-helpers"; + +/** + * Renderer for Kind 30618 - Repository State + * Displays as a compact git push notification in feed view + */ +export function RepositoryStateRenderer({ event }: BaseEventProps) { + const repoId = getRepositoryIdentifier(event); + const headRef = getRepositoryStateHead(event); + const branch = parseHeadBranch(headRef); + const commitHash = getRepositoryStateHeadCommit(event); + + // Format: "pushed <8 chars of HEAD commit> to in " + const shortHash = commitHash?.substring(0, 8) || "unknown"; + const branchName = branch || "unknown"; + const repoName = repoId || "repository"; + + return ( + +
+ {/* Push notification */} +
+ + + pushed{" "} + + {shortHash} + {" "} + to {branchName} in{" "} + {repoName} + +
+
+
+ ); +} diff --git a/src/components/nostr/kinds/index.tsx b/src/components/nostr/kinds/index.tsx index a10da0b..fce03f9 100644 --- a/src/components/nostr/kinds/index.tsx +++ b/src/components/nostr/kinds/index.tsx @@ -30,6 +30,8 @@ import { CommunityNIPRenderer } from "./CommunityNIPRenderer"; import { CommunityNIPDetailRenderer } from "./CommunityNIPDetailRenderer"; import { RepositoryRenderer } from "./RepositoryRenderer"; import { RepositoryDetailRenderer } from "./RepositoryDetailRenderer"; +import { RepositoryStateRenderer } from "./RepositoryStateRenderer"; +import { RepositoryStateDetailRenderer } from "./RepositoryStateDetailRenderer"; import { Kind39701Renderer } from "./BookmarkRenderer"; import { GenericRelayListRenderer } from "./GenericRelayListRenderer"; import { LiveActivityRenderer } from "./LiveActivityRenderer"; @@ -73,6 +75,7 @@ const kindRenderers: Record> = { 30023: Kind30023Renderer, // Long-form Article 30311: LiveActivityRenderer, // Live Streaming Event (NIP-53) 30617: RepositoryRenderer, // Repository (NIP-34) + 30618: RepositoryStateRenderer, // Repository State (NIP-34) 30817: CommunityNIPRenderer, // Community NIP 39701: Kind39701Renderer, // Web Bookmarks (NIP-B0) }; @@ -128,6 +131,7 @@ const detailRenderers: Record< 30023: Kind30023DetailRenderer, // Long-form Article Detail 30311: LiveActivityDetailRenderer, // Live Streaming Event Detail (NIP-53) 30617: RepositoryDetailRenderer, // Repository Detail (NIP-34) + 30618: RepositoryStateDetailRenderer, // Repository State Detail (NIP-34) 30817: CommunityNIPDetailRenderer, // Community NIP Detail }; diff --git a/src/lib/nip34-helpers.ts b/src/lib/nip34-helpers.ts index 97aa0e7..c921686 100644 --- a/src/lib/nip34-helpers.ts +++ b/src/lib/nip34-helpers.ts @@ -281,3 +281,90 @@ export function getPullRequestRepositoryAddress( ): string | undefined { return getTagValue(event, "a"); } + +// ============================================================================ +// Repository State Event Helpers (Kind 30618) +// ============================================================================ + +/** + * Get the HEAD reference from a repository state event + * @param event Repository state event (kind 30618) + * @returns HEAD reference (e.g., "ref: refs/heads/main") or undefined + */ +export function getRepositoryStateHead(event: NostrEvent): string | undefined { + return getTagValue(event, "HEAD"); +} + +/** + * Parse HEAD reference to extract branch name + * @param headRef HEAD reference string (e.g., "ref: refs/heads/main") + * @returns Branch name (e.g., "main") or undefined + */ +export function parseHeadBranch(headRef: string | undefined): string | undefined { + if (!headRef) return undefined; + const match = headRef.match(/^ref:\s*refs\/heads\/(.+)$/); + return match ? match[1] : undefined; +} + +/** + * Get all git refs from a repository state event + * @param event Repository state event (kind 30618) + * @returns Array of { ref: string, hash: string } objects + */ +export function getRepositoryStateRefs( + event: NostrEvent, +): Array<{ ref: string; hash: string }> { + return event.tags + .filter((t) => t[0].startsWith("refs/")) + .map((t) => ({ ref: t[0], hash: t[1] })); +} + +/** + * Get the commit hash that HEAD points to + * @param event Repository state event (kind 30618) + * @returns Commit hash or undefined + */ +export function getRepositoryStateHeadCommit( + event: NostrEvent, +): string | undefined { + const headRef = getRepositoryStateHead(event); + const branch = parseHeadBranch(headRef); + if (!branch) return undefined; + + // Find the refs/heads/{branch} tag + const branchRef = `refs/heads/${branch}`; + const branchTag = event.tags.find((t) => t[0] === branchRef); + return branchTag ? branchTag[1] : undefined; +} + +/** + * Get branches from repository state refs + * @param event Repository state event (kind 30618) + * @returns Array of { name: string, hash: string } objects + */ +export function getRepositoryStateBranches( + event: NostrEvent, +): Array<{ name: string; hash: string }> { + return event.tags + .filter((t) => t[0].startsWith("refs/heads/")) + .map((t) => ({ + name: t[0].replace("refs/heads/", ""), + hash: t[1], + })); +} + +/** + * Get tags from repository state refs + * @param event Repository state event (kind 30618) + * @returns Array of { name: string, hash: string } objects + */ +export function getRepositoryStateTags( + event: NostrEvent, +): Array<{ name: string; hash: string }> { + return event.tags + .filter((t) => t[0].startsWith("refs/tags/")) + .map((t) => ({ + name: t[0].replace("refs/tags/", ""), + hash: t[1], + })); +}