webui v156: a chat room dragged the reader back to the last message

The message pane scrolled to the bottom from its onupdate, unconditionally. That
hook runs on every redraw of the chat hub -- an incoming message, a poll answer,
a keystroke in the composer -- so scrolling up to read anything older was undone
a few tens of milliseconds later, every time.

It now follows the reader: the pane stays pinned while it is already at the
bottom, stops as soon as it is scrolled away from it, and pins again when it
comes back. Sending a message still jumps down whatever the state, and switching
room opens on its last message, since the pane is reused rather than recreated
and its oncreate does not run again.

The scroll handler sets event.redraw = false: mithril redraws after every
handler by default, and this one fires continuously while the pane is dragged.
This commit is contained in:
jolavillette 2026-08-27 13:35:05 +02:00
parent 97a4dd5578
commit e67c4c3170
2 changed files with 38 additions and 4 deletions

View File

@ -110,6 +110,24 @@ function scrollChatToBottom() {
}, 50);
}
// Whether the conversation is still pinned to its last message. It starts
// pinned, follows the user's own scrolling, and decides whether a redraw is
// allowed to jump back down -- without it, reading anything older is
// impossible: the pane redraws on every event and every poll answer, and each
// one dragged the reader back to the bottom a few tens of milliseconds later.
let chatStickToBottom = true;
// A little slack, because a reader who stops one line short of the end still
// means "keep following", and because scrollTop is fractional on zoomed or
// high-density displays.
const CHAT_STICK_SLACK_PX = 80;
function updateChatStickToBottom(element) {
if (!element) return;
chatStickToBottom =
element.scrollHeight - element.scrollTop - element.clientHeight <= CHAT_STICK_SLACK_PX;
}
function renderUserTooltip(gxsId, name) {
const details = ChatHubState.gxsDetails[gxsId];
if (!details) return null;
@ -359,8 +377,20 @@ const ChatConversationView = () => {
m(
'.chat-hub-messages' + (isRoom ? '.compact-container' : ''),
{
oncreate: () => scrollChatToBottom(),
onupdate: () => scrollChatToBottom(),
oncreate: () => {
chatStickToBottom = true;
scrollChatToBottom();
},
onupdate: () => {
if (chatStickToBottom) scrollChatToBottom();
},
onscroll: (event) => {
// A scroll must not trigger a redraw: mithril redraws after
// every handler by default, and this one fires continuously
// while the reader drags the pane.
event.redraw = false;
updateChatStickToBottom(event.target);
},
},
ChatLobbyModel.messages
),
@ -1220,6 +1250,10 @@ const Layout = {
if (lobbyId && ChatHubState.selectedRoomId !== lobbyId) {
ChatHubState.mobilePane = 'detail';
ChatHubState.selectedRoomId = lobbyId;
// Another room, another conversation: it opens on its last message,
// whatever the reader had scrolled to in the previous one. The pane
// itself is reused rather than recreated, so its oncreate does not run.
chatStickToBottom = true;
ChatLobbyModel.loadLobby(lobbyId);
} else if (!lobbyId) {
ChatHubState.mobilePane = 'list';

View File

@ -135,7 +135,7 @@ const navbar = () => {
? 'Connected to RetroShare Core'
: 'Connection Lost',
}),
m('span.webui-version', { style: { fontSize: '0.7em' } }, 'v153'),
m('span.webui-version', { style: { fontSize: '0.7em' } }, 'v156'),
m('i.fas.fa-sync-alt.refresh-icon', {
style: { cursor: 'pointer', fontSize: '0.8em' },
onclick: () => window.location.reload(true),
@ -258,7 +258,7 @@ const MobileStatus = () => {
m('small', statusbar.formatBytes(state.totalOut)),
]),
]),
m('.mobile-status-sheet__version', 'WebUI v153'),
m('.mobile-status-sheet__version', 'WebUI v156'),
])),
];
},