aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/app.js12
-rw-r--r--tools/feed-wheel-reverse-test.js19
2 files changed, 22 insertions, 9 deletions
diff --git a/src/app.js b/src/app.js
index 6f4d10b..f904cc3 100644
--- a/src/app.js
+++ b/src/app.js
@@ -6617,12 +6617,20 @@ if (INTENT_URI) {
// selection, and a drag that moved isn't a click on what it started on.
(() => {
if (typeof window === 'undefined' || matchMedia('(prefers-reduced-motion: reduce)').matches) return;
- const SCOPE = '.feed-page, .notes-feed, .thread-page'; // a thread scrolls the same as a feed (Adam, 2026-10-16)
+ // The wheel coasts whenever a feed or a thread is the page being scrolled
+ // — wherever the pointer happens to be: over the posts, the header, the
+ // rail, the wallet column beside the feed on a wide screen (Adam,
+ // 2026-10-16: "when my cursor is hovered over the right side bar or any
+ // part of the page that's not the feed, wheeling scrolls without
+ // coasting. can we make it coast no matter where my pointer is?") —
+ // except over something that takes the wheel itself: a text box, the
+ // chips strip, the lightbox, the pager, a box that scrolls on its own.
+ const SCOPE = '.feed-page, .notes-feed, .thread-page'; // the pages that coast (a thread scrolls the same as a feed)
const TAU = 325; // ms: how quickly a coast dies down (the phone's feel)
const VMAX = 16; // px/ms
const inScope = (t) => {
const el = t && t.closest ? t : t && t.parentElement;
- if (!el || !el.closest(SCOPE)) return false;
+ if (!el || !document.querySelector(SCOPE)) return false;
// (A video or an audio player is part of the feed: a notch over a clip
// coasts like one over the words — the wheel does nothing to a player
// itself, so there is nothing to leave it. Adam, 2026-10-16: "over a
diff --git a/tools/feed-wheel-reverse-test.js b/tools/feed-wheel-reverse-test.js
index 352bc0e..80e2fde 100644
--- a/tools/feed-wheel-reverse-test.js
+++ b/tools/feed-wheel-reverse-test.js
@@ -5,16 +5,18 @@ const source = await Bun.file('src/app.js').text();
const start = source.indexOf('(() => {', source.indexOf('// The feed scrolls like a phone'));
const end = source.indexOf("\nwindow.addEventListener('load'", start);
assert(start > 0 && end > start);
-function fixture(inside = '') {
+function fixture(inside = '', { feedOnPage = true } = {}) {
let now = 100, id = 0;
const handlers = new Map(), frames = new Map(), steps = [];
const window = { scrollY: 5000,
addEventListener: (name, fn) => handlers.set(name, fn),
scrollBy: ({ top }) => { steps.push(top); window.scrollY += top; },
};
- const document = { body: {}, addEventListener() {}, removeEventListener() {} };
- // the target sits in a feed page (or, for a thread case, a thread page), inside the given element
- const target = { closest: (selector) => selector.split(/,\s*/).some((p) => p === inside || (p === '.feed-page' && inside !== '.thread-page')) ? target : null,
+ // the page shows a feed (or, for a thread case, a thread) unless told otherwise
+ const document = { body: {}, addEventListener() {}, removeEventListener() {},
+ querySelector: (selector) => (feedOnPage && selector.split(/,\s*/).some((p) => p === (inside === '.thread-page' ? '.thread-page' : '.feed-page')) ? {} : null) };
+ // the target sits inside the given element (over the posts unless it names something else)
+ const target = { closest: (selector) => selector.split(/,\s*/).some((p) => p === inside) ? target : null,
scrollHeight: 100, clientHeight: 100, parentElement: document.body };
runInNewContext(source.slice(start, end), { window, document,
matchMedia: () => ({ matches: false }), performance: { now: () => now },
@@ -76,16 +78,19 @@ for (const dir of [1, -1]) {
}
// A notch over a video (or an audio player) in the feed coasts like any
// other — the wheel does nothing to a player itself — and so does one on a
-// thread page.
-for (const sel of ['video', 'audio', '.thread-page']) {
+// thread page, and one with the pointer anywhere else on the page while a
+// feed is up (the rail, the wallet column, the header).
+for (const sel of ['video', 'audio', '.thread-page', '.wallet-column', '']) {
const f = fixture(sel);
const y = f.y; f.wheel(1); settle(f);
assert(f.y - y > 280 && f.y - y < 330, 'a notch over ' + sel + ' coasts a full push: ' + (f.y - y));
}
-// ...while a notch in a text box, or in the media pager, is left alone.
+// ...while a notch in a text box, or in the media pager, is left alone — and
+// so is every notch on a page with no feed on it (the wallet).
for (const sel of ['textarea', '.media-pager']) {
const f = fixture(sel);
const y = f.y; f.wheel(1); settle(f);
assert.equal(f.y, y, 'a notch in ' + sel + ' is not ours');
}
+{ const f = fixture('', { feedOnPage: false }); const y = f.y; f.wheel(1); settle(f); assert.equal(f.y, y, 'no feed on the page: the wheel is the browser\'s'); }
console.log('✓ a turn back against a flight brakes, the next notch is the gentle start, then the wind-up; against a stroll or a dead coast the gentle start at once; stale frames; videos and threads coast');