mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-09 23:16:50 +02:00
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 <commit> to <branch> in <repo>" Detail view shows: HEAD info, all branches, all tags with copyable commit hashes
This commit is contained in:
143
src/components/nostr/kinds/RepositoryStateDetailRenderer.tsx
Normal file
143
src/components/nostr/kinds/RepositoryStateDetailRenderer.tsx
Normal file
@@ -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 (
|
||||
<div className="flex flex-col gap-4 p-4 max-w-3xl mx-auto">
|
||||
{/* Repository Header */}
|
||||
<header className="flex flex-col gap-4 border-b border-border pb-4">
|
||||
{/* Name */}
|
||||
<h1 className="text-3xl font-bold">{displayName}</h1>
|
||||
|
||||
{/* HEAD Info */}
|
||||
{branch && commitHash && (
|
||||
<div className="flex flex-col gap-2 p-3 bg-muted/30 rounded">
|
||||
<div className="flex items-center gap-2">
|
||||
<GitCommit className="size-4 text-muted-foreground" />
|
||||
<span className="text-sm font-semibold">HEAD</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 pl-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">Branch:</span>
|
||||
<code className="text-sm font-mono font-semibold">{branch}</code>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">Commit:</span>
|
||||
<CommitHashItem hash={commitHash} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
|
||||
{/* Branches Section */}
|
||||
{branches.length > 0 && (
|
||||
<section className="flex flex-col gap-4">
|
||||
<h2 className="text-xl font-semibold flex items-center gap-2">
|
||||
<GitBranch className="size-5" />
|
||||
Branches
|
||||
</h2>
|
||||
<ul className="flex flex-col gap-2">
|
||||
{branches.map(({ name, hash }) => (
|
||||
<li
|
||||
key={name}
|
||||
className="flex items-center justify-between gap-4 p-2 bg-muted/30 rounded"
|
||||
>
|
||||
<span className="text-sm font-mono font-semibold truncate">
|
||||
{name}
|
||||
</span>
|
||||
<CommitHashItem hash={hash} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Tags Section */}
|
||||
{tags.length > 0 && (
|
||||
<section className="flex flex-col gap-4">
|
||||
<h2 className="text-xl font-semibold flex items-center gap-2">
|
||||
<Tag className="size-5" />
|
||||
Tags
|
||||
</h2>
|
||||
<ul className="flex flex-col gap-2">
|
||||
{tags.map(({ name, hash }) => (
|
||||
<li
|
||||
key={name}
|
||||
className="flex items-center justify-between gap-4 p-2 bg-muted/30 rounded"
|
||||
>
|
||||
<span className="text-sm font-mono font-semibold truncate">
|
||||
{name}
|
||||
</span>
|
||||
<CommitHashItem hash={hash} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Raw HEAD Reference */}
|
||||
{headRef && (
|
||||
<section className="flex flex-col gap-2">
|
||||
<h2 className="text-sm font-semibold text-muted-foreground">
|
||||
HEAD Reference
|
||||
</h2>
|
||||
<code className="text-xs font-mono p-2 bg-muted/30 rounded">
|
||||
{headRef}
|
||||
</code>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (
|
||||
<div className="flex items-center gap-2 group">
|
||||
<code className="text-xs font-mono text-muted-foreground">
|
||||
{shortHash}
|
||||
</code>
|
||||
<button
|
||||
onClick={() => copy(hash)}
|
||||
className="flex-shrink-0 p-1 hover:bg-muted rounded"
|
||||
aria-label="Copy commit hash"
|
||||
title={hash}
|
||||
>
|
||||
{copied ? (
|
||||
<CopyCheck className="size-3 text-muted-foreground" />
|
||||
) : (
|
||||
<Copy className="size-3 text-muted-foreground" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
51
src/components/nostr/kinds/RepositoryStateRenderer.tsx
Normal file
51
src/components/nostr/kinds/RepositoryStateRenderer.tsx
Normal file
@@ -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 <branch> in <repo>"
|
||||
const shortHash = commitHash?.substring(0, 8) || "unknown";
|
||||
const branchName = branch || "unknown";
|
||||
const repoName = repoId || "repository";
|
||||
|
||||
return (
|
||||
<BaseEventContainer event={event}>
|
||||
<div className="flex flex-col gap-2">
|
||||
{/* Push notification */}
|
||||
<div className="flex items-center gap-2">
|
||||
<GitCommit className="size-4 text-muted-foreground flex-shrink-0" />
|
||||
<ClickableEventTitle
|
||||
event={event}
|
||||
className="text-sm font-medium text-foreground"
|
||||
as="span"
|
||||
>
|
||||
pushed{" "}
|
||||
<code className="px-1.5 py-0.5 rounded bg-muted text-xs font-mono">
|
||||
{shortHash}
|
||||
</code>{" "}
|
||||
to <span className="font-semibold">{branchName}</span> in{" "}
|
||||
<span className="font-semibold">{repoName}</span>
|
||||
</ClickableEventTitle>
|
||||
</div>
|
||||
</div>
|
||||
</BaseEventContainer>
|
||||
);
|
||||
}
|
||||
@@ -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<number, React.ComponentType<BaseEventProps>> = {
|
||||
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
|
||||
};
|
||||
|
||||
|
||||
@@ -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],
|
||||
}));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user