From 6b155620e49a8313e6997bfb6a555412725d0f49 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Tue, 3 Jan 2023 16:35:14 +0100 Subject: [PATCH] Fix emoji in RTE editing (#9827) Fix emoji in RTE editing --- .../wysiwyg_composer/EditWysiwygComposer.tsx | 16 ++++--- .../wysiwyg_composer/components/Emoji.tsx | 15 ++---- .../hooks/useWysiwygEditActionHandler.ts | 21 ++++++++- .../hooks/useWysiwygSendActionHandler.ts | 7 ++- .../EditWysiwygComposer-test.tsx | 46 +++++++++++++++++++ .../SendWysiwygComposer-test.tsx | 2 +- 6 files changed, 85 insertions(+), 22 deletions(-) diff --git a/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx index 2c935b12403..70cdeda8083 100644 --- a/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { forwardRef, RefObject, useRef } from "react"; +import React, { ForwardedRef, forwardRef, MutableRefObject, useRef } from "react"; import classNames from "classnames"; import EditorStateTransfer from "../../../../utils/EditorStateTransfer"; @@ -24,16 +24,18 @@ import { useWysiwygEditActionHandler } from "./hooks/useWysiwygEditActionHandler import { useEditing } from "./hooks/useEditing"; import { useInitialContent } from "./hooks/useInitialContent"; import { ComposerContext, getDefaultContextValue } from "./ComposerContext"; +import { ComposerFunctions } from "./types"; interface ContentProps { - disabled: boolean; + disabled?: boolean; + composerFunctions: ComposerFunctions; } const Content = forwardRef(function Content( - { disabled }: ContentProps, - forwardRef: RefObject, + { disabled = false, composerFunctions }: ContentProps, + forwardRef: ForwardedRef, ) { - useWysiwygEditActionHandler(disabled, forwardRef); + useWysiwygEditActionHandler(disabled, forwardRef as MutableRefObject, composerFunctions); return null; }); @@ -65,9 +67,9 @@ export default function EditWysiwygComposer({ editorStateTransfer, className, .. onSend={editMessage} {...props} > - {(ref) => ( + {(ref, composerFunctions) => ( <> - + { - setSelection(composerContext.selection).then(() => - dis.dispatch({ - action: Action.ComposerInsert, - text: emoji, - timelineRenderingType: roomContext.timelineRenderingType, - }), - ); + dis.dispatch({ + action: Action.ComposerInsert, + text: emoji, + timelineRenderingType: roomContext.timelineRenderingType, + }); return true; }} /> diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygEditActionHandler.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygEditActionHandler.ts index ee00215dda0..cdc34a310fb 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygEditActionHandler.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygEditActionHandler.ts @@ -22,9 +22,18 @@ import { ActionPayload } from "../../../../../dispatcher/payloads"; import { TimelineRenderingType, useRoomContext } from "../../../../../contexts/RoomContext"; import { useDispatcher } from "../../../../../hooks/useDispatcher"; import { focusComposer } from "./utils"; +import { ComposerType } from "../../../../../dispatcher/payloads/ComposerInsertPayload"; +import { ComposerFunctions } from "../types"; +import { setSelection } from "../utils/selection"; +import { useComposerContext } from "../ComposerContext"; -export function useWysiwygEditActionHandler(disabled: boolean, composerElement: RefObject) { +export function useWysiwygEditActionHandler( + disabled: boolean, + composerElement: RefObject, + composerFunctions: ComposerFunctions, +) { const roomContext = useRoomContext(); + const composerContext = useComposerContext(); const timeoutId = useRef(null); const handler = useCallback( @@ -39,9 +48,17 @@ export function useWysiwygEditActionHandler(disabled: boolean, composerElement: case Action.FocusEditMessageComposer: focusComposer(composerElement, context, roomContext, timeoutId); break; + case Action.ComposerInsert: + if (payload.timelineRenderingType !== roomContext.timelineRenderingType) break; + if (payload.composerType !== ComposerType.Edit) break; + + if (payload.text) { + setSelection(composerContext.selection).then(() => composerFunctions.insertText(payload.text)); + } + break; } }, - [disabled, composerElement, timeoutId, roomContext], + [disabled, composerElement, composerFunctions, timeoutId, roomContext, composerContext], ); useDispatcher(defaultDispatcher, handler); diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygSendActionHandler.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygSendActionHandler.ts index 8d6af77b12a..d951fe24795 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygSendActionHandler.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygSendActionHandler.ts @@ -24,6 +24,8 @@ import { useDispatcher } from "../../../../../hooks/useDispatcher"; import { focusComposer } from "./utils"; import { ComposerFunctions } from "../types"; import { ComposerType } from "../../../../../dispatcher/payloads/ComposerInsertPayload"; +import { useComposerContext } from "../ComposerContext"; +import { setSelection } from "../utils/selection"; export function useWysiwygSendActionHandler( disabled: boolean, @@ -31,6 +33,7 @@ export function useWysiwygSendActionHandler( composerFunctions: ComposerFunctions, ) { const roomContext = useRoomContext(); + const composerContext = useComposerContext(); const timeoutId = useRef(null); const handler = useCallback( @@ -59,12 +62,12 @@ export function useWysiwygSendActionHandler( } else if (payload.event) { // TODO insert quote message - see SendMessageComposer } else if (payload.text) { - composerFunctions.insertText(payload.text); + setSelection(composerContext.selection).then(() => composerFunctions.insertText(payload.text)); } break; } }, - [disabled, composerElement, composerFunctions, timeoutId, roomContext], + [disabled, composerElement, roomContext, composerFunctions, composerContext], ); useDispatcher(defaultDispatcher, handler); diff --git a/test/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx index 045d4bf9cbd..7f64be24375 100644 --- a/test/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx @@ -26,6 +26,12 @@ import { IRoomState } from "../../../../../src/components/structures/RoomView"; import { createTestClient, flushPromises, getRoomContext, mkEvent, mkStubRoom } from "../../../../test-utils"; import { EditWysiwygComposer } from "../../../../../src/components/views/rooms/wysiwyg_composer"; import EditorStateTransfer from "../../../../../src/utils/EditorStateTransfer"; +import { Emoji } from "../../../../../src/components/views/rooms/wysiwyg_composer/components/Emoji"; +import { ChevronFace } from "../../../../../src/components/structures/ContextMenu"; +import dis from "../../../../../src/dispatcher/dispatcher"; +import { ComposerInsertPayload, ComposerType } from "../../../../../src/dispatcher/payloads/ComposerInsertPayload"; +import { ActionPayload } from "../../../../../src/dispatcher/payloads"; +import * as EmojiButton from "../../../../../src/components/views/rooms/EmojiButton"; describe("EditWysiwygComposer", () => { afterEach(() => { @@ -269,4 +275,44 @@ describe("EditWysiwygComposer", () => { // Then we don't get it because we are disabled expect(screen.getByRole("textbox")).not.toHaveFocus(); }); + + it("Should add emoji", async () => { + // When + + // We are not testing here the emoji button (open modal, select emoji ...) + // Instead we are directly firing an emoji to make the test easier to write + jest.spyOn(EmojiButton, "EmojiButton").mockImplementation( + ({ addEmoji }: { addEmoji: (emoji: string) => void }) => { + return ( + + ); + }, + ); + render( + + + + + + , + ); + // Same behavior as in RoomView.tsx + // RoomView is re-dispatching the composer messages. + // It adds the composerType fields where the value refers if the composer is in editing or not + // The listeners in the RTE ignore the message if the composerType is missing in the payload + const dispatcherRef = dis.register((payload: ActionPayload) => { + dis.dispatch({ + ...(payload as ComposerInsertPayload), + composerType: ComposerType.Edit, + }); + }); + + screen.getByLabelText("Emoji").click(); + + // Then + await waitFor(() => expect(screen.getByRole("textbox")).toHaveTextContent(/🦫/)); + dis.unregister(dispatcherRef); + }); }); diff --git a/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx index 0b49da23e39..1a5ba88c46a 100644 --- a/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx @@ -294,7 +294,7 @@ describe("SendWysiwygComposer", () => { }); const textNode = screen.getByRole("textbox").firstChild; - setSelection({ + await setSelection({ anchorNode: textNode, anchorOffset: 2, focusNode: textNode,