mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-09 15:07:10 +02:00
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<string>();
|
||||
|
||||
// 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 (
|
||||
<div className="flex flex-col gap-6 p-6 max-w-4xl mx-auto">
|
||||
{/* Header Section */}
|
||||
@@ -206,7 +273,18 @@ export function ZapstoreAppDetailRenderer({
|
||||
|
||||
{/* App Title & Summary */}
|
||||
<div className="flex flex-col gap-2 flex-1 min-w-0">
|
||||
<h1 className="text-3xl font-bold">{appName}</h1>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<h1 className="text-3xl font-bold">{appName}</h1>
|
||||
{latestFileEventId && (
|
||||
<button
|
||||
onClick={handleDownloadLatest}
|
||||
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-primary-foreground bg-primary rounded-lg hover:bg-primary/90 transition-colors flex-shrink-0"
|
||||
>
|
||||
<FileDown className="size-4" />
|
||||
{latestVersion ? `Download v${latestVersion}` : "Download"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{summary && (
|
||||
<p className="text-muted-foreground text-base">{summary}</p>
|
||||
)}
|
||||
|
||||
@@ -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<string>();
|
||||
|
||||
// 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 (
|
||||
<BaseEventContainer event={event}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<ClickableEventTitle
|
||||
event={event}
|
||||
className="text-base font-semibold text-foreground"
|
||||
>
|
||||
{appName}
|
||||
</ClickableEventTitle>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<ClickableEventTitle
|
||||
event={event}
|
||||
className="text-base font-semibold text-foreground"
|
||||
>
|
||||
{appName}
|
||||
</ClickableEventTitle>
|
||||
|
||||
{latestFileEventId && (
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
className="flex items-center gap-1.5 px-2 py-1 text-xs font-medium text-primary border border-primary/20 rounded hover:bg-primary/10 transition-colors flex-shrink-0"
|
||||
title={latestVersion ? `Download v${latestVersion}` : "Download"}
|
||||
>
|
||||
<FileDown className="size-3" />
|
||||
{latestVersion ? `v${latestVersion}` : "Download"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{summary && (
|
||||
<p className="text-sm text-muted-foreground line-clamp-2">
|
||||
|
||||
Reference in New Issue
Block a user