aboutsummaryrefslogtreecommitdiffstats
path: root/src/media-upload.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-09-30 10:23:20 -0700
committerAdam Soltys <[email protected]>2026-09-30 10:23:20 -0700
commit0076c591df9432e2bb5b2281afc1fb4106500297 (patch)
tree7b9ef49079158af7e6e2545f1e847c4c2baeb428 /src/media-upload.js
parent505cd84813a80e0a69b2cc95aa93c68cf2856cb2 (diff)
downloadcoinosv3-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.js26
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);