From be01c7d882123f809478fead58dbab77d74d1315 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20G=C3=B3mez?= Date: Sun, 21 Dec 2025 18:19:36 +0100 Subject: [PATCH] refactor: repo link --- src/components/nostr/RepositoryLink.tsx | 111 ++++++++++++++++++ .../nostr/kinds/IssueDetailRenderer.tsx | 56 ++------- src/components/nostr/kinds/IssueRenderer.tsx | 60 +--------- .../nostr/kinds/PatchDetailRenderer.tsx | 57 ++------- src/components/nostr/kinds/PatchRenderer.tsx | 59 +--------- .../nostr/kinds/PullRequestDetailRenderer.tsx | 57 ++------- .../nostr/kinds/PullRequestRenderer.tsx | 63 ++-------- .../nostr/kinds/RepositoryStateRenderer.tsx | 61 ++++------ 8 files changed, 165 insertions(+), 359 deletions(-) create mode 100644 src/components/nostr/RepositoryLink.tsx diff --git a/src/components/nostr/RepositoryLink.tsx b/src/components/nostr/RepositoryLink.tsx new file mode 100644 index 0000000..85a8a5e --- /dev/null +++ b/src/components/nostr/RepositoryLink.tsx @@ -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 && } + {repoName} + + ); + + const baseClasses = + "flex items-center gap-1 text-muted-foreground cursor-crosshair underline decoration-dotted hover:text-primary"; + + if (inline) { + return ( + + {linkContent} + + ); + } + + return ( +
+ {linkContent} +
+ ); +} diff --git a/src/components/nostr/kinds/IssueDetailRenderer.tsx b/src/components/nostr/kinds/IssueDetailRenderer.tsx index 0ad1fbb..e55d205 100644 --- a/src/components/nostr/kinds/IssueDetailRenderer.tsx +++ b/src/components/nostr/kinds/IssueDetailRenderer.tsx @@ -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 (
{/* Issue Header */} @@ -76,18 +41,11 @@ export function IssueDetailRenderer({ event }: { event: NostrEvent }) { {repoAddress && (
Repository: - +
)} diff --git a/src/components/nostr/kinds/IssueRenderer.tsx b/src/components/nostr/kinds/IssueRenderer.tsx index 5998d7e..cb173ca 100644 --- a/src/components/nostr/kinds/IssueRenderer.tsx +++ b/src/components/nostr/kinds/IssueRenderer.tsx @@ -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 (
@@ -79,17 +33,9 @@ export function IssueRenderer({ event }: BaseEventProps) { {/* Repository Reference */} - {repoAddress && repoPointer && ( + {repoAddress && (
-
- - {repoName} -
+
)}
diff --git a/src/components/nostr/kinds/PatchDetailRenderer.tsx b/src/components/nostr/kinds/PatchDetailRenderer.tsx index ca75e92..80ca4b5 100644 --- a/src/components/nostr/kinds/PatchDetailRenderer.tsx +++ b/src/components/nostr/kinds/PatchDetailRenderer.tsx @@ -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 && (
Repository: - +
)} diff --git a/src/components/nostr/kinds/PatchRenderer.tsx b/src/components/nostr/kinds/PatchRenderer.tsx index b5d955a..3f4620d 100644 --- a/src/components/nostr/kinds/PatchRenderer.tsx +++ b/src/components/nostr/kinds/PatchRenderer.tsx @@ -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) {
in {/* Repository */} - {repoAddress && repoPointer && ( -
- - {repoName} -
- )} + {repoAddress && } {/* Commit ID */} {shortCommitId && ( diff --git a/src/components/nostr/kinds/PullRequestDetailRenderer.tsx b/src/components/nostr/kinds/PullRequestDetailRenderer.tsx index ddcac8e..5322573 100644 --- a/src/components/nostr/kinds/PullRequestDetailRenderer.tsx +++ b/src/components/nostr/kinds/PullRequestDetailRenderer.tsx @@ -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 (
{/* PR Header */} @@ -93,18 +57,11 @@ export function PullRequestDetailRenderer({ event }: { event: NostrEvent }) { {repoAddress && (
Repository: - +
)} diff --git a/src/components/nostr/kinds/PullRequestRenderer.tsx b/src/components/nostr/kinds/PullRequestRenderer.tsx index 458e313..97ab93e 100644 --- a/src/components/nostr/kinds/PullRequestRenderer.tsx +++ b/src/components/nostr/kinds/PullRequestRenderer.tsx @@ -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 (
@@ -82,14 +36,11 @@ export function PullRequestRenderer({ event }: BaseEventProps) {
{/* Repository */} - {repoAddress && repoPointer && ( -
- - {repoName} -
+ {repoAddress && ( + )} {/* Branch Name */} {branchName && ( diff --git a/src/components/nostr/kinds/RepositoryStateRenderer.tsx b/src/components/nostr/kinds/RepositoryStateRenderer.tsx index c239b10..9aef180 100644 --- a/src/components/nostr/kinds/RepositoryStateRenderer.tsx +++ b/src/components/nostr/kinds/RepositoryStateRenderer.tsx @@ -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 (
{/* Push notification */} -
-
- - pushed{" "} - - {shortHash} - - {" "} - to in{" "} - {repoPointer ? ( - - - {repoName} - - ) : ( - {repoName} - )} -
+
+ + + pushed{" "} + + {shortHash} + {" "} + to + {" "} + in{" "} + {repoPointer ? ( + + ) : ( + {repoId || "repository"} + )}