atagen

malice

public
Star0
Watch0

Commit e590609b6343

tarball

atagen <boss@atagen.co>2026-10-27 10:33 UTC

2 files changed+20-3
web: hold the timeline still while a message is selected

New messages kept pulling the view to the bottom while a message was click-selected, so its toolbar or menu slid out from under the pointer mid-action. Following and read receipts pause while a row is selected. Reacting, replying, editing or clicking off the message clears the selection and catches up to the bottom.
Modified file: web/src/App.svelte+17 additions−3 deletions
@@ -107,6 +107,7 @@
107107 import { isRoomMuted, mutingSpace, type MutedSpaces } from './lib/mute-model';
108108 import { paneBadges } from './lib/pane-badges';
109109 import { withoutLiveMarker } from './lib/read-marker';
110+ import { selectedRow } from './lib/selected-row';
110111 import { loadMutedSpaces, mutedSpaces } from './lib/muted-spaces';
111112 import { loadMessageSound } from './lib/message-sound';
112113 import {
@@ -767,6 +768,19 @@
767768 return count;
768769 }
769770
771+ /** A selected message's toolbar and menu would slide away under the pointer if new messages kept pulling the view down. */
772+ $: followHeld = $selectedRow !== null;
773+ let wasHeld = false;
774+ $: releaseFollow(followHeld);
775+
776+ function releaseFollow(held: boolean): void {
777+ if (wasHeld && !held && stickToBottom) {
778+ scrollToBottom(true);
779+ queueReadReceipt();
780+ }
781+ wasHeld = held;
782+ }
783+
770784 let windowActive = document.visibilityState === 'visible' && document.hasFocus();
771785 let liveFrom: string | null = null;
772786 let liveRoom: string | null = null;
@@ -850,7 +864,7 @@
850864 timelineViewport = viewport;
851865 const observer = new ResizeObserver(() => {
852866 if (timelineViewport !== viewport || viewport.roomId !== $selectedRoomId) return;
853- if (stickToBottom) viewport.scrollToBottom(false);
867+ if (stickToBottom && !followHeld) viewport.scrollToBottom(false);
854868 else {
855869 viewport.restore();
856870 measureLatest();
@@ -878,7 +892,7 @@
878892 const tail = `${roomId}|${lastEventId(list)}`;
879893 if (!followedTail.startsWith(`${roomId}|`)) followedSince = performance.now();
880894 const arrived = tail !== followedTail && performance.now() - followedSince > SETTLE_MS;
881- if (stickToBottom) timelineViewport.scrollToBottom(arrived && !motionActive());
895+ if (stickToBottom && !followHeld) timelineViewport.scrollToBottom(arrived && !motionActive());
882896 else timelineViewport.restore();
883897 followedTail = tail;
884898 queueReadReceipt();
@@ -923,7 +937,7 @@
923937 async function sendReadReceipt(): Promise<void> {
924938 const roomId = $selectedRoomId;
925939 if (!roomId || roomId === peeking) return;
926- if (document.hidden || !document.hasFocus() || !stickToBottom) return;
940+ if (document.hidden || !document.hasFocus() || !stickToBottom || followHeld) return;
927941 const list = $timelines[roomId] ?? [];
928942 for (let i = list.length - 1; i >= 0; i -= 1) {
929943 const entry = list[i];
Modified file: web/src/lib/MessageRow.svelte+3 additions
@@ -280,6 +280,7 @@
280280 function react(key: string, shortcode: string | null) {
281281 if (shortcode && isImageKey(key)) onReact(event.event_id ?? '', key, shortcode);
282282 else onReact(event.event_id ?? '', key);
283+ select(false);
283284 }
284285
285286 function reply() {
@@ -288,6 +289,7 @@
288289 event_id: event.event_id,
289290 details: { kind: 'Ready', sender: event.sender, content: event.content, timestamp_ms: event.timestamp_ms }
290291 });
292+ select(false);
291293 }
292294
293295 $: editableBody =
@@ -298,6 +300,7 @@
298300 function edit() {
299301 if (!event.event_id || editableBody === null || !onEdit) return;
300302 onEdit(event.event_id, editableBody);
303+ select(false);
301304 }
302305
303306 function reactorNames(senders: string[]): string {