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.
This commit is contained in:
Claude
2026-01-15 14:48:11 +00:00
parent dade9a79a6
commit d930bf4071
2 changed files with 97 additions and 8 deletions

View File

@@ -147,6 +147,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);
@@ -186,6 +187,19 @@ 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) {
addWindow("open", { pointer: { id: latestFileEventId } });
}
};
return (
<div className="flex flex-col gap-6 p-6 max-w-4xl mx-auto">
{/* Header Section */}
@@ -206,7 +220,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>
)}

View File

@@ -6,28 +6,92 @@ import {
import {
getAppName,
getAppSummary,
getAppIdentifier,
detectPlatforms,
getReleaseVersion,
getReleaseFileEventId,
} from "@/lib/zapstore-helpers";
import { PlatformIcon } from "./zapstore/PlatformIcon";
import { use$ } from "applesauce-react/hooks";
import eventStore from "@/services/event-store";
import { useMemo } from "react";
import { useGrimoire } from "@/core/state";
import { FileDown } from "lucide-react";
/**
* 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);
// 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]);
const releases = use$(
() => eventStore.timeline(releasesFilter),
[releasesFilter],
);
// 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) {
addWindow("open", { pointer: { id: latestFileEventId } });
}
};
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">