Commit a6320fb35dc6
tarball3 files changed+60-2
@@ -105,6 +105,7 @@
105105
import { setRoomMode, setSpaceMuted, spaceMutedRooms, startMuteSync } from './lib/mute';106106
import { isRoomMuted, mutingSpace, type MutedSpaces } from './lib/mute-model';107107
import { paneBadges } from './lib/pane-badges';108
+ import { withoutLiveMarker } from './lib/read-marker';108109
import { loadMutedSpaces, mutedSpaces } from './lib/muted-spaces';109110
import { loadMessageSound } from './lib/message-sound';110111
import {@@ -327,7 +328,10 @@
327328
const onPopState = (ev: PopStateEvent) => {328329
if (!backStack.pop(ev.state)) void restoreRoute();329330
};330
- const onAttention = () => queueReadReceipt();331
+ const onAttention = () => {332
+ windowActive = document.visibilityState === 'visible' && document.hasFocus();333
+ queueReadReceipt();334
+ };331335
let pointerAt: { x: number; y: number } | null = null;332336
let pointerScroller: HTMLElement | null = null;333337
const hidePointer = (e: KeyboardEvent) => {@@ -351,6 +355,7 @@
351355
backStack.init();352356
window.addEventListener('popstate', onPopState);353357
window.addEventListener('focus', onAttention);358
+ window.addEventListener('blur', onAttention);354359
document.addEventListener('visibilitychange', onAttention);355360
window.addEventListener('keydown', hidePointer, true);356361
window.addEventListener('pointermove', movePointer);@@ -367,6 +372,7 @@
367372
stopMuteSync();368373
window.removeEventListener('popstate', onPopState);369374
window.removeEventListener('focus', onAttention);375
+ window.removeEventListener('blur', onAttention);370376
document.removeEventListener('visibilitychange', onAttention);371377
window.removeEventListener('keydown', hidePointer, true);372378
window.removeEventListener('pointermove', movePointer);@@ -760,6 +766,24 @@
760766
return count;761767
}762768
769
+ let windowActive = document.visibilityState === 'visible' && document.hasFocus();770
+ let liveFrom: string | null = null;771
+ let liveRoom: string | null = null;772
+ $: watching = windowActive && stickToBottom && $selectedRoomId !== null;773
+ $: trackLive(watching, $selectedRoomId, items);774
+ $: shownItems = withoutLiveMarker(items, watching ? liveFrom : null);775
+776
+ function trackLive(on: boolean, roomId: string | null, list: TimelineItem[]): void {777
+ if (!on) {778
+ liveFrom = null;779
+ return;780
+ }781
+ if (liveRoom !== roomId || liveFrom === null) {782
+ liveRoom = roomId;783
+ liveFrom = lastEventId(list);784
+ }785
+ }786
+763787
let seenLatest: string | null = null;764788
$: if (stickToBottom) seenLatest = lastEventId(items);765789
$: if (stickToBottom) farFromLatest = false;@@ -2729,7 +2753,7 @@
27292753
<RoomTimelineRows27302754
bind:this={timelineRows}27312755
following={stickToBottom}2732
- {items}2756
+ items={shownItems}27332757
pending={pendingItems}27342758
joined={selectedRoom.membership === 'Joined'}27352759
self={$sessionUser ?? null}@@ -0,0 +1,11 @@
1
+import type { TimelineItem } from './types';2
+3
+/** The SDK's read marker trails the fully-read receipt, so messages watched as they arrived briefly sat under a New divider. */4
+export function withoutLiveMarker(items: TimelineItem[], liveFrom: string | null): TimelineItem[] {5
+ if (liveFrom === null) return items;6
+ const marker = items.findIndex((item) => item.kind === 'readMarker');7
+ if (marker < 0) return items;8
+ const seen = items.findIndex((item) => item.kind === 'event' && item.event.item_id === liveFrom);9
+ if (seen < 0 || marker < seen) return items;10
+ return items.filter((_, index) => index !== marker);11
+}@@ -0,0 +1,23 @@
1
+import assert from 'node:assert/strict';2
+import { test } from 'node:test';3
+import { withoutLiveMarker } from '../src/lib/read-marker.ts';4
+import type { TimelineItem } from '../src/lib/types.ts';5
+6
+const event = (id: string) => ({ kind: 'event', event: { item_id: id } }) as unknown as TimelineItem;7
+const marker = { kind: 'readMarker' } as TimelineItem;8
+9
+test('a marker above messages that were unread before watching stays', () => {10
+ const items = [event('a'), marker, event('b'), event('c')];11
+ assert.equal(withoutLiveMarker(items, 'c').length, 4);12
+});13
+14
+test('a marker above messages that arrived while watching goes', () => {15
+ const items = [event('a'), event('b'), marker, event('c')];16
+ assert.deepEqual(withoutLiveMarker(items, 'b'), [event('a'), event('b'), event('c')]);17
+});18
+19
+test('nothing is hidden while not watching or when the watch point is gone', () => {20
+ const items = [event('a'), marker, event('b')];21
+ assert.equal(withoutLiveMarker(items, null).length, 3);22
+ assert.equal(withoutLiveMarker(items, 'z').length, 3);23
+});