atagen

malice

public
Star0
Watch0

Commit 984f84f9bf6c

tarball

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

2 files changed+17-0
web: open the message menu on right-click and add reply in thread

Right-clicking a message gave the browser's own menu, so its actions were only reachable through the hover toolbar's more button. Links, media, inputs and an active text selection keep the native menu, and shift-right-click always does. The menu also gains Reply in thread, which opens the message's thread and focuses its composer.
Modified file: web/src/App.svelte+7 additions
@@ -1504,6 +1504,12 @@
15041504 $: threadShown = !!threadView && (bothPanesFit || !membersOpen || rightFront === 'thread');
15051505 $: membersShown = !!selectedRoom && membersOpen && (bothPanesFit || !threadView || rightFront === 'members');
15061506
1507+ async function replyInThread(rootEventId: string): Promise<void> {
1508+ openThreadHere(rootEventId);
1509+ await tick();
1510+ document.getElementById(`thread-composer-${rootEventId}`)?.focus();
1511+ }
1512+
15071513 function openThreadHere(rootEventId: string, focusEventId?: string): void {
15081514 if (!$selectedRoomId) return;
15091515 rightFront = 'thread';
@@ -1850,6 +1856,7 @@
18501856 details: { kind: 'Ready', sender: event.sender, content, timestamp_ms: event.timestamp_ms }
18511857 })
18521858 });
1859+ if (!event.thread_root && content.kind !== 'Redacted') items.push({ label: 'Reply in thread', action: () => void replyInThread(eventId) });
18531860 }
18541861 if (joined && event.own && event.editable && content.kind === 'Message' && content.message.kind === 'Text') {
18551862 const body = content.message.body;
Modified file: web/src/lib/MessageRow.svelte+10 additions
@@ -219,6 +219,15 @@
219219 onMore?.(event, p.clientX, p.clientY);
220220 }
221221
222+ function contextMore(ev: MouseEvent) {
223+ if (ev.defaultPrevented || ev.shiftKey || !onMore || readOnly || !interactive || !event.event_id) return;
224+ if ((ev.target as Element).closest('a[href], img, video, audio, input, textarea, .picker-pop')) return;
225+ const selection = window.getSelection();
226+ if (selection && !selection.isCollapsed && row.contains(selection.anchorNode)) return;
227+ ev.preventDefault();
228+ holdMore(ev);
229+ }
230+
222231 function select(on: boolean) {
223232 selectedRow.update((o) => (on ? owner : o === owner ? null : o));
224233 }
@@ -385,6 +394,7 @@
385394 out:rowOut|global
386395 bind:this={row}
387396 on:click={toggleSelected}
397+ on:contextmenu={contextMore}
388398 use:longpress={{ on: holdMore, allowSelection: true, ignore: 'a, button, input, img, video, audio, .text, .picker-pop' }}
389399 on:keydown={onRowKeydown}
390400 on:pointerenter={(e) => (hovered = e.pointerType === 'mouse')}