Skip to content

Commit 3779a8f

Browse files
IM.codesclaude
andcommitted
fix: iOS download — skip redundant blob fetch, go straight to token URL
On iOS, downloadAttachment was fetching the entire file via rawFetch (blob), then fetching a token, then opening Browser.open to download AGAIN. The first fetch was wasted and the second relay could fail. Now iOS skips the blob fetch entirely — gets a token and opens the URL in SFSafariViewController directly. Single download, no waste. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 00b2718 commit 3779a8f

File tree

1 file changed

+22
-22
lines changed

1 file changed

+22
-22
lines changed

web/src/api.ts

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -917,6 +917,20 @@ export async function uploadFile(
917917
}
918918

919919
export async function downloadAttachment(serverId: string, attachmentId: string): Promise<void> {
920+
// Native (iOS): skip blob fetch — WKWebView can't trigger downloads from blob URLs.
921+
// Get a one-time token and open in system browser which handles save natively.
922+
const isNative = !!(globalThis as Record<string, unknown>).Capacitor;
923+
if (isNative) {
924+
const tokenRes = await apiFetch(`/api/server/${serverId}/uploads/${attachmentId}/download-token`, { method: 'POST' });
925+
const downloadToken = (tokenRes as { token: string }).token;
926+
const baseUrl = _baseUrl || window.location.origin;
927+
const downloadUrl = `${baseUrl}/api/server/${serverId}/uploads/${attachmentId}/download?token=${downloadToken}`;
928+
const { Browser } = await import('@capacitor/browser');
929+
await Browser.open({ url: downloadUrl });
930+
return;
931+
}
932+
933+
// Desktop: fetch blob and trigger <a download>
920934
const res = await rawFetch(`/api/server/${serverId}/uploads/${attachmentId}/download`);
921935
if (!res.ok) {
922936
const body = await res.text().catch(() => '');
@@ -926,7 +940,6 @@ export async function downloadAttachment(serverId: string, attachmentId: string)
926940
const disposition = res.headers.get('Content-Disposition');
927941
let filename = attachmentId;
928942
if (disposition) {
929-
// Prefer filename* (RFC 5987) for non-ASCII names
930943
const starMatch = disposition.match(/filename\*\s*=\s*UTF-8''([^\s;]+)/i);
931944
if (starMatch) {
932945
try { filename = decodeURIComponent(starMatch[1]); } catch { /* keep default */ }
@@ -935,27 +948,14 @@ export async function downloadAttachment(serverId: string, attachmentId: string)
935948
if (plainMatch) filename = plainMatch[1];
936949
}
937950
}
938-
// Trigger download — WKWebView (iOS Capacitor) ignores <a download>,
939-
// so on native platforms get a one-time download token and open the URL
940-
// in the system browser. No extra native plugins needed.
941-
const isNative = !!(globalThis as Record<string, unknown>).Capacitor;
942-
if (isNative) {
943-
const tokenRes = await apiFetch(`/api/server/${serverId}/uploads/${attachmentId}/download-token`, { method: 'POST' });
944-
const downloadToken = (tokenRes as { token: string }).token;
945-
const baseUrl = _baseUrl || window.location.origin;
946-
const downloadUrl = `${baseUrl}/api/server/${serverId}/uploads/${attachmentId}/download?token=${downloadToken}`;
947-
const { Browser } = await import('@capacitor/browser');
948-
await Browser.open({ url: downloadUrl });
949-
} else {
950-
const url = URL.createObjectURL(blob);
951-
const a = document.createElement('a');
952-
a.href = url;
953-
a.download = filename;
954-
document.body.appendChild(a);
955-
a.click();
956-
document.body.removeChild(a);
957-
URL.revokeObjectURL(url);
958-
}
951+
const url = URL.createObjectURL(blob);
952+
const a = document.createElement('a');
953+
a.href = url;
954+
a.download = filename;
955+
document.body.appendChild(a);
956+
a.click();
957+
document.body.removeChild(a);
958+
URL.revokeObjectURL(url);
959959
}
960960

961961
export async function previewAttachment(serverId: string, attachmentId: string): Promise<void> {

0 commit comments

Comments
 (0)