fix: in-app browser download opens file inline
- Detect in-app WebViews (WeChat / TokenPocket / imToken / Telegram / iOS WKWebView, etc.) and show a guide modal asking the user to open the link in their system browser, with a copy-link action. - For normal browsers, fetch the attachment as a Blob and trigger download from a same-origin object URL so the file always lands in the user's Downloads folder with the original filename, even when the browser would otherwise inline-preview the response. - Fall back to the anchor download for files larger than 50MB (avoid loading them entirely into memory) or when fetch fails. - Pass `sizeBytes` from known call sites so the threshold actually applies. - Add localized strings for the guide modal in all 7 locales. See .unipi/docs/debug/2026-06-05-in-app-browser-download-debug.md.
This commit is contained in:
@@ -1,4 +1,18 @@
|
||||
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) => {
|
||||
@@ -18,12 +32,62 @@ export async function downloadAttachment(
|
||||
postId: string,
|
||||
attachmentId: string,
|
||||
filename: string,
|
||||
options?: DownloadOptions,
|
||||
) {
|
||||
return downloadFile(attachmentDownloadUrl(postId, attachmentId), filename);
|
||||
return downloadFile(
|
||||
attachmentDownloadUrl(postId, attachmentId),
|
||||
filename,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
export async function downloadFile(url: string, filename: string) {
|
||||
triggerDownload(url, filename || "download");
|
||||
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<InAppDownloadGuideDetail>(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) {
|
||||
|
||||
Reference in New Issue
Block a user