mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-06-05 10:11:12 +02:00
feat: Add Zapstore app and app curation set renderers
Add support for rendering Zapstore app-related Nostr events: - Kind 32267 (App Metadata): Display app details, icon, platforms, screenshots - Kind 30267 (App Curation Set): Display curated app collections New files: - src/lib/zapstore-helpers.ts: Helper functions for extracting app metadata - src/lib/zapstore-helpers.test.ts: Comprehensive test coverage (43 tests) - src/components/nostr/kinds/ZapstoreAppRenderer.tsx: Feed view for apps - src/components/nostr/kinds/ZapstoreAppDetailRenderer.tsx: Detail view for apps - src/components/nostr/kinds/ZapstoreAppSetRenderer.tsx: Feed view for collections - src/components/nostr/kinds/ZapstoreAppSetDetailRenderer.tsx: Detail view for collections Modified: - src/components/nostr/kinds/index.tsx: Register new renderers in kind registry All tests pass (726 total), build succeeds, no lint errors.
This commit is contained in:
144
src/components/nostr/kinds/ZapstoreAppDetailRenderer.tsx
Normal file
144
src/components/nostr/kinds/ZapstoreAppDetailRenderer.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import { NostrEvent } from "@/types/nostr";
|
||||
import {
|
||||
getAppName,
|
||||
getAppSummary,
|
||||
getAppIcon,
|
||||
getAppImages,
|
||||
getAppPlatforms,
|
||||
getAppRepository,
|
||||
getAppLicense,
|
||||
getAppIdentifier,
|
||||
} from "@/lib/zapstore-helpers";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { UserName } from "../UserName";
|
||||
import { ExternalLink } from "@/components/ExternalLink";
|
||||
import { MediaEmbed } from "../MediaEmbed";
|
||||
import { Package } from "lucide-react";
|
||||
|
||||
interface ZapstoreAppDetailRendererProps {
|
||||
event: NostrEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detail renderer for Kind 32267 - Zapstore App Metadata
|
||||
* Shows comprehensive app information including screenshots
|
||||
* Note: Zapstore helpers wrap getTagValue which caches internally
|
||||
*/
|
||||
export function ZapstoreAppDetailRenderer({
|
||||
event,
|
||||
}: ZapstoreAppDetailRendererProps) {
|
||||
const appName = getAppName(event);
|
||||
const summary = getAppSummary(event);
|
||||
const iconUrl = getAppIcon(event);
|
||||
const images = getAppImages(event);
|
||||
const platforms = getAppPlatforms(event);
|
||||
const repository = getAppRepository(event);
|
||||
const license = getAppLicense(event);
|
||||
const identifier = getAppIdentifier(event);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 p-6 max-w-4xl mx-auto">
|
||||
{/* Header Section */}
|
||||
<div className="flex gap-4">
|
||||
{/* App Icon */}
|
||||
{iconUrl ? (
|
||||
<img
|
||||
src={iconUrl}
|
||||
alt={appName}
|
||||
className="size-20 rounded-lg object-cover flex-shrink-0"
|
||||
loading="lazy"
|
||||
/>
|
||||
) : (
|
||||
<div className="size-20 rounded-lg bg-muted flex items-center justify-center flex-shrink-0">
|
||||
<Package className="size-10 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* App Title & Summary */}
|
||||
<div className="flex flex-col gap-2 flex-1 min-w-0">
|
||||
<h1 className="text-3xl font-bold">{appName}</h1>
|
||||
{summary && (
|
||||
<p className="text-muted-foreground text-base">{summary}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Metadata Grid */}
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
{/* Publisher */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-muted-foreground">Publisher</h3>
|
||||
<UserName pubkey={event.pubkey} />
|
||||
</div>
|
||||
|
||||
{/* Identifier */}
|
||||
{identifier && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-muted-foreground">Package ID</h3>
|
||||
<code className="font-mono text-sm truncate" title={identifier}>
|
||||
{identifier}
|
||||
</code>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* License */}
|
||||
{license && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-muted-foreground">License</h3>
|
||||
<code className="font-mono text-sm">{license}</code>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Repository */}
|
||||
{repository && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-muted-foreground">Repository</h3>
|
||||
<ExternalLink href={repository} className="truncate">
|
||||
{repository}
|
||||
</ExternalLink>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Platforms Section */}
|
||||
{platforms.length > 0 && (
|
||||
<div className="flex flex-col gap-3">
|
||||
<h2 className="text-xl font-semibold">
|
||||
Platforms ({platforms.length})
|
||||
</h2>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{platforms.map((platform) => (
|
||||
<Badge
|
||||
key={platform}
|
||||
variant="secondary"
|
||||
className="text-sm px-3 py-1"
|
||||
>
|
||||
{platform}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Screenshots Section */}
|
||||
{images.length > 0 && (
|
||||
<div className="flex flex-col gap-3">
|
||||
<h2 className="text-xl font-semibold">
|
||||
Screenshots ({images.length})
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||
{images.map((imageUrl, idx) => (
|
||||
<MediaEmbed
|
||||
key={idx}
|
||||
url={imageUrl}
|
||||
preset="preview"
|
||||
enableZoom
|
||||
className="w-full rounded-lg overflow-hidden aspect-video"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
104
src/components/nostr/kinds/ZapstoreAppRenderer.tsx
Normal file
104
src/components/nostr/kinds/ZapstoreAppRenderer.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import {
|
||||
BaseEventContainer,
|
||||
BaseEventProps,
|
||||
ClickableEventTitle,
|
||||
} from "./BaseEventRenderer";
|
||||
import {
|
||||
getAppName,
|
||||
getAppSummary,
|
||||
getAppIcon,
|
||||
getAppPlatforms,
|
||||
getAppRepository,
|
||||
getAppLicense,
|
||||
} from "@/lib/zapstore-helpers";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { ExternalLink } from "@/components/ExternalLink";
|
||||
import { Package } from "lucide-react";
|
||||
|
||||
/**
|
||||
* Renderer for Kind 32267 - Zapstore App Metadata
|
||||
* Displays app name, icon, summary, and platforms in feed
|
||||
*/
|
||||
export function ZapstoreAppRenderer({ event }: BaseEventProps) {
|
||||
const appName = getAppName(event);
|
||||
const summary = getAppSummary(event);
|
||||
const iconUrl = getAppIcon(event);
|
||||
const platforms = getAppPlatforms(event);
|
||||
const repository = getAppRepository(event);
|
||||
const license = getAppLicense(event);
|
||||
|
||||
return (
|
||||
<BaseEventContainer event={event}>
|
||||
<div className="flex gap-3">
|
||||
{/* App Icon */}
|
||||
{iconUrl ? (
|
||||
<img
|
||||
src={iconUrl}
|
||||
alt={appName}
|
||||
className="size-12 rounded-lg object-cover flex-shrink-0"
|
||||
loading="lazy"
|
||||
/>
|
||||
) : (
|
||||
<div className="size-12 rounded-lg bg-muted flex items-center justify-center flex-shrink-0">
|
||||
<Package className="size-6 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* App Info */}
|
||||
<div className="flex flex-col gap-2 flex-1 min-w-0">
|
||||
{/* App Name */}
|
||||
<ClickableEventTitle
|
||||
event={event}
|
||||
className="text-base font-semibold text-foreground"
|
||||
>
|
||||
{appName}
|
||||
</ClickableEventTitle>
|
||||
|
||||
{/* Summary */}
|
||||
{summary && (
|
||||
<p className="text-sm text-muted-foreground line-clamp-2">
|
||||
{summary}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Platforms & License */}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{platforms.length > 0 && (
|
||||
<>
|
||||
{platforms.slice(0, 4).map((platform) => (
|
||||
<Badge
|
||||
key={platform}
|
||||
variant="secondary"
|
||||
className="text-[10px] px-2 py-0.5"
|
||||
>
|
||||
{platform}
|
||||
</Badge>
|
||||
))}
|
||||
{platforms.length > 4 && (
|
||||
<Badge variant="outline" className="text-[10px] px-2 py-0">
|
||||
+{platforms.length - 4} more
|
||||
</Badge>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{license && (
|
||||
<Badge variant="outline" className="text-[10px] px-2 py-0.5">
|
||||
{license}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Repository Link */}
|
||||
{repository && (
|
||||
<ExternalLink
|
||||
href={repository}
|
||||
className="text-xs truncate max-w-full"
|
||||
>
|
||||
{repository}
|
||||
</ExternalLink>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</BaseEventContainer>
|
||||
);
|
||||
}
|
||||
171
src/components/nostr/kinds/ZapstoreAppSetDetailRenderer.tsx
Normal file
171
src/components/nostr/kinds/ZapstoreAppSetDetailRenderer.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
import { NostrEvent } from "@/types/nostr";
|
||||
import {
|
||||
getCurationSetName,
|
||||
getAppReferences,
|
||||
getAppName,
|
||||
getAppSummary,
|
||||
getAppIcon,
|
||||
getAppPlatforms,
|
||||
getCurationSetIdentifier,
|
||||
} from "@/lib/zapstore-helpers";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { useNostrEvent } from "@/hooks/useNostrEvent";
|
||||
import { useGrimoire } from "@/core/state";
|
||||
import { UserName } from "../UserName";
|
||||
import { Package } from "lucide-react";
|
||||
|
||||
interface ZapstoreAppSetDetailRendererProps {
|
||||
event: NostrEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expanded app card showing full app details
|
||||
*/
|
||||
function AppCard({
|
||||
address,
|
||||
}: {
|
||||
address: { kind: number; pubkey: string; identifier: string };
|
||||
}) {
|
||||
const { addWindow } = useGrimoire();
|
||||
const appEvent = useNostrEvent(address);
|
||||
|
||||
if (!appEvent) {
|
||||
return (
|
||||
<div className="p-4 bg-muted/20 rounded-lg border border-border">
|
||||
<div className="flex items-center gap-2">
|
||||
<Package className="size-5 text-muted-foreground" />
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Loading {address?.identifier || "app"}...
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const appName = getAppName(appEvent);
|
||||
const summary = getAppSummary(appEvent);
|
||||
const iconUrl = getAppIcon(appEvent);
|
||||
const platforms = getAppPlatforms(appEvent);
|
||||
|
||||
const handleClick = () => {
|
||||
addWindow("open", { pointer: address });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-muted/20 rounded-lg border border-border flex gap-4 hover:bg-muted/30 transition-colors">
|
||||
{/* App Icon */}
|
||||
{iconUrl ? (
|
||||
<img
|
||||
src={iconUrl}
|
||||
alt={appName}
|
||||
className="size-16 rounded-lg object-cover flex-shrink-0"
|
||||
loading="lazy"
|
||||
/>
|
||||
) : (
|
||||
<div className="size-16 rounded-lg bg-muted flex items-center justify-center flex-shrink-0">
|
||||
<Package className="size-8 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* App Info */}
|
||||
<div className="flex-1 flex flex-col gap-2 min-w-0">
|
||||
{/* App Name */}
|
||||
<button
|
||||
onClick={handleClick}
|
||||
className="text-lg font-semibold hover:underline cursor-crosshair text-left"
|
||||
>
|
||||
{appName}
|
||||
</button>
|
||||
|
||||
{/* Summary */}
|
||||
{summary && (
|
||||
<p className="text-sm text-muted-foreground line-clamp-2">
|
||||
{summary}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Platforms */}
|
||||
{platforms.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{platforms.slice(0, 6).map((platform) => (
|
||||
<Badge
|
||||
key={platform}
|
||||
variant="secondary"
|
||||
className="text-[10px] px-2 py-0.5"
|
||||
>
|
||||
{platform}
|
||||
</Badge>
|
||||
))}
|
||||
{platforms.length > 6 && (
|
||||
<Badge variant="outline" className="text-[10px] px-2 py-0">
|
||||
+{platforms.length - 6} more
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detail renderer for Kind 30267 - Zapstore App Curation Set
|
||||
* Shows comprehensive view of all apps in the collection
|
||||
*/
|
||||
export function ZapstoreAppSetDetailRenderer({
|
||||
event,
|
||||
}: ZapstoreAppSetDetailRendererProps) {
|
||||
const setName = getCurationSetName(event);
|
||||
const apps = getAppReferences(event);
|
||||
const identifier = getCurationSetIdentifier(event);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 p-6">
|
||||
{/* Header Section */}
|
||||
<div className="flex flex-col gap-3">
|
||||
<h1 className="text-3xl font-bold">{setName}</h1>
|
||||
|
||||
{/* Metadata */}
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
{/* Curator */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-muted-foreground">Curated by</h3>
|
||||
<UserName pubkey={event.pubkey} />
|
||||
</div>
|
||||
|
||||
{/* Identifier */}
|
||||
{identifier && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-muted-foreground">Collection ID</h3>
|
||||
<code className="font-mono text-sm truncate" title={identifier}>
|
||||
{identifier}
|
||||
</code>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* App Count */}
|
||||
<p className="text-muted-foreground">
|
||||
{apps.length} {apps.length === 1 ? "app" : "apps"} in this collection
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Apps Section */}
|
||||
<div className="flex flex-col gap-3">
|
||||
<h2 className="text-xl font-semibold">Apps</h2>
|
||||
|
||||
{apps.length === 0 ? (
|
||||
<p className="text-muted-foreground">
|
||||
No apps in this collection yet.
|
||||
</p>
|
||||
) : (
|
||||
<div className="flex flex-col gap-3">
|
||||
{apps.map((ref, idx) => (
|
||||
<AppCard key={idx} address={ref.address} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
91
src/components/nostr/kinds/ZapstoreAppSetRenderer.tsx
Normal file
91
src/components/nostr/kinds/ZapstoreAppSetRenderer.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import {
|
||||
BaseEventContainer,
|
||||
BaseEventProps,
|
||||
ClickableEventTitle,
|
||||
} from "./BaseEventRenderer";
|
||||
import {
|
||||
getCurationSetName,
|
||||
getAppReferences,
|
||||
getAppName,
|
||||
} from "@/lib/zapstore-helpers";
|
||||
import { useNostrEvent } from "@/hooks/useNostrEvent";
|
||||
import { useGrimoire } from "@/core/state";
|
||||
import { Package } from "lucide-react";
|
||||
|
||||
/**
|
||||
* Individual app item - fetches and displays app info
|
||||
*/
|
||||
function AppItem({
|
||||
address,
|
||||
}: {
|
||||
address: { kind: number; pubkey: string; identifier: string };
|
||||
}) {
|
||||
const { addWindow } = useGrimoire();
|
||||
const appEvent = useNostrEvent(address);
|
||||
const appName = appEvent
|
||||
? getAppName(appEvent)
|
||||
: address?.identifier || "Unknown App";
|
||||
|
||||
const handleClick = () => {
|
||||
addWindow("open", { pointer: address });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Package className="size-3 text-muted-foreground" />
|
||||
<button
|
||||
onClick={handleClick}
|
||||
className="text-sm hover:underline cursor-crosshair text-primary truncate"
|
||||
>
|
||||
{appName}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renderer for Kind 30267 - Zapstore App Curation Set
|
||||
* Displays collection name and list of apps
|
||||
*/
|
||||
export function ZapstoreAppSetRenderer({ event }: BaseEventProps) {
|
||||
const setName = getCurationSetName(event);
|
||||
const apps = getAppReferences(event);
|
||||
|
||||
// Show max 5 apps in feed view
|
||||
const MAX_APPS_IN_FEED = 5;
|
||||
const displayApps = apps.slice(0, MAX_APPS_IN_FEED);
|
||||
const remainingCount = apps.length - displayApps.length;
|
||||
|
||||
return (
|
||||
<BaseEventContainer event={event}>
|
||||
<div className="flex flex-col gap-2">
|
||||
{/* Collection Name */}
|
||||
<ClickableEventTitle
|
||||
event={event}
|
||||
className="text-base font-semibold text-foreground"
|
||||
>
|
||||
{setName}
|
||||
</ClickableEventTitle>
|
||||
|
||||
{/* App Count */}
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{apps.length} {apps.length === 1 ? "app" : "apps"}
|
||||
</p>
|
||||
|
||||
{/* App List */}
|
||||
{displayApps.length > 0 && (
|
||||
<div className="flex flex-col gap-1.5 pl-4 border-l-2 border-muted">
|
||||
{displayApps.map((ref, idx) => (
|
||||
<AppItem key={idx} address={ref.address} />
|
||||
))}
|
||||
{remainingCount > 0 && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
+{remainingCount} more app{remainingCount > 1 ? "s" : ""}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</BaseEventContainer>
|
||||
);
|
||||
}
|
||||
@@ -54,6 +54,10 @@ import { CalendarTimeEventRenderer } from "./CalendarTimeEventRenderer";
|
||||
import { CalendarTimeEventDetailRenderer } from "./CalendarTimeEventDetailRenderer";
|
||||
import { EmojiSetRenderer } from "./EmojiSetRenderer";
|
||||
import { EmojiSetDetailRenderer } from "./EmojiSetDetailRenderer";
|
||||
import { ZapstoreAppRenderer } from "./ZapstoreAppRenderer";
|
||||
import { ZapstoreAppDetailRenderer } from "./ZapstoreAppDetailRenderer";
|
||||
import { ZapstoreAppSetRenderer } from "./ZapstoreAppSetRenderer";
|
||||
import { ZapstoreAppSetDetailRenderer } from "./ZapstoreAppSetDetailRenderer";
|
||||
import { NostrEvent } from "@/types/nostr";
|
||||
import { BaseEventContainer, type BaseEventProps } from "./BaseEventRenderer";
|
||||
|
||||
@@ -94,6 +98,7 @@ const kindRenderers: Record<number, React.ComponentType<BaseEventProps>> = {
|
||||
30002: GenericRelayListRenderer, // Relay Sets (NIP-51)
|
||||
30023: Kind30023Renderer, // Long-form Article
|
||||
30030: EmojiSetRenderer, // Emoji Sets (NIP-30)
|
||||
30267: ZapstoreAppSetRenderer, // App Curation Set (Zapstore)
|
||||
30311: LiveActivityRenderer, // Live Streaming Event (NIP-53)
|
||||
34235: Kind21Renderer, // Horizontal Video (NIP-71 legacy)
|
||||
34236: Kind22Renderer, // Vertical Video (NIP-71 legacy)
|
||||
@@ -105,6 +110,7 @@ const kindRenderers: Record<number, React.ComponentType<BaseEventProps>> = {
|
||||
31923: CalendarTimeEventRenderer, // Time-Based Calendar Event (NIP-52)
|
||||
31989: HandlerRecommendationRenderer, // Handler Recommendation (NIP-89)
|
||||
31990: ApplicationHandlerRenderer, // Application Handler (NIP-89)
|
||||
32267: ZapstoreAppRenderer, // App Metadata (Zapstore)
|
||||
39701: Kind39701Renderer, // Web Bookmarks (NIP-B0)
|
||||
};
|
||||
|
||||
@@ -159,6 +165,7 @@ const detailRenderers: Record<
|
||||
777: SpellDetailRenderer, // Spell Detail
|
||||
30023: Kind30023DetailRenderer, // Long-form Article Detail
|
||||
30030: EmojiSetDetailRenderer, // Emoji Sets Detail (NIP-30)
|
||||
30267: ZapstoreAppSetDetailRenderer, // App Curation Set Detail (Zapstore)
|
||||
30311: LiveActivityDetailRenderer, // Live Streaming Event Detail (NIP-53)
|
||||
30617: RepositoryDetailRenderer, // Repository Detail (NIP-34)
|
||||
30618: RepositoryStateDetailRenderer, // Repository State Detail (NIP-34)
|
||||
@@ -168,6 +175,7 @@ const detailRenderers: Record<
|
||||
31923: CalendarTimeEventDetailRenderer, // Time-Based Calendar Event Detail (NIP-52)
|
||||
31989: HandlerRecommendationDetailRenderer, // Handler Recommendation Detail (NIP-89)
|
||||
31990: ApplicationHandlerDetailRenderer, // Application Handler Detail (NIP-89)
|
||||
32267: ZapstoreAppDetailRenderer, // App Metadata Detail (Zapstore)
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
475
src/lib/zapstore-helpers.test.ts
Normal file
475
src/lib/zapstore-helpers.test.ts
Normal file
@@ -0,0 +1,475 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
getAppName,
|
||||
getAppIdentifier,
|
||||
getAppSummary,
|
||||
getAppRepository,
|
||||
getAppIcon,
|
||||
getAppImages,
|
||||
getAppLicense,
|
||||
getAppPlatforms,
|
||||
getAppReleases,
|
||||
getCurationSetName,
|
||||
getCurationSetIdentifier,
|
||||
getAppReferences,
|
||||
parseAddressPointer,
|
||||
} from "./zapstore-helpers";
|
||||
import { NostrEvent } from "@/types/nostr";
|
||||
|
||||
// Helper to create a minimal kind 32267 event (App Metadata)
|
||||
function createAppEvent(overrides?: Partial<NostrEvent>): NostrEvent {
|
||||
return {
|
||||
id: "test-id",
|
||||
pubkey: "test-pubkey",
|
||||
created_at: 1234567890,
|
||||
kind: 32267,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: "test-sig",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
// Helper to create a minimal kind 30267 event (App Curation Set)
|
||||
function createCurationSetEvent(overrides?: Partial<NostrEvent>): NostrEvent {
|
||||
return {
|
||||
id: "test-id",
|
||||
pubkey: "test-pubkey",
|
||||
created_at: 1234567890,
|
||||
kind: 30267,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: "test-sig",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("Kind 32267 (App Metadata) Helpers", () => {
|
||||
describe("getAppName", () => {
|
||||
it("should extract name from name tag", () => {
|
||||
const event = createAppEvent({
|
||||
tags: [
|
||||
["name", "0xchat"],
|
||||
["d", "com.oxchat.nostr"],
|
||||
],
|
||||
});
|
||||
expect(getAppName(event)).toBe("0xchat");
|
||||
});
|
||||
|
||||
it("should fallback to d tag if no name tag", () => {
|
||||
const event = createAppEvent({
|
||||
tags: [["d", "com.example.app"]],
|
||||
});
|
||||
expect(getAppName(event)).toBe("com.example.app");
|
||||
});
|
||||
|
||||
it("should return 'Unknown App' if no name and no d tag", () => {
|
||||
const event = createAppEvent({
|
||||
tags: [],
|
||||
});
|
||||
expect(getAppName(event)).toBe("Unknown App");
|
||||
});
|
||||
|
||||
it("should return empty string for non-32267 events", () => {
|
||||
const event = createAppEvent({
|
||||
kind: 1,
|
||||
tags: [["name", "Test"]],
|
||||
});
|
||||
expect(getAppName(event)).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAppIdentifier", () => {
|
||||
it("should extract d tag value", () => {
|
||||
const event = createAppEvent({
|
||||
tags: [["d", "com.oxchat.nostr"]],
|
||||
});
|
||||
expect(getAppIdentifier(event)).toBe("com.oxchat.nostr");
|
||||
});
|
||||
|
||||
it("should return undefined if no d tag", () => {
|
||||
const event = createAppEvent({
|
||||
tags: [],
|
||||
});
|
||||
expect(getAppIdentifier(event)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return undefined for non-32267 events", () => {
|
||||
const event = createAppEvent({
|
||||
kind: 1,
|
||||
tags: [["d", "test"]],
|
||||
});
|
||||
expect(getAppIdentifier(event)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAppSummary", () => {
|
||||
it("should extract summary from summary tag", () => {
|
||||
const event = createAppEvent({
|
||||
tags: [["summary", "A secure chat app built on Nostr"]],
|
||||
});
|
||||
expect(getAppSummary(event)).toBe("A secure chat app built on Nostr");
|
||||
});
|
||||
|
||||
it("should fallback to content if no summary tag", () => {
|
||||
const event = createAppEvent({
|
||||
content: "Fallback description from content",
|
||||
tags: [],
|
||||
});
|
||||
expect(getAppSummary(event)).toBe("Fallback description from content");
|
||||
});
|
||||
|
||||
it("should return undefined if no summary and empty content", () => {
|
||||
const event = createAppEvent({
|
||||
content: "",
|
||||
tags: [],
|
||||
});
|
||||
expect(getAppSummary(event)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should prefer summary tag over content", () => {
|
||||
const event = createAppEvent({
|
||||
content: "Content description",
|
||||
tags: [["summary", "Summary description"]],
|
||||
});
|
||||
expect(getAppSummary(event)).toBe("Summary description");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAppRepository", () => {
|
||||
it("should extract repository URL", () => {
|
||||
const event = createAppEvent({
|
||||
tags: [["repository", "https://github.com/0xchat-app/0xchat-app-main"]],
|
||||
});
|
||||
expect(getAppRepository(event)).toBe(
|
||||
"https://github.com/0xchat-app/0xchat-app-main",
|
||||
);
|
||||
});
|
||||
|
||||
it("should return undefined if no repository tag", () => {
|
||||
const event = createAppEvent({
|
||||
tags: [],
|
||||
});
|
||||
expect(getAppRepository(event)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAppIcon", () => {
|
||||
it("should extract icon URL", () => {
|
||||
const event = createAppEvent({
|
||||
tags: [["icon", "https://cdn.zapstore.dev/icon.png"]],
|
||||
});
|
||||
expect(getAppIcon(event)).toBe("https://cdn.zapstore.dev/icon.png");
|
||||
});
|
||||
|
||||
it("should return undefined if no icon tag", () => {
|
||||
const event = createAppEvent({
|
||||
tags: [],
|
||||
});
|
||||
expect(getAppIcon(event)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAppImages", () => {
|
||||
it("should extract all image URLs", () => {
|
||||
const event = createAppEvent({
|
||||
tags: [
|
||||
["image", "https://cdn.zapstore.dev/image1.png"],
|
||||
["image", "https://cdn.zapstore.dev/image2.png"],
|
||||
["image", "https://cdn.zapstore.dev/image3.png"],
|
||||
["name", "App"],
|
||||
],
|
||||
});
|
||||
expect(getAppImages(event)).toEqual([
|
||||
"https://cdn.zapstore.dev/image1.png",
|
||||
"https://cdn.zapstore.dev/image2.png",
|
||||
"https://cdn.zapstore.dev/image3.png",
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return empty array if no image tags", () => {
|
||||
const event = createAppEvent({
|
||||
tags: [["name", "App"]],
|
||||
});
|
||||
expect(getAppImages(event)).toEqual([]);
|
||||
});
|
||||
|
||||
it("should return empty array for non-32267 events", () => {
|
||||
const event = createAppEvent({
|
||||
kind: 1,
|
||||
tags: [["image", "test.png"]],
|
||||
});
|
||||
expect(getAppImages(event)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAppLicense", () => {
|
||||
it("should extract license", () => {
|
||||
const event = createAppEvent({
|
||||
tags: [["license", "MIT"]],
|
||||
});
|
||||
expect(getAppLicense(event)).toBe("MIT");
|
||||
});
|
||||
|
||||
it("should return undefined if no license tag", () => {
|
||||
const event = createAppEvent({
|
||||
tags: [],
|
||||
});
|
||||
expect(getAppLicense(event)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAppPlatforms", () => {
|
||||
it("should extract all platform/architecture values from f tags", () => {
|
||||
const event = createAppEvent({
|
||||
tags: [
|
||||
["f", "android-arm64-v8a"],
|
||||
["f", "android-armeabi-v7a"],
|
||||
["name", "App"],
|
||||
],
|
||||
});
|
||||
expect(getAppPlatforms(event)).toEqual([
|
||||
"android-arm64-v8a",
|
||||
"android-armeabi-v7a",
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return empty array if no f tags", () => {
|
||||
const event = createAppEvent({
|
||||
tags: [["name", "App"]],
|
||||
});
|
||||
expect(getAppPlatforms(event)).toEqual([]);
|
||||
});
|
||||
|
||||
it("should return empty array for non-32267 events", () => {
|
||||
const event = createAppEvent({
|
||||
kind: 1,
|
||||
tags: [["f", "test"]],
|
||||
});
|
||||
expect(getAppPlatforms(event)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAppReleases", () => {
|
||||
it("should extract release references from a tags", () => {
|
||||
const event = createAppEvent({
|
||||
tags: [
|
||||
[
|
||||
"a",
|
||||
"30063:5eca50a04afaefe55659fb74810b42654e2268c1acca6e53801b9862db74a83a:com.oxchat.nostr@v1.5.1-release",
|
||||
],
|
||||
],
|
||||
});
|
||||
const releases = getAppReleases(event);
|
||||
expect(releases).toHaveLength(1);
|
||||
expect(releases[0]).toEqual({
|
||||
kind: 30063,
|
||||
pubkey:
|
||||
"5eca50a04afaefe55659fb74810b42654e2268c1acca6e53801b9862db74a83a",
|
||||
identifier: "com.oxchat.nostr@v1.5.1-release",
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle multiple release references", () => {
|
||||
const event = createAppEvent({
|
||||
tags: [
|
||||
["a", "30063:pubkey1:release1"],
|
||||
["a", "30063:pubkey2:release2"],
|
||||
],
|
||||
});
|
||||
const releases = getAppReleases(event);
|
||||
expect(releases).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("should filter out invalid a tags", () => {
|
||||
const event = createAppEvent({
|
||||
tags: [
|
||||
["a", "30063:pubkey1:release1"],
|
||||
["a", "invalid"],
|
||||
["a", "30063:pubkey2:release2"],
|
||||
],
|
||||
});
|
||||
const releases = getAppReleases(event);
|
||||
expect(releases).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("should return empty array if no a tags", () => {
|
||||
const event = createAppEvent({
|
||||
tags: [["name", "App"]],
|
||||
});
|
||||
expect(getAppReleases(event)).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Kind 30267 (App Curation Set) Helpers", () => {
|
||||
describe("getCurationSetName", () => {
|
||||
it("should extract name from name tag", () => {
|
||||
const event = createCurationSetEvent({
|
||||
tags: [
|
||||
["name", "Nostr Social"],
|
||||
["d", "nostr-social"],
|
||||
],
|
||||
});
|
||||
expect(getCurationSetName(event)).toBe("Nostr Social");
|
||||
});
|
||||
|
||||
it("should fallback to d tag if no name tag", () => {
|
||||
const event = createCurationSetEvent({
|
||||
tags: [["d", "my-collection"]],
|
||||
});
|
||||
expect(getCurationSetName(event)).toBe("my-collection");
|
||||
});
|
||||
|
||||
it("should return 'Unnamed Collection' if no name and no d tag", () => {
|
||||
const event = createCurationSetEvent({
|
||||
tags: [],
|
||||
});
|
||||
expect(getCurationSetName(event)).toBe("Unnamed Collection");
|
||||
});
|
||||
|
||||
it("should return empty string for non-30267 events", () => {
|
||||
const event = createCurationSetEvent({
|
||||
kind: 1,
|
||||
tags: [["name", "Test"]],
|
||||
});
|
||||
expect(getCurationSetName(event)).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getCurationSetIdentifier", () => {
|
||||
it("should extract d tag value", () => {
|
||||
const event = createCurationSetEvent({
|
||||
tags: [["d", "nostr-social"]],
|
||||
});
|
||||
expect(getCurationSetIdentifier(event)).toBe("nostr-social");
|
||||
});
|
||||
|
||||
it("should return undefined if no d tag", () => {
|
||||
const event = createCurationSetEvent({
|
||||
tags: [],
|
||||
});
|
||||
expect(getCurationSetIdentifier(event)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return undefined for non-30267 events", () => {
|
||||
const event = createCurationSetEvent({
|
||||
kind: 1,
|
||||
tags: [["d", "test"]],
|
||||
});
|
||||
expect(getCurationSetIdentifier(event)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAppReferences", () => {
|
||||
it("should extract app references from a tags", () => {
|
||||
const event = createCurationSetEvent({
|
||||
tags: [
|
||||
["d", "nostr-social"],
|
||||
[
|
||||
"a",
|
||||
"32267:4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0:to.iris",
|
||||
"wss://relay.com",
|
||||
],
|
||||
[
|
||||
"a",
|
||||
"32267:b090908101cc6498893cc7f14d745dcea0b2ab6842cc4b512515643d272a375c:net.primal.android",
|
||||
],
|
||||
],
|
||||
});
|
||||
const refs = getAppReferences(event);
|
||||
expect(refs).toHaveLength(2);
|
||||
expect(refs[0].address).toEqual({
|
||||
kind: 32267,
|
||||
pubkey:
|
||||
"4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0",
|
||||
identifier: "to.iris",
|
||||
});
|
||||
expect(refs[0].relayHint).toBe("wss://relay.com");
|
||||
expect(refs[1].relayHint).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should only include kind 32267 references", () => {
|
||||
const event = createCurationSetEvent({
|
||||
tags: [
|
||||
["d", "collection"],
|
||||
["a", "32267:pubkey1:app1"],
|
||||
["a", "30023:pubkey2:article1"],
|
||||
["a", "32267:pubkey3:app2"],
|
||||
],
|
||||
});
|
||||
const refs = getAppReferences(event);
|
||||
expect(refs).toHaveLength(2);
|
||||
expect(refs[0].address.kind).toBe(32267);
|
||||
expect(refs[1].address.kind).toBe(32267);
|
||||
});
|
||||
|
||||
it("should filter out invalid a tags", () => {
|
||||
const event = createCurationSetEvent({
|
||||
tags: [
|
||||
["d", "collection"],
|
||||
["a", "32267:pubkey1:app1"],
|
||||
["a", "invalid-format"],
|
||||
["a", "32267:pubkey2:app2"],
|
||||
],
|
||||
});
|
||||
const refs = getAppReferences(event);
|
||||
expect(refs).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("should return empty array if no a tags", () => {
|
||||
const event = createCurationSetEvent({
|
||||
tags: [["d", "collection"]],
|
||||
});
|
||||
expect(getAppReferences(event)).toEqual([]);
|
||||
});
|
||||
|
||||
it("should return empty array for non-30267 events", () => {
|
||||
const event = createCurationSetEvent({
|
||||
kind: 1,
|
||||
tags: [["a", "32267:pubkey:app"]],
|
||||
});
|
||||
expect(getAppReferences(event)).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Shared Helpers", () => {
|
||||
describe("parseAddressPointer", () => {
|
||||
it("should parse valid address pointer", () => {
|
||||
const result = parseAddressPointer("32267:abcd1234:com.example.app");
|
||||
expect(result).toEqual({
|
||||
kind: 32267,
|
||||
pubkey: "abcd1234",
|
||||
identifier: "com.example.app",
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle empty identifier", () => {
|
||||
const result = parseAddressPointer("30267:abcd1234:");
|
||||
expect(result).toEqual({
|
||||
kind: 30267,
|
||||
pubkey: "abcd1234",
|
||||
identifier: "",
|
||||
});
|
||||
});
|
||||
|
||||
it("should return null for invalid format", () => {
|
||||
expect(parseAddressPointer("invalid")).toBeNull();
|
||||
expect(parseAddressPointer("32267:abcd")).toBeNull();
|
||||
expect(parseAddressPointer("not-a-kind:pubkey:id")).toBeNull();
|
||||
});
|
||||
|
||||
it("should handle long pubkeys and identifiers", () => {
|
||||
const longPubkey =
|
||||
"5eca50a04afaefe55659fb74810b42654e2268c1acca6e53801b9862db74a83a";
|
||||
const longId = "com.oxchat.nostr@v1.5.1-release";
|
||||
const result = parseAddressPointer(`30063:${longPubkey}:${longId}`);
|
||||
expect(result).toEqual({
|
||||
kind: 30063,
|
||||
pubkey: longPubkey,
|
||||
identifier: longId,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
216
src/lib/zapstore-helpers.ts
Normal file
216
src/lib/zapstore-helpers.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
import { NostrEvent } from "@/types/nostr";
|
||||
import { getTagValue } from "applesauce-core/helpers";
|
||||
import { AddressPointer } from "nostr-tools/nip19";
|
||||
|
||||
/**
|
||||
* Zapstore Helper Functions
|
||||
* For working with App Metadata (32267) and App Curation Set (30267) events
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get all values for a tag name (plural version of getTagValue)
|
||||
* Unlike getTagValue which returns first match, this returns all matches
|
||||
*/
|
||||
function getTagValues(event: NostrEvent, tagName: string): string[] {
|
||||
return event.tags
|
||||
.filter((tag) => tag[0] === tagName)
|
||||
.map((tag) => tag[1])
|
||||
.filter((val): val is string => val !== undefined);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Kind 32267 (App Metadata) Helpers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get app name from kind 32267 name tag
|
||||
*/
|
||||
export function getAppName(event: NostrEvent): string {
|
||||
if (event.kind !== 32267) return "";
|
||||
|
||||
const name = getTagValue(event, "name");
|
||||
if (name && typeof name === "string") {
|
||||
return name;
|
||||
}
|
||||
|
||||
// Fallback to d tag identifier
|
||||
const dTag = getTagValue(event, "d");
|
||||
return dTag && typeof dTag === "string" ? dTag : "Unknown App";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get app identifier from kind 32267 d tag (like package name)
|
||||
*/
|
||||
export function getAppIdentifier(event: NostrEvent): string | undefined {
|
||||
if (event.kind !== 32267) return undefined;
|
||||
return getTagValue(event, "d");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get app summary/description from kind 32267 summary tag
|
||||
*/
|
||||
export function getAppSummary(event: NostrEvent): string | undefined {
|
||||
if (event.kind !== 32267) return undefined;
|
||||
|
||||
const summary = getTagValue(event, "summary");
|
||||
if (summary && typeof summary === "string") {
|
||||
return summary;
|
||||
}
|
||||
|
||||
// Fallback to content if no summary tag
|
||||
return event.content || undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get repository URL from kind 32267 repository tag
|
||||
*/
|
||||
export function getAppRepository(event: NostrEvent): string | undefined {
|
||||
if (event.kind !== 32267) return undefined;
|
||||
return getTagValue(event, "repository");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get app icon URL from kind 32267 icon tag
|
||||
*/
|
||||
export function getAppIcon(event: NostrEvent): string | undefined {
|
||||
if (event.kind !== 32267) return undefined;
|
||||
return getTagValue(event, "icon");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get app screenshot URLs from kind 32267 image tags (multiple)
|
||||
*/
|
||||
export function getAppImages(event: NostrEvent): string[] {
|
||||
if (event.kind !== 32267) return [];
|
||||
return getTagValues(event, "image");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get app license from kind 32267 license tag
|
||||
*/
|
||||
export function getAppLicense(event: NostrEvent): string | undefined {
|
||||
if (event.kind !== 32267) return undefined;
|
||||
return getTagValue(event, "license");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get supported platforms/architectures from kind 32267 f tags
|
||||
*/
|
||||
export function getAppPlatforms(event: NostrEvent): string[] {
|
||||
if (event.kind !== 32267) return [];
|
||||
return getTagValues(event, "f");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get release artifact references from kind 32267 a tags (usually kind 30063)
|
||||
*/
|
||||
export function getAppReleases(event: NostrEvent): AddressPointer[] {
|
||||
if (event.kind !== 32267) return [];
|
||||
|
||||
const aTags = event.tags.filter((tag) => tag[0] === "a");
|
||||
const releases: AddressPointer[] = [];
|
||||
|
||||
for (const tag of aTags) {
|
||||
const aTagValue = tag[1];
|
||||
if (!aTagValue) continue;
|
||||
|
||||
const address = parseAddressPointer(aTagValue);
|
||||
if (address) {
|
||||
releases.push(address);
|
||||
}
|
||||
}
|
||||
|
||||
return releases;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Kind 30267 (App Curation Set) Helpers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get curation set name from kind 30267 name tag
|
||||
*/
|
||||
export function getCurationSetName(event: NostrEvent): string {
|
||||
if (event.kind !== 30267) return "";
|
||||
|
||||
const name = getTagValue(event, "name");
|
||||
if (name && typeof name === "string") {
|
||||
return name;
|
||||
}
|
||||
|
||||
// Fallback to d tag identifier
|
||||
const dTag = getTagValue(event, "d");
|
||||
return dTag && typeof dTag === "string" ? dTag : "Unnamed Collection";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get curation set identifier from kind 30267 d tag
|
||||
*/
|
||||
export function getCurationSetIdentifier(
|
||||
event: NostrEvent,
|
||||
): string | undefined {
|
||||
if (event.kind !== 30267) return undefined;
|
||||
return getTagValue(event, "d");
|
||||
}
|
||||
|
||||
/**
|
||||
* App reference with relay hint from a tag
|
||||
*/
|
||||
export interface AppReference {
|
||||
address: AddressPointer;
|
||||
relayHint?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all app references from kind 30267 a tags
|
||||
*/
|
||||
export function getAppReferences(event: NostrEvent): AppReference[] {
|
||||
if (event.kind !== 30267) return [];
|
||||
|
||||
const references: AppReference[] = [];
|
||||
const aTags = event.tags.filter((tag) => tag[0] === "a");
|
||||
|
||||
for (const tag of aTags) {
|
||||
const aTagValue = tag[1];
|
||||
if (!aTagValue) continue;
|
||||
|
||||
const address = parseAddressPointer(aTagValue);
|
||||
if (!address) continue;
|
||||
|
||||
// Kind 32267 apps are expected in curation sets
|
||||
if (address.kind === 32267) {
|
||||
const relayHint = tag[2];
|
||||
references.push({
|
||||
address,
|
||||
relayHint: relayHint || undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return references;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Shared Helpers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Parse an address pointer from an a tag value
|
||||
* Format: "kind:pubkey:identifier"
|
||||
*/
|
||||
export function parseAddressPointer(aTagValue: string): AddressPointer | null {
|
||||
const parts = aTagValue.split(":");
|
||||
if (parts.length !== 3) return null;
|
||||
|
||||
const kind = parseInt(parts[0], 10);
|
||||
const pubkey = parts[1];
|
||||
const identifier = parts[2];
|
||||
|
||||
if (isNaN(kind) || !pubkey || identifier === undefined) return null;
|
||||
|
||||
return {
|
||||
kind,
|
||||
pubkey,
|
||||
identifier,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user