diff options
| author | Adam Soltys <[email protected]> | 2026-10-14 23:01:38 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-14 23:01:38 -0700 |
| commit | 63d43428468bb5dc336006bc345769b071905204 (patch) | |
| tree | bc1b7dbaa1fef2e93ae73b0fe2511a23cc0a9807 /src | |
| parent | 7a3e813d199857ca503864737b7723c7ea916ba5 (diff) | |
| download | coinosv3-63d43428468bb5dc336006bc345769b071905204.tar.gz coinosv3-63d43428468bb5dc336006bc345769b071905204.zip | |
Profile pages: a picture of theirs opens in the pager, over their timeline's pictures. Tapped on someone's profile page, a picture opened alone in the lightbox (the pager ran only over the feed), so there was nothing to swipe to (Adam). Now the page a row is on says what the pager runs over (pageRowOf, pagerSource): the feed, or this person's timeline — their posts' pictures (and clips) in the timeline's order, nobody else's, more fetched as the reader nears the end (loadOlderNotes, the way the feed pages on); the tap rules are the feed's, and closing after moving on lands the profile on that post (landOnPost judges the page by what is on screen, since a refetch replaces the timeline's cache entry). The profile screen syncs the pager as the feed does. tools/profile-pager-test.js (her pictures only and in order, the end past her last, Escape lands on the post, a tap on the opening picture closes).
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J1Kon8bxJoHRpJdSzshCkp
Diffstat (limited to 'src')
| -rw-r--r-- | src/features/messages.js | 90 |
1 files changed, 60 insertions, 30 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index 55deaa1..6d3aefa 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -10614,6 +10614,7 @@ export function messagesFeature(ctx) { function profileScreen() { const pk = ui.profilePk; + queueMicrotask(syncMediaPager); // a picture of theirs opened full screen: the pager over their timeline const mine = isMe(pk) || (ctx.shownPubkey && pk === ctx.shownPubkey()); // History can outlive an account switch; only restore our own editor. if (!mine && ui.profEdit) { ui.profEdit = null; ui.profEditFilled = false; } @@ -11845,16 +11846,38 @@ export function messagesFeature(ctx) { const inFeed = ui.chatOpen && ui.msgView === 'feed' && !ui.profilePk && !ui.noteThread && feed && !feed.stopped && feed.winList; return inFeed && el && el.closest ? el.closest('.notes-feed > [data-key]')?.getAttribute('data-key') || null : null; } + // the page the row is on: the feed, or a person's timeline (their + // profile page) — the pager runs over that page's pictures (or clips) + const onProfilePage = () => !!ui.profilePk && (!ui.noteThread || ui.profOverThread); + function pageRowOf(el) { + const rowId = el && el.closest ? el.closest('.notes-feed > [data-key]')?.getAttribute('data-key') || null : null; + if (!rowId) return null; + if (onProfilePage()) { const c = notesCache.get(ui.profilePk); return c && c.winList ? { c, rowId, profile: ui.profilePk } : null; } + return feedRowOf(el) ? { c: feed, rowId, profile: null } : null; + } + // Where a pager opened from a page gets its pictures: the feed on screen, + // or a person's timeline — its posts' pictures in its order, more fetched + // as the reader nears the end. The media grid's pager has no source here. + function pagerSource(p = pager) { + if (!p) return null; + if (p.profile) { + const c = notesCache.get(p.profile); + return c ? { c, profile: p.profile, items: (kind) => feedMediaItems(c, kind), more: () => loadOlderNotes(p.profile), done: () => !!c.end && !c.loadingMore, + live: () => onProfilePage() && ui.profilePk === p.profile } : null; // (the entry is replaced by a refetch: the page, not the object, says it is still there) + } + if (p.fromFeed) { const c = feedNow(); return { c, items: (kind) => feedMediaItems(c, kind), more: () => loadOlderFeed(), done: () => !!c.end && c.shown >= c.notes.length && !c.loadingMore, live: () => c === feed && !onProfilePage() }; } + return null; + } // 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 rowId = feedRowOf(e && e.target); - if (rowId) { - const it = feedMediaItems(feed, 'images').find((x) => x.row.id === rowId && x.url === url); - if (it) { openMediaPager('images', mediaKey(it), { fromFeed: true }); return; } + const at = pageRowOf(e && e.target); + if (at) { + const it = feedMediaItems(at.c, 'images').find((x) => x.row.id === at.rowId && x.url === url); + if (it) { openMediaPager('images', mediaKey(it), { fromFeed: !at.profile, profile: at.profile }); return; } } } catch {} ctx.openImage && ctx.openImage(url); @@ -11866,14 +11889,14 @@ export function messagesFeature(ctx) { // himself — so it follows the phone throughout. function openClip(v, url) { try { - const rowId = feedRowOf(v); - if (!rowId) return false; - const it = feedMediaItems(feed, 'videos').find((x) => x.row.id === rowId && x.url === url); + const on = pageRowOf(v); + if (!on) return false; + const it = feedMediaItems(on.c, 'videos').find((x) => x.row.id === on.rowId && x.url === url); if (!it) return false; tappedClips.add(url); // asked for by hand: no tap-to-play cover const at = v.currentTime || 0; v.pause(); - openMediaPager('videos', mediaKey(it), { fromFeed: true, at }); + openMediaPager('videos', mediaKey(it), { fromFeed: !on.profile, profile: on.profile, at }); pagerFullscreen(); return true; } catch { return false; } @@ -12031,13 +12054,13 @@ export function messagesFeature(ctx) { // `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 - function openMediaPager(kind, start, { fromFeed = false, at = 0 } = {}) { + function openMediaPager(kind, start, { fromFeed = false, profile = null, at = 0 } = {}) { // the page's own players stop: the pager's is the one to hear for (const v of document.querySelectorAll('video')) { try { v.pause(); } catch {} } for (const f of document.querySelectorAll('.yt-embed[data-playing] iframe')) { try { f.contentWindow.postMessage(JSON.stringify({ event: 'command', func: 'pauseVideo', args: [] }), '*'); f.closest('.yt-embed')._pausedByUs = true; } catch {} } - ui.mediaPager = { kind, start, feed: curFeedId, fromFeed, at }; + ui.mediaPager = { kind, start, feed: curFeedId, fromFeed, profile, at }; render(); } const closeMediaPager = () => ctx.closeLayer('mediaPager'); @@ -12047,17 +12070,17 @@ export function messagesFeature(ctx) { // 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); + if (pager && pagerSource() && !pagerOnStart() && pager.cur) closePagerTo(pager.cur._rowId); else closeMediaPager(); } function closePagerTo(rowId) { - const c = feedNow(); + const src = pagerSource(); 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; } - landOnPost(c, rowId); + landOnPost(src, rowId); }; requestAnimationFrame(go); } @@ -12068,12 +12091,14 @@ export function messagesFeature(ctx) { // draw, and set the row under the top again each frame — until it has sat // still a moment, a couple of seconds have gone, or the reader touches the // page. A jump, not a glide: the pager was covering the feed. - function landOnPost(c, rowId) { + function landOnPost(src, rowId) { const MARGIN = 8, t0 = Date.now(); const find = () => document.querySelector('.notes-feed > [data-key="' + CSS.escape(rowId) + '"]'); let still = 0, misses = 0, lastY = null; const step = () => { - if (c !== feed || ui.mediaPager || !document.querySelector('.notes-feed') || lastInputAt > t0 || Date.now() - t0 > 2500) return; + if (!src.live() || ui.mediaPager || !document.querySelector('.notes-feed') || lastInputAt > t0 || Date.now() - t0 > 2500) return; + // the list as it is now (a profile's entry is replaced by a refetch) + const c = src.profile ? notesCache.get(src.profile) || src.c : src.c; // the page sent somewhere else meanwhile (not by a finger: a glide, a link) if (lastY !== null && Math.abs(window.scrollY - lastY) > window.innerHeight / 2) return; const el = find(); @@ -12106,9 +12131,11 @@ export function messagesFeature(ctx) { let pager = null; // the live pager: { el, scroller, kind, keys, cur, loud, io, near, tick } let pagerLoud = true; // sound on, unless the browser said no without a tap function syncMediaPager() { - const want = ui.mediaPager && ui.chatOpen && ui.msgView === 'feed' && !ui.profilePk && !ui.noteThread ? ui.mediaPager : null; + const m = ui.mediaPager; + const want = m && (m.profile ? onProfilePage() && ui.profilePk === m.profile && notesCache.has(m.profile) + : ui.chatOpen && ui.msgView === 'feed' && !ui.profilePk && !ui.noteThread) ? m : null; if (!want) { if (pager) dropPager(); return; } - if (pager && (pager.kind !== want.kind || pager.fromFeed !== !!want.fromFeed)) dropPager(); + if (pager && (pager.kind !== want.kind || pager.fromFeed !== !!want.fromFeed || pager.profile !== (want.profile || null))) dropPager(); if (!pager) buildPager(want); else growPager(); } @@ -12686,8 +12713,8 @@ export function messagesFeature(ctx) { else if (pager.cur) playSlide(pager.cur); return; } - if (pager && pager.fromFeed) { - // Opened from a post in the feed: a tap on the picture that opened + if (pager && pagerSource()) { + // Opened from a post in the feed (or on a profile): 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. @@ -12699,7 +12726,7 @@ export function messagesFeature(ctx) { 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, fromFeed: !!want.fromFeed, start: want.start, at: want.at || 0 }; + pager = { el, scroller, kind: want.kind, keys: new Set(), cur: null, feed: want.feed, fromFeed: !!want.fromFeed, profile: want.profile || null, start: want.start, at: want.at || 0 }; // The end of the pager: swiping past the last slide lands here, on // something saying more is coming (not a nudge that goes nowhere) — and // asks for it at once. Slides that arrive go in above it; one arriving @@ -12721,7 +12748,7 @@ export function messagesFeature(ctx) { const v = pager.cur && pager.cur.querySelector('video.mp-media'); if (v) v.pause(); el.classList.remove('show'); - if (pager.fromFeed) pagerFeedAhead(); else crawlMedia().catch(() => {}); + if (pagerSource()) pagerFeedAhead(); else crawlMedia().catch(() => {}); pagerTailState(); } }, { root: scroller, threshold: [0, 0.6] }); @@ -12743,17 +12770,18 @@ export function messagesFeature(ctx) { const v = pager.cur.querySelector('video.mp-media'); if (v) v.pause(); // how long a clip held the reader (the head start's measure of them) if (v && pager.curAt) { const secs = (Date.now() - pager.curAt) / 1000; dwell = dwell ? dwell * 0.7 + secs * 0.3 : secs; } - if (pager.fromFeed && video) el.classList.remove('show'); + if (pagerSource() && video) el.classList.remove('show'); } pager.cur = slide; pager.anchor = slide; pager.curAt = Date.now(); pagerArrows(slide); if (pager.fromFeed) { const row = feedNow().notes.find((x) => x.id === slide._rowId); markSeen(row || slide._ev); } + else if (pager.profile) markSeen(slide._ev); playSlide(slide); pagerCounts(slide); // near the end: ask the feed for more, the pager picks them up - if (pager.fromFeed) pagerFeedAhead(); + if (pagerSource()) pagerFeedAhead(); else { const gs = pagerGroups(); if (gs.indexOf(slide.closest('.mp-group')) >= gs.length - 3) crawlMedia().catch(() => {}); } }; pager.io = new IntersectionObserver((entries) => { @@ -12808,15 +12836,16 @@ export function messagesFeature(ctx) { const PAGER_AHEAD = { images: 4, videos: 6 }; let pagerFeeding = false; async function pagerFeedAhead() { - if (pagerFeeding || !pager || !pager.fromFeed) return; + const src = pagerSource(); + if (pagerFeeding || !src) return; pagerFeeding = true; try { - const c = feedNow(); - for (let pages = 0; pages < 8 && pager && pager.fromFeed; pages++) { + const c = src.c; + for (let pages = 0; pages < 8 && pager && pagerSource(); pages++) { const gs = pagerGroups(); if (gs.length - 1 - gs.indexOf(pager.cur && pager.cur.closest('.mp-group')) > (PAGER_AHEAD[pager.kind] || 4)) break; const shown = c.shown, held = c.notes.length; - await loadOlderFeed(); + await src.more(); growPager(); if (c.shown === shown && c.notes.length === held) break; } @@ -12831,7 +12860,8 @@ export function messagesFeature(ctx) { const waiting = pager.onTail; let first = null; pager.groups ||= new Map(); - for (const it of (pager.fromFeed ? feedMediaItems(c, pager.kind) : mediaItems(c, pager.kind))) { + const src = pagerSource(); + for (const it of (src ? src.items(pager.kind) : mediaItems(c, pager.kind))) { const k = mediaKey(it); if (pager.keys.has(k)) continue; pager.keys.add(k); @@ -12873,8 +12903,8 @@ export function messagesFeature(ctx) { // more to give — that this is everything function pagerTailState() { if (!pager || !pager.tail) return; - const c = feedNow(); - const done = pager.fromFeed ? !!c.end && c.shown >= c.notes.length && !c.loadingMore : !!mediaIndex(c.id).end; + const src = pagerSource(); + const done = src ? src.done() : !!mediaIndex(feedNow().id).end; if (pager.tail.classList.contains('done') === done) return; pager.tail.classList.toggle('done', done); pager.tail.querySelector('.mp-tail-text').textContent = done ? t('pagerCaughtUp') : t(pager.kind === 'videos' ? 'pagerFetchingVideo' : 'pagerFetchingPictures'); |
