From 88b8b151be7d79be396b18b5bf4ea849bb092151 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Jan 2026 20:20:08 +0000 Subject: [PATCH] feat: Add releases section to app detail view - Query for all releases (kind 30063) that reference the app - Display releases sorted by version (newest first) - Each release shows version badge and download link - Clicking release opens full release detail view - Clicking download opens file metadata view --- .../nostr/kinds/ZapstoreAppDetailRenderer.tsx | 109 +++++++++++++++++- 1 file changed, 108 insertions(+), 1 deletion(-) diff --git a/src/components/nostr/kinds/ZapstoreAppDetailRenderer.tsx b/src/components/nostr/kinds/ZapstoreAppDetailRenderer.tsx index 89d7e41..4305591 100644 --- a/src/components/nostr/kinds/ZapstoreAppDetailRenderer.tsx +++ b/src/components/nostr/kinds/ZapstoreAppDetailRenderer.tsx @@ -8,11 +8,18 @@ import { getAppRepository, getAppLicense, getAppIdentifier, + getReleaseVersion, + getReleaseFileEventId, } from "@/lib/zapstore-helpers"; import type { Platform } from "@/lib/zapstore-helpers"; import { UserName } from "../UserName"; import { ExternalLink } from "@/components/ExternalLink"; import { MediaEmbed } from "../MediaEmbed"; +import { Badge } from "@/components/ui/badge"; +import { use$ } from "applesauce-react/hooks"; +import eventStore from "@/services/event-store"; +import { useMemo } from "react"; +import { useGrimoire } from "@/core/state"; import { Package, Globe, @@ -20,12 +27,68 @@ import { TabletSmartphone, Monitor, Laptop, + FileDown, } from "lucide-react"; interface ZapstoreAppDetailRendererProps { event: NostrEvent; } +/** + * Release item component showing version and download link + */ +function ReleaseItem({ release }: { release: NostrEvent }) { + const { addWindow } = useGrimoire(); + const version = getReleaseVersion(release); + const fileEventId = getReleaseFileEventId(release); + + const handleClick = () => { + addWindow("open", { + pointer: { + kind: release.kind, + pubkey: release.pubkey, + identifier: release.tags.find((t) => t[0] === "d")?.[1] || "", + }, + }); + }; + + const handleDownload = (e: React.MouseEvent) => { + e.stopPropagation(); + if (fileEventId) { + addWindow("open", { pointer: { id: fileEventId } }); + } + }; + + return ( +
+ + + {fileEventId && ( + + )} +
+ ); +} + /** * Platform icon and label component */ @@ -79,7 +142,7 @@ function PlatformItem({ platform }: { platform: Platform }) { /** * Detail renderer for Kind 32267 - App - * Shows comprehensive app information including screenshots and platforms + * Shows comprehensive app information including screenshots, platforms, and releases */ export function ZapstoreAppDetailRenderer({ event, @@ -93,6 +156,36 @@ export function ZapstoreAppDetailRenderer({ const license = getAppLicense(event); const identifier = getAppIdentifier(event); + // Query for releases that reference this app + const releasesFilter = useMemo(() => { + if (!identifier) { + // Return a filter that matches nothing when no identifier + return { kinds: [30063], ids: [] }; + } + return { + kinds: [30063], + "#a": [`32267:${event.pubkey}:${identifier}`], + }; + }, [event.pubkey, identifier]); + + const releases = use$( + () => eventStore.timeline(releasesFilter), + [releasesFilter], + ); + + // Sort releases by version (newest first) or created_at + const sortedReleases = useMemo(() => { + const releasesList = releases || []; + return [...releasesList].sort((a, b) => { + const versionA = getReleaseVersion(a); + const versionB = getReleaseVersion(b); + if (versionA && versionB) { + return versionB.localeCompare(versionA, undefined, { numeric: true }); + } + return b.created_at - a.created_at; + }); + }, [releases]); + return (
{/* Header Section */} @@ -169,6 +262,20 @@ export function ZapstoreAppDetailRenderer({
)} + {/* Releases Section */} + {sortedReleases.length > 0 && ( +
+

+ Releases ({sortedReleases.length}) +

+
+ {sortedReleases.map((release) => ( + + ))} +
+
+ )} + {/* Screenshots Section */} {images.length > 0 && (