diff --git a/src/components/nostr/kinds/BookmarkListRenderer.tsx b/src/components/nostr/kinds/BookmarkListRenderer.tsx
index 145bf87..6c29e09 100644
--- a/src/components/nostr/kinds/BookmarkListRenderer.tsx
+++ b/src/components/nostr/kinds/BookmarkListRenderer.tsx
@@ -1,9 +1,9 @@
import { Bookmark, FileText, Link } from "lucide-react";
-import { getTagValues } from "@/lib/nostr-utils";
import {
- getEventPointerFromETag,
- getAddressPointerFromATag,
-} from "applesauce-core/helpers";
+ getTagValues,
+ getEventPointers,
+ getAddressPointers,
+} from "@/lib/nostr-utils";
import {
BaseEventProps,
BaseEventContainer,
@@ -11,39 +11,6 @@ import {
} from "./BaseEventRenderer";
import { EventRefListFull, UrlListFull } from "../lists";
import type { NostrEvent } from "@/types/nostr";
-import type { EventPointer, AddressPointer } from "nostr-tools/nip19";
-
-/**
- * Extract event pointers from e tags
- */
-function getEventPointers(event: NostrEvent): EventPointer[] {
- const pointers: EventPointer[] = [];
- for (const tag of event.tags) {
- if (tag[0] === "e" && tag[1]) {
- const pointer = getEventPointerFromETag(tag);
- if (pointer) {
- pointers.push(pointer);
- }
- }
- }
- return pointers;
-}
-
-/**
- * Extract address pointers from a tags
- */
-function getAddressPointers(event: NostrEvent): AddressPointer[] {
- const pointers: AddressPointer[] = [];
- for (const tag of event.tags) {
- if (tag[0] === "a" && tag[1]) {
- const pointer = getAddressPointerFromATag(tag);
- if (pointer) {
- pointers.push(pointer);
- }
- }
- }
- return pointers;
-}
/**
* Kind 10003 Renderer - Bookmark List (Feed View)
diff --git a/src/components/nostr/kinds/BookmarkSetRenderer.tsx b/src/components/nostr/kinds/BookmarkSetRenderer.tsx
index e596fa0..cefd4ff 100644
--- a/src/components/nostr/kinds/BookmarkSetRenderer.tsx
+++ b/src/components/nostr/kinds/BookmarkSetRenderer.tsx
@@ -1,10 +1,10 @@
import { Bookmark, FileText, Link } from "lucide-react";
import { getTagValue } from "applesauce-core/helpers";
-import { getTagValues } from "@/lib/nostr-utils";
import {
- getEventPointerFromETag,
- getAddressPointerFromATag,
-} from "applesauce-core/helpers";
+ getTagValues,
+ getEventPointers,
+ getAddressPointers,
+} from "@/lib/nostr-utils";
import {
BaseEventProps,
BaseEventContainer,
@@ -12,39 +12,6 @@ import {
} from "./BaseEventRenderer";
import { EventRefListFull, UrlListFull } from "../lists";
import type { NostrEvent } from "@/types/nostr";
-import type { EventPointer, AddressPointer } from "nostr-tools/nip19";
-
-/**
- * Extract event pointers from e tags
- */
-function getEventPointers(event: NostrEvent): EventPointer[] {
- const pointers: EventPointer[] = [];
- for (const tag of event.tags) {
- if (tag[0] === "e" && tag[1]) {
- const pointer = getEventPointerFromETag(tag);
- if (pointer) {
- pointers.push(pointer);
- }
- }
- }
- return pointers;
-}
-
-/**
- * Extract address pointers from a tags
- */
-function getAddressPointers(event: NostrEvent): AddressPointer[] {
- const pointers: AddressPointer[] = [];
- for (const tag of event.tags) {
- if (tag[0] === "a" && tag[1]) {
- const pointer = getAddressPointerFromATag(tag);
- if (pointer) {
- pointers.push(pointer);
- }
- }
- }
- return pointers;
-}
/**
* Kind 30003 Renderer - Bookmark Set (Feed View)
diff --git a/src/components/nostr/kinds/ChannelListRenderer.tsx b/src/components/nostr/kinds/ChannelListRenderer.tsx
index b8a8e37..c2dca99 100644
--- a/src/components/nostr/kinds/ChannelListRenderer.tsx
+++ b/src/components/nostr/kinds/ChannelListRenderer.tsx
@@ -1,5 +1,5 @@
import { MessageCircle, Hash } from "lucide-react";
-import { getEventPointerFromETag } from "applesauce-core/helpers";
+import { getEventPointers } from "@/lib/nostr-utils";
import {
BaseEventProps,
BaseEventContainer,
@@ -7,24 +7,6 @@ import {
} from "./BaseEventRenderer";
import { EventRefListFull } from "../lists";
import type { NostrEvent } from "@/types/nostr";
-import type { EventPointer } from "nostr-tools/nip19";
-
-/**
- * Extract event pointers from e tags (for channel references)
- * Channels are kind 40 events
- */
-function getChannelPointers(event: NostrEvent): EventPointer[] {
- const pointers: EventPointer[] = [];
- for (const tag of event.tags) {
- if (tag[0] === "e" && tag[1]) {
- const pointer = getEventPointerFromETag(tag);
- if (pointer) {
- pointers.push(pointer);
- }
- }
- }
- return pointers;
-}
/**
* Kind 10005 Renderer - Public Chats List (Feed View)
@@ -32,7 +14,7 @@ function getChannelPointers(event: NostrEvent): EventPointer[] {
* Note: This is different from kind 10009 which is for NIP-29 groups
*/
export function ChannelListRenderer({ event }: BaseEventProps) {
- const channels = getChannelPointers(event);
+ const channels = getEventPointers(event);
if (channels.length === 0) {
return (
@@ -66,7 +48,7 @@ export function ChannelListRenderer({ event }: BaseEventProps) {
* Kind 10005 Detail View - Full channel list
*/
export function ChannelListDetailRenderer({ event }: { event: NostrEvent }) {
- const channels = getChannelPointers(event);
+ const channels = getEventPointers(event);
return (
diff --git a/src/components/nostr/kinds/CurationSetRenderer.tsx b/src/components/nostr/kinds/CurationSetRenderer.tsx
index 0b0f2e8..4f1bc2c 100644
--- a/src/components/nostr/kinds/CurationSetRenderer.tsx
+++ b/src/components/nostr/kinds/CurationSetRenderer.tsx
@@ -1,9 +1,6 @@
import { Library, FileText, Video, Image } from "lucide-react";
import { getTagValue } from "applesauce-core/helpers";
-import {
- getEventPointerFromETag,
- getAddressPointerFromATag,
-} from "applesauce-core/helpers";
+import { getEventPointers, getAddressPointers } from "@/lib/nostr-utils";
import {
BaseEventProps,
BaseEventContainer,
@@ -11,39 +8,6 @@ import {
} from "./BaseEventRenderer";
import { EventRefListFull } from "../lists";
import type { NostrEvent } from "@/types/nostr";
-import type { EventPointer, AddressPointer } from "nostr-tools/nip19";
-
-/**
- * Extract event pointers from e tags
- */
-function getEventPointers(event: NostrEvent): EventPointer[] {
- const pointers: EventPointer[] = [];
- for (const tag of event.tags) {
- if (tag[0] === "e" && tag[1]) {
- const pointer = getEventPointerFromETag(tag);
- if (pointer) {
- pointers.push(pointer);
- }
- }
- }
- return pointers;
-}
-
-/**
- * Extract address pointers from a tags
- */
-function getAddressPointers(event: NostrEvent): AddressPointer[] {
- const pointers: AddressPointer[] = [];
- for (const tag of event.tags) {
- if (tag[0] === "a" && tag[1]) {
- const pointer = getAddressPointerFromATag(tag);
- if (pointer) {
- pointers.push(pointer);
- }
- }
- }
- return pointers;
-}
interface CurationSetRendererProps extends BaseEventProps {
/** Icon to display */
diff --git a/src/components/nostr/kinds/FavoriteReposRenderer.tsx b/src/components/nostr/kinds/FavoriteReposRenderer.tsx
new file mode 100644
index 0000000..e59ecb9
--- /dev/null
+++ b/src/components/nostr/kinds/FavoriteReposRenderer.tsx
@@ -0,0 +1,116 @@
+import { FolderGit2, GitBranch } from "lucide-react";
+import { getAddressPointers } from "@/lib/nostr-utils";
+import {
+ BaseEventProps,
+ BaseEventContainer,
+ ClickableEventTitle,
+} from "./BaseEventRenderer";
+import { EventRefListFull } from "../lists";
+import { useNostrEvent } from "@/hooks/useNostrEvent";
+import { useAddWindow } from "@/core/state";
+import { getRepositoryName } from "@/lib/nip34-helpers";
+import { getReplaceableIdentifier } from "applesauce-core/helpers";
+import type { NostrEvent } from "@/types/nostr";
+import type { AddressPointer } from "nostr-tools/nip19";
+
+/**
+ * Clickable repo name that loads and displays the repository name
+ */
+function RepoNameItem({ pointer }: { pointer: AddressPointer }) {
+ const event = useNostrEvent(pointer);
+ const addWindow = useAddWindow();
+
+ const displayName = event
+ ? getRepositoryName(event) ||
+ getReplaceableIdentifier(event) ||
+ "Repository"
+ : pointer.identifier || "Loading...";
+
+ const handleClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ addWindow("open", { pointer });
+ };
+
+ return (
+
+ );
+}
+
+/**
+ * Kind 10018 Renderer - Favorite Repositories (Feed View)
+ */
+export function FavoriteReposRenderer({ event }: BaseEventProps) {
+ const pointers = getAddressPointers(event).filter((p) => p.kind === 30617);
+
+ if (pointers.length === 0) {
+ return (
+
+
+ No favorite repositories
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ Favorite Repositories ({pointers.length})
+
+
+
+ {pointers.map((pointer) => (
+
+ ))}
+
+
+
+ );
+}
+
+/**
+ * Kind 10018 Detail Renderer - Favorite Repositories (Full View)
+ */
+export function FavoriteReposDetailRenderer({ event }: { event: NostrEvent }) {
+ const pointers = getAddressPointers(event).filter((p) => p.kind === 30617);
+
+ return (
+
+
+
+ Favorite Repositories
+
+ ({pointers.length})
+
+
+
+ {pointers.length === 0 ? (
+
+ No favorite repositories yet
+
+ ) : (
+
}
+ />
+ )}
+
+ );
+}
diff --git a/src/components/nostr/kinds/GitAuthorsRenderer.tsx b/src/components/nostr/kinds/GitAuthorsRenderer.tsx
new file mode 100644
index 0000000..7466519
--- /dev/null
+++ b/src/components/nostr/kinds/GitAuthorsRenderer.tsx
@@ -0,0 +1,70 @@
+import { FileCode, Users } from "lucide-react";
+import { getTagValues } from "@/lib/nostr-utils";
+import {
+ BaseEventProps,
+ BaseEventContainer,
+ ClickableEventTitle,
+} from "./BaseEventRenderer";
+import { PubkeyListPreview, PubkeyListFull } from "../lists";
+import type { NostrEvent } from "@/types/nostr";
+
+/**
+ * Kind 10017 Renderer - Git Authors (Feed View)
+ * Follow list of people who produce NIP-34 code events
+ */
+export function GitAuthorsRenderer({ event }: BaseEventProps) {
+ const pubkeys = getTagValues(event, "p");
+
+ if (pubkeys.length === 0) {
+ return (
+
+
+ No git authors followed
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ Git Authors
+
+
+
}
+ />
+
+
+ );
+}
+
+/**
+ * Kind 10017 Detail View - Full git authors list
+ */
+export function GitAuthorsDetailRenderer({ event }: { event: NostrEvent }) {
+ const pubkeys = getTagValues(event, "p");
+
+ return (
+
+
+
+ Git Authors
+
+
+
}
+ />
+
+ );
+}
diff --git a/src/components/nostr/kinds/InterestListRenderer.tsx b/src/components/nostr/kinds/InterestListRenderer.tsx
index 294d5d1..67e03a5 100644
--- a/src/components/nostr/kinds/InterestListRenderer.tsx
+++ b/src/components/nostr/kinds/InterestListRenderer.tsx
@@ -1,7 +1,6 @@
import { Sparkles } from "lucide-react";
import { getTagValue } from "applesauce-core/helpers";
-import { getTagValues } from "@/lib/nostr-utils";
-import { getAddressPointerFromATag } from "applesauce-core/helpers";
+import { getTagValues, getAddressPointers } from "@/lib/nostr-utils";
import {
BaseEventProps,
BaseEventContainer,
@@ -13,23 +12,6 @@ import {
EventRefListFull,
} from "../lists";
import type { NostrEvent } from "@/types/nostr";
-import type { AddressPointer } from "nostr-tools/nip19";
-
-/**
- * Extract address pointers from a tags (for interest sets references)
- */
-function getAddressPointers(event: NostrEvent): AddressPointer[] {
- const pointers: AddressPointer[] = [];
- for (const tag of event.tags) {
- if (tag[0] === "a" && tag[1]) {
- const pointer = getAddressPointerFromATag(tag);
- if (pointer) {
- pointers.push(pointer);
- }
- }
- }
- return pointers;
-}
/**
* Kind 10015 Renderer - Interest List (Feed View)
diff --git a/src/components/nostr/kinds/MuteListRenderer.tsx b/src/components/nostr/kinds/MuteListRenderer.tsx
index cb40fb1..50d5314 100644
--- a/src/components/nostr/kinds/MuteListRenderer.tsx
+++ b/src/components/nostr/kinds/MuteListRenderer.tsx
@@ -1,6 +1,5 @@
import { VolumeX, Users, Hash, Type, FileText } from "lucide-react";
-import { getTagValues } from "@/lib/nostr-utils";
-import { getEventPointerFromETag } from "applesauce-core/helpers";
+import { getTagValues, getEventPointers } from "@/lib/nostr-utils";
import {
BaseEventProps,
BaseEventContainer,
@@ -13,23 +12,6 @@ import {
EventRefListFull,
} from "../lists";
import type { NostrEvent } from "@/types/nostr";
-import type { EventPointer } from "nostr-tools/nip19";
-
-/**
- * Extract event pointers from e tags
- */
-function getEventPointers(event: NostrEvent): EventPointer[] {
- const pointers: EventPointer[] = [];
- for (const tag of event.tags) {
- if (tag[0] === "e" && tag[1]) {
- const pointer = getEventPointerFromETag(tag);
- if (pointer) {
- pointers.push(pointer);
- }
- }
- }
- return pointers;
-}
/**
* Kind 10000 Renderer - Mute List (Feed View)
diff --git a/src/components/nostr/kinds/PinListRenderer.tsx b/src/components/nostr/kinds/PinListRenderer.tsx
index a29a602..d8ab8e1 100644
--- a/src/components/nostr/kinds/PinListRenderer.tsx
+++ b/src/components/nostr/kinds/PinListRenderer.tsx
@@ -1,8 +1,5 @@
import { Pin, FileText } from "lucide-react";
-import {
- getEventPointerFromETag,
- getAddressPointerFromATag,
-} from "applesauce-core/helpers";
+import { getEventPointers, getAddressPointers } from "@/lib/nostr-utils";
import {
BaseEventProps,
BaseEventContainer,
@@ -10,39 +7,6 @@ import {
} from "./BaseEventRenderer";
import { EventRefListFull } from "../lists";
import type { NostrEvent } from "@/types/nostr";
-import type { EventPointer, AddressPointer } from "nostr-tools/nip19";
-
-/**
- * Extract event pointers from e tags
- */
-function getEventPointers(event: NostrEvent): EventPointer[] {
- const pointers: EventPointer[] = [];
- for (const tag of event.tags) {
- if (tag[0] === "e" && tag[1]) {
- const pointer = getEventPointerFromETag(tag);
- if (pointer) {
- pointers.push(pointer);
- }
- }
- }
- return pointers;
-}
-
-/**
- * Extract address pointers from a tags
- */
-function getAddressPointers(event: NostrEvent): AddressPointer[] {
- const pointers: AddressPointer[] = [];
- for (const tag of event.tags) {
- if (tag[0] === "a" && tag[1]) {
- const pointer = getAddressPointerFromATag(tag);
- if (pointer) {
- pointers.push(pointer);
- }
- }
- }
- return pointers;
-}
/**
* Kind 10001 Renderer - Pin List (Feed View)
diff --git a/src/components/nostr/kinds/RepositoryRenderer.tsx b/src/components/nostr/kinds/RepositoryRenderer.tsx
index 3d34e38..c306f42 100644
--- a/src/components/nostr/kinds/RepositoryRenderer.tsx
+++ b/src/components/nostr/kinds/RepositoryRenderer.tsx
@@ -61,7 +61,7 @@ export function RepositoryRenderer({ event }: BaseEventProps) {
className="flex items-center gap-1 text-muted-foreground underline decoration-dotted cursor-crosshair line-clamp-1"
onClick={(e) => e.stopPropagation()}
>
-
+
{webUrls[0]}
)}
diff --git a/src/components/nostr/kinds/index.tsx b/src/components/nostr/kinds/index.tsx
index ca56cc2..78ff2da 100644
--- a/src/components/nostr/kinds/index.tsx
+++ b/src/components/nostr/kinds/index.tsx
@@ -104,6 +104,14 @@ import {
InterestSetRenderer,
InterestSetDetailRenderer,
} from "./InterestListRenderer";
+import {
+ FavoriteReposRenderer,
+ FavoriteReposDetailRenderer,
+} from "./FavoriteReposRenderer";
+import {
+ GitAuthorsRenderer,
+ GitAuthorsDetailRenderer,
+} from "./GitAuthorsRenderer";
import {
MediaFollowListRenderer,
MediaFollowListDetailRenderer,
@@ -238,6 +246,8 @@ const kindRenderers: Record
> = {
10009: PublicChatsRenderer, // User Groups List (NIP-51)
10012: GenericRelayListRenderer, // Favorite Relays (NIP-51)
10015: InterestListRenderer, // Interest List (NIP-51)
+ 10017: GitAuthorsRenderer, // Git Authors (NIP-51)
+ 10018: FavoriteReposRenderer, // Favorite Repositories (NIP-51)
10020: MediaFollowListRenderer, // Media Follow List (NIP-51)
10030: EmojiListRenderer, // User Emoji List (NIP-51)
10040: TrustedProviderListRenderer, // Trusted Provider List (NIP-85)
@@ -359,6 +369,8 @@ const detailRenderers: Record<
10004: CommunityListDetailRenderer, // Community List Detail (NIP-51)
10005: ChannelListDetailRenderer, // Channel List Detail (NIP-51)
10015: InterestListDetailRenderer, // Interest List Detail (NIP-51)
+ 10017: GitAuthorsDetailRenderer, // Git Authors Detail (NIP-34)
+ 10018: FavoriteReposDetailRenderer, // Favorite Repositories Detail (NIP-34)
10040: TrustedProviderListDetailRenderer, // Trusted Provider List Detail (NIP-85)
10020: MediaFollowListDetailRenderer, // Media Follow List Detail (NIP-51)
10030: EmojiListDetailRenderer, // User Emoji List Detail (NIP-51)
diff --git a/src/constants/kinds.ts b/src/constants/kinds.ts
index eccd60a..8c64189 100644
--- a/src/constants/kinds.ts
+++ b/src/constants/kinds.ts
@@ -825,6 +825,20 @@ export const EVENT_KINDS: Record = {
nip: "51",
icon: Heart,
},
+ 10017: {
+ kind: 10017,
+ name: "Git Authors",
+ description: "Code follow list (NIP-34 event producers)",
+ nip: "51",
+ icon: FileCode,
+ },
+ 10018: {
+ kind: 10018,
+ name: "Favorite Repositories",
+ description: "Favorite git repositories list",
+ nip: "51",
+ icon: FolderGit2,
+ },
10019: {
kind: 10019,
name: "Nutzap Mint Rec",
diff --git a/src/lib/nostr-utils.ts b/src/lib/nostr-utils.ts
index cc457ab..1e9ec95 100644
--- a/src/lib/nostr-utils.ts
+++ b/src/lib/nostr-utils.ts
@@ -5,6 +5,11 @@ import { getNip10References } from "applesauce-common/helpers/threading";
import { getCommentReplyPointer } from "applesauce-common/helpers/comment";
import type { EventPointer, AddressPointer } from "nostr-tools/nip19";
import { isSafeRelayURL } from "applesauce-core/helpers/relays";
+import {
+ getEventPointerFromETag,
+ getAddressPointerFromATag,
+ getOrComputeCachedValue,
+} from "applesauce-core/helpers";
export function derivePlaceholderName(pubkey: string): string {
return `${pubkey.slice(0, 4)}:${pubkey.slice(-4)}`;
@@ -63,6 +68,45 @@ export function getTagValues(event: NostrEvent, tagName: string): string[] {
.map((tag) => tag[1]);
}
+const EventPointersSymbol = Symbol("eventPointers");
+const AddressPointersSymbol = Symbol("addressPointers");
+
+/**
+ * Extract all EventPointers from e tags on an event (cached)
+ */
+export function getEventPointers(event: NostrEvent): EventPointer[] {
+ return getOrComputeCachedValue(event, EventPointersSymbol, () => {
+ const pointers: EventPointer[] = [];
+ for (const tag of event.tags) {
+ if (tag[0] === "e" && tag[1]) {
+ const pointer = getEventPointerFromETag(tag);
+ if (pointer) {
+ pointers.push(pointer);
+ }
+ }
+ }
+ return pointers;
+ });
+}
+
+/**
+ * Extract all AddressPointers from a tags on an event (cached)
+ */
+export function getAddressPointers(event: NostrEvent): AddressPointer[] {
+ return getOrComputeCachedValue(event, AddressPointersSymbol, () => {
+ const pointers: AddressPointer[] = [];
+ for (const tag of event.tags) {
+ if (tag[0] === "a" && tag[1]) {
+ const pointer = getAddressPointerFromATag(tag);
+ if (pointer) {
+ pointers.push(pointer);
+ }
+ }
+ }
+ return pointers;
+ });
+}
+
/**
* Parse a q-tag (quote tag) into an EventPointer with relay hints
* Q-tag format per NIP-18: ["q", eventId, relayUrl?, pubkey?]