feat(threads): up-arrow edits your last thread reply (+ fix cross-fire)
The thread composer reused RoomInput's hardcoded editableName="RoomInput", so the main timeline's global up-arrow "edit last message" handler fired while focused in a thread composer and targeted the MAIN timeline's last message (wrong), and there was no up-arrow edit for the thread itself. - Make editableName a RoomInput prop (default "RoomInput"); the thread composer passes "ThreadInput", so the two up-arrow handlers never cross-fire. - Add an up-arrow-edit handler to ThreadTimeline (parity with RoomTimeline): empty thread composer + Up -> edit the latest editable reply in that thread, using thread.liveTimeline + canEditEvent + setEditId. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -901,6 +901,8 @@ Root messages in the main timeline show a **"N replies · time"** chip (server-a
|
||||
|
||||
The panel embeds the full composer (uploads, emoji, stickers, GIFs, voice, location, polls) with drafts, reply state, and upload queues **isolated per thread** (`roomId::threadRootId` keys). Replies-to-replies produce spec-correct `m.thread` + `m.in_reply_to` (`is_falling_back: false`). Scheduling and slash commands are disabled inside threads (v1).
|
||||
|
||||
**↑ to edit last reply**: pressing Up-arrow in the empty thread composer opens the editor on your most recent editable reply *in that thread* — parity with the main timeline. The thread composer carries a distinct `editableName="ThreadInput"` so the main timeline's global up-arrow handler and the thread's no longer cross-fire (previously the thread composer had the same name, so Up-arrow there wrongly targeted the main timeline's last message).
|
||||
|
||||
### Notifications (Slack-style, P4-1)
|
||||
|
||||
By default you're notified for a thread reply only when you **participate** in that thread (you've posted in it) or the reply **@mentions** you — other threads accumulate quietly behind their chip badges. Every thread can be overridden from the bell menu in the panel header: **Default (participating) / All replies / Mentions only / Mute**. Modes sync across your devices (`io.lotus.thread_notifications` account data, auto-pruned). Muting a thread silences notifications and sounds, removes the chip's unread badge (a small bell-mute glyph shows instead), and subtracts that thread from the room's sidebar unread badge (client-side — other Matrix clients on the account still count it).
|
||||
|
||||
@@ -151,9 +151,13 @@ interface RoomInputProps {
|
||||
roomId: string;
|
||||
room: Room;
|
||||
threadRootId?: string;
|
||||
// Identifies this composer to global key handlers (e.g. the up-arrow "edit last
|
||||
// message" handler). Threads pass a distinct name so the main timeline's handler
|
||||
// doesn't fire for the thread composer, and vice-versa.
|
||||
editableName?: string;
|
||||
}
|
||||
export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
({ editor, fileDropContainerRef, roomId, room, threadRootId }, ref) => {
|
||||
({ editor, fileDropContainerRef, roomId, room, threadRootId, editableName = 'RoomInput' }, ref) => {
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const [enterForNewline] = useSetting(settingsAtom, 'enterForNewline');
|
||||
@@ -981,7 +985,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
)}
|
||||
<ScheduledMessagesTray roomId={roomId} />
|
||||
<CustomEditor
|
||||
editableName="RoomInput"
|
||||
editableName={editableName}
|
||||
editor={editor}
|
||||
placeholder="Send a message..."
|
||||
onKeyDown={handleKeyDown}
|
||||
|
||||
@@ -196,6 +196,7 @@ export function ThreadPanel({ room, threadId, requestClose }: ThreadPanelProps)
|
||||
roomId={room.roomId}
|
||||
threadRootId={threadId}
|
||||
editor={editor}
|
||||
editableName="ThreadInput"
|
||||
fileDropContainerRef={fileDropContainerRef}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -32,11 +32,12 @@ import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import { Badge, Box, Chip, Icon, Icons, Line, Scroll, Spinner, Text, color, config } from 'folds';
|
||||
import classNames from 'classnames';
|
||||
import { Opts as LinkifyOpts } from 'linkifyjs';
|
||||
import { isKeyHotkey } from 'is-hotkey';
|
||||
import { eventWithShortcode, factoryEventSentBy } from '../../../utils/matrix';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { useVirtualPaginator, ItemRange } from '../../../hooks/useVirtualPaginator';
|
||||
import { useAlive } from '../../../hooks/useAlive';
|
||||
import { scrollToBottom } from '../../../utils/dom';
|
||||
import { editableActiveElement, scrollToBottom } from '../../../utils/dom';
|
||||
import {
|
||||
DefaultPlaceholder,
|
||||
MessageBase,
|
||||
@@ -55,9 +56,11 @@ import {
|
||||
renderMatrixMention,
|
||||
} from '../../../plugins/react-custom-html-parser';
|
||||
import {
|
||||
canEditEvent,
|
||||
decryptAllTimelineEvent,
|
||||
getEditedEvent,
|
||||
getEventReactions,
|
||||
getLatestEditableEvt,
|
||||
getMemberName,
|
||||
getReactionContent,
|
||||
reactionOrEditEvent,
|
||||
@@ -76,7 +79,8 @@ import {
|
||||
today,
|
||||
yesterday,
|
||||
} from '../../../utils/time';
|
||||
import { createMentionElement, moveCursor } from '../../../components/editor';
|
||||
import { createMentionElement, isEmptyEditor, moveCursor } from '../../../components/editor';
|
||||
import { useKeyDown } from '../../../hooks/useKeyDown';
|
||||
import { roomIdToReplyDraftAtomFamily } from '../../../state/room/roomInputDrafts';
|
||||
import { usePowerLevelsContext } from '../../../hooks/usePowerLevels';
|
||||
import { GetContentCallback, MessageEvent, StateEvent } from '../../../../types/matrix/room';
|
||||
@@ -580,6 +584,32 @@ export function ThreadTimeline({ room, thread, editor }: ThreadTimelineProps) {
|
||||
[editor],
|
||||
);
|
||||
|
||||
// Up-arrow in the (empty) thread composer edits your last editable reply in
|
||||
// this thread — parity with the main timeline. Gated on the thread composer's
|
||||
// distinct editableName so it and the main handler never cross-fire.
|
||||
useKeyDown(
|
||||
window,
|
||||
useCallback(
|
||||
(evt) => {
|
||||
if (
|
||||
isKeyHotkey('arrowup', evt) &&
|
||||
editableActiveElement() &&
|
||||
document.activeElement?.getAttribute('data-editable-name') === 'ThreadInput' &&
|
||||
isEmptyEditor(editor)
|
||||
) {
|
||||
const editableEvt = getLatestEditableEvt(thread.liveTimeline, (mEvt) =>
|
||||
canEditEvent(mx, mEvt),
|
||||
);
|
||||
const editableEvtId = editableEvt?.getId();
|
||||
if (!editableEvtId) return;
|
||||
setEditId(editableEvtId);
|
||||
evt.preventDefault();
|
||||
}
|
||||
},
|
||||
[mx, thread, editor],
|
||||
),
|
||||
);
|
||||
|
||||
const handleOpenReply: MouseEventHandler = useCallback(
|
||||
(evt) => {
|
||||
const targetId = evt.currentTarget.getAttribute('data-event-id');
|
||||
|
||||
Reference in New Issue
Block a user