aboutsummaryrefslogtreecommitdiffstats
path: root/src/features
diff options
context:
space:
mode:
Diffstat (limited to 'src/features')
-rw-r--r--src/features/messages.js54
1 files changed, 52 insertions, 2 deletions
diff --git a/src/features/messages.js b/src/features/messages.js
index b218235..030d397 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -14137,6 +14137,7 @@ export function messagesFeature(ctx) {
for (const u of missing) tileLooked.add(u);
if (missing.length) warmTileThumbs(missing).then(() => queueTileThumbs(missing.filter((u) => !tileThumbs.has(u))));
}
+ if (mediaMode === 'videos') for (const it of items.slice(0, 24)) preconnect(playSrc(it.url));
return h('div', { class: 'media-grid', 'data-key': 'media-grid:' + mediaMode },
...items.map((it) => {
const k = mediaKey(it);
@@ -14151,6 +14152,7 @@ export function messagesFeature(ctx) {
return h('button', {
class: 'media-tile' + (mediaMode === 'videos' ? ' video' : ''), type: 'button', 'data-key': 'tile:' + k,
'aria-label': displayName(it.ev.pubkey), onClick: () => openMediaPager(mediaMode, k),
+ onPointerDown: mediaMode === 'videos' ? () => earlyPlayer(it.url) : undefined,
},
mediaMode === 'images' || poster
? h('img', { src: poster || tileThumbs.get(it.url) || mediaSrc(it.url), alt: '', draggable: 'false',
@@ -14226,6 +14228,51 @@ export function messagesFeature(ctx) {
}
}
+ // Opening a clip from the grid: on Adam's phone a player made at the tap
+ // took ~0.65 s to move (making it ~0.25, its first frame ~0.15, starting
+ // its sound ~0.3 — the clip's opening already in the cache saved nothing),
+ // and 1.8 s when the host's connection had to be made too. So the grid's
+ // clip hosts are connected to ahead (a preconnect: cheap), and the
+ // player starts loading when the finger comes down on the tile — the
+ // pager adopts that same element (pagerSlide), so the load overlaps the
+ // tap and the pager's opening. One the tap doesn't follow (a scroll) is
+ // let go.
+ const preconnected = new Set();
+ function preconnect(url) {
+ let origin; try { origin = new URL(url, location.href).origin; } catch { return; }
+ if (origin === location.origin || preconnected.has(origin) || preconnected.size > 24 || typeof document === 'undefined') return;
+ preconnected.add(origin);
+ const l = document.createElement('link');
+ l.rel = 'preconnect'; l.href = origin;
+ document.head.append(l);
+ }
+ let early = null; // { url, v, timer }
+ function earlyPlayer(url) {
+ if (frugalData() || (early && early.url === url)) return;
+ dropEarly();
+ const v = document.createElement('video');
+ v.playsInline = true; v.loop = true; v.preload = 'auto'; v.muted = true;
+ v.setAttribute('src', playSrc(url));
+ v.dataset.early = '1'; // (the pager's adoption of it, for tests)
+ // on the page (out of sight) while it waits: a detached player isn't loaded everywhere
+ v.style.cssText = 'position:fixed;left:-9999px;top:0;width:2px;height:2px;opacity:0;pointer-events:none';
+ document.body.append(v);
+ early = { url, v, timer: setTimeout(dropEarly, 2500) };
+ }
+ function dropEarly() {
+ if (!early) return;
+ clearTimeout(early.timer);
+ try { early.v.removeAttribute('src'); early.v.load(); early.v.remove(); } catch {}
+ early = null;
+ }
+ function takeEarly(url) {
+ if (!early || early.url !== url) return null;
+ const { v } = early;
+ clearTimeout(early.timer);
+ early = null;
+ return v;
+ }
+
// `fromFeed`: opened from a picture or clip in the feed (its pictures or
// clips, in the feed's order), rather than from the media grid; `at`: where
// the clip it opened on was, so it carries on from there
@@ -14376,7 +14423,10 @@ export function messagesFeature(ctx) {
// was tried: it pulled the whole file down.) On a connection to be
// careful with the poster stays: the clip may never load (a heavy one
// waits for a tap), and the still is what there is.
- const media = video
+ // (the clip a grid tile was pressed on: the player already loading since the finger came down)
+ const adopted = video && !frugalData() ? takeEarly(url) : null;
+ if (adopted) { adopted.removeAttribute('style'); adopted.className = 'mp-media'; adopted.setAttribute('data-src', url); adopted.setAttribute('playsinline', ''); adopted.setAttribute('loop', ''); }
+ const media = adopted || (video
? h('video', { class: 'mp-media', 'data-src': url, loop: true, playsinline: true, preload: 'none',
poster: frugalData() ? videoThumbs.get(url) || posterOf(ev, url) || (videoInfo.get(url) || {}).image || serverThumb(url) : undefined })
: h('img', { class: 'mp-media', 'data-src': quick, 'data-full': url, alt: '', draggable: 'false', decoding: 'async',
@@ -14388,7 +14438,7 @@ export function messagesFeature(ctx) {
let host = ''; try { host = new URL(url).hostname; } catch {}
m.closest('.mp-slide').classList.add('mp-gone');
m.insertAdjacentElement('afterend', h('div', { class: 'mp-missing' }, h('div', { class: 'mp-missing-ico' }, '🖼'), h('div', {}, t('mediaPicGone')), host ? h('div', { class: 'small muted' }, host) : null));
- } });
+ } }));
const caption = mediaCaption(ev, true);
const stop = (fn) => (e) => { e.stopPropagation(); fn(e); };
const slide = h('div', { class: 'mp-slide', 'data-k': mediaKey(it) },