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