refactor: repo link

This commit is contained in:
Alejandro Gómez
2025-12-21 18:19:36 +01:00
parent fffb80cd5c
commit be01c7d882
8 changed files with 165 additions and 359 deletions

View File

@@ -0,0 +1,111 @@
import { useMemo } from "react";
import { FolderGit2 } from "lucide-react";
import { useGrimoire } from "@/core/state";
import { useNostrEvent } from "@/hooks/useNostrEvent";
import {
getRepositoryName,
getRepositoryIdentifier,
} from "@/lib/nip34-helpers";
import { cn } from "@/lib/utils";
interface RepoPointer {
kind: number;
pubkey: string;
identifier: string;
}
interface RepositoryLinkProps {
/** Repository address in "kind:pubkey:identifier" format */
repoAddress?: string;
/** Direct repository pointer (takes precedence over repoAddress) */
repoPointer?: RepoPointer | null;
/** Additional CSS classes */
className?: string;
/** Icon size class (default: "size-3") */
iconSize?: string;
/** Whether to show inline (no wrapping div) */
inline?: boolean;
/** Whether to show the icon */
showIcon?: boolean;
}
/**
* Reusable repository link component for git-related events.
* Fetches repository metadata and renders a clickable link.
*/
export function RepositoryLink({
repoAddress,
repoPointer: externalPointer,
className,
iconSize = "size-3",
inline = false,
showIcon = true,
}: RepositoryLinkProps) {
const { addWindow } = useGrimoire();
// Parse repository address to get the pointer (if not provided directly)
const repoPointer = useMemo(() => {
if (externalPointer) return externalPointer;
if (!repoAddress) return null;
try {
const [kindStr, pubkey, identifier] = repoAddress.split(":");
return {
kind: parseInt(kindStr),
pubkey,
identifier,
};
} catch {
return null;
}
}, [externalPointer, repoAddress]);
// Fetch the repository event to get its name
const repoEvent = useNostrEvent(repoPointer || undefined);
// Get repository display name
const repoName = useMemo(() => {
if (repoEvent) {
return (
getRepositoryName(repoEvent) ||
getRepositoryIdentifier(repoEvent) ||
"Repository"
);
}
// Fall back to identifier from address or pointer
if (repoPointer?.identifier) return repoPointer.identifier;
if (repoAddress) return repoAddress.split(":")[2] || "Unknown Repository";
return "Unknown Repository";
}, [repoEvent, repoPointer, repoAddress]);
const handleClick = () => {
if (!repoPointer) return;
addWindow("open", { pointer: repoPointer });
};
if (!repoAddress && !externalPointer) return null;
const linkContent = (
<>
{showIcon && <FolderGit2 className={cn(iconSize, "flex-shrink-0")} />}
<span>{repoName}</span>
</>
);
const baseClasses =
"flex items-center gap-1 text-muted-foreground cursor-crosshair underline decoration-dotted hover:text-primary";
if (inline) {
return (
<span onClick={handleClick} className={cn(baseClasses, className)}>
{linkContent}
</span>
);
}
return (
<div onClick={handleClick} className={cn(baseClasses, className)}>
{linkContent}
</div>
);
}

View File

