diff options
| author | Adam Soltys <[email protected]> | 2026-09-29 08:33:28 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-09-29 08:33:28 -0700 |
| commit | bdb8c3878df0439e3ebbcede5e2555f949a0cd7a (patch) | |
| tree | f3f4632643dfcbc5cd6d4cf4906b1df565449e9b /src | |
| parent | b9c04f573567d69d94fb120aa8feed89cf75585c (diff) | |
| download | coinosv3-bdb8c3878df0439e3ebbcede5e2555f949a0cd7a.tar.gz coinosv3-bdb8c3878df0439e3ebbcede5e2555f949a0cd7a.zip | |
Fix public media uploads and feed tab navigation
Diffstat (limited to 'src')
| -rw-r--r-- | src/app.js | 34 | ||||
| -rw-r--r-- | src/features/messages.js | 14 | ||||
| -rw-r--r-- | src/i18n.js | 8 | ||||
| -rw-r--r-- | src/locales/ar.json | 4 | ||||
| -rw-r--r-- | src/locales/bn.json | 4 | ||||
| -rw-r--r-- | src/locales/de.json | 4 | ||||
| -rw-r--r-- | src/locales/es.json | 4 | ||||
| -rw-r--r-- | src/locales/fr.json | 4 | ||||
| -rw-r--r-- | src/locales/hi.json | 4 | ||||
| -rw-r--r-- | src/locales/ja.json | 4 | ||||
| -rw-r--r-- | src/locales/ko.json | 4 | ||||
| -rw-r--r-- | src/locales/pt.json | 4 | ||||
| -rw-r--r-- | src/locales/ru.json | 4 | ||||
| -rw-r--r-- | src/locales/tr.json | 4 | ||||
| -rw-r--r-- | src/locales/zh.json | 4 | ||||
| -rw-r--r-- | src/media-upload.js | 86 |
16 files changed, 145 insertions, 45 deletions
@@ -9,6 +9,7 @@ import { qrSvg } from './qr.js'; import { makeSearcher, resultRows, searchable, punkUrl, warmSearch } from './recipient-search.js'; import { npubOf, seedPubkey, neventOf } from './nostr.js'; import { nip98Header } from './nip98.js'; +import { uploadPublicMedia, MediaUploadError } from './media-upload.js'; import { NOSTR_MARK } from './features/nostrlogin.js'; import { scanQr } from './scan.js'; import { dataSources, getSource, setSource, getNetwork, setNetwork, NETWORKS } from './api.js'; @@ -3452,21 +3453,27 @@ async function onbUpload(file) { const fd = new FormData(); fd.append('file', file); const endpoint = 'https://nostr.build/api/v2/upload/files'; - // nostr.build no longer takes anonymous uploads — sign with the wallet's - // nostr key, the one key that's always available without a signer prompt. - const headers = {}; - try { - if (wallet.nostrPubkey && wallet.nostrPubkey()) { - headers.authorization = await nip98Header({ signEvent: (e) => wallet.nostrSign(e) }, endpoint, 'POST'); - } - } catch {} - const r = await fetch(endpoint, { method: 'POST', body: fd, headers }); - const j = await r.json(); - const url = j?.data?.[0]?.url; + const authorization = await nip98Header({ signEvent: (event) => wallet.nostrSign(event) }, endpoint, 'POST'); + const response = await fetch(endpoint, { + method: 'POST', body: fd, headers: { authorization }, signal: AbortSignal.timeout(60_000), + }); + let body = null; + try { body = await response.json(); } catch {} + const url = response.ok && body?.data?.[0]?.url; if (!url) throw new Error(t('onbUploadFailed')); return url; } +async function publicMediaUpload(file) { + try { + return await uploadPublicMedia(file, (event) => wallet.nostrSign(event)); + } catch (error) { + if (error instanceof MediaUploadError && error.code === 'too-big') throw new Error(t('mediaUploadTooBig')); + if (error instanceof MediaUploadError && error.code === 'unsupported') throw new Error(t('mediaUploadUnsupported')); + throw new Error(t('msgUploadFailed')); + } +} + // Whether the wizard can offer Spending at all: an Ark-capable build on a // network with the names registrar, for a full (signing) account. Elsewhere // the offer would be a spinner that never resolves. @@ -5720,9 +5727,10 @@ const ctx = { setAccount: (a, dir) => setAccountSel(a, dir), // Open (or create) a wallet from a mnemonic — used by nostr login. openMnemonic: async (mnemonic, passphrase, opts) => enterWallet(mnemonic, passphrase, opts), - // Upload an image (avatar) and get back its URL — the same signed - // nostr.build path the onboarding picker uses. + // Profile pictures retain the metadata-scrubbing image uploader. Public + // post media uses resilient Blossom storage and media-specific failures. uploadImage: (file) => onbUpload(file), + uploadMedia: (file) => publicMediaUpload(file), // A nostr login that lands mid-wizard: a restored wallet has been through // onboarding elsewhere, so the wizard ends without tour or prompts; a fresh // one falls through to the wallet + tour on the next render. diff --git a/src/features/messages.js b/src/features/messages.js index 7505dbf..fb88a56 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -6995,7 +6995,7 @@ export function messagesFeature(ctx) { // A reply can carry a picture too — same upload, same imeta tag, same // paperclip. The URL lands in the draft, which is what every client // reads as the media. - ctx.uploadImage ? h('button', { + (ctx.uploadMedia || ctx.uploadImage) ? h('button', { class: 'attach-btn', title: t('feedAttach'), 'aria-label': t('feedAttach'), disabled: !!ui.postUploading, onClick: () => document.getElementById('reply-file')?.click(), }, ui.postUploading ? h('span', { class: 'spinner sm' }) : h('span', { style: 'display:flex', html: CLIP })) : null, @@ -7588,10 +7588,11 @@ export function messagesFeature(ctx) { const composeText = () => (ui.profCompose == null ? draftFor(POST_DRAFT) : ui.profCompose || ''); // Upload, then hand the URL to whichever draft asked for it. async function attachTo(file, place) { - if (!file || !ctx.uploadImage) return; + const upload = ctx.uploadMedia || ctx.uploadImage; + if (!file || !upload) return; ui.postUploading = true; render(); try { - const url = await ctx.uploadImage(file); + const url = await upload(file); (ui.postMedia ||= []).push({ url, m: file.type || '' }); place(url); } catch (e) { toast(e.message || String(e)); } @@ -7667,7 +7668,7 @@ export function messagesFeature(ctx) { render(); } } }, t('profPostBtn')), - ctx.uploadImage ? h('button', { + (ctx.uploadMedia || ctx.uploadImage) ? h('button', { class: 'attach-btn', title: t('feedAttach'), 'aria-label': t('feedAttach'), disabled: !!ui.postUploading, onClick: () => document.getElementById('post-file')?.click(), }, ui.postUploading ? h('span', { class: 'spinner sm' }) : h('span', { style: 'display:flex', html: CLIP })) : null, @@ -7804,7 +7805,10 @@ export function messagesFeature(ctx) { class: 'app-bottom-nav', 'data-key': 'app-bottom-nav', }, button('feed', t('feedTitle'), '<path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-2 2m0 0a2 2 0 0 1-2-2v-9c0-1.1.9-2 2-2h2"/><path d="M18 14h-8M15 18h-5M10 6h8v4h-8z"/>', () => { - if (active === 'feed' && ui.msgView === 'feed' && !ui.profilePk && !ui.noteThread && !ui.feedEdit) { jumpToNew(); return; } + // A destination tab is a destination: tapping Feed while already + // there always returns to its top. Only the explicit "x new posts" + // pill jumps to the first unread arrival. + if (active === 'feed' && ui.msgView === 'feed' && !ui.profilePk && !ui.noteThread && !ui.feedEdit) { glideToTop(); return; } leavePage(); if (visitor) { ui.pubProf = true; openFirehose(); } else showFeed(curFeedId); window.scrollTo({ top: 0 }); diff --git a/src/i18n.js b/src/i18n.js index afc76ef..475f919 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -198,7 +198,9 @@ const STR = { msgAttachFailed: 'Attachment unavailable', msgAttach: 'Attach a picture', msgAttachTooBig: 'That file is too big (20 MB max)', - msgUploadFailed: 'Upload failed. Try again', + msgUploadFailed: 'Upload failed. Check your connection and try again', + mediaUploadTooBig: 'That file is too big for the available media hosts', + mediaUploadUnsupported: 'That file format is not supported', msgPhoto: 'Photo', searchPeopleFor: 'Search people for {q}', msgEmojiSearch: 'Search emoji…', @@ -1006,9 +1008,9 @@ const STR = { onbNameWait: 'Getting things ready…', onbNotNow: 'Not now', onbAvatarTitle: 'Choose your look', - onbAvatarBody: 'Pick a punk or upload your own picture. You can change it anytime.', + onbAvatarBody: 'Choose an avatar or upload your own picture. You can change it anytime.', onbUpload: 'Upload a photo', - onbUploadFailed: 'Upload failed. Try a punk for now', + onbUploadFailed: 'Upload failed. Try another picture', onbProfileSkipped: 'Couldn’t publish your profile. Set it later from Edit profile', onbContinue: 'Continue', onbSkipAvatar: 'Continue with my default', diff --git a/src/locales/ar.json b/src/locales/ar.json index 6763c56..2e914c3 100644 --- a/src/locales/ar.json +++ b/src/locales/ar.json @@ -616,9 +616,9 @@ "onbNameWait": "جهز الأشياء", "onbNotNow": "ليس الآن", "onbAvatarTitle": "اختر مظهرك", - "onbAvatarBody": "إلتقط قطعة أو ارفع صورتك يمكنك تغييره في أي وقت", + "onbAvatarBody": "اختر صورة رمزية أو ارفع صورتك. يمكنك تغييرها في أي وقت", "onbUpload": "تحميل الصورة", - "onbUploadFailed": "لقد فشل التحميل - حاول أن تفشل الآن", + "onbUploadFailed": "فشل الرفع. جرّب صورة أخرى", "onbProfileSkipped": "لم يتمكن من نشر ملفك - وضعه في وقت لاحق من ملف إديت", "onbContinue": "استمر", "onbSkipAvatar": "استمر في إخفاقي", diff --git a/src/locales/bn.json b/src/locales/bn.json index 8c52809..44b2700 100644 --- a/src/locales/bn.json +++ b/src/locales/bn.json @@ -616,9 +616,9 @@ "onbNameWait": "জিনিস প্রস্তুত করা হচ্ছে...", "onbNotNow": "এই মুহূর্তে নয়", "onbAvatarTitle": "আপনার চেহারা পছন্দ করুন", - "onbAvatarBody": "তোমার নিজের ছবি আপলোড করে নাও। তুমি যেকোন সময় এটাকে পরিবর্তন করতে পারো।", + "onbAvatarBody": "একটি অবতার বেছে নাও অথবা নিজের ছবি আপলোড করো। যেকোনো সময় এটি বদলাতে পারো।", "onbUpload": "একটি ছবি আপলোড করুন", - "onbUploadFailed": "আপলোড করতে ব্যর্থ - এখন একটা বাজে প্রচেষ্টা করুন", + "onbUploadFailed": "আপলোড ব্যর্থ হয়েছে। অন্য একটি ছবি চেষ্টা করো", "onbProfileSkipped": "প্রোফাইল সম্পাদনা করতে ব্যর্থ - পরে নির্ধারণ করা সম্ভব নয়", "onbContinue": "এগিয়ে চলুন", "onbSkipAvatar": "ডিফল্ট হিসেবে নির্দিষ্ট করো", diff --git a/src/locales/de.json b/src/locales/de.json index 6409d81..bf827b4 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -616,9 +616,9 @@ "onbNameWait": "Bereitmachen...", "onbNotNow": "Nicht jetzt", "onbAvatarTitle": "Wählen Sie Ihren Look", - "onbAvatarBody": "Wählen Sie einen Punk oder laden Sie Ihr eigenes Bild hoch. Sie können es jederzeit ändern.", + "onbAvatarBody": "Wählen Sie einen Avatar oder laden Sie Ihr eigenes Bild hoch. Sie können ihn jederzeit ändern.", "onbUpload": "Foto hochladen", - "onbUploadFailed": "Upload fehlgeschlagen Versuchen Sie einen Punk für jetzt", + "onbUploadFailed": "Upload fehlgeschlagen. Versuchen Sie ein anderes Bild", "onbProfileSkipped": "Konnte dein Profil nicht publizieren", "onbContinue": "Weiter", "onbSkipAvatar": "Weiter mit meinem Standard", diff --git a/src/locales/es.json b/src/locales/es.json index 6cf00b6..52e417b 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -616,9 +616,9 @@ "onbNameWait": "Preparando las cosas...", "onbNotNow": "Ahora no.", "onbAvatarTitle": "Elige tu look", - "onbAvatarBody": "Escoge un punk o sube tu propia imagen. Puedes cambiarlo en cualquier momento.", + "onbAvatarBody": "Escoge un avatar o sube tu propia imagen. Puedes cambiarlo en cualquier momento.", "onbUpload": "Sube una foto", - "onbUploadFailed": "Subir falló, probar un punk por ahora", + "onbUploadFailed": "La subida falló. Prueba con otra imagen", "onbProfileSkipped": "No se pudo publicar su perfil - establecerlo más tarde del perfil Editar", "onbContinue": "Continuar", "onbSkipAvatar": "Continuar con mi defecto", diff --git a/src/locales/fr.json b/src/locales/fr.json index 9dd56c0..471eb5b 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -616,9 +616,9 @@ "onbNameWait": "Préparer les choses...", "onbNotNow": "Pas maintenant.", "onbAvatarTitle": "Choisissez votre look", - "onbAvatarBody": "Choisissez un punk ou téléchargez votre propre photo. Tu peux le changer à tout moment.", + "onbAvatarBody": "Choisissez un avatar ou téléchargez votre propre photo. Vous pouvez le changer à tout moment.", "onbUpload": "Télécharger une photo", - "onbUploadFailed": "Télécharger échoué, essayez un punk pour l'instant", + "onbUploadFailed": "Le téléversement a échoué. Essayez une autre image", "onbProfileSkipped": "Impossible de publier votre profil, définissez-le plus tard à partir du profil Édition", "onbContinue": "Continuer", "onbSkipAvatar": "Continuer avec mon défaut", diff --git a/src/locales/hi.json b/src/locales/hi.json index be3e6a3..4fcd47d 100644 --- a/src/locales/hi.json +++ b/src/locales/hi.json @@ -616,9 +616,9 @@ "onbNameWait": "चीजों को तैयार करना", "onbNotNow": "अब नहीं", "onbAvatarTitle": "अपना लुक चुनें", - "onbAvatarBody": "एक पंक चुनें या अपनी खुद की तस्वीर अपलोड करें। आप इसे कभी भी बदल सकते हैं।", + "onbAvatarBody": "एक अवतार चुनें या अपनी तस्वीर अपलोड करें। आप इसे कभी भी बदल सकते हैं।", "onbUpload": "फ़ोटो अपलोड करें", - "onbUploadFailed": "असफल रहा - अब के लिए एक पंक का प्रयास करें", + "onbUploadFailed": "अपलोड विफल रहा। कोई दूसरी तस्वीर आज़माएँ", "onbProfileSkipped": "अपने प्रोफ़ाइल को प्रकाशित नहीं कर सका - इसे बाद में संपादित प्रोफ़ाइल से सेट करें", "onbContinue": "जारी", "onbSkipAvatar": "जारी रखें मेरी डिफ़ॉल्ट के साथ", diff --git a/src/locales/ja.json b/src/locales/ja.json index d9cd967..2ab8916 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -616,9 +616,9 @@ "onbNameWait": "物事を準備する...", "onbNotNow": "今は今", "onbAvatarTitle": "お問い合わせ", - "onbAvatarBody": "パンクを選ぶか、自分の写真をアップロードしてください。 いつでも変更できます。", + "onbAvatarBody": "アバターを選ぶか、自分の写真をアップロードしてください。いつでも変更できます。", "onbUpload": "写真をアップロードする", - "onbUploadFailed": "アップロード失敗, 今すぐパンクを試してみる", + "onbUploadFailed": "アップロードに失敗しました。別の画像をお試しください", "onbProfileSkipped": "プロフィールを公開できません。編集プロファイルから後で設定できます。", "onbContinue": "お問い合わせ", "onbSkipAvatar": "デフォルトで続ける", diff --git a/src/locales/ko.json b/src/locales/ko.json index 6413666..e125415 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -615,9 +615,9 @@ "onbNameWait": "준비하는 것들 ...", "onbNotNow": "현재 위치", "onbAvatarTitle": "자주 묻는 질문", - "onbAvatarBody": "펑크를 선택하거나 자신의 사진을 업로드하십시오. 언제든지 변경할 수 있습니다.", + "onbAvatarBody": "아바타를 선택하거나 사진을 업로드하세요. 언제든지 변경할 수 있습니다.", "onbUpload": "사진 업로드", - "onbUploadFailed": "업로드 실패, 지금 punk 시도", + "onbUploadFailed": "업로드하지 못했습니다. 다른 이미지를 사용해 보세요", "onbProfileSkipped": "프로필을 게시 할 수 없습니다 - 나중에 편집 프로필에서 설정", "onbContinue": "계속하기", "onbSkipAvatar": "기본 설정", diff --git a/src/locales/pt.json b/src/locales/pt.json index 8c36922..9f5b2ad 100644 --- a/src/locales/pt.json +++ b/src/locales/pt.json @@ -616,9 +616,9 @@ "onbNameWait": "Preparando as coisas...", "onbNotNow": "Agora não.", "onbAvatarTitle": "Escolha o seu look", - "onbAvatarBody": "Escolha um punk ou carregue a sua própria foto. Podes mudá-lo quando quiseres.", + "onbAvatarBody": "Escolha um avatar ou carregue a sua própria foto. Podes mudá-lo quando quiseres.", "onbUpload": "Enviar uma foto", - "onbUploadFailed": "O envio falhou, tente um punk por enquanto", + "onbUploadFailed": "O envio falhou. Tente outra imagem", "onbProfileSkipped": "Não foi possível publicar o seu perfil, defini-lo mais tarde a partir de Editar perfil", "onbContinue": "Continuar", "onbSkipAvatar": "Continuar com o meu padrão", diff --git a/src/locales/ru.json b/src/locales/ru.json index 7ea2c0f..4e472fc 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -616,9 +616,9 @@ "onbNameWait": "Готовить вещи...", "onbNotNow": "Не сейчас", "onbAvatarTitle": "Выберите свой взгляд", - "onbAvatarBody": "Выберите панк или загрузите свою фотографию. Вы можете изменить его в любое время.", + "onbAvatarBody": "Выберите аватар или загрузите свою фотографию. Его можно изменить в любое время.", "onbUpload": "Загрузить фото", - "onbUploadFailed": "Загрузить не удалось, попробуйте панк на данный момент", + "onbUploadFailed": "Не удалось загрузить. Попробуйте другое изображение", "onbProfileSkipped": "Не смог опубликовать свой профиль, установите его позже из профиля Edit", "onbContinue": "Продолжать", "onbSkipAvatar": "Продолжить мой дефолт", diff --git a/src/locales/tr.json b/src/locales/tr.json index 1f261b7..e1e9928 100644 --- a/src/locales/tr.json +++ b/src/locales/tr.json @@ -615,9 +615,9 @@ "onbNameWait": "İşler hazır olsun...", "onbNotNow": "Şimdi Değil", "onbAvatarTitle": "görünümünü seçin", - "onbAvatarBody": "Bir punk seçin veya kendi resmini yükleyin. Her zaman değiştirebilirsiniz.", + "onbAvatarBody": "Bir avatar seçin veya kendi resminizi yükleyin. İstediğiniz zaman değiştirebilirsiniz.", "onbUpload": "Bir fotoğraf", - "onbUploadFailed": "Yükleme başarısız oldu - şimdilik bir punk deneyin", + "onbUploadFailed": "Yükleme başarısız oldu. Başka bir resim deneyin", "onbProfileSkipped": "Profilinizi yayınlayamaz - daha sonra Edit profilinden ayarlayın", "onbContinue": "Devam etmeye devam et", "onbSkipAvatar": "Benim varsayılanimle devam et", diff --git a/src/locales/zh.json b/src/locales/zh.json index 04748fc..6bb4c50 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -616,9 +616,9 @@ "onbNameWait": "准备...", "onbNotNow": "现在不行", "onbAvatarTitle": "选择你的外观", - "onbAvatarBody": "选择一个朋克或上传自己的相片. 你可以随时改变", + "onbAvatarBody": "选择一个头像或上传自己的照片。你可以随时更改", "onbUpload": "上传相片", - "onbUploadFailed": "上传失败,, 暂时尝试一个朋克", + "onbUploadFailed": "上传失败。请尝试其他图片", "onbProfileSkipped": "无法发布您的配置文件, 稍后从编辑配置文件中设置 。", "onbContinue": "继续", "onbSkipAvatar": "继续我的默认", diff --git a/src/media-upload.js b/src/media-upload.js new file mode 100644 index 0000000..686fec5 --- /dev/null +++ b/src/media-upload.js @@ -0,0 +1,86 @@ +import { sha256 } from '@noble/hashes/sha256'; +import { bytesToHex } from '@noble/hashes/utils'; + +// Independent public hosts: a phone upload should not fail just because one +// provider is down or rejects a particular container. nostr.download's NIP-96 +// document currently advertises large image/video/audio uploads; nostr.build +// is the second, widely-used Blossom home. +export const PUBLIC_MEDIA_SERVERS = ['https://nostr.download', 'https://blossom.nostr.build']; + +export class MediaUploadError extends Error { + constructor(code, details = '') { + super(code); + this.name = 'MediaUploadError'; + this.code = code; + this.details = details; + } +} + +async function fileHash(file) { + const hash = sha256.create(); + const chunk = 4 * 1024 * 1024; + for (let at = 0; at < file.size; at += chunk) + hash.update(new Uint8Array(await file.slice(at, at + chunk).arrayBuffer())); + return bytesToHex(hash.digest()); +} + +const responseUrl = (body) => body?.url + || body?.data?.[0]?.url + || (body?.tags || []).find((x) => x?.[0] === 'url')?.[1] + || null; + +function mediaUrl(url, server, file) { + const out = new URL(url, server); + const tail = out.pathname.split('/').at(-1) || ''; + if (/^[0-9a-f]{64}$/i.test(tail)) { + const named = String(file.name || '').match(/\.([a-z0-9]{1,8})$/i)?.[1]; + const byType = { 'video/quicktime': 'mov', 'video/mp4': 'mp4', 'video/webm': 'webm', + 'image/jpeg': 'jpg', 'image/png': 'png', 'image/webp': 'webp', 'image/gif': 'gif', 'image/avif': 'avif' }[file.type]; + const ext = (named || byType || '').toLowerCase(); + if (ext) out.pathname += '.' + ext; + } + return out.href; +} + +// BUD-02 upload. `signEvent` is deliberately injected: the app can use its +// always-available wallet identity, while tests need no secret key. +export async function uploadPublicMedia(file, signEvent, { + servers = PUBLIC_MEDIA_SERVERS, + fetcher = globalThis.fetch, + timeoutMs = 5 * 60_000, +} = {}) { + if (!file || !file.size || typeof signEvent !== 'function') throw new MediaUploadError('failed'); + const x = await fileHash(file); + const now = Math.floor(Date.now() / 1000); + const auth = await signEvent({ + kind: 24242, created_at: now - 5, content: 'upload', + tags: [['t', 'upload'], ['x', x], ['expiration', String(now + Math.ceil(timeoutMs / 1000) + 120)]], + }); + if (!auth) throw new MediaUploadError('failed'); + const authorization = 'Nostr ' + btoa(JSON.stringify(auth)); + const failures = []; + for (const server of servers) { + try { + const response = await fetcher(server.replace(/\/$/, '') + '/upload', { + method: 'PUT', body: file, signal: AbortSignal.timeout(timeoutMs), + headers: { + authorization, + 'content-type': file.type || 'application/octet-stream', + 'x-sha-256': x, + 'x-content-length': String(file.size), + }, + }); + let body = null; + try { body = await response.json(); } catch {} + const url = response.ok && responseUrl(body); + if (url) return mediaUrl(url, server, file); + failures.push({ status: response.status, reason: response.headers.get('x-reason') || body?.message || '' }); + } catch (error) { + failures.push({ status: 0, reason: error?.message || '' }); + } + } + const details = failures.map((x) => [x.status, x.reason].filter(Boolean).join(' ')).filter(Boolean).join('; '); + if (failures.length && failures.every((x) => x.status === 413)) throw new MediaUploadError('too-big', details); + if (failures.length && failures.every((x) => x.status === 415)) throw new MediaUploadError('unsupported', details); + throw new MediaUploadError('failed', details); +} |
