diff options
| author | Adam Soltys <[email protected]> | 2026-09-30 10:23:20 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-09-30 10:23:20 -0700 |
| commit | 0076c591df9432e2bb5b2281afc1fb4106500297 (patch) | |
| tree | 7b9ef49079158af7e6e2545f1e847c4c2baeb428 /src/media-upload.js | |
| parent | 505cd84813a80e0a69b2cc95aa93c68cf2856cb2 (diff) | |
| download | coinosv3-0076c591df9432e2bb5b2281afc1fb4106500297.tar.gz coinosv3-0076c591df9432e2bb5b2281afc1fb4106500297.zip | |
A picture or video on its way shows a bar with its percentage — shrinking, then uploading; the pencil reads "New post".
ui.mediaProgress { stage, pct } (a render per whole percent) drawn as a
labelled bar under the reply and post composers, instead of only the
paperclip turning into a spinner. Upload progress comes from
XMLHttpRequest upload events (fetch() has none) in uploadPublicMedia's
new onProgress; injected fetchers (tests) keep the fetch path.
upload-progress-test: 1.2 MB at 400 KB/s climbs 0→100 in steps.
video-compress-test: the bar counts the shrink, then the upload.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Du1qT1wbkYwYK6zkGH79Rp
Diffstat (limited to 'src/media-upload.js')
| -rw-r--r-- | src/media-upload.js | 26 |
1 files changed, 24 insertions, 2 deletions
diff --git a/src/media-upload.js b/src/media-upload.js index 8e5e8ed..9301939 100644 --- a/src/media-upload.js +++ b/src/media-upload.js @@ -43,13 +43,32 @@ function mediaUrl(url, server, file) { return out.href; } +// A PUT that reports how much of the body has gone out: fetch() can't, so in +// the browser (with the real fetch) it's XMLHttpRequest. Answers like fetch. +function putWithProgress(url, { body, headers, signal }, onProgress) { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open('PUT', url); + for (const [k, v] of Object.entries(headers)) xhr.setRequestHeader(k, v); + xhr.upload.onprogress = (e) => { if (e.lengthComputable) onProgress(e.loaded / e.total); }; + xhr.onload = () => resolve(new Response(xhr.responseText, { + status: xhr.status, headers: { 'x-reason': xhr.getResponseHeader('x-reason') || '' } })); + xhr.onerror = () => reject(new Error('network error')); + xhr.ontimeout = () => reject(new Error('timeout')); + if (signal) signal.addEventListener('abort', () => { xhr.abort(); reject(new Error('timeout')); }); + xhr.send(body); + }); +} + // 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, + onProgress = null, // (fraction 0..1) as the bytes go out } = {}) { + const tracked = onProgress && typeof XMLHttpRequest !== 'undefined' && fetcher === globalThis.fetch; if (!file || !file.size || typeof signEvent !== 'function') throw new MediaUploadError('failed'); const x = await fileHash(file); const now = Math.floor(Date.now() / 1000); @@ -68,7 +87,7 @@ export async function uploadPublicMedia(file, signEvent, { catch (error) { failures.push({ status: 0, reason: 'unreachable' }); continue; } } try { - const response = await fetcher(server.replace(/\/$/, '') + '/upload', { + const init = { method: 'PUT', body: file, signal: AbortSignal.timeout(timeoutMs), headers: { authorization, @@ -76,7 +95,10 @@ export async function uploadPublicMedia(file, signEvent, { 'x-sha-256': x, 'x-content-length': String(file.size), }, - }); + }; + if (onProgress) onProgress(0); + const target = server.replace(/\/$/, '') + '/upload'; + const response = tracked ? await putWithProgress(target, init, onProgress) : await fetcher(target, init); let body = null; try { body = await response.json(); } catch {} const url = response.ok && responseUrl(body); |