@@ -1,55 +1,25 @@
import { useMemo } from "react";
import { Tag, FolderGit2 } from "lucide-react";
import { Tag } from "lucide-react";
import { UserName } from "../UserName";
import { MarkdownContent } from "../MarkdownContent";
import { useGrimoire } from "@/core/state";
import { useNostrEvent } from "@/hooks/useNostrEvent";
import type { NostrEvent } from "@/types/nostr";
import {
getIssueTitle,
getIssueLabels,
getIssueRepositoryAddress,
getRepositoryName,
getRepositoryIdentifier,
} from "@/lib/nip34-helpers";
import { Label } from "@/components/ui/label";
import { RepositoryLink } from "../RepositoryLink";
/**
* Detail renderer for Kind 1621 - Issue (NIP-34)
* Full view with repository context and markdown description
*/
export function IssueDetailRenderer({ event }: { event: NostrEvent }) {
const { addWindow } = useGrimoire();
const title = useMemo(() => getIssueTitle(event), [event]);
const labels = useMemo(() => getIssueLabels(event), [event]);
const repoAddress = useMemo(() => getIssueRepositoryAddress(event), [event]);
// Parse repository address if present
const repoPointer = useMemo(() => {
if (!repoAddress) return null;
try {
const [kindStr, pubkey, identifier] = repoAddress.split(":");
return {
kind: parseInt(kindStr),
pubkey,
identifier,
};
} catch {
return null;
}
}, [repoAddress]);
// Fetch repository event
const repoEvent = useNostrEvent(repoPointer || undefined);
// Get repository display name
const repoName = repoEvent
? getRepositoryName(repoEvent) ||
getRepositoryIdentifier(repoEvent) ||
"Repository"
: repoPointer?.identifier || "Unknown Repository";
// Format created date
const createdDate = new Date(event.created_at * 1000).toLocaleDateString(
"en-US",
@@ -60,11 +30,6 @@ export function IssueDetailRenderer({ event }: { event: NostrEvent }) {
},
);
const handleRepoClick = () => {
if (!repoPointer || !repoEvent) return;
addWindow("open", { pointer: repoPointer });
};
return (
<div className="flex flex-col gap-4 p-4 max-w-3xl mx-auto">
{/* Issue Header */}
@@ -76,18 +41,11 @@ export function IssueDetailRenderer({ event }: { event: NostrEvent }) {
{repoAddress && (
<div className="flex items-center gap-2 text-sm">
<span className="text-muted-foreground">Repository:</span>
<button
onClick={repoEvent ? handleRepoClick : undefined}
disabled={!repoEvent}
className={`flex items-center gap-2 font-mono ${
repoEvent
? "text-muted-foreground underline decoration-dotted cursor-crosshair hover:text-primary"
: "text-muted-foreground cursor-not-allowed"
}`}
>
<FolderGit2 className="size-4" />
{repoName}
</button>
<RepositoryLink
repoAddress={repoAddress}
iconSize="size-4"
className="font-mono"
/>
</div>
)}

View File

@@ -3,69 +3,23 @@ import {
type BaseEventProps,
ClickableEventTitle,
} from "./BaseEventRenderer";
import { FolderGit2 } from "lucide-react";
import { useGrimoire } from "@/core/state";
import { useNostrEvent } from "@/hooks/useNostrEvent";
import {
getIssueTitle,
getIssueLabels,
getIssueRepositoryAddress,
} from "@/lib/nip34-helpers";
import {
getRepositoryName,
getRepositoryIdentifier,
} from "@/lib/nip34-helpers";
import { Label } from "@/components/ui/label";
import { RepositoryLink } from "../RepositoryLink";
/**
* Renderer for Kind 1621 - Issue
* Displays as a compact issue card in feed view
*/
export function IssueRenderer({ event }: BaseEventProps) {
const { addWindow } = useGrimoire();
const title = getIssueTitle(event);
const labels = getIssueLabels(event);
const repoAddress = getIssueRepositoryAddress(event);
// Parse repository address to get the pointer
const repoPointer = repoAddress
? (() => {
try {
// Address format: "kind:pubkey:identifier"
const [kindStr, pubkey, identifier] = repoAddress.split(":");
return {
kind: parseInt(kindStr),
pubkey,
identifier,
};
} catch {
return null;
}
})()
: null;
// Fetch the repository event to get its name
const repoEvent = useNostrEvent(
repoPointer
? {
kind: repoPointer.kind,
pubkey: repoPointer.pubkey,
identifier: repoPointer.identifier,
}
: undefined,
);
// Get repository display name
const repoName = repoEvent
? getRepositoryName(repoEvent) ||
getRepositoryIdentifier(repoEvent) ||
"Repository"
: repoAddress?.split(":")[2] || "Unknown Repository";
const handleRepoClick = () => {
addWindow("open", { pointer: repoPointer });
};
return (
<BaseEventContainer event={event}>
<div className="flex flex-col gap-2">
@@ -79,17 +33,9 @@ export function IssueRenderer({ event }: BaseEventProps) {
</ClickableEventTitle>
{/* Repository Reference */}
{repoAddress && repoPointer && (
{repoAddress && (
<div className="text-xs line-clamp-1">
<div
onClick={handleRepoClick}
className={`flex items-center gap-1 text-muted-foreground
cursor-crosshair underline decoration-dotted hover:text-primary
`}
>
<FolderGit2 className="size-3" />
<span>{repoName}</span>
</div>
<RepositoryLink repoAddress={repoAddress} />
</div>
)}
</div>

View File

@@ -1,10 +1,8 @@
import { useMemo } from "react";
import { GitCommit, FolderGit2, User, Copy, CopyCheck } from "lucide-react";
import { GitCommit, User, Copy, CopyCheck } from "lucide-react";
import { UserName } from "../UserName";
import { CodeCopyButton } from "@/components/CodeCopyButton";
import { useCopy } from "@/hooks/useCopy";
import { useGrimoire } from "@/core/state";
import { useNostrEvent } from "@/hooks/useNostrEvent";
import { SyntaxHighlight } from "@/components/SyntaxHighlight";
import type { NostrEvent } from "@/types/nostr";
import {
@@ -16,17 +14,13 @@ import {
isPatchRoot,
isPatchRootRevision,
} from "@/lib/nip34-helpers";
import {
getRepositoryName,
getRepositoryIdentifier,
} from "@/lib/nip34-helpers";
import { RepositoryLink } from "../RepositoryLink";
/**
* Detail renderer for Kind 1617 - Patch
* Displays full patch metadata and content
*/
export function PatchDetailRenderer({ event }: { event: NostrEvent }) {
const { addWindow } = useGrimoire();
const { copy, copied } = useCopy();
const subject = useMemo(() => getPatchSubject(event), [event]);
@@ -37,36 +31,6 @@ export function PatchDetailRenderer({ event }: { event: NostrEvent }) {
const isRoot = useMemo(() => isPatchRoot(event), [event]);
const isRootRevision = useMemo(() => isPatchRootRevision(event), [event]);
// Parse repository address
const repoPointer = useMemo(() => {
if (!repoAddress) return null;
try {
const [kindStr, pubkey, identifier] = repoAddress.split(":");
return {
kind: parseInt(kindStr),
pubkey,
identifier,
};
} catch {
return null;
}
}, [repoAddress]);
// Fetch repository event
const repoEvent = useNostrEvent(repoPointer || undefined);
// Get repository display name
const repoName = repoEvent
? getRepositoryName(repoEvent) ||
getRepositoryIdentifier(repoEvent) ||
"Repository"
: repoPointer?.identifier || "Unknown Repository";
const handleRepoClick = () => {
if (!repoPointer || !repoEvent) return;
addWindow("open", { pointer: repoPointer });
};
// Format created date
const createdDate = new Date(event.created_at * 1000).toLocaleDateString(
"en-US",
@@ -104,18 +68,11 @@ export function PatchDetailRenderer({ event }: { event: NostrEvent }) {
{repoAddress && (
<div className="flex items-center gap-2 text-sm">
<span className="text-muted-foreground">Repository:</span>
<button
onClick={repoEvent ? handleRepoClick : undefined}
disabled={!repoEvent}
className={`flex items-center gap-2 font-mono ${
repoEvent
? "text-muted-foreground underline decoration-dotted cursor-crosshair hover:text-primary"
: "text-muted-foreground cursor-not-allowed"
}`}
>
<FolderGit2 className="size-4" />
{repoName}
</button>
<RepositoryLink
repoAddress={repoAddress}
iconSize="size-4"
className="font-mono"
/>
</div>
)}

View File

@@ -3,69 +3,22 @@ import {
type BaseEventProps,
ClickableEventTitle,
} from "./BaseEventRenderer";
import { FolderGit2 } from "lucide-react";
import { useGrimoire } from "@/core/state";
import { useNostrEvent } from "@/hooks/useNostrEvent";
import {
getPatchSubject,
getPatchCommitId,
getPatchRepositoryAddress,
} from "@/lib/nip34-helpers";
import {
getRepositoryName,
getRepositoryIdentifier,
} from "@/lib/nip34-helpers";
import { RepositoryLink } from "../RepositoryLink";
/**
* Renderer for Kind 1617 - Patch
* Displays as a compact patch card in feed view
*/
export function PatchRenderer({ event }: BaseEventProps) {
const { addWindow } = useGrimoire();
const subject = getPatchSubject(event);
const commitId = getPatchCommitId(event);
const repoAddress = getPatchRepositoryAddress(event);
// Parse repository address to get the pointer
const repoPointer = repoAddress
? (() => {
try {
// Address format: "kind:pubkey:identifier"
const [kindStr, pubkey, identifier] = repoAddress.split(":");
return {
kind: parseInt(kindStr),
pubkey,
identifier,
};
} catch {
return null;
}
})()
: null;
// Fetch the repository event to get its name
const repoEvent = useNostrEvent(
repoPointer
? {
kind: repoPointer.kind,
pubkey: repoPointer.pubkey,
identifier: repoPointer.identifier,
}
: undefined,
);
// Get repository display name
const repoName = repoEvent
? getRepositoryName(repoEvent) ||
getRepositoryIdentifier(repoEvent) ||
"Repository"
: repoAddress?.split(":")[2] || "Unknown Repository";
const handleRepoClick = () => {
if (!repoPointer) return;
addWindow("open", { pointer: repoPointer });
};
// Shorten commit ID for display
const shortCommitId = commitId ? commitId.slice(0, 7) : undefined;
@@ -84,15 +37,7 @@ export function PatchRenderer({ event }: BaseEventProps) {
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs">
<span>in</span>
{/* Repository */}
{repoAddress && repoPointer && (
<div
onClick={handleRepoClick}
className="flex items-center gap-1 text-muted-foreground cursor-crosshair underline decoration-dotted hover:text-primary"
>
<FolderGit2 className="size-3" />
<span>{repoName}</span>
</div>
)}
{repoAddress && <RepositoryLink repoAddress={repoAddress} />}
{/* Commit ID */}
{shortCommitId && (

View File

@@ -1,10 +1,8 @@
import { useMemo } from "react";
import { GitBranch, FolderGit2, Tag, Copy, CopyCheck } from "lucide-react";
import { GitBranch, Tag, Copy, CopyCheck } from "lucide-react";
import { UserName } from "../UserName";
import { MarkdownContent } from "../MarkdownContent";
import { useCopy } from "@/hooks/useCopy";
import { useGrimoire } from "@/core/state";
import { useNostrEvent } from "@/hooks/useNostrEvent";
import type { NostrEvent } from "@/types/nostr";
import {
getPullRequestSubject,
@@ -15,18 +13,14 @@ import {
getPullRequestMergeBase,
getPullRequestRepositoryAddress,
} from "@/lib/nip34-helpers";
import {
getRepositoryName,
getRepositoryIdentifier,
} from "@/lib/nip34-helpers";
import { Label } from "@/components/ui/label";
import { RepositoryLink } from "../RepositoryLink";
/**
* Detail renderer for Kind 1618 - Pull Request
* Displays full PR content with markdown rendering
*/
export function PullRequestDetailRenderer({ event }: { event: NostrEvent }) {
const { addWindow } = useGrimoire();
const { copy, copied } = useCopy();
const subject = useMemo(() => getPullRequestSubject(event), [event]);
@@ -40,31 +34,6 @@ export function PullRequestDetailRenderer({ event }: { event: NostrEvent }) {
[event],
);
// Parse repository address
const repoPointer = useMemo(() => {
if (!repoAddress) return null;
try {
const [kindStr, pubkey, identifier] = repoAddress.split(":");
return {
kind: parseInt(kindStr),
pubkey,
identifier,
};
} catch {
return null;
}
}, [repoAddress]);
// Fetch repository event
const repoEvent = useNostrEvent(repoPointer || undefined);
// Get repository display name
const repoName = repoEvent
? getRepositoryName(repoEvent) ||
getRepositoryIdentifier(repoEvent) ||
"Repository"
: repoPointer?.identifier || "Unknown Repository";
// Format created date
const createdDate = new Date(event.created_at * 1000).toLocaleDateString(
"en-US",
@@ -75,11 +44,6 @@ export function PullRequestDetailRenderer({ event }: { event: NostrEvent }) {
},
);
const handleRepoClick = () => {
if (!repoPointer || !repoEvent) return;
addWindow("open", { pointer: repoPointer });
};
return (
<div className="flex flex-col gap-4 p-4 max-w-3xl mx-auto">
{/* PR Header */}
@@ -93,18 +57,11 @@ export function PullRequestDetailRenderer({ event }: { event: NostrEvent }) {
{repoAddress && (
<div className="flex items-center gap-2 text-sm">
<span className="text-muted-foreground">Repository:</span>
<button
onClick={repoEvent ? handleRepoClick : undefined}
disabled={!repoEvent}
className={`flex items-center gap-2 font-mono ${
repoEvent
? "text-muted-foreground underline decoration-dotted cursor-crosshair hover:text-primary"
: "text-muted-foreground cursor-not-allowed"
}`}
>
<FolderGit2 className="size-4" />
{repoName}
</button>
<RepositoryLink
repoAddress={repoAddress}
iconSize="size-4"
className="font-mono"
/>
</div>
)}

View File

@@ -3,72 +3,26 @@ import {
type BaseEventProps,
ClickableEventTitle,
} from "./BaseEventRenderer";
import { FolderGit2, GitBranch } from "lucide-react";
import { useGrimoire } from "@/core/state";
import { useNostrEvent } from "@/hooks/useNostrEvent";
import { GitBranch } from "lucide-react";
import {
getPullRequestSubject,
getPullRequestLabels,
getPullRequestBranchName,
getPullRequestRepositoryAddress,
} from "@/lib/nip34-helpers";
import {
getRepositoryName,
getRepositoryIdentifier,
} from "@/lib/nip34-helpers";
import { Label } from "@/components/ui/label";
import { RepositoryLink } from "../RepositoryLink";
/**
* Renderer for Kind 1618 - Pull Request
* Displays as a compact PR card in feed view
*/
export function PullRequestRenderer({ event }: BaseEventProps) {
const { addWindow } = useGrimoire();
const subject = getPullRequestSubject(event);
const labels = getPullRequestLabels(event);
const branchName = getPullRequestBranchName(event);
const repoAddress = getPullRequestRepositoryAddress(event);
// Parse repository address to get the pointer
const repoPointer = repoAddress
? (() => {
try {
// Address format: "kind:pubkey:identifier"
const [kindStr, pubkey, identifier] = repoAddress.split(":");
return {
kind: parseInt(kindStr),
pubkey,
identifier,
};
} catch {
return null;
}
})()
: null;
// Fetch the repository event to get its name
const repoEvent = useNostrEvent(
repoPointer
? {
kind: repoPointer.kind,
pubkey: repoPointer.pubkey,
identifier: repoPointer.identifier,
}
: undefined,
);
// Get repository display name
const repoName = repoEvent
? getRepositoryName(repoEvent) ||
getRepositoryIdentifier(repoEvent) ||
"Repository"
: repoAddress?.split(":")[2] || "Unknown Repository";
const handleRepoClick = () => {
if (!repoPointer) return;
addWindow("open", { pointer: repoPointer });
};
return (
<BaseEventContainer event={event}>
<div className="flex flex-col gap-2">
@@ -82,14 +36,11 @@ export function PullRequestRenderer({ event }: BaseEventProps) {
<div className="flex flex-col gap-1">
{/* Repository */}
{repoAddress && repoPointer && (
<div
onClick={handleRepoClick}
className="flex items-center gap-1 text-muted-foreground cursor-crosshair underline decoration-dotted hover:text-primary truncate line-clamp-1 text-xs"
>
<FolderGit2 className="size-3 flex-shrink-0" />
<span>{repoName}</span>
</div>
{repoAddress && (
<RepositoryLink
repoAddress={repoAddress}
className="truncate line-clamp-1 text-xs"
/>
)}
{/* Branch Name */}
{branchName && (

View File

@@ -3,24 +3,21 @@ import {
type BaseEventProps,
ClickableEventTitle,
} from "./BaseEventRenderer";
import { GitCommit, FolderGit2 } from "lucide-react";
import { useGrimoire } from "@/core/state";
import { useNostrEvent } from "@/hooks/useNostrEvent";
import { GitCommit } from "lucide-react";
import {
getRepositoryIdentifier,
getRepositoryStateHeadCommit,
parseHeadBranch,
getRepositoryStateHead,
getRepositoryName,
} from "@/lib/nip34-helpers";
import { Label } from "@/components/ui/Label";
import { RepositoryLink } from "../RepositoryLink";
/**
* Renderer for Kind 30618 - Repository State
* Displays as a compact git push notification in feed view
*/
export function RepositoryStateRenderer({ event }: BaseEventProps) {
const { addWindow } = useGrimoire();
const repoId = getRepositoryIdentifier(event);
const headRef = getRepositoryStateHead(event);
const branch = parseHeadBranch(headRef);
@@ -35,48 +32,32 @@ export function RepositoryStateRenderer({ event }: BaseEventProps) {
}
: null;
// Fetch the repository event to get its name
const repoEvent = useNostrEvent(repoPointer || undefined);
// Get repository display name
const repoName = repoEvent
? getRepositoryName(repoEvent) || repoId || "Repository"
: repoId || "repository";
const shortHash = commitHash?.substring(0, 8) || "unknown";
const branchName = branch || "unknown";
const handleRepoClick = () => {
if (repoPointer) {
addWindow("open", { pointer: repoPointer });
}
};
return (
<BaseEventContainer event={event}>
<div className="flex flex-col gap-2">
{/* Push notification */}
<div className="flex items-center gap-2 flex-wrap">
<div className="text-sm font-medium text-foreground">
<ClickableEventTitle event={event} className="" as="span">
pushed{" "}
<code className="px-1.5 py-0.5 rounded bg-muted text-xs font-mono">
{shortHash}
</code>
</ClickableEventTitle>{" "}
to <Label className="inline">{branchName}</Label> in{" "}
{repoPointer ? (
<span
onClick={handleRepoClick}
className="inline-flex items-center gap-1 cursor-crosshair underline decoration-dotted hover:text-primary"
>
<FolderGit2 className="size-3" />
<span className="font-semibold">{repoName}</span>
</span>
) : (
<span className="font-semibold">{repoName}</span>
)}
</div>
<div className="flex items-center gap-2 flex-wrap text-sm font-medium text-foreground">
<GitCommit className="size-4 text-muted-foreground flex-shrink-0" />
<ClickableEventTitle event={event} className="" as="span">
pushed{" "}
<code className="px-1.5 py-0.5 rounded bg-muted text-xs font-mono">
{shortHash}
</code>{" "}
to
</ClickableEventTitle>{" "}
<Label className="inline">{branchName}</Label> in{" "}
{repoPointer ? (
<RepositoryLink
repoPointer={repoPointer}
inline
className="inline-flex font-semibold"
/>
) : (
<span className="font-semibold">{repoId || "repository"}</span>
)}
</div>
</div>
</BaseEventContainer>