diff --git a/src/components/RecommendedCard.tsx b/src/components/RecommendedCard.tsx index 707c9e2..6e02a58 100644 --- a/src/components/RecommendedCard.tsx +++ b/src/components/RecommendedCard.tsx @@ -1,12 +1,15 @@ import { Download } from "lucide-react"; import { Link } from "react-router-dom"; import type { Resource } from "../api"; -import { assetUrl, postJSON, postNoBody } from "../api"; +import { assetUrl, postJSON } from "../api"; import { useI18n } from "../i18n"; import { useMemo } from "react"; import { formatDateYmd } from "../utils/format"; import { officialRecommendationCoverFallbacks } from "./FigmaBanner"; -import { downloadFile } from "./messageStream/utils/downloadFile"; +import { + downloadAttachment, + downloadFile, +} from "./messageStream/utils/downloadFile"; function isPlaceholderAsset(path: string | undefined | null) { return !path || path.includes("placeholder-cover"); @@ -88,14 +91,16 @@ export function RecommendedCard({ onClick={async (e) => { e.preventDefault(); e.stopPropagation(); + if (r.downloadPostId && r.downloadAttachmentId) { + void downloadAttachment( + r.downloadPostId, + r.downloadAttachmentId, + r.title, + ).catch(() => {}); + return; + } try { - if (r.downloadPostId && r.downloadAttachmentId) { - await postNoBody( - `/api/posts/${r.downloadPostId}/attachments/${r.downloadAttachmentId}/download`, - ); - } else { - await postJSON(`/api/resources/${r.id}/download`, {}); - } + await postJSON(`/api/resources/${r.id}/download`, {}); } catch { /* ignore */ } diff --git a/src/components/messageStream/bubbles/FileDocBubble.tsx b/src/components/messageStream/bubbles/FileDocBubble.tsx index 6df5051..c74498f 100644 --- a/src/components/messageStream/bubbles/FileDocBubble.tsx +++ b/src/components/messageStream/bubbles/FileDocBubble.tsx @@ -1,8 +1,7 @@ import { ArrowDownToLine } from "lucide-react"; -import { postNoBody } from "../../../api"; import { useI18n } from "../../../i18n"; import type { Attachment, Post } from "../../../types/post"; -import { downloadFile } from "../utils/downloadFile"; +import { downloadAttachment } from "../utils/downloadFile"; import { fileIcon } from "../utils/fileIcon"; import { filenameWithExtension, @@ -17,8 +16,7 @@ function AttachmentRow({ postId, att }: { postId: string; att: Attachment }) { const displayFilename = filenameWithExtension(att.filename, att.mime); const handleDownload = () => { - void postNoBody(`/api/posts/${postId}/attachments/${att.id}/download`); - void downloadFile(att.url, displayFilename).catch(() => {}); + void downloadAttachment(postId, att.id, displayFilename).catch(() => {}); }; return ( diff --git a/src/components/messageStream/bubbles/VideoBubble.tsx b/src/components/messageStream/bubbles/VideoBubble.tsx index c94ee14..24611f7 100644 --- a/src/components/messageStream/bubbles/VideoBubble.tsx +++ b/src/components/messageStream/bubbles/VideoBubble.tsx @@ -1,11 +1,10 @@ import { ArrowDownToLine, Play } from "lucide-react"; import { useRef, useState } from "react"; -import { postNoBody } from "../../../api"; import { useI18n } from "../../../i18n"; import type { Post } from "../../../types/post"; import { useVideoPlayer } from "../overlays/VideoPlayer"; import { autolink } from "../utils/autolink"; -import { downloadFile } from "../utils/downloadFile"; +import { downloadAttachment } from "../utils/downloadFile"; import { formatBytes } from "../utils/formatBytes"; import { postDisplayText } from "../utils/postText"; @@ -74,10 +73,9 @@ export function VideoBubble({ post }: { post: Post }) { type="button" onClick={(e) => { e.stopPropagation(); - void postNoBody( - `/api/posts/${post.id}/attachments/${att.id}/download`, + void downloadAttachment(post.id, att.id, att.filename).catch( + () => {}, ); - void downloadFile(att.url, att.filename).catch(() => {}); }} className="group absolute left-3 top-3 z-10 inline-flex overflow-hidden rounded-full bg-black/45 text-xs text-white shadow-lg ring-1 ring-white/15 backdrop-blur-md transition hover:bg-black/60" aria-label={`Download ${att.filename}`} diff --git a/src/components/messageStream/overlays/ImageLightbox.tsx b/src/components/messageStream/overlays/ImageLightbox.tsx index 62d9db6..bfee347 100644 --- a/src/components/messageStream/overlays/ImageLightbox.tsx +++ b/src/components/messageStream/overlays/ImageLightbox.tsx @@ -9,10 +9,9 @@ import { } from "react"; import { createPortal } from "react-dom"; import { ChevronLeft, ChevronRight, Download, X } from "lucide-react"; -import { postNoBody } from "../../../api"; import type { Attachment } from "../../../types/post"; import { autolink } from "../utils/autolink"; -import { downloadFile } from "../utils/downloadFile"; +import { downloadAttachment, downloadFile } from "../utils/downloadFile"; type LightboxState = { images: Attachment[]; @@ -143,11 +142,12 @@ function LightboxView({ onClick={(e) => { e.stopPropagation(); if (postId) { - void postNoBody( - `/api/posts/${postId}/attachments/${current.id}/download`, + void downloadAttachment(postId, current.id, current.filename).catch( + () => {}, ); + } else { + void downloadFile(current.url, current.filename).catch(() => {}); } - void downloadFile(current.url, current.filename).catch(() => {}); }} className="absolute right-16 top-4 z-10 flex h-10 w-10 items-center justify-center rounded-full bg-white/10 text-white transition hover:bg-white/20" aria-label="Download" diff --git a/src/components/messageStream/utils/downloadFile.ts b/src/components/messageStream/utils/downloadFile.ts index 5ac05cf..99d2fdf 100644 --- a/src/components/messageStream/utils/downloadFile.ts +++ b/src/components/messageStream/utils/downloadFile.ts @@ -1,3 +1,5 @@ +import { assetUrl } from "../../../api"; + type SaveFilePicker = (options?: { suggestedName?: string; types?: Array<{ @@ -20,6 +22,22 @@ type WindowWithSavePicker = Window & { showSaveFilePicker?: SaveFilePicker; }; +export function attachmentDownloadUrl(postId: string, attachmentId: string) { + return assetUrl( + `/api/posts/${encodeURIComponent(postId)}/attachments/${encodeURIComponent( + attachmentId, + )}/download`, + ); +} + +export async function downloadAttachment( + postId: string, + attachmentId: string, + filename: string, +) { + return downloadFile(attachmentDownloadUrl(postId, attachmentId), filename); +} + export async function downloadFile(url: string, filename: string) { const res = await fetch(url, { credentials: "include" }); if (!res.ok) throw new Error(await res.text());