Files
Haven/public/js/modules/app-thread-scroll.js
T
ancsemi 4a37d297ca Keep the thread panel still for readers scrolled up
Every new reply scrolled the thread to the bottom. It now follows only a
reader within 40px of the end (or their own reply); otherwise the view
stays put and a button at the foot counts the new replies and jumps to
the latest.
2026-10-16 11:39:40 -04:00

77 lines
2.8 KiB
JavaScript

// The thread panel follows new replies only while you are reading the newest.
//
// Every reply used to scroll the panel to the bottom, so someone reading a
// busy thread from the start was pulled away each time the others posted.
// Now a reply keeps the view where it is unless the reader was already at
// the bottom (the same 40px rule the chat feed uses, app-feed-anchor.js).
// Your own reply always takes you down. Replies that arrive while you are
// scrolled up are counted on a button at the foot of the panel, which also
// works as "jump to latest" once you are far from the end. The pop-out
// thread is the same panel, so it behaves the same.
// How far from the end the jump button appears with nothing new to count.
const JUMP_SHOW_PX = 400;
export default {
_setupThreadScroll() {
const box = document.getElementById('thread-messages');
const btn = document.getElementById('thread-jump-latest');
if (!box || !btn) return;
this._threadUnseen = 0;
box.addEventListener('scroll', () => {
if (this._threadGap(box) <= 40) this._threadUnseen = 0;
this._updateThreadJump();
}, { passive: true });
btn.addEventListener('click', () => this._threadJumpToLatest());
},
_threadGap(box) {
return box.scrollHeight - box.scrollTop - box.clientHeight;
},
// A reply was just added at the end. gapBefore is how far from the end the
// reader was before it went in.
_threadAfterLiveReply(box, gapBefore, msg) {
const mine = !!(msg && this.user && msg.user_id != null && msg.user_id === this.user.id);
const blog = !!this._forumBlogThread;
const follow = this._feedShouldStick({ live: true, coupled: mine, gapBefore, startsAtEnd: !blog, scrolled: box.scrollTop > 0 });
if (follow) {
box.scrollTop = box.scrollHeight;
this._threadUnseen = 0;
} else if (!mine) {
this._threadUnseen = (this._threadUnseen || 0) + 1;
}
this._updateThreadJump();
},
// A thread was just opened or loaded again: nothing is waiting.
_resetThreadJump() {
this._threadUnseen = 0;
this._updateThreadJump();
},
_threadJumpToLatest() {
const box = document.getElementById('thread-messages');
if (!box) return;
box.scrollTop = box.scrollHeight;
this._threadUnseen = 0;
this._updateThreadJump();
},
_updateThreadJump() {
const box = document.getElementById('thread-messages');
const btn = document.getElementById('thread-jump-latest');
const label = document.getElementById('thread-jump-count');
if (!box || !btn) return;
const n = this._threadUnseen || 0;
const show = n > 0 || this._threadGap(box) > JUMP_SHOW_PX;
btn.classList.toggle('visible', show);
btn.classList.toggle('has-count', n > 0);
const text = n > 0 ? t(n === 1 ? 'thread_runtime.new_replies_one' : 'thread_runtime.new_replies_other', { count: n > 99 ? '99+' : n }) : '';
if (label) label.textContent = text;
btn.title = text || t('thread_runtime.jump_latest');
},
};