feat: fav repos and git author lists

This commit is contained in:
Alejandro Gómez
2026-03-31 13:18:14 +02:00
parent 0a4318ae8c
commit 297c7cdbe0
13 changed files with 272 additions and 208 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -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 (
<div className="flex flex-col gap-6 p-4">

View File

@@ -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 */

View File

@@ -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 (
<button
onClick={handleClick}
className="flex items-center gap-1.5 text-xs text-foreground hover:text-foreground cursor-crosshair transition-colors"
>
<GitBranch className="size-3.5 flex-shrink-0 text-muted-foreground" />
<span className="hover:underline hover:decoration-dotted truncate">
{displayName}
</span>
</button>
);
}
/**
* 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 (
<BaseEventContainer event={event}>
<div className="text-xs text-muted-foreground italic">
No favorite repositories
</div>
</BaseEventContainer>
);
}
return (
<BaseEventContainer event={event}>
<div className="flex flex-col gap-2">
<ClickableEventTitle
event={event}
className="flex items-center gap-1.5 text-sm font-medium"
>
<FolderGit2 className="size-4 text-muted-foreground" />
<span>Favorite Repositories ({pointers.length})</span>
</ClickableEventTitle>
<div className="flex flex-col gap-1">
{pointers.map((pointer) => (
<RepoNameItem
key={`${pointer.kind}:${pointer.pubkey}:${pointer.identifier}`}
pointer={pointer}
/>
))}
</div>
</div>
</BaseEventContainer>
);
}
/**
* Kind 10018 Detail Renderer - Favorite Repositories (Full View)
*/
export function FavoriteReposDetailRenderer({ event }: { event: NostrEvent }) {
const pointers = getAddressPointers(event).filter((p) => p.kind === 30617);
return (
<div className="flex flex-col gap-6 p-4">
<div className="flex items-center gap-2">
<FolderGit2 className="size-6 text-muted-foreground" />
<span className="text-lg font-semibold">Favorite Repositories</span>
<span className="text-sm text-muted-foreground">
({pointers.length})
</span>
</div>
{pointers.length === 0 ? (
<div className="text-sm text-muted-foreground italic">
No favorite repositories yet
</div>
) : (
<EventRefListFull
addressPointers={pointers}
label="Repositories"
icon={<GitBranch className="size-5" />}
/>
)}
</div>
);
}

View File

@@ -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 (
<BaseEventContainer event={event}>
<div className="text-xs text-muted-foreground italic">
No git authors followed
</div>
</BaseEventContainer>
);
}
return (
<BaseEventContainer event={event}>
<div className="flex flex-col gap-2">
<ClickableEventTitle
event={event}
className="flex items-center gap-1.5 text-sm font-medium"
>
<FileCode className="size-4 text-muted-foreground" />
<span>Git Authors</span>
</ClickableEventTitle>
<PubkeyListPreview
pubkeys={pubkeys}
previewLimit={3}
label="authors"
icon={<Users className="size-4 text-muted-foreground" />}
/>
</div>
</BaseEventContainer>
);
}
/**
* Kind 10017 Detail View - Full git authors list
*/
export function GitAuthorsDetailRenderer({ event }: { event: NostrEvent }) {
const pubkeys = getTagValues(event, "p");
return (
<div className="flex flex-col gap-6 p-4">
<div className="flex items-center gap-2">
<FileCode className="size-6 text-muted-foreground" />
<span className="text-lg font-semibold">Git Authors</span>
</div>
<PubkeyListFull
pubkeys={pubkeys}
label="Authors"
icon={<Users className="size-5" />}
/>
</div>
);
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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()}
>
<Globe className="size-3" />
<Globe className="size-3 flex-shrink-0" />
<span className="truncate line-clamp-1">{webUrls[0]}</span>
</a>
)}

View File

@@ -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<number, React.ComponentType<BaseEventProps>> = {
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)

View File

@@ -825,6 +825,20 @@ export const EVENT_KINDS: Record<number | string, EventKind> = {
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",

View File

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