From d172d675847bfd5eb6c3b139d025992d2e4b36e5 Mon Sep 17 00:00:00 2001 From: Alejandro Date: Thu, 15 Jan 2026 16:30:19 +0100 Subject: [PATCH] Add download button to Zapstore app renderers (#108) * Add download button to Zapstore app renderers Add a download button for the latest release to both the feed and detail renderers for kind 32267 (Zapstore App Metadata). The feed renderer shows a compact version button, while the detail renderer shows a prominent download button in the header. Both fetch the latest release and link to its file metadata event (kind 1063) for download. * Add proper relay hints for fetching Zapstore releases Use useLiveTimeline instead of eventStore.timeline() to actually fetch release events from relays. Relay selection includes: - Seen relays (where the app event was received from) - Publisher's outbox relays (NIP-65) - Aggregator relays as fallback This ensures releases are properly fetched rather than just read from the local event store cache. * Add relay hints when opening file events for download Pass relay hints from the release event's seen relays when opening file metadata events (kind 1063) for download. This ensures the event loader knows where to fetch the file event from. Also adds relay hints to the ReleaseItem component for both opening the release detail and the download file. --------- Co-authored-by: Claude --- .../nostr/kinds/ZapstoreAppDetailRenderer.tsx | 92 ++++++++++++- .../nostr/kinds/ZapstoreAppRenderer.tsx | 122 +++++++++++++++++- 2 files changed, 200 insertions(+), 14 deletions(-) diff --git a/src/components/nostr/kinds/ZapstoreAppDetailRenderer.tsx b/src/components/nostr/kinds/ZapstoreAppDetailRenderer.tsx index 2465308..5b66c5d 100644 --- a/src/components/nostr/kinds/ZapstoreAppDetailRenderer.tsx +++ b/src/components/nostr/kinds/ZapstoreAppDetailRenderer.tsx @@ -16,8 +16,6 @@ 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 { @@ -29,6 +27,10 @@ import { Laptop, FileDown, } from "lucide-react"; +import { getSeenRelays } from "applesauce-core/helpers/relays"; +import { relayListCache } from "@/services/relay-list-cache"; +import { AGGREGATOR_RELAYS } from "@/services/loaders"; +import { useLiveTimeline } from "@/hooks/useLiveTimeline"; interface ZapstoreAppDetailRendererProps { event: NostrEvent; @@ -42,12 +44,19 @@ function ReleaseItem({ release }: { release: NostrEvent }) { const version = getReleaseVersion(release); const fileEventId = getReleaseFileEventId(release); + // Get relay hints from the release event + const releaseSeenRelays = getSeenRelays(release); + const relayHints = releaseSeenRelays + ? Array.from(releaseSeenRelays).slice(0, 3) + : []; + const handleClick = () => { addWindow("open", { pointer: { kind: release.kind, pubkey: release.pubkey, identifier: release.tags.find((t) => t[0] === "d")?.[1] || "", + relays: relayHints, }, }); }; @@ -55,7 +64,9 @@ function ReleaseItem({ release }: { release: NostrEvent }) { const handleDownload = (e: React.MouseEvent) => { e.stopPropagation(); if (fileEventId) { - addWindow("open", { pointer: { id: fileEventId } }); + addWindow("open", { + pointer: { id: fileEventId, relays: relayHints }, + }); } }; @@ -147,6 +158,7 @@ function PlatformItem({ platform }: { platform: Platform }) { export function ZapstoreAppDetailRenderer({ event, }: ZapstoreAppDetailRendererProps) { + const { addWindow } = useGrimoire(); const appName = getAppName(event); const summary = getAppSummary(event); const iconUrl = getAppIcon(event); @@ -156,6 +168,37 @@ export function ZapstoreAppDetailRenderer({ const license = getAppLicense(event); const identifier = getAppIdentifier(event); + // Build relay list for fetching releases: + // 1. Seen relays (where we received this app event) + // 2. Publisher's outbox relays (NIP-65) + // 3. Aggregator relays (fallback) + const relays = useMemo(() => { + const relaySet = new Set(); + + // Add seen relays from the app event + const seenRelays = getSeenRelays(event); + if (seenRelays) { + for (const relay of seenRelays) { + relaySet.add(relay); + } + } + + // Add publisher's outbox relays + const outboxRelays = relayListCache.getOutboxRelaysSync(event.pubkey); + if (outboxRelays) { + for (const relay of outboxRelays.slice(0, 3)) { + relaySet.add(relay); + } + } + + // Add aggregator relays + for (const relay of AGGREGATOR_RELAYS) { + relaySet.add(relay); + } + + return Array.from(relaySet); + }, [event]); + // Query for releases that reference this app const releasesFilter = useMemo(() => { if (!identifier) { @@ -168,9 +211,12 @@ export function ZapstoreAppDetailRenderer({ }; }, [event.pubkey, identifier]); - const releases = use$( - () => eventStore.timeline(releasesFilter), - [releasesFilter], + // Use useLiveTimeline to fetch releases from relays with proper hints + const { events: releases } = useLiveTimeline( + `zapstore-releases-detail-${event.id}`, + releasesFilter, + relays, + { limit: 50 }, ); // Sort releases by version (newest first) or created_at @@ -186,6 +232,27 @@ export function ZapstoreAppDetailRenderer({ }); }, [releases]); + // Get the latest release for the header download button + const latestRelease = sortedReleases[0] || null; + const latestFileEventId = latestRelease + ? getReleaseFileEventId(latestRelease) + : null; + const latestVersion = latestRelease ? getReleaseVersion(latestRelease) : null; + + const handleDownloadLatest = () => { + if (latestFileEventId && latestRelease) { + // Get relay hints from the release event (where we found it) + const releaseSeenRelays = getSeenRelays(latestRelease); + const relayHints = releaseSeenRelays + ? Array.from(releaseSeenRelays).slice(0, 3) + : relays.slice(0, 3); + + addWindow("open", { + pointer: { id: latestFileEventId, relays: relayHints }, + }); + } + }; + return (
{/* Header Section */} @@ -206,7 +273,18 @@ export function ZapstoreAppDetailRenderer({ {/* App Title & Summary */}
-

{appName}

+
+

{appName}

+ {latestFileEventId && ( + + )} +
{summary && (

{summary}

)} diff --git a/src/components/nostr/kinds/ZapstoreAppRenderer.tsx b/src/components/nostr/kinds/ZapstoreAppRenderer.tsx index 204bb7a..dcfccf3 100644 --- a/src/components/nostr/kinds/ZapstoreAppRenderer.tsx +++ b/src/components/nostr/kinds/ZapstoreAppRenderer.tsx @@ -6,28 +6,136 @@ import { import { getAppName, getAppSummary, + getAppIdentifier, detectPlatforms, + getReleaseVersion, + getReleaseFileEventId, } from "@/lib/zapstore-helpers"; import { PlatformIcon } from "./zapstore/PlatformIcon"; +import { useMemo } from "react"; +import { useGrimoire } from "@/core/state"; +import { FileDown } from "lucide-react"; +import { getSeenRelays } from "applesauce-core/helpers/relays"; +import { relayListCache } from "@/services/relay-list-cache"; +import { AGGREGATOR_RELAYS } from "@/services/loaders"; +import { useLiveTimeline } from "@/hooks/useLiveTimeline"; /** * Renderer for Kind 32267 - App Metadata - * Clean feed view with app name, summary, and platform icons + * Clean feed view with app name, summary, platform icons, and download button */ export function ZapstoreAppRenderer({ event }: BaseEventProps) { + const { addWindow } = useGrimoire(); const appName = getAppName(event); const summary = getAppSummary(event); + const identifier = getAppIdentifier(event); const platforms = detectPlatforms(event); + // Build relay list for fetching releases: + // 1. Seen relays (where we received this app event) + // 2. Publisher's outbox relays (NIP-65) + // 3. Aggregator relays (fallback) + const relays = useMemo(() => { + const relaySet = new Set(); + + // Add seen relays from the app event + const seenRelays = getSeenRelays(event); + if (seenRelays) { + for (const relay of seenRelays) { + relaySet.add(relay); + } + } + + // Add publisher's outbox relays + const outboxRelays = relayListCache.getOutboxRelaysSync(event.pubkey); + if (outboxRelays) { + for (const relay of outboxRelays.slice(0, 3)) { + relaySet.add(relay); + } + } + + // Add aggregator relays + for (const relay of AGGREGATOR_RELAYS) { + relaySet.add(relay); + } + + return Array.from(relaySet); + }, [event]); + + // Query for releases that reference this app + const releasesFilter = useMemo(() => { + if (!identifier) { + return { kinds: [30063], ids: [] }; + } + return { + kinds: [30063], + "#a": [`32267:${event.pubkey}:${identifier}`], + }; + }, [event.pubkey, identifier]); + + // Use useLiveTimeline to actually fetch releases from relays + const { events: releases } = useLiveTimeline( + `zapstore-releases-${event.id}`, + releasesFilter, + relays, + { limit: 10 }, + ); + + // Get the latest release (by version or created_at) + const latestRelease = useMemo(() => { + if (!releases || releases.length === 0) return null; + return [...releases].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; + })[0]; + }, [releases]); + + const latestFileEventId = latestRelease + ? getReleaseFileEventId(latestRelease) + : null; + const latestVersion = latestRelease ? getReleaseVersion(latestRelease) : null; + + const handleDownload = (e: React.MouseEvent) => { + e.stopPropagation(); + if (latestFileEventId && latestRelease) { + // Get relay hints from the release event (where we found it) + const releaseSeenRelays = getSeenRelays(latestRelease); + const relayHints = releaseSeenRelays + ? Array.from(releaseSeenRelays).slice(0, 3) + : relays.slice(0, 3); + + addWindow("open", { + pointer: { id: latestFileEventId, relays: relayHints }, + }); + } + }; + return (
- - {appName} - +
+ + {appName} + + + {latestFileEventId && ( + + )} +
{summary && (