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 && (