import { assetUrl } from "../../../api"; import { isInAppBrowser } from "../../../utils/inAppBrowser"; export const IN_APP_DOWNLOAD_GUIDE_EVENT = "ark:in-app-download-guide"; export type InAppDownloadGuideDetail = { url: string; filename: string }; /** * Files larger than this skip the fetch→Blob path so mobile WebViews and * memory-constrained devices do not OOM. They fall back to the anchor * download (relies on the backend's `Content-Disposition: attachment`). */ const MAX_BLOB_DOWNLOAD_BYTES = 50 * 1024 * 1024; export type DownloadOptions = { sizeBytes?: number }; export function pauseActiveVideos() { document.querySelectorAll("video").forEach((video) => { video.pause(); }); } 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, options?: DownloadOptions, ) { return downloadFile( attachmentDownloadUrl(postId, attachmentId), filename, options, ); } export async function downloadFile( url: string, filename: string, options?: DownloadOptions, ) { const safeFilename = filename || "download"; // In-app WebViews (WeChat / TokenPocket / Telegram / iOS WKWebView / …) // ignore `Content-Disposition: attachment` and have no system download // manager, so an anchor click would just open the file inline. Surface an // "open in external browser" guide instead of silently failing the user. if (typeof window !== "undefined" && isInAppBrowser()) { window.dispatchEvent( new CustomEvent(IN_APP_DOWNLOAD_GUIDE_EVENT, { detail: { url, filename: safeFilename }, }), ); return; } // Normal browsers: prefer fetch → Blob → object URL so the file always lands // in the Downloads folder with the original filename, even when the browser // would otherwise inline-preview the response (Chrome and Safari do this for // PDFs / images regardless of Content-Disposition). Fall back to the anchor // download for large files (avoid loading them entirely into memory) or when // fetch fails for any reason. const sizeBytes = options?.sizeBytes ?? 0; if (sizeBytes <= MAX_BLOB_DOWNLOAD_BYTES) { try { const res = await fetch(url, { credentials: "omit" }); if (res.ok) { const blob = await res.blob(); const objectUrl = URL.createObjectURL(blob); try { triggerDownload(objectUrl, safeFilename); } finally { // Give the browser a moment to start the download before revoking. window.setTimeout(() => URL.revokeObjectURL(objectUrl), 4000); } return; } } catch { // fall through to anchor fallback } } triggerDownload(url, safeFilename); } function triggerDownload(url: string, filename: string) { const a = document.createElement("a"); a.href = url; a.download = filename; a.style.display = "none"; document.body.append(a); a.click(); a.remove(); }