Add rich renderer for kind 10063 Blossom server list

- Create BlossomServerListRenderer.tsx with feed and detail views
- Show user's configured Blossom servers with clickable links
- Clicking a server opens the Blossom window with server info
- Register renderers for kind 10063 (BUD-03)
- Fix lint error by renaming useFallbackServers to applyFallbackServers
This commit is contained in:
Claude
2026-01-13 15:50:24 +00:00
parent bccdcf880b
commit 94788a7926
3 changed files with 136 additions and 3 deletions

View File

@@ -109,7 +109,7 @@ export function BlossomUploadDialog({
}, [open]);
// Helper to set fallback servers
const useFallbackServers = useCallback(() => {
const applyFallbackServers = useCallback(() => {
setServers(FALLBACK_SERVERS);
setSelectedServers(new Set([FALLBACK_SERVERS[0]])); // Select first by default
setUsingFallback(true);
@@ -172,7 +172,7 @@ export function BlossomUploadDialog({
const timeout = setTimeout(() => {
setLoadingServers(false);
if (!foundUserServers) {
useFallbackServers();
applyFallbackServers();
}
}, 3000);
@@ -180,7 +180,7 @@ export function BlossomUploadDialog({
subscription?.unsubscribe();
clearTimeout(timeout);
};
}, [open, pubkey, eventStore, useFallbackServers]);
}, [open, pubkey, eventStore, applyFallbackServers]);
// Create preview URL for selected file
useEffect(() => {

View File

@@ -0,0 +1,127 @@
import { BaseEventProps, BaseEventContainer } from "./BaseEventRenderer";
import { NostrEvent } from "@/types/nostr";
import { getServersFromEvent } from "@/services/blossom";
import { useGrimoire } from "@/core/state";
import { HardDrive, ExternalLink } from "lucide-react";
import { Button } from "@/components/ui/button";
/**
* Kind 10063 Renderer - Blossom User Server List (Feed View)
* Shows the user's configured Blossom blob storage servers
*/
export function BlossomServerListRenderer({ event }: BaseEventProps) {
const { addWindow } = useGrimoire();
const servers = getServersFromEvent(event);
const handleServerClick = (serverUrl: string) => {
// Open the blossom viewer with server info
addWindow(
"blossom",
{ subcommand: "servers", serverUrl },
`blossom servers`,
undefined,
);
};
if (servers.length === 0) {
return (
<BaseEventContainer event={event}>
<div className="text-xs text-muted-foreground italic">
No Blossom servers configured
</div>
</BaseEventContainer>
);
}
return (
<BaseEventContainer event={event}>
<div className="flex flex-col gap-0.5">
{servers.map((url) => (
<div
key={url}
className="flex items-center gap-2 py-0.5 group cursor-pointer hover:bg-muted/30 rounded px-1 -mx-1"
onClick={() => handleServerClick(url)}
>
<HardDrive className="size-4 text-muted-foreground flex-shrink-0" />
<span className="font-mono text-xs underline decoration-dotted flex-1 truncate">
{url}
</span>
<Button
variant="ghost"
size="icon"
className="size-6 opacity-0 group-hover:opacity-100 transition-opacity"
onClick={(e) => {
e.stopPropagation();
window.open(url, "_blank");
}}
>
<ExternalLink className="size-3" />
</Button>
</div>
))}
</div>
</BaseEventContainer>
);
}
/**
* Kind 10063 Detail Renderer - Blossom User Server List (Detail View)
* Shows full Blossom server list with clickable links
*/
export function BlossomServerListDetailRenderer({
event,
}: {
event: NostrEvent;
}) {
const { addWindow } = useGrimoire();
const servers = getServersFromEvent(event);
const handleServerClick = (serverUrl: string) => {
addWindow(
"blossom",
{ subcommand: "servers", serverUrl },
`blossom servers`,
undefined,
);
};
if (servers.length === 0) {
return (
<div className="p-4 text-center text-muted-foreground text-sm">
No Blossom servers configured
</div>
);
}
return (
<div className="flex flex-col gap-2 p-4">
<div className="flex items-center gap-2 text-sm text-muted-foreground mb-2">
<HardDrive className="size-4" />
<span>Blossom Servers ({servers.length})</span>
</div>
{servers.map((url) => (
<div
key={url}
className="flex items-center gap-3 p-2 rounded hover:bg-muted/30 cursor-pointer group"
onClick={() => handleServerClick(url)}
>
<HardDrive className="size-4 text-muted-foreground flex-shrink-0" />
<span className="font-mono text-sm underline decoration-dotted flex-1 truncate">
{url}
</span>
<Button
variant="ghost"
size="icon"
className="size-7 opacity-0 group-hover:opacity-100 transition-opacity"
onClick={(e) => {
e.stopPropagation();
window.open(url, "_blank");
}}
>
<ExternalLink className="size-4" />
</Button>
</div>
))}
</div>
);
}

View File

@@ -26,6 +26,10 @@ import { Kind9802Renderer } from "./HighlightRenderer";
import { Kind9802DetailRenderer } from "./HighlightDetailRenderer";
import { Kind10002Renderer } from "./RelayListRenderer";
import { Kind10002DetailRenderer } from "./RelayListDetailRenderer";
import {
BlossomServerListRenderer,
BlossomServerListDetailRenderer,
} from "./BlossomServerListRenderer";
import { Kind10317Renderer } from "./GraspListRenderer";
import { Kind10317DetailRenderer } from "./GraspListDetailRenderer";
import { Kind30023Renderer } from "./ArticleRenderer";
@@ -96,6 +100,7 @@ const kindRenderers: Record<number, React.ComponentType<BaseEventProps>> = {
9802: Kind9802Renderer, // Highlight
777: SpellRenderer, // Spell (Grimoire)
10002: Kind10002Renderer, // Relay List Metadata (NIP-65)
10063: BlossomServerListRenderer, // Blossom User Server List (BUD-03)
10317: Kind10317Renderer, // User Grasp List (NIP-34)
10006: GenericRelayListRenderer, // Blocked Relays (NIP-51)
10007: GenericRelayListRenderer, // Search Relays (NIP-51)
@@ -170,6 +175,7 @@ const detailRenderers: Record<
1621: IssueDetailRenderer, // Issue Detail (NIP-34)
9802: Kind9802DetailRenderer, // Highlight Detail
10002: Kind10002DetailRenderer, // Relay List Detail (NIP-65)
10063: BlossomServerListDetailRenderer, // Blossom User Server List Detail (BUD-03)
10317: Kind10317DetailRenderer, // User Grasp List Detail (NIP-34)
777: SpellDetailRenderer, // Spell Detail
30023: Kind30023DetailRenderer, // Long-form Article Detail