aboutsummaryrefslogtreecommitdiffstats
path: root/src/features/messages.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/features/messages.js')
-rw-r--r--src/features/messages.js66
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