import { filenameFromUrl } from './fileMeta' /** * Download a remote asset with the desired filename. * * Tries the same-origin `fetch` → `Blob` → invisible-`.download` * route first so the file lands on disk under `filename` instead of * navigating the tab to it. When CORS / network errors trip us up we * fall back to `window.open` and then to a no-window anchor — the * popup-blocker fallback ensures the download still fires even when * a browser blocks the `_blank` window. * * Shared by `DownloadAction` (rendered as an explicit Download button * in PDF / image / video viewers) and `MessageAttachment.File` rows * (where the entire row is the download trigger). Centralizing here * keeps one source of truth for the fallback chain. */ export async function triggerDownload( url: string, filename?: string ): Promise { const name = filename ?? filenameFromUrl(url) try { const res = await fetch(url, { mode: 'cors' }) if (!res.ok) throw new Error(`HTTP ${res.status}`) const blob = await res.blob() const objectUrl = URL.createObjectURL(blob) const a = document.createElement('a') a.href = objectUrl a.download = name a.style.display = 'none' document.body.appendChild(a) a.click() document.body.removeChild(a) URL.revokeObjectURL(objectUrl) } catch { const fallback = window.open(url, '_blank', 'noopener,noreferrer') if (!fallback) { // popup blocked — fall back to a non-windowed anchor const a = document.createElement('a') a.href = url a.download = name a.target = '_blank' a.rel = 'noopener noreferrer' a.style.display = 'none' document.body.appendChild(a) a.click() document.body.removeChild(a) } } }