Commit 4f91f89d99fa
tarball5 files changed+62-11
@@ -75,6 +75,7 @@
7575
selectedRoomId,7676
sessionUser,7777
softLogout,78
+ threadTimelines,7879
timelines,7980
pinnedTimelines,8081
pinnedTimelineVersions,@@ -1094,7 +1095,12 @@
10941095
const roomId = $selectedRoomId;10951096
if (!roomId || !eventId || pinJumpBusy) return;10961097
pinsExpanded = false;1097
- pinPreview = pins.find((event) => event.event_id === eventId) ?? null;1098
+ const pinned = pins.find((event) => event.event_id === eventId) ?? null;1099
+ if (pinned?.thread_root) {1100
+ openThreadHere(pinned.thread_root, eventId);1101
+ return;1102
+ }1103
+ pinPreview = pinned;10981104
pinJumpBusy = true;10991105
try {11001106
for (let attempt = 0; attempt < 24; attempt += 1) {@@ -1117,6 +1123,11 @@
11171123
11181124
function jumpTo(eventId: string): void {11191125
const el = timelineEl?.querySelector<HTMLElement>(`[data-event="${CSS.escape(eventId)}"]`);1126
+ const root = !el && $selectedRoomId ? threadHolding($selectedRoomId, eventId) : null;1127
+ if (root) {1128
+ openThreadHere(root, eventId);1129
+ return;1130
+ }11201131
if (!el) {11211132
pushBanner('The original message is not loaded. Scroll up to load older messages.');11221133
return;@@ -1274,10 +1285,29 @@
12741285
$: threadShown = !!threadView && (bothPanesFit || !membersOpen || rightFront === 'thread');12751286
$: membersShown = !!selectedRoom && membersOpen && (bothPanesFit || !threadView || rightFront === 'members');12761287
1277
- function openThreadHere(rootEventId: string): void {1288
+ function openThreadHere(rootEventId: string, focusEventId?: string): void {12781289
if (!$selectedRoomId) return;12791290
rightFront = 'thread';1280
- void showThread({ room_id: $selectedRoomId, root_event_id: rootEventId });1291
+ void showThread({ room_id: $selectedRoomId, root_event_id: rootEventId, focus_event_id: focusEventId });1292
+ }1293
+1294
+ async function openLive(roomId: string, eventId: string, threadRoot: string | null): Promise<void> {1295
+ await selectRoom(roomId);1296
+ if ($selectedRoomId !== roomId) return;1297
+ if (threadRoot) {1298
+ openThreadHere(threadRoot, eventId);1299
+ return;1300
+ }1301
+ await tick();1302
+ if (timelineEl?.querySelector(`[data-event="${CSS.escape(eventId)}"]`)) jumpTo(eventId);1303
+ }1304
+1305
+ function threadHolding(roomId: string, eventId: string): string | null {1306
+ for (const [key, list] of Object.entries($threadTimelines)) {1307
+ const [room, root] = key.split('\u0000');1308
+ if (room === roomId && list.some((item) => item.kind === 'event' && item.event.event_id === eventId)) return root;1309
+ }1310
+ return null;12811311
}12821312
12831313
async function openThreadIn(roomId: string, rootEventId: string): Promise<void> {@@ -2267,7 +2297,7 @@
22672297
{#key mentionsOpen ? 'mentions' : $selectedRoomId ?? 'home'}22682298
<div class="chat-view">22692299
{#if mentionsOpen && $sessionUser}2270
- <MentionsInbox self={$sessionUser} onLive={(roomId) => void selectRoom(roomId)} />2300
+ <MentionsInbox self={$sessionUser} onLive={(roomId, eventId, threadRoot) => void openLive(roomId, eventId, threadRoot)} />22712301
{:else if !selectedRoom}22722302
<div class="empty" in:fade={{ duration: motionDuration(180) }}>22732303
<div class="empty-mark" aria-hidden="true">◈</div>@@ -2552,6 +2582,7 @@
25522582
onUser={(userId, x, y, asMenu) => void openUser(userId, x, y, asMenu)}25532583
onMore={(ev, x, y) => void openMessageMenu(ev, x, y)}25542584
onMentionStart={() => !$membersByRoom[threadView.room_id] && void loadMembers(threadView.room_id)}2585
+ focusEventId={threadView.focus_event_id ?? null}25552586
onClose={() => void showThread(null)}25562587
/>25572588
{/if}@@ -14,10 +14,11 @@
1414
export let self: string | null = null;1515
export let canPaginate = true;1616
export let onClose: () => void;17
- export let onLive: (roomId: string) => void;17
+ export let onLive: (roomId: string, eventId: string, threadRoot: string | null) => void;1818
export let closeLabel = 'Back to mentions';1919
2020
let rows: TimelineItem[] = [];21
+ $: targetThread = rows.reduce<string | null>((found, item) => found ?? (item.kind === 'event' && item.event.event_id === eventId ? item.event.thread_root : null), null);2122
let beforeToken: string | null = null;2223
let afterToken: string | null = null;2324
let beforeEnded = false;@@ -186,7 +187,7 @@
186187
<header>187188
<button bind:this={backButton} onclick={onClose}><EmojiText text={closeLabel} /></button>188189
<strong><EmojiText text={roomName} /></strong>189
- <button disabled={!canPaginate} onclick={() => onLive(roomId)}>Go to current messages</button>190
+ <button disabled={!canPaginate} onclick={() => onLive(roomId, eventId, targetThread)}>Go to current messages</button>190191
</header>191192
{#if error}<div class="error" role="alert"><EmojiText text={error} /></div>{/if}192193
<button class="page-control" disabled={!beforeToken || beforeEnded || beforeBusy || !canPaginate} onclick={() => load('before')}>@@ -13,7 +13,7 @@
1313
import type { MentionEntry, MentionsPage, SavedMentionContext } from './mentions-model';1414
1515
export let self: string;16
- export let onLive: (roomId: string) => void;16
+ export let onLive: (roomId: string, eventId: string, threadRoot: string | null) => void;1717
1818
let page: MentionsPage | null = null;1919
let entries: MentionEntry[] = [];@@ -49,11 +49,13 @@
4949
export let onUser: ((userId: string, x: number, y: number, menu: boolean) => void) | null = null;5050
export let onMore: ((event: TimelineEvent, x: number, y: number) => void) | null = null;5151
export let onMentionStart: () => void = () => {};52
+ export let focusEventId: string | null = null;5253
export let onClose: () => void;5354
5455
const GROUP_GAP_MS = 5 * 60 * 1000;5556
5657
let list: HTMLDivElement | null = null;58
+ let thread: HTMLDivElement | null = null;5759
let composerRef: Composer | null = null;5860
let draft = '';5961
let picked: Mention[] = [];@@ -77,6 +79,8 @@
7779
$: emotes = customEmotes(packs);7880
$: switchTo(key);7981
$: void follow(events);82
+ let focused: string | null = null;83
+ $: void focusOn(focusEventId, events);8084
8185
function threadEvents(list: TimelineItem[], root: TimelineItem | null): TimelineEvent[] {8286
const out = list.flatMap((item) => (item.kind === 'event' ? [item.event] : []));@@ -103,6 +107,20 @@
103107
editing = null;104108
stickToBottom = true;105109
lastRead = '';110
+ focused = null;111
+ }112
+113
+ async function focusOn(target: string | null, list: TimelineEvent[]): Promise<void> {114
+ if (!target || target === focused || !list.some((event) => event.event_id === target)) return;115
+ focused = target;116
+ stickToBottom = false;117
+ await tick();118
+ const row = list.length && thread ? thread.querySelector<HTMLElement>(`[data-event="${CSS.escape(target)}"]`) : null;119
+ if (!row) return;120
+ row.scrollIntoView({ block: 'center', behavior: 'smooth' });121
+ row.classList.remove('flash');122
+ void row.offsetWidth;123
+ row.classList.add('flash');106124
}107125
108126
async function follow(_events: TimelineEvent[]): Promise<void> {@@ -260,7 +278,7 @@
260278
<svelte:window on:focus={sendReceipt} />261279
<svelte:document on:visibilitychange={sendReceipt} />262280
263
-<div class="thread-pane">281
+<div class="thread-pane" bind:this={thread}>264282
<header class="thread-head">265283
<h2>Thread</h2>266284
<span class="thread-room"><EmojiText text={roomName} /></span>@@ -9,6 +9,7 @@ const PINNED_TYPE = 'dev.malice.pinned_threads';
99
export interface OpenThread {1010
room_id: string;1111
root_event_id: string;12
+ focus_event_id?: string;1213
}1314
1415
export const openThread = writable<OpenThread | null>(null);@@ -62,13 +63,13 @@ let opened: OpenThread | null = null;
6263
/** The room must already be open, since macltok builds thread timelines inside it. */6364
export async function showThread(target: OpenThread | null): Promise<void> {6465
const previous = opened;65
- if (previous && target && previous.room_id === target.room_id && previous.root_event_id === target.root_event_id) return;6666
opened = target;6767
openThread.set(target);68
- if (previous) runCommand({ op: 'close_thread', ...previous }).catch(() => undefined);68
+ if (previous && target && previous.room_id === target.room_id && previous.root_event_id === target.root_event_id) return;69
+ if (previous) runCommand({ op: 'close_thread', room_id: previous.room_id, root_event_id: previous.root_event_id }).catch(() => undefined);6970
if (!target) return;7071
try {71
- await runCommand({ op: 'open_thread', ...target });72
+ await runCommand({ op: 'open_thread', room_id: target.room_id, root_event_id: target.root_event_id });7273
} catch (e) {7374
if (opened === target) {7475
opened = null;