Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit b350d3c

Browse files
committed
Add keyboard UP behaviour for RTE
1 parent 156b45b commit b350d3c

File tree

5 files changed

+124
-28
lines changed

5 files changed

+124
-28
lines changed

src/components/views/rooms/wysiwyg_composer/ComposerContext.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,18 @@ limitations under the License.
1717
import { createContext, useContext } from "react";
1818

1919
import { SubSelection } from "./types";
20+
import EditorStateTransfer from "../../../../utils/EditorStateTransfer";
2021

21-
export function getDefaultContextValue(): { selection: SubSelection } {
22+
export function getDefaultContextValue(defaultValue?: Partial<ComposerContextState>): { selection: SubSelection } {
2223
return {
2324
selection: { anchorNode: null, anchorOffset: 0, focusNode: null, focusOffset: 0 },
25+
...defaultValue,
2426
};
2527
}
2628

2729
export interface ComposerContextState {
2830
selection: SubSelection;
31+
editorStateTransfer?: EditorStateTransfer;
2932
}
3033

3134
export const ComposerContext = createContext<ComposerContextState>(getDefaultContextValue());

src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export default function EditWysiwygComposer({
5252
className,
5353
...props
5454
}: EditWysiwygComposerProps): JSX.Element {
55-
const defaultContextValue = useRef(getDefaultContextValue());
55+
const defaultContextValue = useRef(getDefaultContextValue({ editorStateTransfer }));
5656
const initialContent = useInitialContent(editorStateTransfer);
5757
const isReady = !editorStateTransfer || initialContent !== undefined;
5858

src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export const WysiwygComposer = memo(function WysiwygComposer({
4747
rightComponent,
4848
children,
4949
}: WysiwygComposerProps) {
50-
const inputEventProcessor = useInputEventProcessor(onSend);
50+
const inputEventProcessor = useInputEventProcessor(onSend, initialContent);
5151

5252
const { ref, isWysiwygReady, content, actionStates, wysiwyg } = useWysiwyg({ initialContent, inputEventProcessor });
5353

src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts

Lines changed: 100 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,110 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import { WysiwygEvent } from "@matrix-org/matrix-wysiwyg";
17+
import { Wysiwyg, WysiwygEvent } from "@matrix-org/matrix-wysiwyg";
1818
import { useCallback } from "react";
19+
import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
1920

2021
import { useSettingValue } from "../../../../../hooks/useSettings";
2122
import { getKeyBindingsManager } from "../../../../../KeyBindingsManager";
2223
import { KeyBindingAction } from "../../../../../accessibility/KeyboardShortcuts";
24+
import { findEditableEvent } from "../../../../../utils/EventUtils";
25+
import dis from "../../../../../dispatcher/dispatcher";
26+
import { Action } from "../../../../../dispatcher/actions";
27+
import { useRoomContext } from "../../../../../contexts/RoomContext";
28+
import { IRoomState } from "../../../../structures/RoomView";
29+
import { ComposerContextState, useComposerContext } from "../ComposerContext";
30+
import EditorStateTransfer from "../../../../../utils/EditorStateTransfer";
31+
import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext";
32+
import { isCaretAtStart } from "../utils/selection";
33+
34+
export function useInputEventProcessor(
35+
onSend: () => void,
36+
initialContent?: string,
37+
): (event: WysiwygEvent, composer: Wysiwyg, editor: HTMLElement) => WysiwygEvent | null {
38+
const roomContext = useRoomContext();
39+
const composerContext = useComposerContext();
40+
const mxClient = useMatrixClientContext();
41+
const isCtrlEnter = useSettingValue<boolean>("MessageComposerInput.ctrlEnterToSend");
42+
43+
return useCallback(
44+
(event: WysiwygEvent, composer: Wysiwyg, editor: HTMLElement) => {
45+
if (event instanceof ClipboardEvent) {
46+
return event;
47+
}
48+
49+
const send = (): void => {
50+
event.stopPropagation?.();
51+
event.preventDefault?.();
52+
onSend();
53+
};
54+
55+
const isKeyboardEvent = event instanceof KeyboardEvent;
56+
if (isKeyboardEvent) {
57+
return handleKeyboardEvent(
58+
event,
59+
send,
60+
initialContent,
61+
composer,
62+
editor,
63+
roomContext,
64+
composerContext,
65+
mxClient,
66+
);
67+
} else {
68+
return handleInputEvent(event, send, isCtrlEnter);
69+
}
70+
},
71+
[isCtrlEnter, onSend, initialContent, roomContext, composerContext, mxClient],
72+
);
73+
}
2374

2475
type Send = () => void;
2576

26-
function handleKeyboardEvent(event: KeyboardEvent, send: Send): KeyboardEvent | null {
77+
function handleKeyboardEvent(
78+
event: KeyboardEvent,
79+
send: Send,
80+
initialContent: string,
81+
composer: Wysiwyg,
82+
editor: HTMLElement,
83+
roomContext: IRoomState,
84+
composerContext: ComposerContextState,
85+
mxClient: MatrixClient,
86+
): KeyboardEvent | null {
2787
const action = getKeyBindingsManager().getMessageComposerAction(event);
2888

2989
switch (action) {
3090
case KeyBindingAction.SendMessage:
3191
send();
3292
return null;
93+
case KeyBindingAction.EditPrevMessage: {
94+
const { editorStateTransfer } = composerContext;
95+
96+
const isEditorModified = initialContent !== composer.content();
97+
98+
// If not in edition
99+
// Or if the caret is not at the beginning of the editor
100+
// Or the editor is modified
101+
if (!editorStateTransfer || !isCaretAtStart(editor) || isEditorModified) {
102+
break;
103+
}
104+
105+
const previousEvent = findEditableEvent({
106+
events: getEventsFromEditorStateTransfer(editorStateTransfer, roomContext, mxClient),
107+
isForward: false,
108+
fromEventId: editorStateTransfer.getEvent().getId(),
109+
});
110+
if (previousEvent) {
111+
dis.dispatch({
112+
action: Action.EditEvent,
113+
event: previousEvent,
114+
timelineRenderingType: roomContext.timelineRenderingType,
115+
});
116+
event.stopPropagation();
117+
event.preventDefault();
118+
}
119+
return null;
120+
}
33121
}
34122

35123
return event;
@@ -54,27 +142,14 @@ function handleInputEvent(event: InputEvent, send: Send, isCtrlEnter: boolean):
54142
return event;
55143
}
56144

57-
export function useInputEventProcessor(onSend: () => void): (event: WysiwygEvent) => WysiwygEvent | null {
58-
const isCtrlEnter = useSettingValue<boolean>("MessageComposerInput.ctrlEnterToSend");
59-
return useCallback(
60-
(event: WysiwygEvent) => {
61-
if (event instanceof ClipboardEvent) {
62-
return event;
63-
}
64-
65-
const send = (): void => {
66-
event.stopPropagation?.();
67-
event.preventDefault?.();
68-
onSend();
69-
};
70-
71-
const isKeyboardEvent = event instanceof KeyboardEvent;
72-
if (isKeyboardEvent) {
73-
return handleKeyboardEvent(event, send);
74-
} else {
75-
return handleInputEvent(event, send, isCtrlEnter);
76-
}
77-
},
78-
[isCtrlEnter, onSend],
79-
);
145+
// From EditMessageComposer private get events(): MatrixEvent[]
146+
function getEventsFromEditorStateTransfer(
147+
editorStateTransfer: EditorStateTransfer,
148+
roomContext: IRoomState,
149+
mxClient: MatrixClient,
150+
): MatrixEvent[] {
151+
const liveTimelineEvents = roomContext.liveTimeline.getEvents();
152+
const pendingEvents = mxClient.getRoom(editorStateTransfer.getEvent().getRoomId()).getPendingEvents();
153+
const isInThread = Boolean(editorStateTransfer.getEvent().getThread());
154+
return liveTimelineEvents.concat(isInThread ? [] : pendingEvents);
80155
}

src/components/views/rooms/wysiwyg_composer/utils/selection.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,21 @@ export function isSelectionEmpty(): boolean {
3434
const selection = document.getSelection();
3535
return Boolean(selection?.isCollapsed);
3636
}
37+
38+
export function isCaretAtStart(editor: HTMLElement): Boolean {
39+
const selection = document.getSelection();
40+
41+
if (!selection || selection.anchorOffset !== 0) {
42+
return false;
43+
}
44+
45+
// In case of nested html elements (list, code blocks), we are going through all the first child
46+
let child = editor.firstChild;
47+
do {
48+
if (child === selection.anchorNode) {
49+
return true;
50+
}
51+
} while ((child = child.firstChild));
52+
53+
return false;
54+
}

0 commit comments

Comments
 (0)