From 75be1f995472fe2196e9bbccfc02620d91d75047 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 30 Nov 2022 15:29:39 +0100 Subject: [PATCH 1/8] Add emoji handling for rich text mode --- .../wysiwyg_composer/SendWysiwygComposer.tsx | 9 +++- .../wysiwyg_composer/components/Editor.tsx | 9 +++- .../components/PlainTextComposer.tsx | 9 +++- .../components/WysiwygComposer.tsx | 9 +++- .../hooks/useComposerFunctions.ts | 3 ++ .../wysiwyg_composer/hooks/useSelection.ts | 54 +++++++++++++++++++ .../views/rooms/wysiwyg_composer/types.ts | 1 + 7 files changed, 87 insertions(+), 7 deletions(-) create mode 100644 src/components/views/rooms/wysiwyg_composer/hooks/useSelection.ts diff --git a/src/components/views/rooms/wysiwyg_composer/SendWysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/SendWysiwygComposer.tsx index a63a013cc47..ea424895d6c 100644 --- a/src/components/views/rooms/wysiwyg_composer/SendWysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/SendWysiwygComposer.tsx @@ -59,7 +59,14 @@ export function SendWysiwygComposer( className="mx_SendWysiwygComposer" leftComponent={e2eStatus && } // TODO add emoji support - rightComponent={ false} />} + rightComponent={(composerFunctions, selectPreviousSelection) => + { + selectPreviousSelection(); + setTimeout(() => composerFunctions.insertText(unicode), 100); + return true; + }} + />} {...props} > { (ref, composerFunctions) => ( diff --git a/src/components/views/rooms/wysiwyg_composer/components/Editor.tsx b/src/components/views/rooms/wysiwyg_composer/components/Editor.tsx index 6ebd189089c..4a2958cbc8e 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/Editor.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/Editor.tsx @@ -18,6 +18,7 @@ import classNames from 'classnames'; import React, { CSSProperties, forwardRef, memo, MutableRefObject, ReactNode } from 'react'; import { useIsExpanded } from '../hooks/useIsExpanded'; +import { useSelection } from '../hooks/useSelection'; const HEIGHT_BREAKING_POINT = 20; @@ -25,7 +26,7 @@ interface EditorProps { disabled: boolean; placeholder?: string; leftComponent?: ReactNode; - rightComponent?: ReactNode; + rightComponent?: (selectPreviousSelection: () => void) => ReactNode; } export const Editor = memo( @@ -33,6 +34,8 @@ export const Editor = memo( function Editor({ disabled, placeholder, leftComponent, rightComponent }: EditorProps, ref, ) { const isExpanded = useIsExpanded(ref as MutableRefObject, HEIGHT_BREAKING_POINT); + const { onFocus, onBlur, selectPreviousSelection } = + useSelection(ref as MutableRefObject); return
- { rightComponent } + { rightComponent(selectPreviousSelection) } ; }, ), diff --git a/src/components/views/rooms/wysiwyg_composer/components/PlainTextComposer.tsx b/src/components/views/rooms/wysiwyg_composer/components/PlainTextComposer.tsx index f019c2e1788..c8972f923e3 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/PlainTextComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/PlainTextComposer.tsx @@ -33,7 +33,10 @@ interface PlainTextComposerProps { initialContent?: string; className?: string; leftComponent?: ReactNode; - rightComponent?: ReactNode; + rightComponent?: ( + composerFunctions: ComposerFunctions, + selectPreviousSelection: () => void + ) => ReactNode; children?: ( ref: MutableRefObject, composerFunctions: ComposerFunctions, @@ -58,6 +61,8 @@ export function PlainTextComposer({ useSetCursorPosition(disabled, ref); const { isFocused, onFocus } = useIsFocused(); const computedPlaceholder = !content && placeholder || undefined; + const rightComp = + (selectPreviousSelection: () => void) => rightComponent(composerFunctions, selectPreviousSelection); return
- + { children?.(ref, composerFunctions) }
; } diff --git a/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx index 05afc3d3283..509218e0d5c 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx @@ -32,7 +32,10 @@ interface WysiwygComposerProps { initialContent?: string; className?: string; leftComponent?: ReactNode; - rightComponent?: ReactNode; + rightComponent?: ( + composerFunctions: FormattingFunctions, + selectPreviousSelection: () => void + ) => ReactNode; children?: ( ref: MutableRefObject, wysiwyg: FormattingFunctions, @@ -69,10 +72,12 @@ export const WysiwygComposer = memo(function WysiwygComposer( const { isFocused, onFocus } = useIsFocused(); const computedPlaceholder = !content && placeholder || undefined; + const rightComp = (selectPreviousSelection: () => void) => rightComponent(wysiwyg, selectPreviousSelection); + return (
- + { children?.(ref, wysiwyg) }
); diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useComposerFunctions.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useComposerFunctions.ts index 99a89589ee4..abfde035a5f 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useComposerFunctions.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useComposerFunctions.ts @@ -23,5 +23,8 @@ export function useComposerFunctions(ref: RefObject) { ref.current.innerHTML = ''; } }, + insertText: (text: string) => { + // TODO + }, }), [ref]); } diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useSelection.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useSelection.ts new file mode 100644 index 00000000000..48aeda1ff8f --- /dev/null +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useSelection.ts @@ -0,0 +1,54 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { RefObject, useCallback, useEffect, useRef } from "react"; + +import useFocus from "../../../../../hooks/useFocus"; + +export function useSelection(ref: RefObject) { + const selectionRef = useRef({ + anchorOffset: 0, + focusOffset: 0, + }); + const [isFocused, focusProps] = useFocus(); + + useEffect(() => { + function onSelectionChange() { + const selection = document.getSelection(); + console.log('selection', selection); + selectionRef.current = { + anchorOffset: selection.anchorOffset, + focusOffset: selection.focusOffset, + }; + } + + if (isFocused) { + document.addEventListener('selectionchange', onSelectionChange); + } + + return () => document.removeEventListener('selectionchange', onSelectionChange); + }, [isFocused]); + + const selectPreviousSelection = useCallback(() => { + const range = new Range(); + range.setStart(ref.current.firstChild, selectionRef.current.anchorOffset); + range.setEnd(ref.current.firstChild, selectionRef.current.focusOffset); + document.getSelection().removeAllRanges(); + document.getSelection().addRange(range); + }, [selectionRef, ref]); + + return { ...focusProps, selectPreviousSelection }; +} diff --git a/src/components/views/rooms/wysiwyg_composer/types.ts b/src/components/views/rooms/wysiwyg_composer/types.ts index 96095abebfd..60367933530 100644 --- a/src/components/views/rooms/wysiwyg_composer/types.ts +++ b/src/components/views/rooms/wysiwyg_composer/types.ts @@ -16,4 +16,5 @@ limitations under the License. export type ComposerFunctions = { clear: () => void; + insertText: (text: string) => void; }; From 7fcc65a3fe43278ce06eb2294a08509f5c258c87 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Fri, 2 Dec 2022 16:05:14 +0100 Subject: [PATCH 2/8] Use Event for emoji --- .../wysiwyg_composer/SendWysiwygComposer.tsx | 13 ++---- .../wysiwyg_composer/components/Emoji.tsx | 45 +++++++++++++++++++ .../components/PlainTextComposer.tsx | 5 +-- .../components/WysiwygComposer.tsx | 5 +-- .../hooks/useWysiwygSendActionHandler.ts | 14 +++++- 5 files changed, 63 insertions(+), 19 deletions(-) create mode 100644 src/components/views/rooms/wysiwyg_composer/components/Emoji.tsx diff --git a/src/components/views/rooms/wysiwyg_composer/SendWysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/SendWysiwygComposer.tsx index ea424895d6c..bec2b9a08a6 100644 --- a/src/components/views/rooms/wysiwyg_composer/SendWysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/SendWysiwygComposer.tsx @@ -22,8 +22,8 @@ import { PlainTextComposer } from './components/PlainTextComposer'; import { ComposerFunctions } from './types'; import { E2EStatus } from '../../../../utils/ShieldUtils'; import E2EIcon from '../E2EIcon'; -import { EmojiButton } from '../EmojiButton'; import { AboveLeftOf } from '../../../structures/ContextMenu'; +import { Emoji } from './components/Emoji'; interface ContentProps { disabled?: boolean; @@ -58,15 +58,8 @@ export function SendWysiwygComposer( return } - // TODO add emoji support - rightComponent={(composerFunctions, selectPreviousSelection) => - { - selectPreviousSelection(); - setTimeout(() => composerFunctions.insertText(unicode), 100); - return true; - }} - />} + rightComponent={(selectPreviousSelection) => + } {...props} > { (ref, composerFunctions) => ( diff --git a/src/components/views/rooms/wysiwyg_composer/components/Emoji.tsx b/src/components/views/rooms/wysiwyg_composer/components/Emoji.tsx new file mode 100644 index 00000000000..d8a4d04972d --- /dev/null +++ b/src/components/views/rooms/wysiwyg_composer/components/Emoji.tsx @@ -0,0 +1,45 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; + +import { AboveLeftOf } from "../../../../structures/ContextMenu"; +import { EmojiButton } from "../../EmojiButton"; +import dis from '../../../../../dispatcher/dispatcher'; +import { ComposerInsertPayload } from "../../../../../dispatcher/payloads/ComposerInsertPayload"; +import { Action } from "../../../../../dispatcher/actions"; +import { useRoomContext } from "../../../../../contexts/RoomContext"; + +interface EmojiProps { + selectPreviousSelection: () => void; + menuPosition: AboveLeftOf; +} + +export function Emoji({ selectPreviousSelection, menuPosition }: EmojiProps) { + const roomContext = useRoomContext(); + + return { + selectPreviousSelection(); + dis.dispatch({ + action: Action.ComposerInsert, + text: emoji, + timelineRenderingType: roomContext.timelineRenderingType, + }); + return true; + }} + />; +} diff --git a/src/components/views/rooms/wysiwyg_composer/components/PlainTextComposer.tsx b/src/components/views/rooms/wysiwyg_composer/components/PlainTextComposer.tsx index c8972f923e3..5339e986cda 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/PlainTextComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/PlainTextComposer.tsx @@ -34,7 +34,6 @@ interface PlainTextComposerProps { className?: string; leftComponent?: ReactNode; rightComponent?: ( - composerFunctions: ComposerFunctions, selectPreviousSelection: () => void ) => ReactNode; children?: ( @@ -61,8 +60,6 @@ export function PlainTextComposer({ useSetCursorPosition(disabled, ref); const { isFocused, onFocus } = useIsFocused(); const computedPlaceholder = !content && placeholder || undefined; - const rightComp = - (selectPreviousSelection: () => void) => rightComponent(composerFunctions, selectPreviousSelection); return
- + { children?.(ref, composerFunctions) }
; } diff --git a/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx index 509218e0d5c..c346ceb1a43 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx @@ -33,7 +33,6 @@ interface WysiwygComposerProps { className?: string; leftComponent?: ReactNode; rightComponent?: ( - composerFunctions: FormattingFunctions, selectPreviousSelection: () => void ) => ReactNode; children?: ( @@ -72,12 +71,10 @@ export const WysiwygComposer = memo(function WysiwygComposer( const { isFocused, onFocus } = useIsFocused(); const computedPlaceholder = !content && placeholder || undefined; - const rightComp = (selectPreviousSelection: () => void) => rightComponent(wysiwyg, selectPreviousSelection); - return (
- + { children?.(ref, wysiwyg) }
); diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygSendActionHandler.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygSendActionHandler.ts index 500f0270491..f2ee55ad46d 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygSendActionHandler.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygSendActionHandler.ts @@ -23,6 +23,7 @@ import { TimelineRenderingType, useRoomContext } from "../../../../../contexts/R import { useDispatcher } from "../../../../../hooks/useDispatcher"; import { focusComposer } from "./utils"; import { ComposerFunctions } from "../types"; +import { ComposerType } from "../../../../../dispatcher/payloads/ComposerInsertPayload"; export function useWysiwygSendActionHandler( disabled: boolean, @@ -48,7 +49,18 @@ export function useWysiwygSendActionHandler( composerFunctions.clear(); focusComposer(composerElement, context, roomContext, timeoutId); break; - // TODO: case Action.ComposerInsert: - see SendMessageComposer + case Action.ComposerInsert: + if (payload.timelineRenderingType !== roomContext.timelineRenderingType) break; + if (payload.composerType !== ComposerType.Send) break; + + if (payload.userId) { + // TODO insert mention - see SendMessageComposer + } else if (payload.event) { + // TODO insert quote message - see SendMessageComposer + } else if (payload.text) { + composerFunctions.insertText(payload.text); + } + break; } }, [disabled, composerElement, composerFunctions, timeoutId, roomContext]); From 5db885e3379f904fd79d41790f07c04d3e8ad2ed Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Fri, 2 Dec 2022 17:01:18 +0100 Subject: [PATCH 3/8] Support multiple lines --- .../rooms/wysiwyg_composer/components/Editor.tsx | 3 +-- .../rooms/wysiwyg_composer/hooks/useSelection.ts | 15 +++++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/components/views/rooms/wysiwyg_composer/components/Editor.tsx b/src/components/views/rooms/wysiwyg_composer/components/Editor.tsx index 4a2958cbc8e..660681f9130 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/Editor.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/Editor.tsx @@ -34,8 +34,7 @@ export const Editor = memo( function Editor({ disabled, placeholder, leftComponent, rightComponent }: EditorProps, ref, ) { const isExpanded = useIsExpanded(ref as MutableRefObject, HEIGHT_BREAKING_POINT); - const { onFocus, onBlur, selectPreviousSelection } = - useSelection(ref as MutableRefObject); + const { onFocus, onBlur, selectPreviousSelection } = useSelection(); return
) { +export function useSelection() { const selectionRef = useRef({ + anchorNode: null, anchorOffset: 0, + focusNode: null, focusOffset: 0, }); const [isFocused, focusProps] = useFocus(); @@ -28,9 +30,10 @@ export function useSelection(ref: RefObject) { useEffect(() => { function onSelectionChange() { const selection = document.getSelection(); - console.log('selection', selection); selectionRef.current = { + anchorNode: selection.anchorNode, anchorOffset: selection.anchorOffset, + focusNode: selection.focusNode, focusOffset: selection.focusOffset, }; } @@ -44,11 +47,11 @@ export function useSelection(ref: RefObject) { const selectPreviousSelection = useCallback(() => { const range = new Range(); - range.setStart(ref.current.firstChild, selectionRef.current.anchorOffset); - range.setEnd(ref.current.firstChild, selectionRef.current.focusOffset); + range.setStart(selectionRef.current.anchorNode, selectionRef.current.anchorOffset); + range.setEnd(selectionRef.current.focusNode, selectionRef.current.focusOffset); document.getSelection().removeAllRanges(); document.getSelection().addRange(range); - }, [selectionRef, ref]); + }, [selectionRef]); return { ...focusProps, selectPreviousSelection }; } From be3a66b0e605ab0481beae4ca3c1ce7b137e8741 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Mon, 5 Dec 2022 16:17:25 +0100 Subject: [PATCH 4/8] Fix edition --- .../views/rooms/wysiwyg_composer/components/Editor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/wysiwyg_composer/components/Editor.tsx b/src/components/views/rooms/wysiwyg_composer/components/Editor.tsx index 660681f9130..b738847ec68 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/Editor.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/Editor.tsx @@ -61,7 +61,7 @@ export const Editor = memo( onBlur={onBlur} />
- { rightComponent(selectPreviousSelection) } + { rightComponent?.(selectPreviousSelection) } ; }, ), From f5efa858827bd12cf4e531f6a0a667b7d31f9ce4 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Mon, 5 Dec 2022 17:40:33 +0100 Subject: [PATCH 5/8] Fix types and console.log --- .../wysiwyg_composer/hooks/useSelection.ts | 31 ++++++++++++------- .../SendWysiwygComposer-test.tsx | 1 - 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useSelection.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useSelection.ts index faddc1c1a53..62d5d1a3cbe 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useSelection.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useSelection.ts @@ -18,8 +18,10 @@ import { useCallback, useEffect, useRef } from "react"; import useFocus from "../../../../../hooks/useFocus"; +type SubSelection = Pick; + export function useSelection() { - const selectionRef = useRef({ + const selectionRef = useRef({ anchorNode: null, anchorOffset: 0, focusNode: null, @@ -30,12 +32,15 @@ export function useSelection() { useEffect(() => { function onSelectionChange() { const selection = document.getSelection(); - selectionRef.current = { - anchorNode: selection.anchorNode, - anchorOffset: selection.anchorOffset, - focusNode: selection.focusNode, - focusOffset: selection.focusOffset, - }; + + if (selection) { + selectionRef.current = { + anchorNode: selection.anchorNode, + anchorOffset: selection.anchorOffset, + focusNode: selection.focusNode, + focusOffset: selection.focusOffset, + }; + } } if (isFocused) { @@ -47,10 +52,14 @@ export function useSelection() { const selectPreviousSelection = useCallback(() => { const range = new Range(); - range.setStart(selectionRef.current.anchorNode, selectionRef.current.anchorOffset); - range.setEnd(selectionRef.current.focusNode, selectionRef.current.focusOffset); - document.getSelection().removeAllRanges(); - document.getSelection().addRange(range); + const selection = selectionRef.current; + + if (selection.anchorNode && selection.focusNode) { + range.setStart(selection.anchorNode, selectionRef.current.anchorOffset); + range.setEnd(selection.focusNode, selectionRef.current.focusOffset); + document.getSelection()?.removeAllRanges(); + document.getSelection()?.addRange(range); + } }, [selectionRef]); return { ...focusProps, selectPreviousSelection }; diff --git a/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx index e51bd3bc6ca..bd080331a9d 100644 --- a/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx @@ -177,7 +177,6 @@ describe('SendWysiwygComposer', () => { it('Should not has placeholder', async () => { // When - console.log('here'); customRender(jest.fn(), jest.fn(), false, isRichTextEnabled); await waitFor(() => expect(screen.getByRole('textbox')).toHaveAttribute('contentEditable', "true")); From 29f9ccfb633dd2bac9082fff9016b90945a77b53 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Tue, 6 Dec 2022 13:49:26 +0100 Subject: [PATCH 6/8] Update matrix-wysiwyg dependency --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 110d4fc6ebc..4594988e6aa 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "dependencies": { "@babel/runtime": "^7.12.5", "@matrix-org/analytics-events": "^0.3.0", - "@matrix-org/matrix-wysiwyg": "^0.8.0", + "@matrix-org/matrix-wysiwyg": "^0.9.0", "@matrix-org/react-sdk-module-api": "^0.0.3", "@sentry/browser": "^7.0.0", "@sentry/tracing": "^7.0.0", diff --git a/yarn.lock b/yarn.lock index db7e293f8d4..bd158677a65 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1520,10 +1520,10 @@ resolved "https://registry.yarnpkg.com/@matrix-org/analytics-events/-/analytics-events-0.3.0.tgz#a428f7e3f164ffadf38f35bc0f0f9a3e47369ce6" integrity sha512-f1WIMA8tjNB3V5g1C34yIpIJK47z6IJ4SLiY4j+J9Gw4X8C3TKGTAx563rMcMvW3Uk/PFqnIBXtkavHBXoYJ9A== -"@matrix-org/matrix-wysiwyg@^0.8.0": - version "0.8.0" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-wysiwyg/-/matrix-wysiwyg-0.8.0.tgz#3b64c6a16cf2027e395766c950c13752b1a81282" - integrity sha512-q3lpMNbD/GF2RPOuDR3COYDGR6BQWZBHUPtRYGaDf1i9eL/8vWD/WruwjzpI/RwNbYyPDm9Cs6vZj9BNhHB3Jw== +"@matrix-org/matrix-wysiwyg@^0.9.0": + version "0.9.0" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-wysiwyg/-/matrix-wysiwyg-0.9.0.tgz#8651eacdc0bbfa313501e4feeb713c74dbf099cc" + integrity sha512-utxLZPSmBR/oKFeLLteAfqprhSW8prrH9IKzeMK1VswQYganPusYYO8u86kCQt4SuDz/1Zc8C7r76xmOiVJ9JQ== "@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz": version "3.2.8" From 27139ca68eb075a4438c18fca184887002a4ffbc Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Tue, 6 Dec 2022 16:38:25 +0100 Subject: [PATCH 7/8] Add test for emoji --- .../wysiwyg_composer/hooks/useSelection.ts | 11 +-- .../rooms/wysiwyg_composer/utils/selection.ts | 29 +++++++ .../SendWysiwygComposer-test.tsx | 77 +++++++++++++++++++ 3 files changed, 108 insertions(+), 9 deletions(-) create mode 100644 src/components/views/rooms/wysiwyg_composer/utils/selection.ts diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useSelection.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useSelection.ts index 62d5d1a3cbe..2ae61790dbf 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useSelection.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useSelection.ts @@ -17,6 +17,7 @@ limitations under the License. import { useCallback, useEffect, useRef } from "react"; import useFocus from "../../../../../hooks/useFocus"; +import { setSelection } from "../utils/selection"; type SubSelection = Pick; @@ -51,15 +52,7 @@ export function useSelection() { }, [isFocused]); const selectPreviousSelection = useCallback(() => { - const range = new Range(); - const selection = selectionRef.current; - - if (selection.anchorNode && selection.focusNode) { - range.setStart(selection.anchorNode, selectionRef.current.anchorOffset); - range.setEnd(selection.focusNode, selectionRef.current.focusOffset); - document.getSelection()?.removeAllRanges(); - document.getSelection()?.addRange(range); - } + setSelection(selectionRef.current); }, [selectionRef]); return { ...focusProps, selectPreviousSelection }; diff --git a/src/components/views/rooms/wysiwyg_composer/utils/selection.ts b/src/components/views/rooms/wysiwyg_composer/utils/selection.ts new file mode 100644 index 00000000000..9e1ae0424e8 --- /dev/null +++ b/src/components/views/rooms/wysiwyg_composer/utils/selection.ts @@ -0,0 +1,29 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export function setSelection(selection: + Pick, +) { + if (selection.anchorNode && selection.focusNode) { + const range = new Range(); + range.setStart(selection.anchorNode, selection.anchorOffset); + range.setEnd(selection.focusNode, selection.focusOffset); + + document.getSelection()?.removeAllRanges(); + document.getSelection()?.addRange(range); + } +} + diff --git a/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx index bd080331a9d..d5611f39fff 100644 --- a/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx @@ -26,6 +26,14 @@ import { IRoomState } from "../../../../../src/components/structures/RoomView"; import { createTestClient, flushPromises, getRoomContext, mkEvent, mkStubRoom } from "../../../../test-utils"; import { SendWysiwygComposer } from "../../../../../src/components/views/rooms/wysiwyg_composer"; import { aboveLeftOf } from "../../../../../src/components/structures/ContextMenu"; +import { ComposerInsertPayload, ComposerType } from "../../../../../src/dispatcher/payloads/ComposerInsertPayload"; +import { setSelection } from "../../../../../src/components/views/rooms/wysiwyg_composer/utils/selection"; + +jest.mock("../../../../../src/components/views/rooms/EmojiButton", () => ({ + EmojiButton: ({ addEmoji }: {addEmoji: (emoji: string) => void}) => { + return ; + }, +})); describe('SendWysiwygComposer', () => { afterEach(() => { @@ -47,6 +55,25 @@ describe('SendWysiwygComposer', () => { const defaultRoomContext: IRoomState = getRoomContext(mockRoom, {}); + const registerId = defaultDispatcher.register((payload) => { + switch (payload.action) { + case Action.ComposerInsert: { + if (payload.composerType) break; + + // re-dispatch to the correct composer + defaultDispatcher.dispatch({ + ...(payload as ComposerInsertPayload), + composerType: ComposerType.Send, + }); + break; + } + } + }); + + afterAll(() => { + defaultDispatcher.unregister(registerId); + }); + const customRender = ( onChange = (_content: string) => void 0, onSend = () => void 0, @@ -221,5 +248,55 @@ describe('SendWysiwygComposer', () => { ); }); }); + + describe.each([ + { isRichTextEnabled: true }, + // TODO { isRichTextEnabled: false }, + ])('Emoji when %s', ({ isRichTextEnabled }) => { + let emojiButton: HTMLElement; + + beforeEach(async () => { + customRender(jest.fn(), jest.fn(), false, isRichTextEnabled); + await waitFor(() => expect(screen.getByRole('textbox')).toHaveAttribute('contentEditable', "true")); + emojiButton = screen.getByLabelText('Emoji'); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('Should add an emoji in an empty composer', async () => { + // When + emojiButton.click(); + + // Then + await waitFor(() => expect(screen.getByRole('textbox')).toHaveTextContent(/🦫/)); + }); + + it('Should add an emoji in the middle of a word', async () => { + // When + screen.getByRole('textbox').focus(); + screen.getByRole('textbox').innerHTML = 'word'; + fireEvent.input(screen.getByRole('textbox'), { + data: 'word', + inputType: 'insertText', + }); + + const textNode = screen.getByRole('textbox').firstChild; + setSelection({ + anchorNode: textNode, + anchorOffset: 2, + focusNode: textNode, + focusOffset: 2, + }); + // the event is not automatically fired by jest + document.dispatchEvent(new CustomEvent('selectionchange')); + + emojiButton.click(); + + // Then + await waitFor(() => expect(screen.getByRole('textbox')).toHaveTextContent(/wo🦫rd/)); + }); + }); }); From bc001c2b883d3ccc0a215598a94ef35bddb7d94f Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Tue, 6 Dec 2022 16:45:25 +0100 Subject: [PATCH 8/8] Fix types --- .../views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx index d5611f39fff..1b28c6ed2e9 100644 --- a/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx @@ -75,8 +75,8 @@ describe('SendWysiwygComposer', () => { }); const customRender = ( - onChange = (_content: string) => void 0, - onSend = () => void 0, + onChange = (_content: string): void => void 0, + onSend = (): void => void 0, disabled = false, isRichTextEnabled = true, placeholder?: string) => {