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?]