diff options
Diffstat (limited to 'src/features/messages.js')
| -rw-r--r-- | src/features/messages.js | 87 |
1 files changed, 78 insertions, 9 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index 7c81d27..b1da365 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -7965,7 +7965,7 @@ export function messagesFeature(ctx) { if (mediaFailed.has(url)) return h('a', { href: url, target: '_blank', rel: 'noopener noreferrer' }, label || url); return h('img', { src: url, class: 'note-img note-img-late clickable', loading: 'lazy', decoding: 'async', alt: label || '', - onClick: (e) => { if (!onPicture(e)) return; e.stopPropagation(); ctx.openImage && ctx.openImage(url); }, + onClick: (e) => { if (!onPicture(e)) return; e.stopPropagation(); openPicture(url, e); }, onError: (e) => { e.target.style.display = 'none'; }, }); } @@ -7978,7 +7978,7 @@ export function messagesFeature(ctx) { decoding: known && known.thumb ? 'sync' : 'async', width: size?.width, height: size?.height, style: size ? 'height:auto;aspect-ratio:' + size.width + '/' + size.height + ';--ar:' + (size.width / size.height).toFixed(4) : undefined, - onClick: (e) => { if (!onPicture(e)) return; e.stopPropagation(); ctx.openImage && ctx.openImage(url); }, + onClick: (e) => { if (!onPicture(e)) return; e.stopPropagation(); openPicture(url, e); }, // a picture we were TOLD was a picture and which won't load leaves // nothing behind — the alt text is already in the sentence above it onError: (e) => { if (!size) e.target.style.display = 'none'; }, @@ -11597,6 +11597,37 @@ export function messagesFeature(ctx) { } return out; } + // The feed's pictures in reading order — every one of a post before the + // next post's — for the pager opened from a picture in the feed: the + // feed's own list (its order, its moderation), not the media index. + function feedImageItems(c) { + const out = []; + const list = c.winList ? c.winList() : c.notes; + for (const row of list) { + const ev = row.kind === REPOST ? repostInner(row) || row : row; + if (modOn('sensitive') && sensitiveWhy(ev) !== null && !isMe(ev.pubkey)) continue; + for (const url of noteMediaUrls(ev.content)) { + if (url.startsWith('https://i.ytimg.com/')) continue; + out.push({ ev, url, row }); + } + } + return out; + } + // A picture tapped in the feed opens in the media pager, on that picture, + // with the feed's other pictures a swipe (or an arrow) away, the way the + // media grid's are. On any other page — a thread, a profile, a chat — the + // picture opens alone, as before. + function openPicture(url, e) { + try { + const inFeed = ui.chatOpen && ui.msgView === 'feed' && !ui.profilePk && !ui.noteThread && feed && !feed.stopped && feed.winList; + const rowId = e && e.target && e.target.closest ? e.target.closest('.notes-feed > [data-key]')?.getAttribute('data-key') : null; + if (inFeed && rowId) { + const it = feedImageItems(feed).find((x) => x.row.id === rowId && x.url === url); + if (it) { openMediaPager('images', mediaKey(it), { fromFeed: true }); return; } + } + } catch {} + ctx.openImage && ctx.openImage(url); + } // The words of the post, without the links to the media it carries. function mediaCaption(ev) { let s = String(ev.content || ''); @@ -11722,11 +11753,35 @@ export function messagesFeature(ctx) { } } - function openMediaPager(kind, start) { - ui.mediaPager = { kind, start, feed: curFeedId }; + // `fromFeed`: opened from a picture in the feed (its pictures, in the + // feed's order), rather than from the media grid + function openMediaPager(kind, start, { fromFeed = false } = {}) { + ui.mediaPager = { kind, start, feed: curFeedId, fromFeed }; render(); } const closeMediaPager = () => ctx.goBack(() => { ui.mediaPager = null; }); + // still on the picture that opened the pager (nothing swiped to yet) + const pagerOnStart = () => !pager || !pager.cur || pager.cur.getAttribute('data-k') === pager.start; + // Dismissing a pager opened from the feed after moving through it lands + // the feed on the post of the picture last looked at; otherwise (the + // media grid's pager, or nothing moved) it simply closes. + function dismissPager() { + if (pager && pager.fromFeed && !pagerOnStart() && pager.cur) closePagerTo(pager.cur._rowId); + else closeMediaPager(); + } + function closePagerTo(rowId) { + const c = feedNow(); + closeMediaPager(); + // the pop that closes it lands on a later turn: the glide waits for the feed + let tries = 0; + const go = () => { + if (ui.mediaPager || !document.querySelector('.notes-feed')) { if (++tries < 90) requestAnimationFrame(go); return; } + const list = c.winList ? c.winList() : c.notes; + const i = list.findIndex((e) => e.id === rowId); + if (i >= 0) glideToPost(c, list, i); + }; + requestAnimationFrame(go); + } // Leaving the pager for another page (a profile, sign-in, the zap amount): // only the feed's own render syncs the pager, and those pages don't paint // the feed — so the overlay goes now, not on the next feed render. @@ -11800,6 +11855,9 @@ export function messagesFeature(ctx) { setTimeout(() => pagerCounts(slide), 50); }) }, h('span', { class: 'i', html: I_ZAP }), h('span', { class: 'n' }))))); slide._ev = ev; + slide._rowId = it.row ? it.row.id : ev.id; // the feed row to land on (a repost's wrapper, not its inner post) + // pinch to zoom a picture (drag pans it zoomed; the wheel is the pager's) + if (!video && ctx.attachZoom) ctx.attachZoom(slide, media, { wheel: false }); if (video) { guardPicture(media, url); media.muted = !pagerLoud; @@ -11911,16 +11969,26 @@ export function messagesFeature(ctx) { const el = h('div', { class: 'media-pager' + (video ? ' videos' : '') + (pagerLoud ? '' : ' quiet'), role: 'dialog', 'aria-modal': 'true' }, scroller, h('div', { class: 'mp-top' }, - h('button', { class: 'mp-x', type: 'button', 'aria-label': t('close'), onClick: (e) => { e.stopPropagation(); closeMediaPager(); } }, '×'), + h('button', { class: 'mp-x', type: 'button', 'aria-label': t('close'), onClick: (e) => { e.stopPropagation(); dismissPager(); } }, '×'), video ? h('button', { class: 'mp-sound', type: 'button', onClick: (e) => { e.stopPropagation(); pagerSound(!pagerLoud); } }, h('span', { class: 'on' }, '\u{1F50A}'), h('span', { class: 'off' }, '\u{1F507} ' + t('videoUnmute'))) : null)); // a tap shows (or hides) who posted it and what they said el.addEventListener('click', (e) => { if (e.target.closest('button, a, .mp-caption')) return; + if (pager && pager.fromFeed) { + // Opened from a post in the feed: a tap on the picture that opened + // it closes it, as a full-screen picture always has. Once the reader + // has moved on, the first tap shows the post's details, and the next + // closes the pager onto that post in the feed. + if (pagerOnStart()) { closeMediaPager(); return; } + if (!el.classList.contains('show')) { el.classList.add('show'); pagerCounts(pager.cur); return; } + dismissPager(); + return; + } el.classList.toggle('show'); if (el.classList.contains('show')) pagerCounts(pager && pager.cur); }); - pager = { el, scroller, kind: want.kind, keys: new Set(), cur: null, feed: want.feed }; + pager = { el, scroller, kind: want.kind, keys: new Set(), cur: null, feed: want.feed, fromFeed: !!want.fromFeed, start: want.start }; pager.near = new IntersectionObserver((entries) => { for (const e of entries) { if (!e.isIntersecting) continue; @@ -11953,7 +12021,7 @@ export function messagesFeature(ctx) { function growPager() { if (!pager) return; const c = feedNow(); - for (const it of mediaItems(c, pager.kind)) { + for (const it of (pager.fromFeed ? feedImageItems(c) : mediaItems(c, pager.kind))) { const k = mediaKey(it); if (pager.keys.has(k)) continue; pager.keys.add(k); @@ -11966,8 +12034,9 @@ export function messagesFeature(ctx) { if (typeof window !== 'undefined') { window.addEventListener('keydown', (e) => { if (!pager || e.defaultPrevented) return; - if (e.key === 'Escape') { e.preventDefault(); closeMediaPager(); return; } - const step = e.key === 'ArrowDown' || e.key === 'j' || e.key === 'PageDown' ? 1 : e.key === 'ArrowUp' || e.key === 'k' || e.key === 'PageUp' ? -1 : 0; + if (e.key === 'Escape') { e.preventDefault(); dismissPager(); return; } + // the arrows, any of the four: next (down, right) or previous (up, left) + const step = ['ArrowDown', 'ArrowRight', 'j', 'PageDown'].includes(e.key) ? 1 : ['ArrowUp', 'ArrowLeft', 'k', 'PageUp'].includes(e.key) ? -1 : 0; if (step) { e.preventDefault(); pager.scroller.scrollBy({ top: step * pager.scroller.clientHeight, behavior: 'smooth' }); return; } if (e.key === ' ' && pager.cur) { const v = pager.cur.querySelector('video.mp-media'); |
