diff options
Diffstat (limited to 'src/features')
| -rw-r--r-- | src/features/messages.js | 66 |
1 files changed, 53 insertions, 13 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index f634866..e1468ce 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -15,7 +15,7 @@ import { mediaServers } from '../media-servers.js'; // listens for both identities and decrypts with whichever keys are present. import { - subscribeOn, subscribeAuthed, publishOn, queryOn, queryManyOn, queryStreamOn, fetchInboxRelays, relayAlive, liveRelayList, readRelayList, resetRelay, + subscribeOn, subscribeAuthed, publishOn, queryOn, queryManyOn, queryStreamOn, fetchInboxRelays, relayAlive, liveRelayList, readRelayList, resetRelay, wakeRelays, npubOf, neventOf, parseNostrPubkey, parseNostrRef, generateSecretKey, getPublicKey, finalizeEvent, nip44, PROFILE_RELAYS, openWrapsOffthread, unwrapDMsOffthread, verifyEventsAsync, setSyncConfig, DEFAULT_SYNC_RELAYS, } from '../nostr.js'; @@ -6947,7 +6947,7 @@ export function messagesFeature(ctx) { if (!url || mediaReady.has(url)) return Promise.resolve(); const known = videoDims.get(url) || dim; if (known) { rememberVideo(url, known.width, known.height); return Promise.resolve(); } - if (typeof document === 'undefined' || videoUnknown.has(url)) return Promise.resolve(); + if (typeof document === 'undefined' || videoUnknown.has(url) || clipGone(url)) return Promise.resolve(); // (gone: nothing to measure) if (mediaWarming.has(url)) return mediaWarming.get(url); // the 64 KB probe first: it reads the shape from the file's own header // and costs nothing like a player's metadata load of an index-last file @@ -7992,7 +7992,7 @@ export function messagesFeature(ctx) { // it (with sound — the reader asked for this one). The box is left alone // by the morph from here, or a repaint would take the cover off. function heavyCover(box, url, onPlay) { - if (box.querySelector('.vid-heavy')) return; + if (box.querySelector('.vid-heavy') || box.classList.contains('gone')) return; box._skipMorph = true; const cover = h('button', { class: 'vid-heavy', type: 'button', 'aria-label': t('videoTapToPlay'), onClick: (e) => { @@ -8096,13 +8096,34 @@ export function messagesFeature(ctx) { const playSrc = (url) => !vmediaOk(url) || isHls(url) ? url : streamsHere(url) ? hlsOf(url) : !h264Failed.has(url) && (noPicture.has(url) || ((videoInfo.get(url) || {}).hevc && !canHevc())) ? h264Of(url) : url; + // A clip whose file its host no longer has: the player is the first to + // know (Primal's blossom answers a 404 with no CORS headers, so the + // probe couldn't read it and the box sat as a dead "Tap to play" — a + // post whose video "won't play", Adam, 2026-11-01). It says so where the + // player stood, with the file's link, and the grid leaves the clip out + // for a day, as it does a 404 the probe could read. The copy is tried + // first: a file the browser can't decode fails the same way, and the + // copy plays that one. + function clipDead(v, url) { + noteVideoInfo(url, { goneAt: Date.now() }); + v._gone = true; + v.controls = false; + const box = v.parentElement; + if (!box) return; + box._skipMorph = true; + box.classList.add('gone'); + for (const el of box.querySelectorAll('.vid-heavy, .vid-pp, .vid-sound')) el.remove(); + if (!box.querySelector('.vid-nopic')) box.append(h('div', { class: 'vid-nopic' }, h('span', {}, t('videoGone')), + h('a', { href: url, target: '_blank', rel: 'noopener noreferrer', onClick: (e) => e.stopPropagation() }, t('videoOpenFile')))); + } // A player whose clip turns out to have no picture it can show (its // metadata says zero width) switches to the copy; if the copy fails too, // the original's sound plays under a line saying why, with the file. + // One whose original won't load at all tries the copy too; the copy + // failing as well means the file is gone (clipDead). function guardPicture(v, url) { - if (!vmediaOk(url)) return; const onCopy = () => (v.getAttribute('src') || '').startsWith(VMEDIA); - v.addEventListener('loadedmetadata', () => { + if (vmediaOk(url)) v.addEventListener('loadedmetadata', () => { if (v.videoWidth || onCopy() || h264Failed.has(url)) return; noPicture.add(url); const want = !v.paused; @@ -8110,7 +8131,18 @@ export function messagesFeature(ctx) { if (want) v.play().catch(() => {}); }); v.addEventListener('error', () => { - if (!onCopy()) return; + if (!v.getAttribute('src')) return; // let go of (scrolled off), not failed + if (!onCopy()) { + if (vmediaOk(url) && !h264Failed.has(url) && !v._origFailed) { + v._origFailed = true; + const want = !v.paused; + v.setAttribute('src', h264Of(url)); + if (want) v.play().catch(() => {}); + return; + } + clipDead(v, url); + return; + } // the stream failed: the copy or the original, as it would have been if (isHls(v.getAttribute('src'))) { hlsFailed.add(url); @@ -8120,6 +8152,7 @@ export function messagesFeature(ctx) { return; } h264Failed.add(url); + if (v._origFailed) { clipDead(v, url); return; } // neither the file nor a copy of it: gone const want = !v.paused; v.setAttribute('src', url); if (want) v.play().catch(() => {}); @@ -8150,7 +8183,7 @@ export function messagesFeature(ctx) { // — a second seek bar — stay off; the middle button and the sound button // are its controls then (Adam: "hide the main seek bar … and only show a // moments seek bar"). Otherwise the player's controls, once it has woken. - const nativeControls = (v) => { v.controls = !v._ownBar; }; + const nativeControls = (v) => { v.controls = !v._ownBar && !v._gone; }; function ownBar(player, on) { const box = player.closest('.note-video-box'); if (!box || player._ownBar === on) return; @@ -8244,8 +8277,7 @@ export function messagesFeature(ctx) { // learned now, used by the next paint of any row with this clip onLoadedmetadata: (e) => { rememberVideo(url, e.target.videoWidth, e.target.videoHeight); attachMediaTimeline(e.target, url); }, // a touch's tap was taken at pointerup (below): its click, if one comes, is spent - onClick: (e) => { if (Date.now() - (e.currentTarget._tapAt || 0) < 800) { e.stopPropagation(); e.preventDefault(); return; } videoTap(e, url); }, - onError: (e) => { if (!stable && !(e.target.getAttribute('src') || '').startsWith(VMEDIA)) { const b = e.target.parentElement; if (b) b.style.display = 'none'; } } }); + onClick: (e) => { if (Date.now() - (e.currentTarget._tapAt || 0) < 800) { e.stopPropagation(); e.preventDefault(); return; } videoTap(e, url); } }); guardPicture(v, url); // With its own controls up, Chrome on Android keeps a tap on the clip for // them (it shows or hides the bar) and the page never gets the click — so @@ -8328,6 +8360,7 @@ export function messagesFeature(ctx) { () => { box._inView = true; clearTimeout(box._release); + if (v._gone) return; // nothing to load, and nothing to ask if (!v.getAttribute('src') && !v.getAttribute('data-lazy-src')) { v.setAttribute('src', playSrc(url)); v.addEventListener('loadedmetadata', () => { v.style.minHeight = ''; }, { once: true }); @@ -8356,6 +8389,9 @@ export function messagesFeature(ctx) { try { v.removeAttribute('src'); v.load(); } catch {} }, 2500); }); + // known gone already (this player, or one before it, found the file + // missing): said at once, no request made + if (clipGone(url)) clipDead(v, url); return box; } @@ -18475,10 +18511,14 @@ export function messagesFeature(ctx) { resumed(awayMs) { // a blink between apps didn't kill anything if (awayMs && awayMs < 5_000) return; - resubscribeStreams(); // DMs and rooms, whether or not chat is on screen - if (!ui.chatOpen || !['feed', 'notifs'].includes(ui.msgView)) return true; - if (ui.msgView === 'notifs') refreshNotifs(true).catch(() => {}); - else refreshFeed({ force: true }).catch(() => {}); + // the sockets first: after a freeze they read open and are dead, and + // a refresh sent on them finds nothing (wakeRelays) + wakeRelays().catch(() => {}).then(() => { + resubscribeStreams(); // DMs and rooms, whether or not chat is on screen + if (!ui.chatOpen || !['feed', 'notifs'].includes(ui.msgView)) return; + if (ui.msgView === 'notifs') refreshNotifs(true).catch(() => {}); + else refreshFeed({ force: true }).catch(() => {}); + }); return true; }, // A zap flow reporting how its payment went, so the chip that appeared |
