diff options
Diffstat (limited to 'src/features/messages.js')
| -rw-r--r-- | src/features/messages.js | 51 |
1 files changed, 34 insertions, 17 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index 7e92374..a6f49bb 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -9933,12 +9933,15 @@ export function messagesFeature(ctx) { if (!ui.reactPick) return null; const ev = ui.reactPick; const close = () => { ui.reactPick = null; render(); }; + // over the media pager (a reaction from a slide): above it, and its + // like button hears the pick + const after = () => { if (pager && pager.cur) pagerCounts(pager.cur); }; return h('div', { - class: 'confirm-pop-backdrop', + class: 'confirm-pop-backdrop' + (pager ? ' over-pager' : ''), onClick: (e) => { if (e.target === e.currentTarget) close(); }, }, h('div', { class: 'card col msg-sheet' }, - reactRow(null, (e2) => { close(); reactTo(ev, e2).catch(() => {}); }), + reactRow(null, (e2) => { close(); reactTo(ev, e2).catch(() => {}).finally(after); setTimeout(after, 50); }), h('button', { class: 'btn-ghost btn-block', onClick: close }, t('back')))); } @@ -12251,8 +12254,10 @@ export function messagesFeature(ctx) { const zp = pendingOf(ev.id); const sats = s.sats + (zp ? zp.sats : 0); const like = slide.querySelector('.mp-like'), zap = slide.querySelector('.mp-zap'); - like.classList.toggle('on', !!myReactOn(ev.id)); - like.querySelector('.i').innerHTML = I_HEART(!!myReactOn(ev.id)); + const mine = myReactOn(ev.id); + like.classList.toggle('on', !!mine); + if (mine) like.querySelector('.i').replaceChildren(postReactNode(mine)); // the emoji chosen, a picture for a :code: + else like.querySelector('.i').innerHTML = I_HEART(false); like.querySelector('.n').textContent = s.likes ? String(s.likes) : ''; zap.classList.toggle('on', sats > 0); const replies = replyCount(ev.id); @@ -12276,10 +12281,12 @@ export function messagesFeature(ctx) { avatar(ev.pubkey, 'chat-avatar mini', false), h('span', {}, displayName(ev.pubkey))), caption ? h('div', { class: 'mp-caption', onClick: stop((e) => e.currentTarget.classList.toggle('open')) }, caption) : null), h('div', { class: 'mp-counts' }, + // as in the feed: a tap picks how you feel about it (the emoji + // sheet, over the pager), a tap again takes it back h('button', { class: 'mp-like', type: 'button', 'aria-label': t('postLike'), onClick: stop(() => { if (signinAsk()) { leavePager(); render(); return; } - (myReactOn(ev.id) ? unreact(ev) : reactTo(ev, '❤️')).catch(() => {}).finally(() => pagerCounts(slide)); - setTimeout(() => pagerCounts(slide), 50); + if (myReactOn(ev.id)) { unreact(ev).catch(() => {}).finally(() => pagerCounts(slide)); setTimeout(() => pagerCounts(slide), 50); return; } + ui.reactPick = ev; render(); }) }, h('span', { class: 'i', html: I_HEART(false) }), h('span', { class: 'n' })), // the post itself, as the feed would open it: the whole text, the replies h('button', { class: 'mp-post', type: 'button', 'aria-label': t('mediaOpenPost'), title: t('mediaOpenPost'), onClick: stop(() => { @@ -12657,12 +12664,12 @@ export function messagesFeature(ctx) { // The flight itself: a glide to the post, the pictures passing, landing // snapped on it — only the landing is arrived at. A flick of a finger // (pace: a little slower per screen than the wheel's) comes here too. - function pagerFlyTo(to, { vel = 0, pace = 110 } = {}) { + function pagerFlyTo(to, { vel = 0, pace = 110, most = 1200 } = {}) { const dest = pagerAt(pager.scroller, to, true); const screens = Math.abs(dest - pager.scroller.scrollTop) / Math.max(1, pager.scroller.clientHeight); pager.flying = true; pagerGlide(pager.scroller, 'y', dest, vel, { - ms: screens > 1.05 ? Math.min(1200, 100 + pace * screens) : 0, + ms: screens > 1.05 ? Math.min(most, 100 + pace * screens) : 0, onDone: () => { if (!pager) return; pager.flying = false; const s = pagerHere(); if (s && s !== pager.cur) pagerArrive(s); }, }); } @@ -12764,8 +12771,11 @@ export function messagesFeature(ctx) { // FLY_V up; a slower swipe is the one post it was. The ends are not // flown past: a fling that would, lands on the last (or first) post. // (3 px/ms and a 500 ms coast took a hard flick to fly at all — Adam, - // 2026-10-16: "too hard to get it to fly".) - const FLY_V = 1.6, FLY_TAU = 800, FLY_MAX = 6; // px/ms; ms; posts + // 2026-10-16: "too hard to get it to fly"; then 800 ms and six posts: + // "make it fly longer before it settles down… I can slow it down by + // touching again when I see something I like".) So a long coast, taken + // slowly enough to watch and to catch: a finger down holds it. + const FLY_V = 1.6, FLY_TAU = 1600, FLY_MAX = 14; // px/ms; ms; posts const TRACK_MS = 80; // the speed is the finger's over its last moment, not its last few samples let touch = null, pendingTap = 0, tapDoneAt = 0; el.addEventListener('touchstart', (e) => { @@ -12807,7 +12817,7 @@ export function messagesFeature(ctx) { const next = from + step * posts; if (y && posts > 1) { const i = Math.min(stops.length - 1, Math.max(0, next)); - if (i !== from) { pagerFlyTo(pagerGroups()[i], { vel, pace: 150 }); return; } + if (i !== from) { pagerFlyTo(pagerGroups()[i], { vel, pace: 260, most: 4000 }); return; } } // past either end: round to the other (a fling at the end too — one post of wrap, not its whole count) if (y && (next < 0 || next >= stops.length)) { pagerWrap(pagerGroups()[(from + step + stops.length) % stops.length]); return; } @@ -12832,17 +12842,23 @@ export function messagesFeature(ctx) { clearTimeout(pendingTap); pendingTap = setTimeout(() => { pendingTap = 0; - tapDoneAt = Date.now(); const btn = d.target.closest && d.target.closest('button, a'); - if (btn) btn.click(); else pagerTap(d.target); + if (btn) btn.click(); else pagerTap(d.target); // (the click here is before tapDoneAt: not swallowed) + tapDoneAt = Date.now(); }, 120); }, { passive: true }); el.addEventListener('touchcancel', () => { if (touch && touch.box) settle(touch, 0); touch = null; }, { passive: true }); + // The browser's click for a touch: it IS the tap, so the pending one + // stands down — judged on the way down (capture), before a button's own + // handler stops the bubble: the sound button's click never reached this + // listener, the pending tap clicked it again 120 ms later, and a clip + // could not be muted (Adam, 2026-10-16). A click after the pending tap + // has already been done is swallowed before it reaches anything. el.addEventListener('click', (e) => { - if (pendingTap) { clearTimeout(pendingTap); pendingTap = 0; } // the browser's click came: it's the tap - else if (Date.now() - tapDoneAt < 700) { e.stopPropagation(); e.preventDefault(); return; } // done already - pagerTap(e.target); - }); + if (pendingTap) { clearTimeout(pendingTap); pendingTap = 0; } + else if (Date.now() - tapDoneAt < 700) { e.stopPropagation(); e.preventDefault(); } + }, true); + el.addEventListener('click', (e) => pagerTap(e.target)); // a tap shows (or hides) who posted it and what they said function pagerTap(target) { if (target.closest('button, a, .mp-caption, .mp-seek')) return; @@ -13038,6 +13054,7 @@ export function messagesFeature(ctx) { if (typeof window !== 'undefined') { window.addEventListener('keydown', (e) => { if (!pager || e.defaultPrevented) return; + if (ui.reactPick) { if (e.key === 'Escape') { e.preventDefault(); ui.reactPick = null; ui.emojiPick = null; render(); } return; } // the emoji sheet's if (e.key === 'Escape') { e.preventDefault(); dismissPager(); return; } // up and down: the next post, or the one before; left and right: along this post's row const v = ['ArrowDown', 'j', 'PageDown'].includes(e.key) ? 1 : ['ArrowUp', 'k', 'PageUp'].includes(e.key) ? -1 : 0; |
