refactor: extract useRepositoryRelays hook from NIP-34 renderers

Removes ~45 lines of identical relay resolution boilerplate duplicated
across 6 renderers (Issue, Patch, PR - feed and detail views).

The hook encapsulates the 3-tier fallback: repo relays → owner outbox →
aggregators, and also returns the repository event needed for
getValidStatusAuthors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Alejandro Gómez
2026-03-03 21:57:49 +01:00
parent bcd58e40dd
commit 772e1b1404
7 changed files with 71 additions and 290 deletions

View File

@@ -7,20 +7,16 @@ import {
getIssueTitle,
getIssueLabels,
getIssueRepositoryAddress,
getRepositoryRelays,
getStatusType,
getValidStatusAuthors,
findCurrentStatus,
} from "@/lib/nip34-helpers";
import { parseReplaceableAddress } from "applesauce-core/helpers/pointers";
import { getOutboxes } from "applesauce-core/helpers";
import { Label } from "@/components/ui/label";
import { RepositoryLink } from "../RepositoryLink";
import { StatusIndicator } from "../StatusIndicator";
import { useTimeline } from "@/hooks/useTimeline";
import { useNostrEvent } from "@/hooks/useNostrEvent";
import { formatTimestamp } from "@/hooks/useLocale";
import { AGGREGATOR_RELAYS } from "@/services/loaders";
import { useRepositoryRelays } from "@/hooks/useRepositoryRelays";
/**
* Detail renderer for Kind 1621 - Issue (NIP-34)
@@ -31,52 +27,8 @@ export function IssueDetailRenderer({ event }: { event: NostrEvent }) {
const labels = getIssueLabels(event);
const repoAddress = getIssueRepositoryAddress(event);
// Parse repository address for fetching repo event
const parsedRepo = useMemo(
() => (repoAddress ? parseReplaceableAddress(repoAddress) : null),
[repoAddress],
);
// Fetch repository event to get maintainers list
const repoPointer = useMemo(() => {
if (!parsedRepo) return undefined;
return {
kind: parsedRepo.kind,
pubkey: parsedRepo.pubkey,
identifier: parsedRepo.identifier,
};
}, [parsedRepo]);
const repositoryEvent = useNostrEvent(repoPointer);
// Fetch repo author's relay list for fallback
const repoAuthorRelayListPointer = useMemo(() => {
if (!parsedRepo?.pubkey) return undefined;
return { kind: 10002, pubkey: parsedRepo.pubkey, identifier: "" };
}, [parsedRepo?.pubkey]);
const repoAuthorRelayList = useNostrEvent(repoAuthorRelayListPointer);
// Build relay list with fallbacks:
// 1. Repository configured relays
// 2. Repo author's outbox (write) relays
// 3. AGGREGATOR_RELAYS as final fallback
const statusRelays = useMemo(() => {
// Try repository relays first
if (repositoryEvent) {
const repoRelays = getRepositoryRelays(repositoryEvent);
if (repoRelays.length > 0) return repoRelays;
}
// Try repo author's outbox relays
if (repoAuthorRelayList) {
const authorOutbox = getOutboxes(repoAuthorRelayList);
if (authorOutbox.length > 0) return authorOutbox;
}
// Fallback to aggregator relays
return AGGREGATOR_RELAYS;
}, [repositoryEvent, repoAuthorRelayList]);
const { relays: statusRelays, repositoryEvent } =
useRepositoryRelays(repoAddress);
// Fetch status events that reference this issue
// Status events use e tag with root marker to reference the issue

View File

@@ -8,18 +8,14 @@ import {
getIssueTitle,
getIssueLabels,
getIssueRepositoryAddress,
getRepositoryRelays,
getValidStatusAuthors,
findCurrentStatus,
} from "@/lib/nip34-helpers";
import { parseReplaceableAddress } from "applesauce-core/helpers/pointers";
import { getOutboxes } from "applesauce-core/helpers";
import { Label } from "@/components/ui/label";
import { RepositoryLink } from "../RepositoryLink";
import { StatusIndicator } from "../StatusIndicator";
import { useTimeline } from "@/hooks/useTimeline";
import { useNostrEvent } from "@/hooks/useNostrEvent";
import { AGGREGATOR_RELAYS } from "@/services/loaders";
import { useRepositoryRelays } from "@/hooks/useRepositoryRelays";
/**
* Renderer for Kind 1621 - Issue
@@ -30,52 +26,8 @@ export function IssueRenderer({ event }: BaseEventProps) {
const labels = getIssueLabels(event);
const repoAddress = getIssueRepositoryAddress(event);
// Parse repository address for fetching repo event
const parsedRepo = useMemo(
() => (repoAddress ? parseReplaceableAddress(repoAddress) : null),
[repoAddress],
);
// Fetch repository event to get maintainers list
const repoPointer = useMemo(() => {
if (!parsedRepo) return undefined;
return {
kind: parsedRepo.kind,
pubkey: parsedRepo.pubkey,
identifier: parsedRepo.identifier,
};
}, [parsedRepo]);
const repositoryEvent = useNostrEvent(repoPointer);
// Fetch repo author's relay list for fallback
const repoAuthorRelayListPointer = useMemo(() => {
if (!parsedRepo?.pubkey) return undefined;
return { kind: 10002, pubkey: parsedRepo.pubkey, identifier: "" };
}, [parsedRepo?.pubkey]);
const repoAuthorRelayList = useNostrEvent(repoAuthorRelayListPointer);
// Build relay list with fallbacks:
// 1. Repository configured relays
// 2. Repo author's outbox (write) relays
// 3. AGGREGATOR_RELAYS as final fallback
const statusRelays = useMemo(() => {
// Try repository relays first
if (repositoryEvent) {
const repoRelays = getRepositoryRelays(repositoryEvent);
if (repoRelays.length > 0) return repoRelays;
}
// Try repo author's outbox relays
if (repoAuthorRelayList) {
const authorOutbox = getOutboxes(repoAuthorRelayList);
if (authorOutbox.length > 0) return authorOutbox;
}
// Fallback to aggregator relays
return AGGREGATOR_RELAYS;
}, [repositoryEvent, repoAuthorRelayList]);
const { relays: statusRelays, repositoryEvent } =
useRepositoryRelays(repoAddress);
// Fetch status events that reference this issue
const statusFilter = useMemo(

View File

@@ -15,18 +15,14 @@ import {
getPatchRepositoryAddress,
isPatchRoot,
isPatchRootRevision,
getRepositoryRelays,
getStatusType,
getValidStatusAuthors,
findCurrentStatus,
} from "@/lib/nip34-helpers";
import { parseReplaceableAddress } from "applesauce-core/helpers/pointers";
import { getOutboxes } from "applesauce-core/helpers";
import { RepositoryLink } from "../RepositoryLink";
import { StatusIndicator } from "../StatusIndicator";
import { useTimeline } from "@/hooks/useTimeline";
import { useNostrEvent } from "@/hooks/useNostrEvent";
import { AGGREGATOR_RELAYS } from "@/services/loaders";
import { useRepositoryRelays } from "@/hooks/useRepositoryRelays";
/**
* Detail renderer for Kind 1617 - Patch
@@ -43,44 +39,8 @@ export function PatchDetailRenderer({ event }: { event: NostrEvent }) {
const isRoot = isPatchRoot(event);
const isRootRevision = isPatchRootRevision(event);
// Parse repository address for fetching repo event
const parsedRepo = useMemo(
() => (repoAddress ? parseReplaceableAddress(repoAddress) : null),
[repoAddress],
);
// Fetch repository event to get maintainers list
const repoPointer = useMemo(() => {
if (!parsedRepo) return undefined;
return {
kind: parsedRepo.kind,
pubkey: parsedRepo.pubkey,
identifier: parsedRepo.identifier,
};
}, [parsedRepo]);
const repositoryEvent = useNostrEvent(repoPointer);
// Fetch repo author's relay list for fallback
const repoAuthorRelayListPointer = useMemo(() => {
if (!parsedRepo?.pubkey) return undefined;
return { kind: 10002, pubkey: parsedRepo.pubkey, identifier: "" };
}, [parsedRepo?.pubkey]);
const repoAuthorRelayList = useNostrEvent(repoAuthorRelayListPointer);
// Build relay list with fallbacks
const statusRelays = useMemo(() => {
if (repositoryEvent) {
const repoRelays = getRepositoryRelays(repositoryEvent);
if (repoRelays.length > 0) return repoRelays;
}
if (repoAuthorRelayList) {
const authorOutbox = getOutboxes(repoAuthorRelayList);
if (authorOutbox.length > 0) return authorOutbox;
}
return AGGREGATOR_RELAYS;
}, [repositoryEvent, repoAuthorRelayList]);
const { relays: statusRelays, repositoryEvent } =
useRepositoryRelays(repoAddress);
// Fetch status events
const statusFilter = useMemo(

View File

@@ -8,17 +8,13 @@ import {
getPatchSubject,
getPatchCommitId,
getPatchRepositoryAddress,
getRepositoryRelays,
getValidStatusAuthors,
findCurrentStatus,
} from "@/lib/nip34-helpers";
import { parseReplaceableAddress } from "applesauce-core/helpers/pointers";
import { getOutboxes } from "applesauce-core/helpers";
import { RepositoryLink } from "../RepositoryLink";
import { StatusIndicator } from "../StatusIndicator";
import { useTimeline } from "@/hooks/useTimeline";
import { useNostrEvent } from "@/hooks/useNostrEvent";
import { AGGREGATOR_RELAYS } from "@/services/loaders";
import { useRepositoryRelays } from "@/hooks/useRepositoryRelays";
/**
* Renderer for Kind 1617 - Patch
@@ -32,52 +28,8 @@ export function PatchRenderer({ event }: BaseEventProps) {
// Shorten commit ID for display
const shortCommitId = commitId ? commitId.slice(0, 7) : undefined;
// Parse repository address for fetching repo event
const parsedRepo = useMemo(
() => (repoAddress ? parseReplaceableAddress(repoAddress) : null),
[repoAddress],
);
// Fetch repository event to get maintainers list
const repoPointer = useMemo(() => {
if (!parsedRepo) return undefined;
return {
kind: parsedRepo.kind,
pubkey: parsedRepo.pubkey,
identifier: parsedRepo.identifier,
};
}, [parsedRepo]);
const repositoryEvent = useNostrEvent(repoPointer);
// Fetch repo author's relay list for fallback
const repoAuthorRelayListPointer = useMemo(() => {
if (!parsedRepo?.pubkey) return undefined;
return { kind: 10002, pubkey: parsedRepo.pubkey, identifier: "" };
}, [parsedRepo?.pubkey]);
const repoAuthorRelayList = useNostrEvent(repoAuthorRelayListPointer);
// Build relay list with fallbacks:
// 1. Repository configured relays
// 2. Repo author's outbox (write) relays
// 3. AGGREGATOR_RELAYS as final fallback
const statusRelays = useMemo(() => {
// Try repository relays first
if (repositoryEvent) {
const repoRelays = getRepositoryRelays(repositoryEvent);
if (repoRelays.length > 0) return repoRelays;
}
// Try repo author's outbox relays
if (repoAuthorRelayList) {
const authorOutbox = getOutboxes(repoAuthorRelayList);
if (authorOutbox.length > 0) return authorOutbox;
}
// Fallback to aggregator relays
return AGGREGATOR_RELAYS;
}, [repositoryEvent, repoAuthorRelayList]);
const { relays: statusRelays, repositoryEvent } =
useRepositoryRelays(repoAddress);
// Fetch status events that reference this patch
const statusFilter = useMemo(

View File

@@ -13,19 +13,15 @@ import {
getPullRequestCloneUrls,
getPullRequestMergeBase,
getPullRequestRepositoryAddress,
getRepositoryRelays,
getStatusType,
getValidStatusAuthors,
findCurrentStatus,
} from "@/lib/nip34-helpers";
import { parseReplaceableAddress } from "applesauce-core/helpers/pointers";
import { getOutboxes } from "applesauce-core/helpers";
import { Label } from "@/components/ui/label";
import { RepositoryLink } from "../RepositoryLink";
import { StatusIndicator } from "../StatusIndicator";
import { useTimeline } from "@/hooks/useTimeline";
import { useNostrEvent } from "@/hooks/useNostrEvent";
import { AGGREGATOR_RELAYS } from "@/services/loaders";
import { useRepositoryRelays } from "@/hooks/useRepositoryRelays";
/**
* Detail renderer for Kind 1618 - Pull Request
@@ -42,44 +38,8 @@ export function PullRequestDetailRenderer({ event }: { event: NostrEvent }) {
const mergeBase = getPullRequestMergeBase(event);
const repoAddress = getPullRequestRepositoryAddress(event);
// Parse repository address for fetching repo event
const parsedRepo = useMemo(
() => (repoAddress ? parseReplaceableAddress(repoAddress) : null),
[repoAddress],
);
// Fetch repository event to get maintainers list
const repoPointer = useMemo(() => {
if (!parsedRepo) return undefined;
return {
kind: parsedRepo.kind,
pubkey: parsedRepo.pubkey,
identifier: parsedRepo.identifier,
};
}, [parsedRepo]);
const repositoryEvent = useNostrEvent(repoPointer);
// Fetch repo author's relay list for fallback
const repoAuthorRelayListPointer = useMemo(() => {
if (!parsedRepo?.pubkey) return undefined;
return { kind: 10002, pubkey: parsedRepo.pubkey, identifier: "" };
}, [parsedRepo?.pubkey]);
const repoAuthorRelayList = useNostrEvent(repoAuthorRelayListPointer);
// Build relay list with fallbacks
const statusRelays = useMemo(() => {
if (repositoryEvent) {
const repoRelays = getRepositoryRelays(repositoryEvent);
if (repoRelays.length > 0) return repoRelays;
}
if (repoAuthorRelayList) {
const authorOutbox = getOutboxes(repoAuthorRelayList);
if (authorOutbox.length > 0) return authorOutbox;
}
return AGGREGATOR_RELAYS;
}, [repositoryEvent, repoAuthorRelayList]);
const { relays: statusRelays, repositoryEvent } =
useRepositoryRelays(repoAddress);
// Fetch status events
const statusFilter = useMemo(

View File

@@ -10,18 +10,14 @@ import {
getPullRequestLabels,
getPullRequestBranchName,
getPullRequestRepositoryAddress,
getRepositoryRelays,
getValidStatusAuthors,
findCurrentStatus,
} from "@/lib/nip34-helpers";
import { parseReplaceableAddress } from "applesauce-core/helpers/pointers";
import { getOutboxes } from "applesauce-core/helpers";
import { Label } from "@/components/ui/label";
import { RepositoryLink } from "../RepositoryLink";
import { StatusIndicator } from "../StatusIndicator";
import { useTimeline } from "@/hooks/useTimeline";
import { useNostrEvent } from "@/hooks/useNostrEvent";
import { AGGREGATOR_RELAYS } from "@/services/loaders";
import { useRepositoryRelays } from "@/hooks/useRepositoryRelays";
/**
* Renderer for Kind 1618 - Pull Request
@@ -33,52 +29,8 @@ export function PullRequestRenderer({ event }: BaseEventProps) {
const branchName = getPullRequestBranchName(event);
const repoAddress = getPullRequestRepositoryAddress(event);
// Parse repository address for fetching repo event
const parsedRepo = useMemo(
() => (repoAddress ? parseReplaceableAddress(repoAddress) : null),
[repoAddress],
);
// Fetch repository event to get maintainers list
const repoPointer = useMemo(() => {
if (!parsedRepo) return undefined;
return {
kind: parsedRepo.kind,
pubkey: parsedRepo.pubkey,
identifier: parsedRepo.identifier,
};
}, [parsedRepo]);
const repositoryEvent = useNostrEvent(repoPointer);
// Fetch repo author's relay list for fallback
const repoAuthorRelayListPointer = useMemo(() => {
if (!parsedRepo?.pubkey) return undefined;
return { kind: 10002, pubkey: parsedRepo.pubkey, identifier: "" };
}, [parsedRepo?.pubkey]);
const repoAuthorRelayList = useNostrEvent(repoAuthorRelayListPointer);
// Build relay list with fallbacks:
// 1. Repository configured relays
// 2. Repo author's outbox (write) relays
// 3. AGGREGATOR_RELAYS as final fallback
const statusRelays = useMemo(() => {
// Try repository relays first
if (repositoryEvent) {
const repoRelays = getRepositoryRelays(repositoryEvent);
if (repoRelays.length > 0) return repoRelays;
}
// Try repo author's outbox relays
if (repoAuthorRelayList) {
const authorOutbox = getOutboxes(repoAuthorRelayList);
if (authorOutbox.length > 0) return authorOutbox;
}
// Fallback to aggregator relays
return AGGREGATOR_RELAYS;
}, [repositoryEvent, repoAuthorRelayList]);
const { relays: statusRelays, repositoryEvent } =
useRepositoryRelays(repoAddress);
// Fetch status events that reference this PR
const statusFilter = useMemo(

View File

@@ -0,0 +1,53 @@
import { useMemo } from "react";
import { parseReplaceableAddress } from "applesauce-core/helpers/pointers";
import { getOutboxes } from "applesauce-core/helpers";
import { getRepositoryRelays } from "@/lib/nip34-helpers";
import { useNostrEvent } from "@/hooks/useNostrEvent";
import { AGGREGATOR_RELAYS } from "@/services/loaders";
import type { NostrEvent } from "@/types/nostr";
/**
* Resolves the relay list for a NIP-34 git repository address.
*
* Fallback chain:
* 1. Relays from the repository event's `relays` tag (kind 30617)
* 2. Repository owner's outbox relays (kind 10002)
* 3. Well-known aggregator relays
*
* Also returns the repository event, needed by callers for getValidStatusAuthors.
*/
export function useRepositoryRelays(repoAddress: string | undefined): {
relays: string[];
repositoryEvent: NostrEvent | undefined;
} {
const repoPointer = useMemo(
() => (repoAddress ? parseReplaceableAddress(repoAddress) : undefined),
[repoAddress],
);
const repositoryEvent = useNostrEvent(repoPointer);
const authorRelayListPointer = useMemo(
() =>
repoPointer
? { kind: 10002, pubkey: repoPointer.pubkey, identifier: "" }
: undefined,
[repoPointer],
);
const authorRelayList = useNostrEvent(authorRelayListPointer);
const relays = useMemo(() => {
if (repositoryEvent) {
const repoRelays = getRepositoryRelays(repositoryEvent);
if (repoRelays.length > 0) return repoRelays;
}
if (authorRelayList) {
const outbox = getOutboxes(authorRelayList);
if (outbox.length > 0) return outbox;
}
return AGGREGATOR_RELAYS;
}, [repositoryEvent, authorRelayList]);
return { relays, repositoryEvent };
}