mirror of
https://github.com/block/buzz.git
synced 2026-11-04 08:21:48 +00:00
Shared .md/.markdown/.mdx attachments now open in a right auxiliary
panel with a Preview/Code toggle instead of triggering a download,
mirroring berd's markdown-artifact viewer.
Nostr-specific adaptations vs berd: the relay stores markdown as
application/octet-stream ({sha256}.bin — no magic bytes), so
classification is by imeta filename only; relay media URLs require
Blossom auth (401 in plain browsers), so the panel fetches through the
authenticated fetch_media_bytes Tauri command; content-addressed URLs
allow staleTime: Infinity caching. Panel state is URL-search-param
backed (doc/docName) like the other channel panes. Strict UTF-8 decode
with a 2 MiB cap keeps binary/oversized files on the download path,
and surfaces without a panel host (forum, projects) keep download
behavior via a nullable context.
Signed-off-by: Riley Crane <rileycrane@squareup.com>
Co-authored-by: Fizz <d4ec90109b43092b0ec94cf136850915a3732a29ab3089a1dec43d2ed43dd6eb@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
103 lines
3.6 KiB
TypeScript
103 lines
3.6 KiB
TypeScript
import * as React from "react";
|
|
import { Download, FileText, PanelRight } from "lucide-react";
|
|
import { toast } from "sonner";
|
|
|
|
import { invokeTauri } from "@/shared/api/tauri";
|
|
import { useSmoothCorners } from "@/shared/ui/smoothCorners";
|
|
|
|
import { isRelayDownloadable } from "./mediaEntry";
|
|
import { isMarkdownDocFilename } from "./markdownDocFile";
|
|
import { useMarkdownDocViewer } from "./markdownDocViewerContext";
|
|
import { useMarkdownRuntime } from "./runtimeContext";
|
|
|
|
/** Human-readable byte size: "820 B", "12.4 KB", "3.1 MB". */
|
|
function formatFileSize(bytes: number): string {
|
|
if (!Number.isFinite(bytes) || bytes < 0) return "";
|
|
if (bytes < 1024) return `${bytes} B`;
|
|
const units = ["KB", "MB", "GB", "TB"];
|
|
let size = bytes / 1024;
|
|
let i = 0;
|
|
while (size >= 1024 && i < units.length - 1) {
|
|
size /= 1024;
|
|
i += 1;
|
|
}
|
|
return `${size < 10 ? size.toFixed(1) : Math.round(size)} ${units[i]}`;
|
|
}
|
|
|
|
/**
|
|
* File card for a generic (non-image, non-video) attachment: icon, filename,
|
|
* size, and a download action.
|
|
*
|
|
* Downloads go through the native `download_file` Tauri command (HTTP inside
|
|
* the app's tunnel + a save dialog), not a plain `<a download>` link. A bare
|
|
* link navigates the webview to the blob URL, which escapes to the OS browser
|
|
* and gets bounced to a corporate CDN interstitial ("browser not supported").
|
|
* The native command mirrors the image-download path.
|
|
*
|
|
* Markdown attachments (`.md`/`.markdown`/`.mdx` by imeta filename) open the
|
|
* in-app markdown viewer panel instead when a hosting surface provides one —
|
|
* relay-hosted only, because the viewer fetches through the authenticated
|
|
* `fetch_media_bytes` command, which accepts relay `/media/` origins alone.
|
|
*/
|
|
export function FileCard({
|
|
href,
|
|
filename,
|
|
size,
|
|
}: {
|
|
href: string;
|
|
filename: string;
|
|
size?: number;
|
|
}) {
|
|
const cardRef = React.useRef<HTMLButtonElement | null>(null);
|
|
const sizeLabel = size != null ? formatFileSize(size) : "";
|
|
useSmoothCorners(cardRef);
|
|
const openMarkdownDoc = useMarkdownDocViewer();
|
|
const { relayOrigin } = useMarkdownRuntime();
|
|
const opensInViewer =
|
|
openMarkdownDoc !== null &&
|
|
isMarkdownDocFilename(filename) &&
|
|
isRelayDownloadable(href, relayOrigin ?? undefined);
|
|
|
|
return (
|
|
<button
|
|
ref={cardRef}
|
|
type="button"
|
|
onClick={() => {
|
|
if (opensInViewer) {
|
|
openMarkdownDoc({ url: href, filename });
|
|
return;
|
|
}
|
|
invokeTauri("download_file", { url: href, filename }).catch(
|
|
(err: unknown) => {
|
|
const msg = err instanceof Error ? err.message : "Download failed";
|
|
toast.error(msg);
|
|
},
|
|
);
|
|
}}
|
|
aria-label={opensInViewer ? `Open ${filename}` : `Download ${filename}`}
|
|
data-testid="file-card"
|
|
className="my-1 inline-flex max-w-sm items-center gap-3 rounded-2xl border border-border/70 bg-muted/40 px-3 py-2 text-left no-underline transition-colors hover:bg-muted/70"
|
|
style={{ borderRadius: "1rem" }}
|
|
>
|
|
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-background text-muted-foreground">
|
|
<FileText className="h-4 w-4" />
|
|
</span>
|
|
<span className="min-w-0 flex-1">
|
|
<span className="block truncate text-sm font-medium text-foreground">
|
|
{filename}
|
|
</span>
|
|
{sizeLabel ? (
|
|
<span className="block text-xs text-muted-foreground">
|
|
{sizeLabel}
|
|
</span>
|
|
) : null}
|
|
</span>
|
|
{opensInViewer ? (
|
|
<PanelRight className="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
) : (
|
|
<Download className="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
)}
|
|
</button>
|
|
);
|
|
}
|