polish(share): picker avatars + target name + pre-warm video poster

Follow-ups from testing the share flow (which now works end-to-end):
- Send-to picker showed only initials — now shows the real profile photo when the
  chat has one (matching the sidebar/forward avatars), coloured initials otherwise.
- During send it only said "Uploading 60%" with no idea WHO to — now the header and
  progress name the target ("Sending to Manasa Rapolu · 60%").
- After sending, a video bubble sat blank (just a timestamp) for a second or two
  while the poster generated on first view. media.js now warms the poster thumbnail
  at UPLOAD (temp-then-rename), and the on-demand /thumbs handler also writes via a
  temp, so the two can't serve a half-written JPEG. The bubble shows its poster
  right away.

Web + server only — live on deploy. Does NOT address the share extension failing to
auto-open the app (an iOS limitation, handled next in the native build).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-24 16:21:02 +05:30
parent 4836b1e197
commit 0c3487fdb0
3 changed files with 38 additions and 7 deletions
+20
View File
@@ -151,10 +151,30 @@ function transcode(id, done) {
});
}
// Poster frame, generated to a temp then renamed so a reader never sees a half-written JPEG (the /thumbs
// handler and this can both target the same file). Warming it at upload means the chat bubble shows the
// poster immediately instead of a blank tile while ffmpeg runs on the first view.
function ensureThumb(id) {
const thumb = path.join(UPLOADS_DIR, id + '.thumb.jpg');
const src = path.join(UPLOADS_DIR, id);
if (!src.startsWith(UPLOADS_DIR)) return;
if (fs.existsSync(thumb)) return;
fs.stat(src, (e) => {
if (e) return;
const tmp = thumb + '.part';
execFile('ffmpeg', ['-y', '-ss', '0.5', '-i', src, '-frames:v', '1', '-vf', 'scale=480:-2', '-q:v', '4', tmp],
{ timeout: 15000 }, (err) => {
if (err) { try { fs.unlinkSync(tmp); } catch (_) {} return; }
try { fs.renameSync(tmp, thumb); } catch (_) { try { fs.unlinkSync(tmp); } catch (__) {} }
});
});
}
// Queue a freshly uploaded (or first-played) video. Cheap and idempotent: safe to call on every
// /stream hit, which is also how pre-existing uploads get backfilled.
function ensureWebRendition(id, mime) {
if (!/^video\//.test(mime || '')) return;
ensureThumb(id); // warm the poster so the bubble isn't blank
if (pending.has(id) || failed.has(id) || hasWebRendition(id)) return;
pending.add(id);
queue.push(id);