From 460f60e99d447fe1e47b14f6e223acbf1413c4c0 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 19 Oct 2022 12:45:51 +0200 Subject: [PATCH 01/14] First attempt to make the edition works in the WysiwygComposer --- src/components/views/messages/TextualBody.tsx | 6 +- .../views/rooms/BasicMessageComposer.tsx | 2 + .../views/rooms/EditMessageComposer.tsx | 1 + .../views/rooms/MessageComposer.tsx | 34 ++-- .../wysiwyg_composer/EditWysiwygComposer.tsx | 105 ++++++++++++ .../wysiwyg_composer/SendWysiwygComposer.tsx | 46 ++++++ .../wysiwyg_composer/WysiwygComposer.tsx | 68 -------- .../components/EditionButtons.tsx | 36 ++++ .../{ => components}/Editor.tsx | 0 .../{ => components}/FormattingButtons.tsx | 13 +- .../components/WysiwygComposer.tsx | 49 ++++++ .../hooks/useWysiwygEditActionHandler.ts | 48 ++++++ .../hooks/useWysiwygSendActionHandler.ts | 56 +++++++ .../utils.ts} | 36 +--- .../views/rooms/wysiwyg_composer/index.ts | 19 +++ .../views/rooms/wysiwyg_composer/types.ts | 21 +++ .../utils/createMessageContent.ts | 117 +++++++++++++ .../rooms/wysiwyg_composer/utils/editing.ts | 50 ++++++ .../utils/isContentModified.ts | 30 ++++ .../wysiwyg_composer/{ => utils}/message.ts | 155 +++++++++--------- src/dispatcher/actions.ts | 5 + .../views/rooms/MessageComposer-test.tsx | 2 +- .../FormattingButtons-test.tsx | 2 +- .../wysiwyg_composer/WysiwygComposer-test.tsx | 2 +- .../rooms/wysiwyg_composer/message-test.ts | 2 +- 25 files changed, 705 insertions(+), 200 deletions(-) create mode 100644 src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx create mode 100644 src/components/views/rooms/wysiwyg_composer/SendWysiwygComposer.tsx delete mode 100644 src/components/views/rooms/wysiwyg_composer/WysiwygComposer.tsx create mode 100644 src/components/views/rooms/wysiwyg_composer/components/EditionButtons.tsx rename src/components/views/rooms/wysiwyg_composer/{ => components}/Editor.tsx (100%) rename src/components/views/rooms/wysiwyg_composer/{ => components}/FormattingButtons.tsx (87%) create mode 100644 src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx create mode 100644 src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygEditActionHandler.ts create mode 100644 src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygSendActionHandler.ts rename src/components/views/rooms/wysiwyg_composer/{useWysiwygActionHandler.ts => hooks/utils.ts} (52%) create mode 100644 src/components/views/rooms/wysiwyg_composer/index.ts create mode 100644 src/components/views/rooms/wysiwyg_composer/types.ts create mode 100644 src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts create mode 100644 src/components/views/rooms/wysiwyg_composer/utils/editing.ts create mode 100644 src/components/views/rooms/wysiwyg_composer/utils/isContentModified.ts rename src/components/views/rooms/wysiwyg_composer/{ => utils}/message.ts (57%) diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 23ba901acdf..983cbe51e37 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -48,6 +48,7 @@ import RoomContext from "../../../contexts/RoomContext"; import AccessibleButton from '../elements/AccessibleButton'; import { options as linkifyOpts } from "../../../linkify-matrix"; import { getParentEventId } from '../../../utils/Reply'; +import { EditWysiwygComposer } from '../rooms/wysiwyg_composer'; const MAX_HIGHLIGHT_LENGTH = 4096; @@ -562,7 +563,10 @@ export default class TextualBody extends React.Component { render() { if (this.props.editState) { - return ; + const isWysiwygComposerEnabled = SettingsStore.getValue("feature_wysiwyg_composer"); + return isWysiwygComposerEnabled ? + : + ; } const mxEvent = this.props.mxEvent; const content = mxEvent.getContent(); diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index d74c7b51484..962059091cc 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -833,6 +833,8 @@ export default class BasicMessageEditor extends React.Component } public insertPlaintext(text: string): void { + console.log('insertPlaintext', text); + debugger; this.modifiedFlag = true; const { model } = this.props; const { partCreator } = model; diff --git a/src/components/views/rooms/EditMessageComposer.tsx b/src/components/views/rooms/EditMessageComposer.tsx index 52312e1a998..bb014541278 100644 --- a/src/components/views/rooms/EditMessageComposer.tsx +++ b/src/components/views/rooms/EditMessageComposer.tsx @@ -350,6 +350,7 @@ class EditMessageComposer extends React.Component { +class MessageComposer extends React.Component { private dispatcherRef?: string; private messageComposerInput = createRef(); private voiceRecordingButton = createRef(); private ref: React.RefObject = createRef(); private instanceId: number; - private composerSendMessage?: () => void; private _voiceRecording: Optional; @@ -124,6 +125,7 @@ export default class MessageComposer extends React.Component { this.state = { isComposerEmpty: true, + composerContent: '', haveRecording: false, recordingTimeLeftSeconds: undefined, // when set to a number, shows a toast isMenuOpen: false, @@ -315,7 +317,15 @@ export default class MessageComposer extends React.Component { } this.messageComposerInput.current?.sendMessage(); - this.composerSendMessage?.(); + // this.composerSendMessage?.(); + const isWysiwygComposerEnabled = SettingsStore.getValue("feature_wysiwyg_composer"); + + if (isWysiwygComposerEnabled) { + const { permalinkCreator, relation, replyToEvent } = this.props; + sendMessage(this.state.composerContent, + { mxClient: this.props.mxClient, roomContext: this.context, permalinkCreator, relation, replyToEvent }); + dis.dispatch({ action: Action.ClearAndFocusSendMessageComposer }); + } }; private onChange = (model: EditorModel) => { @@ -326,6 +336,7 @@ export default class MessageComposer extends React.Component { private onWysiwygChange = (content: string) => { this.setState({ + composerContent: content, isComposerEmpty: content?.length === 0, }); }; @@ -406,16 +417,10 @@ export default class MessageComposer extends React.Component { if (canSendMessages) { if (isWysiwygComposerEnabled) { controls.push( - - { (sendMessage) => { - this.composerSendMessage = sendMessage; - } } - , + />, ); } else { controls.push( @@ -555,3 +560,6 @@ export default class MessageComposer extends React.Component { ); } } + +const MessageComposerWithMatrixClient = withMatrixClientHOC(MessageComposer); +export default MessageComposerWithMatrixClient; diff --git a/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx new file mode 100644 index 00000000000..17b664410ce --- /dev/null +++ b/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx @@ -0,0 +1,105 @@ +/* +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, { forwardRef, RefObject, useMemo } from 'react'; +import { MatrixClient } from "matrix-js-sdk/src/matrix"; + +import { useRoomContext } from '../../../../contexts/RoomContext'; +import { useMatrixClientContext } from '../../../../contexts/MatrixClientContext'; +import EditorStateTransfer from '../../../../utils/EditorStateTransfer'; +import { CommandPartCreator, Part } from '../../../../editor/parts'; +import { IRoomState } from '../../../structures/RoomView'; +import SettingsStore from '../../../../settings/SettingsStore'; +import { parseEvent } from '../../../../editor/deserialize'; +import { WysiwygComposer } from './components/WysiwygComposer'; +import { EditionButtons } from './components/EditionButtons'; +import { useWysiwygEditActionHandler } from './hooks/useWysiwygEditActionHandler'; +import { endEditing } from './utils/editing'; +import { editMessage } from './utils/message'; + +function parseEditorStateTransfer( + editorStateTransfer: EditorStateTransfer, + roomContext: IRoomState, + mxClient: MatrixClient, +) { + if (!roomContext.room) { + return; + } + + const { room } = roomContext; + + const partCreator = new CommandPartCreator(room, mxClient); + + let parts: Part[]; + if (editorStateTransfer.hasEditorState()) { + // if restoring state from a previous editor, + // restore serialized parts from the state + parts = editorStateTransfer.getSerializedParts().map(p => partCreator.deserializePart(p)); + } else { + // otherwise, either restore serialized parts from localStorage or parse the body of the event + // TODO local storage + // const restoredParts = this.restoreStoredEditorState(partCreator); + + if (editorStateTransfer.getEvent().getContent().format === 'org.matrix.custom.html') { + return editorStateTransfer.getEvent().getContent().formatted_body || ""; + } + + parts = parseEvent(editorStateTransfer.getEvent(), partCreator, { + shouldEscape: SettingsStore.getValue("MessageComposerInput.useMarkdown"), + }); + } + + return parts.reduce((content, part) => content + part.text, ''); + // Todo local storage + // this.saveStoredEditorState(); +} + +interface ContentProps { + disabled: boolean; +} + +const Content = forwardRef( + function Content({ disabled }: ContentProps, forwardRef: RefObject) { + useWysiwygEditActionHandler(disabled, forwardRef); + return null; + }, +); + +interface EditWysiwygComposerProps { + disabled?: boolean; + onChange?: (content: string) => void; + editorStateTransfer?: EditorStateTransfer; +} + +export function EditWysiwygComposer({ editorStateTransfer, ...props }: EditWysiwygComposerProps) { + const roomContext = useRoomContext(); + const mxClient = useMatrixClientContext(); + + const initialContent = useMemo(() => { + if (editorStateTransfer) { + return parseEditorStateTransfer(editorStateTransfer, roomContext, mxClient); + } + }, [editorStateTransfer, roomContext, mxClient]); + const isReady = !editorStateTransfer || Boolean(initialContent); + + return isReady && { (ref, wysiwyg, content) => ( + <> + + endEditing(roomContext)} onSaveClick={() => editMessage(content, { roomContext, mxClient, editorStateTransfer })} /> + ) + } + ; +} diff --git a/src/components/views/rooms/wysiwyg_composer/SendWysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/SendWysiwygComposer.tsx new file mode 100644 index 00000000000..577374e116f --- /dev/null +++ b/src/components/views/rooms/wysiwyg_composer/SendWysiwygComposer.tsx @@ -0,0 +1,46 @@ +/* +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, { forwardRef, RefObject } from 'react'; + +import { useWysiwygSendActionHandler } from './hooks/useWysiwygSendActionHandler'; +import { WysiwygComposer } from './components/WysiwygComposer'; +import { Wysiwyg } from './types'; + +interface SendWysiwygComposerProps { + disabled?: boolean; + onChange?: (content: string) => void; +} + +export function SendWysiwygComposer(props: SendWysiwygComposerProps) { + return ( + { (ref, wysiwyg) => ( + + ) } + ); +} + +interface ContentProps { + disabled: boolean; + wysiwyg: Wysiwyg; +} + +const Content = forwardRef( + function Content({ disabled, wysiwyg }: ContentProps, forwardRef: RefObject) { + useWysiwygSendActionHandler(disabled, forwardRef, wysiwyg); + return null; + }, +); diff --git a/src/components/views/rooms/wysiwyg_composer/WysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/WysiwygComposer.tsx deleted file mode 100644 index 8701f5be778..00000000000 --- a/src/components/views/rooms/wysiwyg_composer/WysiwygComposer.tsx +++ /dev/null @@ -1,68 +0,0 @@ -/* -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, { useCallback, useEffect } from 'react'; -import { IEventRelation, MatrixEvent } from 'matrix-js-sdk/src/models/event'; -import { useWysiwyg } from "@matrix-org/matrix-wysiwyg"; - -import { Editor } from './Editor'; -import { FormattingButtons } from './FormattingButtons'; -import { RoomPermalinkCreator } from '../../../../utils/permalinks/Permalinks'; -import { sendMessage } from './message'; -import { useMatrixClientContext } from '../../../../contexts/MatrixClientContext'; -import { useRoomContext } from '../../../../contexts/RoomContext'; -import { useWysiwygActionHandler } from './useWysiwygActionHandler'; - -interface WysiwygProps { - disabled?: boolean; - onChange: (content: string) => void; - relation?: IEventRelation; - replyToEvent?: MatrixEvent; - permalinkCreator: RoomPermalinkCreator; - includeReplyLegacyFallback?: boolean; - children?: (sendMessage: () => void) => void; -} - -export function WysiwygComposer( - { disabled = false, onChange, children, ...props }: WysiwygProps, -) { - const roomContext = useRoomContext(); - const mxClient = useMatrixClientContext(); - - const { ref, isWysiwygReady, content, formattingStates, wysiwyg } = useWysiwyg(); - - useEffect(() => { - if (!disabled && content !== null) { - onChange(content); - } - }, [onChange, content, disabled]); - - const memoizedSendMessage = useCallback(() => { - sendMessage(content, { mxClient, roomContext, ...props }); - wysiwyg.clear(); - ref.current?.focus(); - }, [content, mxClient, roomContext, wysiwyg, props, ref]); - - useWysiwygActionHandler(disabled, ref); - - return ( -
- - - { children?.(memoizedSendMessage) } -
- ); -} diff --git a/src/components/views/rooms/wysiwyg_composer/components/EditionButtons.tsx b/src/components/views/rooms/wysiwyg_composer/components/EditionButtons.tsx new file mode 100644 index 00000000000..20a3df2a7f7 --- /dev/null +++ b/src/components/views/rooms/wysiwyg_composer/components/EditionButtons.tsx @@ -0,0 +1,36 @@ +/* +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, { MouseEventHandler } from 'react'; + +import { _t } from '../../../../../languageHandler'; +import AccessibleButton from '../../../elements/AccessibleButton'; + +interface EditionButtonsProps { + onCancelClick: MouseEventHandler; + onSaveClick: MouseEventHandler; +} + +export function EditionButtons({ onCancelClick, onSaveClick }: EditionButtonsProps) { + return
+ + { _t("Cancel") } + + + { _t("Save") } + +
; +} diff --git a/src/components/views/rooms/wysiwyg_composer/Editor.tsx b/src/components/views/rooms/wysiwyg_composer/components/Editor.tsx similarity index 100% rename from src/components/views/rooms/wysiwyg_composer/Editor.tsx rename to src/components/views/rooms/wysiwyg_composer/components/Editor.tsx diff --git a/src/components/views/rooms/wysiwyg_composer/FormattingButtons.tsx b/src/components/views/rooms/wysiwyg_composer/components/FormattingButtons.tsx similarity index 87% rename from src/components/views/rooms/wysiwyg_composer/FormattingButtons.tsx rename to src/components/views/rooms/wysiwyg_composer/components/FormattingButtons.tsx index 19941ad3f94..c806c278615 100644 --- a/src/components/views/rooms/wysiwyg_composer/FormattingButtons.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/FormattingButtons.tsx @@ -18,11 +18,12 @@ import React, { MouseEventHandler } from "react"; import { useWysiwyg } from "@matrix-org/matrix-wysiwyg"; import classNames from "classnames"; -import AccessibleTooltipButton from "../../elements/AccessibleTooltipButton"; -import { Alignment } from "../../elements/Tooltip"; -import { KeyboardShortcut } from "../../settings/KeyboardShortcut"; -import { KeyCombo } from "../../../../KeyBindingsManager"; -import { _td } from "../../../../languageHandler"; +import AccessibleTooltipButton from "../../../elements/AccessibleTooltipButton"; +import { Alignment } from "../../../elements/Tooltip"; +import { KeyboardShortcut } from "../../../settings/KeyboardShortcut"; +import { KeyCombo } from "../../../../../KeyBindingsManager"; +import { _td } from "../../../../../languageHandler"; +import { Wysiwyg } from "../types"; interface TooltipProps { label: string; @@ -55,7 +56,7 @@ function Button({ label, keyCombo, onClick, isActive, className }: ButtonProps) } interface FormattingButtonsProps { - composer: ReturnType['wysiwyg']; + composer: Wysiwyg; formattingStates: ReturnType['formattingStates']; } diff --git a/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx new file mode 100644 index 00000000000..3e63c35fe35 --- /dev/null +++ b/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx @@ -0,0 +1,49 @@ +/* +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, { MutableRefObject, ReactNode, useEffect } from 'react'; +import { useWysiwyg } from "@matrix-org/matrix-wysiwyg"; + +import { FormattingButtons } from './FormattingButtons'; +import { Editor } from './Editor'; +import { Wysiwyg } from '../types'; + +interface WysiwygComposerProps { + disabled?: boolean; + onChange?: (content: string) => void; + initialContent?: string; + children?: (ref: MutableRefObject, wysiwyg: Wysiwyg, content: string) => ReactNode; +} + +export function WysiwygComposer( + { disabled = false, onChange, initialContent, children }: WysiwygComposerProps, +) { + const { ref, isWysiwygReady, content, formattingStates, wysiwyg } = useWysiwyg({ initialContent }); + + useEffect(() => { + if (!disabled && content !== null) { + onChange?.(content); + } + }, [onChange, content, disabled]); + + return ( +
+ + + { children?.(ref, wysiwyg, content) } +
+ ); +} diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygEditActionHandler.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygEditActionHandler.ts new file mode 100644 index 00000000000..2cbd7cf52cc --- /dev/null +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygEditActionHandler.ts @@ -0,0 +1,48 @@ +/* +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, useRef } from "react"; + +import defaultDispatcher from "../../../../../dispatcher/dispatcher"; +import { Action } from "../../../../../dispatcher/actions"; +import { ActionPayload } from "../../../../../dispatcher/payloads"; +import { TimelineRenderingType, useRoomContext } from "../../../../../contexts/RoomContext"; +import { useDispatcher } from "../../../../../hooks/useDispatcher"; +import { focusComposer } from "./utils"; + +export function useWysiwygEditActionHandler( + disabled: boolean, + composerElement: RefObject, +) { + const roomContext = useRoomContext(); + const timeoutId = useRef(); + + const handler = useCallback((payload: ActionPayload) => { + // don't let the user into the composer if it is disabled - all of these branches lead + // to the cursor being in the composer + if (disabled || !composerElement.current) return; + + const context = payload.context ?? TimelineRenderingType.Room; + + switch (payload.action) { + case Action.FocusSendMessageComposer: + focusComposer(composerElement, context, roomContext, timeoutId); + break; + } + }, [disabled, composerElement, timeoutId, roomContext]); + + 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 new file mode 100644 index 00000000000..41169a4e2d3 --- /dev/null +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygSendActionHandler.ts @@ -0,0 +1,56 @@ +/* +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, useRef } from "react"; + +import defaultDispatcher from "../../../../../dispatcher/dispatcher"; +import { Action } from "../../../../../dispatcher/actions"; +import { ActionPayload } from "../../../../../dispatcher/payloads"; +import { TimelineRenderingType, useRoomContext } from "../../../../../contexts/RoomContext"; +import { useDispatcher } from "../../../../../hooks/useDispatcher"; +import { Wysiwyg } from "../types"; +import { focusComposer } from "./utils"; + +export function useWysiwygSendActionHandler( + disabled: boolean, + composerElement: RefObject, + wysiwyg: Wysiwyg, +) { + const roomContext = useRoomContext(); + const timeoutId = useRef(); + + const handler = useCallback((payload: ActionPayload) => { + // don't let the user into the composer if it is disabled - all of these branches lead + // to the cursor being in the composer + if (disabled || !composerElement.current) return; + + const context = payload.context ?? TimelineRenderingType.Room; + + switch (payload.action) { + case "reply_to_event": + case Action.FocusSendMessageComposer: + focusComposer(composerElement, context, roomContext, timeoutId); + break; + case Action.ClearAndFocusSendMessageComposer: + wysiwyg.clear(); + focusComposer(composerElement, context, roomContext, timeoutId); + break; + // TODO: case Action.ComposerInsert: - see SendMessageComposer + } + }, [disabled, composerElement, wysiwyg, timeoutId, roomContext]); + + useDispatcher(defaultDispatcher, handler); +} diff --git a/src/components/views/rooms/wysiwyg_composer/useWysiwygActionHandler.ts b/src/components/views/rooms/wysiwyg_composer/hooks/utils.ts similarity index 52% rename from src/components/views/rooms/wysiwyg_composer/useWysiwygActionHandler.ts rename to src/components/views/rooms/wysiwyg_composer/hooks/utils.ts index 683498d485e..eab855e0868 100644 --- a/src/components/views/rooms/wysiwyg_composer/useWysiwygActionHandler.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/utils.ts @@ -14,40 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { useRef } from "react"; +import { TimelineRenderingType } from "../../../../../contexts/RoomContext"; +import { IRoomState } from "../../../../structures/RoomView"; -import defaultDispatcher from "../../../../dispatcher/dispatcher"; -import { Action } from "../../../../dispatcher/actions"; -import { ActionPayload } from "../../../../dispatcher/payloads"; -import { IRoomState } from "../../../structures/RoomView"; -import { TimelineRenderingType, useRoomContext } from "../../../../contexts/RoomContext"; -import { useDispatcher } from "../../../../hooks/useDispatcher"; - -export function useWysiwygActionHandler( - disabled: boolean, - composerElement: React.MutableRefObject, -) { - const roomContext = useRoomContext(); - const timeoutId = useRef(); - - useDispatcher(defaultDispatcher, (payload: ActionPayload) => { - // don't let the user into the composer if it is disabled - all of these branches lead - // to the cursor being in the composer - if (disabled) return; - - const context = payload.context ?? TimelineRenderingType.Room; - - switch (payload.action) { - case "reply_to_event": - case Action.FocusSendMessageComposer: - focusComposer(composerElement, context, roomContext, timeoutId); - break; - // TODO: case Action.ComposerInsert: - see SendMessageComposer - } - }); -} - -function focusComposer( +export function focusComposer( composerElement: React.MutableRefObject, renderingType: TimelineRenderingType, roomContext: IRoomState, diff --git a/src/components/views/rooms/wysiwyg_composer/index.ts b/src/components/views/rooms/wysiwyg_composer/index.ts new file mode 100644 index 00000000000..ec8c9cff232 --- /dev/null +++ b/src/components/views/rooms/wysiwyg_composer/index.ts @@ -0,0 +1,19 @@ +/* +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 { SendWysiwygComposer } from './SendWysiwygComposer'; +export { EditWysiwygComposer } from './EditWysiwygComposer'; +export { sendMessage } from './utils/message'; diff --git a/src/components/views/rooms/wysiwyg_composer/types.ts b/src/components/views/rooms/wysiwyg_composer/types.ts new file mode 100644 index 00000000000..6c57ce6a86e --- /dev/null +++ b/src/components/views/rooms/wysiwyg_composer/types.ts @@ -0,0 +1,21 @@ +/* +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 { useWysiwyg } from "@matrix-org/matrix-wysiwyg"; + +// TODO +// Change when the matrix-wysiwyg typescript definition will be refined +export type Wysiwyg = ReturnType['wysiwyg']; diff --git a/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts b/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts new file mode 100644 index 00000000000..fe7f7706b4f --- /dev/null +++ b/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts @@ -0,0 +1,117 @@ +/* +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 { IContent, IEventRelation, MatrixEvent, MsgType } from "matrix-js-sdk/src/matrix"; + +import { RoomPermalinkCreator } from "../../../../../utils/permalinks/Permalinks"; +import { addReplyToMessageContent } from "../../../../../utils/Reply"; + +// Merges favouring the given relation +function attachRelation(content: IContent, relation?: IEventRelation): void { + if (relation) { + content['m.relates_to'] = { + ...(content['m.relates_to'] || {}), + ...relation, + }; + } +} + +function getHtmlReplyFallback(mxEvent: MatrixEvent): string { + const html = mxEvent.getContent().formatted_body; + if (!html) { + return ""; + } + const rootNode = new DOMParser().parseFromString(html, "text/html").body; + const mxReply = rootNode.querySelector("mx-reply"); + return (mxReply && mxReply.outerHTML) || ""; +} + +interface CreateMessageContentParams { + relation?: IEventRelation; + replyToEvent?: MatrixEvent; + permalinkCreator?: RoomPermalinkCreator; + includeReplyLegacyFallback?: boolean; + editedEvent?: MatrixEvent; +} + +export function createMessageContent( + message: string, + { relation, replyToEvent, permalinkCreator, includeReplyLegacyFallback = true, editedEvent }: + CreateMessageContentParams, +): IContent { + // TODO emote ? + + const isReply = Boolean(replyToEvent?.replyEventId); + const isEditing = Boolean(editedEvent); + + /*const isEmote = containsEmote(model); + if (isEmote) { + model = stripEmoteCommand(model); + } + if (startsWith(model, "//")) { + model = stripPrefix(model, "/"); + } + model = unescapeMessage(model);*/ + + // const body = textSerialize(model); + const body = message; + + const content: IContent = { + // TODO emote + // msgtype: isEmote ? "m.emote" : "m.text", + msgtype: MsgType.Text, + body: body, + }; + + // TODO markdown support + + /*const formattedBody = htmlSerializeIfNeeded(model, { + forceHTML: !!replyToEvent, + useMarkdown: SettingsStore.getValue("MessageComposerInput.useMarkdown"), + });*/ + const formattedBody = message; + + if (formattedBody) { + content.format = "org.matrix.custom.html"; + + const htmlPrefix = isReply ? getHtmlReplyFallback(editedEvent) : ''; + content.formatted_body = isEditing ? `${htmlPrefix} * ${formattedBody}` : formattedBody; + + if (isEditing) { + content['m.new_content'] = { + "msgtype": content.msgtype, + "body": body, + "format": "org.matrix.custom.html", + 'formatted_body': formattedBody, + }; + } + } + + const newRelation = isEditing ? + { ...relation, 'rel_type': 'm.replace', 'event_id': editedEvent.getId() } + : relation; + + attachRelation(content, newRelation); + + if (!isEditing && replyToEvent && permalinkCreator) { + addReplyToMessageContent(content, replyToEvent, { + permalinkCreator, + includeLegacyFallback: includeReplyLegacyFallback, + }); + } + + return content; +} diff --git a/src/components/views/rooms/wysiwyg_composer/utils/editing.ts b/src/components/views/rooms/wysiwyg_composer/utils/editing.ts new file mode 100644 index 00000000000..a0cb6083830 --- /dev/null +++ b/src/components/views/rooms/wysiwyg_composer/utils/editing.ts @@ -0,0 +1,50 @@ +/* +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 { EventStatus, MatrixClient } from "matrix-js-sdk/src/matrix"; + +import { IRoomState } from "../../../../structures/RoomView"; +import dis from '../../../../../dispatcher/dispatcher'; +import { Action } from "../../../../../dispatcher/actions"; +import EditorStateTransfer from "../../../../../utils/EditorStateTransfer"; + +export function endEditing(roomContext: IRoomState) { + // todo local storage + // localStorage.removeItem(this.editorRoomKey); + // localStorage.removeItem(this.editorStateKey); + + // close the event editing and focus composer + dis.dispatch({ + action: Action.EditEvent, + event: null, + timelineRenderingType: roomContext.timelineRenderingType, + }); + dis.dispatch({ + action: Action.FocusSendMessageComposer, + context: roomContext.timelineRenderingType, + }); +} + +export function cancelPreviousPendingEdit(mxClient: MatrixClient, editorStateTransfer: EditorStateTransfer) { + const originalEvent = editorStateTransfer.getEvent(); + const previousEdit = originalEvent.replacingEvent(); + if (previousEdit && ( + previousEdit.status === EventStatus.QUEUED || + previousEdit.status === EventStatus.NOT_SENT + )) { + mxClient.cancelPendingEvent(previousEdit); + } +} diff --git a/src/components/views/rooms/wysiwyg_composer/utils/isContentModified.ts b/src/components/views/rooms/wysiwyg_composer/utils/isContentModified.ts new file mode 100644 index 00000000000..88715dda385 --- /dev/null +++ b/src/components/views/rooms/wysiwyg_composer/utils/isContentModified.ts @@ -0,0 +1,30 @@ +/* +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 { IContent } from "matrix-js-sdk/src/matrix"; + +import EditorStateTransfer from "../../../../../utils/EditorStateTransfer"; + +export function isContentModified(newContent: IContent, editorStateTransfer: EditorStateTransfer): boolean { + // if nothing has changed then bail + const oldContent = editorStateTransfer.getEvent().getContent(); + if (oldContent["msgtype"] === newContent["msgtype"] && oldContent["body"] === newContent["body"] && + oldContent["format"] === newContent["format"] && + oldContent["formatted_body"] === newContent["formatted_body"]) { + return false; + } + return true; +} diff --git a/src/components/views/rooms/wysiwyg_composer/message.ts b/src/components/views/rooms/wysiwyg_composer/utils/message.ts similarity index 57% rename from src/components/views/rooms/wysiwyg_composer/message.ts rename to src/components/views/rooms/wysiwyg_composer/utils/message.ts index 5569af02a95..0f4de2d8a30 100644 --- a/src/components/views/rooms/wysiwyg_composer/message.ts +++ b/src/components/views/rooms/wysiwyg_composer/utils/message.ts @@ -19,90 +19,32 @@ import { IContent, IEventRelation, MatrixEvent } from 'matrix-js-sdk/src/models/ import { MatrixClient } from "matrix-js-sdk/src/matrix"; import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread"; -import { PosthogAnalytics } from "../../../../PosthogAnalytics"; -import SettingsStore from "../../../../settings/SettingsStore"; -import { decorateStartSendingTime, sendRoundTripMetric } from "../../../../sendTimePerformanceMetrics"; -import { RoomPermalinkCreator } from "../../../../utils/permalinks/Permalinks"; -import { doMaybeLocalRoomAction } from "../../../../utils/local-room"; -import { CHAT_EFFECTS } from "../../../../effects"; -import { containsEmoji } from "../../../../effects/utils"; -import { IRoomState } from "../../../structures/RoomView"; -import dis from '../../../../dispatcher/dispatcher'; -import { addReplyToMessageContent } from "../../../../utils/Reply"; - -// Merges favouring the given relation -function attachRelation(content: IContent, relation?: IEventRelation): void { - if (relation) { - content['m.relates_to'] = { - ...(content['m.relates_to'] || {}), - ...relation, - }; - } -} +import { PosthogAnalytics } from "../../../../../PosthogAnalytics"; +import SettingsStore from "../../../../../settings/SettingsStore"; +import { decorateStartSendingTime, sendRoundTripMetric } from "../../../../../sendTimePerformanceMetrics"; +import { RoomPermalinkCreator } from "../../../../../utils/permalinks/Permalinks"; +import { doMaybeLocalRoomAction } from "../../../../../utils/local-room"; +import { CHAT_EFFECTS } from "../../../../../effects"; +import { containsEmoji } from "../../../../../effects/utils"; +import { IRoomState } from "../../../../structures/RoomView"; +import dis from '../../../../../dispatcher/dispatcher'; +import { createRedactEventDialog } from "../../../dialogs/ConfirmRedactDialog"; +import { endEditing, cancelPreviousPendingEdit } from "./editing"; +import EditorStateTransfer from "../../../../../utils/EditorStateTransfer"; +import { createMessageContent } from "./createMessageContent"; +import { isContentModified } from "./isContentModified"; interface SendMessageParams { mxClient: MatrixClient; relation?: IEventRelation; replyToEvent?: MatrixEvent; roomContext: IRoomState; - permalinkCreator: RoomPermalinkCreator; + permalinkCreator?: RoomPermalinkCreator; includeReplyLegacyFallback?: boolean; } -// exported for tests -export function createMessageContent( - message: string, - { relation, replyToEvent, permalinkCreator, includeReplyLegacyFallback = true }: - Omit, -): IContent { - // TODO emote ? - - /*const isEmote = containsEmote(model); - if (isEmote) { - model = stripEmoteCommand(model); - } - if (startsWith(model, "//")) { - model = stripPrefix(model, "/"); - } - model = unescapeMessage(model);*/ - - // const body = textSerialize(model); - const body = message; - - const content: IContent = { - // TODO emote - // msgtype: isEmote ? "m.emote" : "m.text", - msgtype: "m.text", - body: body, - }; - - // TODO markdown support - - /*const formattedBody = htmlSerializeIfNeeded(model, { - forceHTML: !!replyToEvent, - useMarkdown: SettingsStore.getValue("MessageComposerInput.useMarkdown"), - });*/ - const formattedBody = message; - - if (formattedBody) { - content.format = "org.matrix.custom.html"; - content.formatted_body = formattedBody; - } - - attachRelation(content, relation); - - if (replyToEvent) { - addReplyToMessageContent(content, replyToEvent, { - permalinkCreator, - includeLegacyFallback: includeReplyLegacyFallback, - }); - } - - return content; -} - export function sendMessage( - message: string, + html: string, { roomContext, mxClient, ...params }: SendMessageParams, ) { const { relation, replyToEvent } = params; @@ -113,6 +55,7 @@ export function sendMessage( eventName: "Composer", isEditing: false, isReply: Boolean(replyToEvent), + // TODO thread inThread: relation?.rel_type === THREAD_RELATION_TYPE.name, }; @@ -133,7 +76,7 @@ export function sendMessage( if (!content) { content = createMessageContent( - message, + html, params, ); } @@ -197,3 +140,65 @@ export function sendMessage( return prom; } + +interface EditMessageParams { + mxClient: MatrixClient; + roomContext: IRoomState; + editorStateTransfer: EditorStateTransfer; +} + +export function editMessage( + html: string, + { roomContext, mxClient, editorStateTransfer }: EditMessageParams, +) { + const editedEvent = editorStateTransfer.getEvent(); + + PosthogAnalytics.instance.trackEvent({ + eventName: "Composer", + isEditing: true, + inThread: Boolean(editedEvent?.getThread()), + isReply: Boolean(editedEvent.replyEventId), + }); + + // Replace emoticon at the end of the message + /* if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) { + const caret = this.editorRef.current?.getCaret(); + const position = this.model.positionForOffset(caret.offset, caret.atNodeEnd); + this.editorRef.current?.replaceEmoticon(position, REGEX_EMOTICON); + }*/ + const editContent = createMessageContent(html, { editedEvent }); + const newContent = editContent["m.new_content"]; + + const shouldSend = true; + + if (newContent?.body === '') { + cancelPreviousPendingEdit(mxClient, editorStateTransfer); + createRedactEventDialog({ + mxEvent: editedEvent, + onCloseDialog: () => { + endEditing(roomContext); + }, + }); + return; + } + + // If content is modified then send an updated event into the room + if (isContentModified(newContent, editorStateTransfer)) { + const roomId = editedEvent.getRoomId(); + + // TODO Slash Commands + + if (shouldSend) { + cancelPreviousPendingEdit(mxClient, editorStateTransfer); + + const event = editorStateTransfer.getEvent(); + const threadId = event.threadRootId || null; + + console.log('editContent', editContent); + mxClient.sendMessage(roomId, threadId, editContent); + dis.dispatch({ action: "message_sent" }); + } + } + + endEditing(roomContext); +} diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts index 2b2e443e81d..7d2d935f705 100644 --- a/src/dispatcher/actions.ts +++ b/src/dispatcher/actions.ts @@ -75,6 +75,11 @@ export enum Action { */ FocusSendMessageComposer = "focus_send_message_composer", + /** + * Clear the to the send message composer. Should be used with a FocusComposerPayload. + */ + ClearAndFocusSendMessageComposer = "clear_focus_send_message_composer", + /** * Focuses the user's cursor to the edit message composer. Should be used with a FocusComposerPayload. */ diff --git a/test/components/views/rooms/MessageComposer-test.tsx b/test/components/views/rooms/MessageComposer-test.tsx index bc0b26f745a..596cf0bcfed 100644 --- a/test/components/views/rooms/MessageComposer-test.tsx +++ b/test/components/views/rooms/MessageComposer-test.tsx @@ -39,7 +39,7 @@ import { SendMessageComposer } from "../../../../src/components/views/rooms/Send import { E2EStatus } from "../../../../src/utils/ShieldUtils"; import { addTextToComposer } from "../../../test-utils/composer"; import UIStore, { UI_EVENTS } from "../../../../src/stores/UIStore"; -import { WysiwygComposer } from "../../../../src/components/views/rooms/wysiwyg_composer/WysiwygComposer"; +import { WysiwygComposer } from "../../../../src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer"; // The wysiwyg fetch wasm bytes and a specific workaround is needed to make it works in a node (jest) environnement // See https://github.com/matrix-org/matrix-wysiwyg/blob/main/platforms/web/test.setup.ts diff --git a/test/components/views/rooms/wysiwyg_composer/FormattingButtons-test.tsx b/test/components/views/rooms/wysiwyg_composer/FormattingButtons-test.tsx index 6c3e8573ae4..a9838ecacaa 100644 --- a/test/components/views/rooms/wysiwyg_composer/FormattingButtons-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/FormattingButtons-test.tsx @@ -18,7 +18,7 @@ import React from 'react'; import { render, screen } from "@testing-library/react"; import userEvent from '@testing-library/user-event'; -import { FormattingButtons } from "../../../../../src/components/views/rooms/wysiwyg_composer/FormattingButtons"; +import { FormattingButtons } from "../../../../../src/components/views/rooms/wysiwyg_composer/components/FormattingButtons"; describe('FormattingButtons', () => { const wysiwyg = { diff --git a/test/components/views/rooms/wysiwyg_composer/WysiwygComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/WysiwygComposer-test.tsx index b0aa838879b..91020250ae7 100644 --- a/test/components/views/rooms/wysiwyg_composer/WysiwygComposer-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/WysiwygComposer-test.tsx @@ -24,7 +24,7 @@ import defaultDispatcher from "../../../../../src/dispatcher/dispatcher"; import { Action } from "../../../../../src/dispatcher/actions"; import { IRoomState } from "../../../../../src/components/structures/RoomView"; import { Layout } from "../../../../../src/settings/enums/Layout"; -import { WysiwygComposer } from "../../../../../src/components/views/rooms/wysiwyg_composer/WysiwygComposer"; +import { WysiwygComposer } from "../../../../../src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer"; import { createTestClient, mkEvent, mkStubRoom } from "../../../../test-utils"; // The wysiwyg fetch wasm bytes and a specific workaround is needed to make it works in a node (jest) environnement diff --git a/test/components/views/rooms/wysiwyg_composer/message-test.ts b/test/components/views/rooms/wysiwyg_composer/message-test.ts index 712b671c9f7..50f0f77c1ab 100644 --- a/test/components/views/rooms/wysiwyg_composer/message-test.ts +++ b/test/components/views/rooms/wysiwyg_composer/message-test.ts @@ -15,7 +15,7 @@ limitations under the License. */ import { IRoomState } from "../../../../../src/components/structures/RoomView"; -import { createMessageContent, sendMessage } from "../../../../../src/components/views/rooms/wysiwyg_composer/message"; +import { createMessageContent, sendMessage } from "../../../../../src/components/views/rooms/wysiwyg_composer/utils/message"; import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext"; import { Layout } from "../../../../../src/settings/enums/Layout"; import { createTestClient, mkEvent, mkStubRoom } from "../../../../test-utils"; From 0a65d919a1a04bb457d59e4b24505a87c65ff001 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 19 Oct 2022 18:17:03 +0200 Subject: [PATCH 02/14] Fix typing --- .../views/rooms/wysiwyg_composer/SendWysiwygComposer.tsx | 3 +-- .../wysiwyg_composer/components/FormattingButtons.tsx | 7 +++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/components/views/rooms/wysiwyg_composer/SendWysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/SendWysiwygComposer.tsx index f36dbd2d9bd..24909bf2ee4 100644 --- a/src/components/views/rooms/wysiwyg_composer/SendWysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/SendWysiwygComposer.tsx @@ -22,10 +22,9 @@ import { WysiwygComposer } from './components/WysiwygComposer'; interface SendWysiwygComposerProps { disabled?: boolean; - onChange?: (content: string) => void; + onChange: (content: string) => void; onSend(): () => void; } - interface ContentProps { disabled: boolean; formattingFunctions: FormattingFunctions; diff --git a/src/components/views/rooms/wysiwyg_composer/components/FormattingButtons.tsx b/src/components/views/rooms/wysiwyg_composer/components/FormattingButtons.tsx index c806c278615..00127e5e430 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/FormattingButtons.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/FormattingButtons.tsx @@ -15,7 +15,7 @@ limitations under the License. */ import React, { MouseEventHandler } from "react"; -import { useWysiwyg } from "@matrix-org/matrix-wysiwyg"; +import { FormattingFunctions, FormattingStates } from "@matrix-org/matrix-wysiwyg"; import classNames from "classnames"; import AccessibleTooltipButton from "../../../elements/AccessibleTooltipButton"; @@ -23,7 +23,6 @@ import { Alignment } from "../../../elements/Tooltip"; import { KeyboardShortcut } from "../../../settings/KeyboardShortcut"; import { KeyCombo } from "../../../../../KeyBindingsManager"; import { _td } from "../../../../../languageHandler"; -import { Wysiwyg } from "../types"; interface TooltipProps { label: string; @@ -56,8 +55,8 @@ function Button({ label, keyCombo, onClick, isActive, className }: ButtonProps) } interface FormattingButtonsProps { - composer: Wysiwyg; - formattingStates: ReturnType['formattingStates']; + composer: FormattingFunctions; + formattingStates: FormattingStates; } export function FormattingButtons({ composer, formattingStates }: FormattingButtonsProps) { From 63c3a55758ed45b3ad90e38f5afd0fd6f8914efe Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 19 Oct 2022 18:57:49 +0200 Subject: [PATCH 03/14] Disable save button until change --- .../views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx | 6 +++--- .../rooms/wysiwyg_composer/components/EditionButtons.tsx | 5 +++-- .../views/rooms/wysiwyg_composer/hooks/useEditing.ts | 9 +++++++-- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx index cef10032855..341d045ba4e 100644 --- a/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx @@ -44,18 +44,18 @@ export function EditWysiwygComposer({ editorStateTransfer, ...props }: EditWysiw const initialContent = useInitialContent(editorStateTransfer); const isReady = !editorStateTransfer || Boolean(initialContent); - const { editMessage, endEditing, setContent } = useEditing(initialContent, editorStateTransfer); + const { editMessage, endEditing, onChange, isSaveDisabled } = useEditing(initialContent, editorStateTransfer); return isReady && { (ref, wysiwyg, content) => ( <> - + ) } ; diff --git a/src/components/views/rooms/wysiwyg_composer/components/EditionButtons.tsx b/src/components/views/rooms/wysiwyg_composer/components/EditionButtons.tsx index 20a3df2a7f7..9e94c12470a 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/EditionButtons.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/EditionButtons.tsx @@ -22,14 +22,15 @@ import AccessibleButton from '../../../elements/AccessibleButton'; interface EditionButtonsProps { onCancelClick: MouseEventHandler; onSaveClick: MouseEventHandler; + isSaveDisabled?: boolean; } -export function EditionButtons({ onCancelClick, onSaveClick }: EditionButtonsProps) { +export function EditionButtons({ onCancelClick, onSaveClick, isSaveDisabled = false }: EditionButtonsProps) { return
{ _t("Cancel") } - + { _t("Save") }
; diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useEditing.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useEditing.ts index 8076c0062ac..fcd4471cb1e 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useEditing.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useEditing.ts @@ -26,13 +26,18 @@ export function useEditing(initialContent: string, editorStateTransfer: EditorSt const roomContext = useRoomContext(); const mxClient = useMatrixClientContext(); + const [isSaveDisabled, setIsSaveDisabled] = useState(true); const [content, setContent] = useState(initialContent); + const onChange = useCallback((_content: string) => { + setContent(_content); + setIsSaveDisabled(_isSaveDisabled => _isSaveDisabled && _content === initialContent); + }, [initialContent]); + const editMessageMemoized = useCallback(() => editMessage(content, { roomContext, mxClient, editorStateTransfer }), [content, roomContext, mxClient, editorStateTransfer], ); const endEditingMemoized = useCallback(() => endEditing(roomContext), [roomContext]); - - return { setContent, editMessage: editMessageMemoized, endEditing: endEditingMemoized }; + return { onChange, editMessage: editMessageMemoized, endEditing: endEditingMemoized, isSaveDisabled }; } From c7e83baa360d874fc2c61d411813e8e100eeb070 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 19 Oct 2022 19:29:42 +0200 Subject: [PATCH 04/14] Remove unused parameters --- .../views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx | 3 +-- .../rooms/wysiwyg_composer/components/WysiwygComposer.tsx | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx index 341d045ba4e..d60d98ee9ff 100644 --- a/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx @@ -51,8 +51,7 @@ export function EditWysiwygComposer({ editorStateTransfer, ...props }: EditWysiw onChange={onChange} onSend={editMessage} {...props}> - { (ref, wysiwyg, - content) => ( + { (ref) => ( <> diff --git a/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx index 308a3fd2028..4a58b3693f2 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx @@ -29,7 +29,7 @@ interface WysiwygComposerProps { children?: ( ref: MutableRefObject, wysiwyg: FormattingFunctions, - content: string) => ReactNode; + ) => ReactNode; } export const WysiwygComposer = memo(function WysiwygComposer( @@ -50,7 +50,7 @@ export const WysiwygComposer = memo(function WysiwygComposer(
- { children?.(ref, wysiwyg, content) } + { children?.(ref, wysiwyg) }
); }); From e9b285c5e058f4349b3856391089c51e4f0858a1 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 19 Oct 2022 19:44:49 +0200 Subject: [PATCH 05/14] Cleaning files --- src/components/views/rooms/BasicMessageComposer.tsx | 2 -- src/components/views/rooms/EditMessageComposer.tsx | 1 - src/components/views/rooms/MessageComposer.tsx | 3 +-- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index 962059091cc..d74c7b51484 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -833,8 +833,6 @@ export default class BasicMessageEditor extends React.Component } public insertPlaintext(text: string): void { - console.log('insertPlaintext', text); - debugger; this.modifiedFlag = true; const { model } = this.props; const { partCreator } = model; diff --git a/src/components/views/rooms/EditMessageComposer.tsx b/src/components/views/rooms/EditMessageComposer.tsx index bb014541278..52312e1a998 100644 --- a/src/components/views/rooms/EditMessageComposer.tsx +++ b/src/components/views/rooms/EditMessageComposer.tsx @@ -350,7 +350,6 @@ class EditMessageComposer extends React.Component { } this.messageComposerInput.current?.sendMessage(); - // this.composerSendMessage?.(); - const isWysiwygComposerEnabled = SettingsStore.getValue("feature_wysiwyg_composer"); + const isWysiwygComposerEnabled = SettingsStore.getValue("feature_wysiwyg_composer"); if (isWysiwygComposerEnabled) { const { permalinkCreator, relation, replyToEvent } = this.props; sendMessage(this.state.composerContent, From 5e6d0f640447cd17998f46512c1d7e67170bbb5c Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Thu, 20 Oct 2022 10:53:57 +0200 Subject: [PATCH 06/14] Copy css for edition --- res/css/_components.pcss | 5 +- .../_EditWysiwygComposer.pcss | 53 +++++++++++++++++++ ...omposer.pcss => _SendWysiwygComposer.pcss} | 2 +- .../{ => components}/_FormattingButtons.pcss | 0 .../wysiwyg_composer/EditWysiwygComposer.tsx | 1 + .../wysiwyg_composer/SendWysiwygComposer.tsx | 2 +- .../components/WysiwygComposer.tsx | 5 +- 7 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 res/css/views/rooms/wysiwyg_composer/_EditWysiwygComposer.pcss rename res/css/views/rooms/wysiwyg_composer/{_WysiwygComposer.pcss => _SendWysiwygComposer.pcss} (98%) rename res/css/views/rooms/wysiwyg_composer/{ => components}/_FormattingButtons.pcss (100%) diff --git a/res/css/_components.pcss b/res/css/_components.pcss index b2fcb0dd4f8..a0300e84324 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -299,8 +299,9 @@ @import "./views/rooms/_TopUnreadMessagesBar.pcss"; @import "./views/rooms/_VoiceRecordComposerTile.pcss"; @import "./views/rooms/_WhoIsTypingTile.pcss"; -@import "./views/rooms/wysiwyg_composer/_FormattingButtons.pcss"; -@import "./views/rooms/wysiwyg_composer/_WysiwygComposer.pcss"; +@import "./views/rooms/wysiwyg_composer/components/_FormattingButtons.pcss"; +@import "./views/rooms/wysiwyg_composer/_SendWysiwygComposer.pcss"; +@import "./views/rooms/wysiwyg_composer/_EditWysiwygComposer.pcss"; @import "./views/settings/_AvatarSetting.pcss"; @import "./views/settings/_CrossSigningPanel.pcss"; @import "./views/settings/_CryptographyPanel.pcss"; diff --git a/res/css/views/rooms/wysiwyg_composer/_EditWysiwygComposer.pcss b/res/css/views/rooms/wysiwyg_composer/_EditWysiwygComposer.pcss new file mode 100644 index 00000000000..15bc00f5f21 --- /dev/null +++ b/res/css/views/rooms/wysiwyg_composer/_EditWysiwygComposer.pcss @@ -0,0 +1,53 @@ +/* +Copyright 2019 New Vector Ltd +Copyright 2019 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. +*/ + +.mx_EditWysiwygComposer { + --EditWysiwygComposer-padding-inline: 3px; + + display: flex; + flex-direction: column; + max-width: 100%; /* disable overflow */ + width: auto; + gap: 5px; + padding: 3px var(--EditWysiwygComposer-padding-inline); + + .mx_WysiwygComposer_container { + border-radius: 4px; + border: solid 1px $primary-hairline-color; + background-color: $background; + max-height: 200px; + padding: 3px 6px; + + &:focus { + border-color: rgba($accent, 0.5); /* Only ever used here */ + } + } + + .mx_EditWysiwygComposer_buttons { + display: flex; + flex-flow: row wrap-reverse; /* display "Save" over "Cancel" */ + justify-content: flex-end; + gap: 5px; + margin-inline-start: auto; + + .mx_AccessibleButton { + flex: 1; + box-sizing: border-box; + min-width: 100px; /* magic number to align the edge of the button with the input area */ + } + } +} diff --git a/res/css/views/rooms/wysiwyg_composer/_WysiwygComposer.pcss b/res/css/views/rooms/wysiwyg_composer/_SendWysiwygComposer.pcss similarity index 98% rename from res/css/views/rooms/wysiwyg_composer/_WysiwygComposer.pcss rename to res/css/views/rooms/wysiwyg_composer/_SendWysiwygComposer.pcss index 133b66388e7..a00f8c7e113 100644 --- a/res/css/views/rooms/wysiwyg_composer/_WysiwygComposer.pcss +++ b/res/css/views/rooms/wysiwyg_composer/_SendWysiwygComposer.pcss @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_WysiwygComposer { +.mx_SendWysiwygComposer { flex: 1; display: flex; flex-direction: column; diff --git a/res/css/views/rooms/wysiwyg_composer/_FormattingButtons.pcss b/res/css/views/rooms/wysiwyg_composer/components/_FormattingButtons.pcss similarity index 100% rename from res/css/views/rooms/wysiwyg_composer/_FormattingButtons.pcss rename to res/css/views/rooms/wysiwyg_composer/components/_FormattingButtons.pcss diff --git a/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx index d60d98ee9ff..53031fc6e10 100644 --- a/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx @@ -47,6 +47,7 @@ export function EditWysiwygComposer({ editorStateTransfer, ...props }: EditWysiw const { editMessage, endEditing, onChange, isSaveDisabled } = useEditing(initialContent, editorStateTransfer); return isReady && ( export function SendWysiwygComposer(props: SendWysiwygComposerProps) { return ( - { (ref, wysiwyg) => ( + { (ref, wysiwyg) => ( ) } ); diff --git a/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx index 4a58b3693f2..7dc059ffb22 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx @@ -26,6 +26,7 @@ interface WysiwygComposerProps { onChange?: (content: string) => void; onSend: () => void; initialContent?: string; + className?: string; children?: ( ref: MutableRefObject, wysiwyg: FormattingFunctions, @@ -33,7 +34,7 @@ interface WysiwygComposerProps { } export const WysiwygComposer = memo(function WysiwygComposer( - { disabled = false, onChange, onSend, initialContent, children }: WysiwygComposerProps, + { disabled = false, onChange, onSend, initialContent, className, children }: WysiwygComposerProps, ) { const inputEventProcessor = useInputEventProcessor(onSend); @@ -47,7 +48,7 @@ export const WysiwygComposer = memo(function WysiwygComposer( }, [onChange, content, disabled]); return ( -
+
{ children?.(ref, wysiwyg) } From 072c767b68491a42d4dcce61370b367ce1590d46 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Thu, 20 Oct 2022 11:52:50 +0200 Subject: [PATCH 07/14] Fix suppression when message is empty --- .../utils/createMessageContent.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts b/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts index fe7f7706b4f..f0488e45995 100644 --- a/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts +++ b/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts @@ -89,15 +89,15 @@ export function createMessageContent( const htmlPrefix = isReply ? getHtmlReplyFallback(editedEvent) : ''; content.formatted_body = isEditing ? `${htmlPrefix} * ${formattedBody}` : formattedBody; + } - if (isEditing) { - content['m.new_content'] = { - "msgtype": content.msgtype, - "body": body, - "format": "org.matrix.custom.html", - 'formatted_body': formattedBody, - }; - } + if (isEditing) { + content['m.new_content'] = { + "msgtype": content.msgtype, + "body": body, + "format": "org.matrix.custom.html", + 'formatted_body': formattedBody, + }; } const newRelation = isEditing ? From 5987a6889b619852f8544c90568b57bff988a487 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Thu, 20 Oct 2022 17:31:17 +0200 Subject: [PATCH 08/14] Add styling to editing --- res/css/_components.pcss | 1 + .../_EditWysiwygComposer.pcss | 12 ++++-- .../wysiwyg_composer/components/_Editor.pcss | 38 +++++++++++++++++++ .../components/_FormattingButtons.pcss | 2 +- src/components/views/messages/TextualBody.tsx | 2 +- .../wysiwyg_composer/EditWysiwygComposer.tsx | 6 ++- .../components/EditionButtons.tsx | 2 +- 7 files changed, 55 insertions(+), 8 deletions(-) create mode 100644 res/css/views/rooms/wysiwyg_composer/components/_Editor.pcss diff --git a/res/css/_components.pcss b/res/css/_components.pcss index a0300e84324..8ee602deee0 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -299,6 +299,7 @@ @import "./views/rooms/_TopUnreadMessagesBar.pcss"; @import "./views/rooms/_VoiceRecordComposerTile.pcss"; @import "./views/rooms/_WhoIsTypingTile.pcss"; +@import "./views/rooms/wysiwyg_composer/components/_Editor.pcss"; @import "./views/rooms/wysiwyg_composer/components/_FormattingButtons.pcss"; @import "./views/rooms/wysiwyg_composer/_SendWysiwygComposer.pcss"; @import "./views/rooms/wysiwyg_composer/_EditWysiwygComposer.pcss"; diff --git a/res/css/views/rooms/wysiwyg_composer/_EditWysiwygComposer.pcss b/res/css/views/rooms/wysiwyg_composer/_EditWysiwygComposer.pcss index 15bc00f5f21..8c245bc90f9 100644 --- a/res/css/views/rooms/wysiwyg_composer/_EditWysiwygComposer.pcss +++ b/res/css/views/rooms/wysiwyg_composer/_EditWysiwygComposer.pcss @@ -22,10 +22,10 @@ limitations under the License. flex-direction: column; max-width: 100%; /* disable overflow */ width: auto; - gap: 5px; - padding: 3px var(--EditWysiwygComposer-padding-inline); + gap: 8px; + padding: 8px var(--EditWysiwygComposer-padding-inline); - .mx_WysiwygComposer_container { + .mx_WysiwygComposer_content { border-radius: 4px; border: solid 1px $primary-hairline-color; background-color: $background; @@ -50,4 +50,10 @@ limitations under the License. min-width: 100px; /* magic number to align the edge of the button with the input area */ } } + + .mx_FormattingButtons_Button { + &:first-child { + margin-left: 0px; + } + } } diff --git a/res/css/views/rooms/wysiwyg_composer/components/_Editor.pcss b/res/css/views/rooms/wysiwyg_composer/components/_Editor.pcss new file mode 100644 index 00000000000..88fa080e23d --- /dev/null +++ b/res/css/views/rooms/wysiwyg_composer/components/_Editor.pcss @@ -0,0 +1,38 @@ +/* +Copyright 2019 New Vector Ltd +Copyright 2019 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. +*/ + +.mx_WysiwygComposer_container { + position: relative; + + @keyframes visualbell { + from { background-color: $visual-bell-bg-color; } + to { background-color: $background; } + } + + .mx_WysiwygComposer_content { + white-space: pre-wrap; + word-wrap: break-word; + outline: none; + overflow-x: hidden; + + /* Force caret nodes to be selected in full so that they can be */ + /* navigated through in a single keypress */ + .caretNode { + user-select: all; + } + } +} diff --git a/res/css/views/rooms/wysiwyg_composer/components/_FormattingButtons.pcss b/res/css/views/rooms/wysiwyg_composer/components/_FormattingButtons.pcss index 36f84ae5f1f..499b2b457be 100644 --- a/res/css/views/rooms/wysiwyg_composer/components/_FormattingButtons.pcss +++ b/res/css/views/rooms/wysiwyg_composer/components/_FormattingButtons.pcss @@ -45,7 +45,7 @@ limitations under the License. left: 6px; height: 16px; width: 16px; - background-color: $icon-button-color; + background-color: $tertiary-content; mask-repeat: no-repeat; mask-size: contain; mask-position: center; diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 983cbe51e37..ab9c27f7fbe 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -565,7 +565,7 @@ export default class TextualBody extends React.Component { if (this.props.editState) { const isWysiwygComposerEnabled = SettingsStore.getValue("feature_wysiwyg_composer"); return isWysiwygComposerEnabled ? - : + : ; } const mxEvent = this.props.mxEvent; diff --git a/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx index 53031fc6e10..c03e87c526a 100644 --- a/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx @@ -15,6 +15,7 @@ limitations under the License. */ import React, { forwardRef, RefObject } from 'react'; +import classNames from 'classnames'; import EditorStateTransfer from '../../../../utils/EditorStateTransfer'; import { WysiwygComposer } from './components/WysiwygComposer'; @@ -38,16 +39,17 @@ interface EditWysiwygComposerProps { disabled?: boolean; onChange?: (content: string) => void; editorStateTransfer: EditorStateTransfer; + className?: string; } -export function EditWysiwygComposer({ editorStateTransfer, ...props }: EditWysiwygComposerProps) { +export function EditWysiwygComposer({ editorStateTransfer, className, ...props }: EditWysiwygComposerProps) { const initialContent = useInitialContent(editorStateTransfer); const isReady = !editorStateTransfer || Boolean(initialContent); const { editMessage, endEditing, onChange, isSaveDisabled } = useEditing(initialContent, editorStateTransfer); return isReady && + return
{ _t("Cancel") } From c9bf7da62965bd03873963bef8a5c49cd22ee7ac Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Fri, 21 Oct 2022 10:35:54 +0200 Subject: [PATCH 09/14] Fix send message on enter --- src/components/views/rooms/MessageComposer.tsx | 2 +- .../views/rooms/wysiwyg_composer/SendWysiwygComposer.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index b94fcdddb12..60bc5ee6410 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -415,7 +415,7 @@ class MessageComposer extends React.Component { this.sendMessage} + onSend={this.sendMessage} />, ); } else { diff --git a/src/components/views/rooms/wysiwyg_composer/SendWysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/SendWysiwygComposer.tsx index 4a7cfb65a06..2a485d99757 100644 --- a/src/components/views/rooms/wysiwyg_composer/SendWysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/SendWysiwygComposer.tsx @@ -23,7 +23,7 @@ import { WysiwygComposer } from './components/WysiwygComposer'; interface SendWysiwygComposerProps { disabled?: boolean; onChange: (content: string) => void; - onSend(): () => void; + onSend: () => void; } interface ContentProps { disabled: boolean; From 50c29502e4666cd49fe5bf75200db6234e7eded0 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Fri, 21 Oct 2022 19:26:33 +0200 Subject: [PATCH 10/14] Add new tests for WysiwygComposer --- .../hooks/useWysiwygEditActionHandler.ts | 2 +- .../utils/createMessageContent.ts | 4 +- .../rooms/wysiwyg_composer/utils/message.ts | 9 +- .../EditWysiwygComposer-test.tsx | 224 +++++++++++++++++ .../SendWysiwygComposer-test.tsx | 150 +++++++++++ .../wysiwyg_composer/WysiwygComposer-test.tsx | 238 ------------------ .../FormattingButtons-test.tsx | 3 +- .../components/WysiwygComposer-test.tsx | 152 +++++++++++ .../utils/createMessageContent-test.ts | 133 ++++++++++ .../{ => utils}/message-test.ts | 211 ++++++++-------- test/test-utils/test-utils.ts | 1 + 11 files changed, 772 insertions(+), 355 deletions(-) create mode 100644 test/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx create mode 100644 test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx delete mode 100644 test/components/views/rooms/wysiwyg_composer/WysiwygComposer-test.tsx rename test/components/views/rooms/wysiwyg_composer/{ => components}/FormattingButtons-test.tsx (95%) create mode 100644 test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx create mode 100644 test/components/views/rooms/wysiwyg_composer/utils/createMessageContent-test.ts rename test/components/views/rooms/wysiwyg_composer/{ => utils}/message-test.ts (53%) diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygEditActionHandler.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygEditActionHandler.ts index 2cbd7cf52cc..b39fe18007b 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygEditActionHandler.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygEditActionHandler.ts @@ -38,7 +38,7 @@ export function useWysiwygEditActionHandler( const context = payload.context ?? TimelineRenderingType.Room; switch (payload.action) { - case Action.FocusSendMessageComposer: + case Action.FocusEditMessageComposer: focusComposer(composerElement, context, roomContext, timeoutId); break; } diff --git a/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts b/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts index f0488e45995..cc0d2235bf9 100644 --- a/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts +++ b/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts @@ -54,8 +54,8 @@ export function createMessageContent( ): IContent { // TODO emote ? - const isReply = Boolean(replyToEvent?.replyEventId); const isEditing = Boolean(editedEvent); + const isReply = isEditing ? Boolean(editedEvent?.replyEventId) : Boolean(replyToEvent); /*const isEmote = containsEmote(model); if (isEmote) { @@ -87,7 +87,7 @@ export function createMessageContent( if (formattedBody) { content.format = "org.matrix.custom.html"; - const htmlPrefix = isReply ? getHtmlReplyFallback(editedEvent) : ''; + const htmlPrefix = isReply && isEditing ? getHtmlReplyFallback(editedEvent) : ''; content.formatted_body = isEditing ? `${htmlPrefix} * ${formattedBody}` : formattedBody; } diff --git a/src/components/views/rooms/wysiwyg_composer/utils/message.ts b/src/components/views/rooms/wysiwyg_composer/utils/message.ts index 0f4de2d8a30..dbea29c848c 100644 --- a/src/components/views/rooms/wysiwyg_composer/utils/message.ts +++ b/src/components/views/rooms/wysiwyg_composer/utils/message.ts @@ -16,7 +16,7 @@ limitations under the License. import { Composer as ComposerEvent } from "@matrix-org/analytics-events/types/typescript/Composer"; import { IContent, IEventRelation, MatrixEvent } from 'matrix-js-sdk/src/models/event'; -import { MatrixClient } from "matrix-js-sdk/src/matrix"; +import { ISendEventResponse, MatrixClient } from "matrix-js-sdk/src/matrix"; import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread"; import { PosthogAnalytics } from "../../../../../PosthogAnalytics"; @@ -160,6 +160,7 @@ export function editMessage( isReply: Boolean(editedEvent.replyEventId), }); + // TODO emoji // Replace emoticon at the end of the message /* if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) { const caret = this.editorRef.current?.getCaret(); @@ -182,6 +183,8 @@ export function editMessage( return; } + let response: Promise | undefined; + // If content is modified then send an updated event into the room if (isContentModified(newContent, editorStateTransfer)) { const roomId = editedEvent.getRoomId(); @@ -194,11 +197,11 @@ export function editMessage( const event = editorStateTransfer.getEvent(); const threadId = event.threadRootId || null; - console.log('editContent', editContent); - mxClient.sendMessage(roomId, threadId, editContent); + response = mxClient.sendMessage(roomId, threadId, editContent); dis.dispatch({ action: "message_sent" }); } } endEditing(roomContext); + return response; } diff --git a/test/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx new file mode 100644 index 00000000000..72fd52be574 --- /dev/null +++ b/test/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx @@ -0,0 +1,224 @@ +/* +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 "@testing-library/jest-dom"; +import React from "react"; +import { render, screen, waitFor } from "@testing-library/react"; +import { WysiwygProps } from "@matrix-org/matrix-wysiwyg"; + +import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; +import RoomContext from "../../../../../src/contexts/RoomContext"; +import defaultDispatcher from "../../../../../src/dispatcher/dispatcher"; +import { Action } from "../../../../../src/dispatcher/actions"; +import { IRoomState } from "../../../../../src/components/structures/RoomView"; +import { createTestClient, getRoomContext, mkEvent, mkStubRoom } from "../../../../test-utils"; +import { EditWysiwygComposer } + from "../../../../../src/components/views/rooms/wysiwyg_composer"; +import EditorStateTransfer from "../../../../../src/utils/EditorStateTransfer"; + +const mockClear = jest.fn(); + +let initialContent: string; +const defaultContent = 'html'; +let mockContent = defaultContent; + +// The wysiwyg fetch wasm bytes and a specific workaround is needed to make it works in a node (jest) environnement +// See https://github.com/matrix-org/matrix-wysiwyg/blob/main/platforms/web/test.setup.ts +jest.mock("@matrix-org/matrix-wysiwyg", () => ({ + useWysiwyg: (props: WysiwygProps) => { + initialContent = props.initialContent; + return { + ref: { current: null }, + content: mockContent, + isWysiwygReady: true, + wysiwyg: { clear: mockClear }, + formattingStates: { + bold: 'enabled', + italic: 'enabled', + underline: 'enabled', + strikeThrough: 'enabled', + }, + }; + }, +})); + +describe('EditWysiwygComposer', () => { + afterEach(() => { + jest.resetAllMocks(); + mockContent = defaultContent; + }); + + const mockClient = createTestClient(); + const mockEvent = mkEvent({ + type: "m.room.message", + room: 'myfakeroom', + user: 'myfakeuser', + content: { + "msgtype": "m.text", + "body": "Replying to this", + "format": "org.matrix.custom.html", + "formatted_body": "Replying to this new content", + }, + event: true, + }); + const mockRoom = mkStubRoom('myfakeroom', 'myfakeroom', mockClient) as any; + mockRoom.findEventById = jest.fn(eventId => { + return eventId === mockEvent.getId() ? mockEvent : null; + }); + + const defaultRoomContext: IRoomState = getRoomContext(mockRoom, {}); + + const editorStateTransfer = new EditorStateTransfer(mockEvent); + + const customRender = (disabled = false, _editorStateTransfer = editorStateTransfer) => { + return render( + + + + + , + ); + }; + + describe('Initialize with content', () => { + it('Should initialize useWysiwyg with html content', async () => { + // When + customRender(true); + + // Then + expect(initialContent).toBe(mockEvent.getContent()['formatted_body']); + }); + + it('Should initialize useWysiwyg with plain text content', async () => { + // When + const mockEvent = mkEvent({ + type: "m.room.message", + room: 'myfakeroom', + user: 'myfakeuser', + content: { + "msgtype": "m.text", + "body": "Replying to this", + }, + event: true, + }); + const editorStateTransfer = new EditorStateTransfer(mockEvent); + + customRender(true, editorStateTransfer); + + // Then + expect(initialContent).toBe(mockEvent.getContent().body); + }); + }); + + describe('Edit and save actions', () => { + const spyDispatcher = jest.spyOn(defaultDispatcher, "dispatch"); + afterEach(() => { + spyDispatcher.mockRestore(); + }); + + it('Should cancel edit on cancel button click', async () => { + // When + customRender(true); + (await screen.findByText('Cancel')).click(); + + // Then + expect(spyDispatcher).toBeCalledWith({ + action: Action.EditEvent, + event: null, + timelineRenderingType: defaultRoomContext.timelineRenderingType, + }); + expect(spyDispatcher).toBeCalledWith({ + action: Action.FocusSendMessageComposer, + context: defaultRoomContext.timelineRenderingType, + }); + }); + + it('Should send message on save button click', async () => { + // When + const spyDispatcher = jest.spyOn(defaultDispatcher, "dispatch"); + + const renderer = customRender(true); + + mockContent = 'my new content'; + renderer.rerender( + + + + ); + + (await screen.findByText('Save')).click(); + + // Then + const expectedContent = { + "body": mockContent, + "format": "org.matrix.custom.html", + "formatted_body": ` * ${mockContent}`, + "m.new_content": { + "body": mockContent, + "format": "org.matrix.custom.html", + "formatted_body": mockContent, + "msgtype": "m.text", + }, + "m.relates_to": { + "event_id": mockEvent.getId(), + "rel_type": "m.replace", + }, + "msgtype": "m.text", + }; + expect(mockClient.sendMessage).toBeCalledWith(mockEvent.getRoomId(), null, expectedContent); + expect(spyDispatcher).toBeCalledWith({ action: 'message_sent' }); + }); + }); + + it('Should focus when receiving an Action.FocusEditMessageComposer action', async () => { + // Given we don't have focus + customRender(); + expect(screen.getByRole('textbox')).not.toHaveFocus(); + + // When we send the right action + defaultDispatcher.dispatch({ + action: Action.FocusEditMessageComposer, + context: null, + }); + + // Then the component gets the focus + await waitFor(() => expect(screen.getByRole('textbox')).toHaveFocus()); + }); + + it('Should not focus when disabled', async () => { + // Given we don't have focus and we are disabled + customRender(true); + expect(screen.getByRole('textbox')).not.toHaveFocus(); + + // When we send an action that would cause us to get focus + defaultDispatcher.dispatch({ + action: Action.FocusEditMessageComposer, + context: null, + }); + // (Send a second event to exercise the clearTimeout logic) + defaultDispatcher.dispatch({ + action: Action.FocusEditMessageComposer, + context: null, + }); + + // Wait for event dispatch to happen + await new Promise((r) => setTimeout(r, 200)); + + // Then we don't get it because we are disabled + expect(screen.getByRole('textbox')).not.toHaveFocus(); + }); +}); + diff --git a/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx new file mode 100644 index 00000000000..20148b802a7 --- /dev/null +++ b/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx @@ -0,0 +1,150 @@ +/* +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 "@testing-library/jest-dom"; +import React from "react"; +import { render, screen, waitFor } from "@testing-library/react"; +import { WysiwygProps } from "@matrix-org/matrix-wysiwyg"; + +import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; +import RoomContext from "../../../../../src/contexts/RoomContext"; +import defaultDispatcher from "../../../../../src/dispatcher/dispatcher"; +import { Action } from "../../../../../src/dispatcher/actions"; +import { IRoomState } from "../../../../../src/components/structures/RoomView"; +import { createTestClient, getRoomContext, mkEvent, mkStubRoom } from "../../../../test-utils"; +import { SendWysiwygComposer } from "../../../../../src/components/views/rooms/wysiwyg_composer"; + +const mockClear = jest.fn(); + +// The wysiwyg fetch wasm bytes and a specific workaround is needed to make it works in a node (jest) environnement +// See https://github.com/matrix-org/matrix-wysiwyg/blob/main/platforms/web/test.setup.ts +jest.mock("@matrix-org/matrix-wysiwyg", () => ({ + useWysiwyg: (props: WysiwygProps) => { + return { + ref: { current: null }, + content: 'html', + isWysiwygReady: true, + wysiwyg: { clear: mockClear }, + formattingStates: { + bold: 'enabled', + italic: 'enabled', + underline: 'enabled', + strikeThrough: 'enabled', + }, + }; + }, +})); + +describe('SendWysiwygComposer', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + const mockClient = createTestClient(); + const mockEvent = mkEvent({ + type: "m.room.message", + room: 'myfakeroom', + user: 'myfakeuser', + content: { "msgtype": "m.text", "body": "Replying to this" }, + event: true, + }); + const mockRoom = mkStubRoom('myfakeroom', 'myfakeroom', mockClient) as any; + mockRoom.findEventById = jest.fn(eventId => { + return eventId === mockEvent.getId() ? mockEvent : null; + }); + + const defaultRoomContext: IRoomState = getRoomContext(mockRoom, {}); + + const customRender = (onChange = (_content: string) => void 0, onSend = () => void 0, disabled = false) => { + return render( + + + + + , + ); + }; + + it('Should focus when receiving an Action.FocusSendMessageComposer action', async () => { + // Given we don't have focus + customRender(jest.fn(), jest.fn()); + expect(screen.getByRole('textbox')).not.toHaveFocus(); + + // When we send the right action + defaultDispatcher.dispatch({ + action: Action.FocusSendMessageComposer, + context: null, + }); + + // Then the component gets the focus + await waitFor(() => expect(screen.getByRole('textbox')).toHaveFocus()); + }); + + it('Should focus and clear when receiving an Action.ClearAndFocusSendMessageComposer', async () => { + // Given we don't have focus + customRender(jest.fn(), jest.fn()); + expect(screen.getByRole('textbox')).not.toHaveFocus(); + + // When we send the right action + defaultDispatcher.dispatch({ + action: Action.ClearAndFocusSendMessageComposer, + context: null, + }); + + // Then the component gets the focus + await waitFor(() => expect(screen.getByRole('textbox')).toHaveFocus()); + expect(mockClear).toBeCalledTimes(1); + }); + + it('Should focus when receiving a reply_to_event action', async () => { + // Given we don't have focus + customRender(jest.fn(), jest.fn()); + expect(screen.getByRole('textbox')).not.toHaveFocus(); + + // When we send the right action + defaultDispatcher.dispatch({ + action: "reply_to_event", + context: null, + }); + + // Then the component gets the focus + await waitFor(() => expect(screen.getByRole('textbox')).toHaveFocus()); + }); + + it('Should not focus when disabled', async () => { + // Given we don't have focus and we are disabled + customRender(jest.fn(), jest.fn(), true); + expect(screen.getByRole('textbox')).not.toHaveFocus(); + + // When we send an action that would cause us to get focus + defaultDispatcher.dispatch({ + action: Action.FocusSendMessageComposer, + context: null, + }); + // (Send a second event to exercise the clearTimeout logic) + defaultDispatcher.dispatch({ + action: Action.FocusSendMessageComposer, + context: null, + }); + + // Wait for event dispatch to happen + await new Promise((r) => setTimeout(r, 200)); + + // Then we don't get it because we are disabled + expect(screen.getByRole('textbox')).not.toHaveFocus(); + }); +}); + diff --git a/test/components/views/rooms/wysiwyg_composer/WysiwygComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/WysiwygComposer-test.tsx deleted file mode 100644 index fba6753ac21..00000000000 --- a/test/components/views/rooms/wysiwyg_composer/WysiwygComposer-test.tsx +++ /dev/null @@ -1,238 +0,0 @@ -/* -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 "@testing-library/jest-dom"; -import React from "react"; -import { act, render, screen, waitFor } from "@testing-library/react"; -import { InputEventProcessor, Wysiwyg, WysiwygProps } from "@matrix-org/matrix-wysiwyg"; - -import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; -import RoomContext from "../../../../../src/contexts/RoomContext"; -import defaultDispatcher from "../../../../../src/dispatcher/dispatcher"; -import { Action } from "../../../../../src/dispatcher/actions"; -import { IRoomState } from "../../../../../src/components/structures/RoomView"; -import { WysiwygComposer } from "../../../../../src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer"; -import { createTestClient, getRoomContext, mkEvent, mkStubRoom } from "../../../../test-utils"; -import SettingsStore from "../../../../../src/settings/SettingsStore"; - -// Work around missing ClipboardEvent type -class MyClipbardEvent {} -window.ClipboardEvent = MyClipbardEvent as any; - -let inputEventProcessor: InputEventProcessor | null = null; - -// The wysiwyg fetch wasm bytes and a specific workaround is needed to make it works in a node (jest) environnement -// See https://github.com/matrix-org/matrix-wysiwyg/blob/main/platforms/web/test.setup.ts -jest.mock("@matrix-org/matrix-wysiwyg", () => ({ - useWysiwyg: (props: WysiwygProps) => { - inputEventProcessor = props.inputEventProcessor ?? null; - return { - ref: { current: null }, - content: 'html', - isWysiwygReady: true, - wysiwyg: { clear: () => void 0 }, - formattingStates: { - bold: 'enabled', - italic: 'enabled', - underline: 'enabled', - strikeThrough: 'enabled', - }, - }; - }, -})); - -describe('WysiwygComposer', () => { - afterEach(() => { - jest.resetAllMocks(); - }); - - const permalinkCreator = jest.fn() as any; - const mockClient = createTestClient(); - const mockEvent = mkEvent({ - type: "m.room.message", - room: 'myfakeroom', - user: 'myfakeuser', - content: { "msgtype": "m.text", "body": "Replying to this" }, - event: true, - }); - const mockRoom = mkStubRoom('myfakeroom', 'myfakeroom', mockClient) as any; - mockRoom.findEventById = jest.fn(eventId => { - return eventId === mockEvent.getId() ? mockEvent : null; - }); - - const defaultRoomContext: IRoomState = getRoomContext(mockRoom, {}); - - let sendMessage: () => void; - const customRender = (onChange = (_content: string) => void 0, disabled = false) => { - return render( - - - - { (_sendMessage) => { - sendMessage = _sendMessage; - } } - - , - ); - }; - - it('Should have contentEditable at false when disabled', () => { - // When - customRender(null, true); - - // Then - expect(screen.getByRole('textbox')).toHaveAttribute('contentEditable', "false"); - }); - - it('Should call onChange handler', (done) => { - const html = 'html'; - customRender((content) => { - expect(content).toBe((html)); - done(); - }); - // act(() => callOnChange(html)); - }); - - it('Should send message, call clear and focus the textbox', async () => { - // When - const html = 'html'; - await new Promise((resolve) => { - customRender(() => resolve(null)); - }); - act(() => sendMessage()); - - // Then - const expectedContent = { - "body": html, - "format": "org.matrix.custom.html", - "formatted_body": html, - "msgtype": "m.text", - }; - expect(mockClient.sendMessage).toBeCalledWith('myfakeroom', null, expectedContent); - expect(screen.getByRole('textbox')).toHaveFocus(); - }); - - it('Should focus when receiving an Action.FocusSendMessageComposer action', async () => { - // Given we don't have focus - customRender(() => {}, false); - expect(screen.getByRole('textbox')).not.toHaveFocus(); - - // When we send the right action - defaultDispatcher.dispatch({ - action: Action.FocusSendMessageComposer, - context: null, - }); - - // Then the component gets the focus - await waitFor(() => expect(screen.getByRole('textbox')).toHaveFocus()); - }); - - it('Should focus when receiving a reply_to_event action', async () => { - // Given we don't have focus - customRender(() => {}, false); - expect(screen.getByRole('textbox')).not.toHaveFocus(); - - // When we send the right action - defaultDispatcher.dispatch({ - action: "reply_to_event", - context: null, - }); - - // Then the component gets the focus - await waitFor(() => expect(screen.getByRole('textbox')).toHaveFocus()); - }); - - it('Should not focus when disabled', async () => { - // Given we don't have focus and we are disabled - customRender(() => {}, true); - expect(screen.getByRole('textbox')).not.toHaveFocus(); - - // When we send an action that would cause us to get focus - defaultDispatcher.dispatch({ - action: Action.FocusSendMessageComposer, - context: null, - }); - // (Send a second event to exercise the clearTimeout logic) - defaultDispatcher.dispatch({ - action: Action.FocusSendMessageComposer, - context: null, - }); - - // Wait for event dispatch to happen - await new Promise((r) => setTimeout(r, 200)); - - // Then we don't get it because we are disabled - expect(screen.getByRole('textbox')).not.toHaveFocus(); - }); - - it('sends a message when Enter is pressed', async () => { - // Given a composer - customRender(() => {}, false); - - // When we tell its inputEventProcesser that the user pressed Enter - const event = new InputEvent("insertParagraph", { inputType: "insertParagraph" }); - const wysiwyg = { actions: { clear: () => {} } } as Wysiwyg; - inputEventProcessor(event, wysiwyg); - - // Then it sends a message - expect(mockClient.sendMessage).toBeCalledWith( - "myfakeroom", - null, - { - "body": "html", - "format": "org.matrix.custom.html", - "formatted_body": "html", - "msgtype": "m.text", - }, - ); - // TODO: plain text body above is wrong - will be fixed when we provide markdown for it - }); - - describe('when settings require Ctrl+Enter to send', () => { - beforeEach(() => { - jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => { - if (name === "MessageComposerInput.ctrlEnterToSend") return true; - }); - }); - - it('does not send a message when Enter is pressed', async () => { - // Given a composer - customRender(() => {}, false); - - // When we tell its inputEventProcesser that the user pressed Enter - const event = new InputEvent("input", { inputType: "insertParagraph" }); - const wysiwyg = { actions: { clear: () => {} } } as Wysiwyg; - inputEventProcessor(event, wysiwyg); - - // Then it does not send a message - expect(mockClient.sendMessage).toBeCalledTimes(0); - }); - - it('sends a message when Ctrl+Enter is pressed', async () => { - // Given a composer - customRender(() => {}, false); - - // When we tell its inputEventProcesser that the user pressed Ctrl+Enter - const event = new InputEvent("input", { inputType: "sendMessage" }); - const wysiwyg = { actions: { clear: () => {} } } as Wysiwyg; - inputEventProcessor(event, wysiwyg); - - // Then it sends a message - expect(mockClient.sendMessage).toBeCalledTimes(1); - }); - }); -}); - diff --git a/test/components/views/rooms/wysiwyg_composer/FormattingButtons-test.tsx b/test/components/views/rooms/wysiwyg_composer/components/FormattingButtons-test.tsx similarity index 95% rename from test/components/views/rooms/wysiwyg_composer/FormattingButtons-test.tsx rename to test/components/views/rooms/wysiwyg_composer/components/FormattingButtons-test.tsx index a9838ecacaa..e935b62ae5e 100644 --- a/test/components/views/rooms/wysiwyg_composer/FormattingButtons-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/components/FormattingButtons-test.tsx @@ -18,7 +18,8 @@ import React from 'react'; import { render, screen } from "@testing-library/react"; import userEvent from '@testing-library/user-event'; -import { FormattingButtons } from "../../../../../src/components/views/rooms/wysiwyg_composer/components/FormattingButtons"; +import { FormattingButtons } + from "../../../../../../src/components/views/rooms/wysiwyg_composer/components/FormattingButtons"; describe('FormattingButtons', () => { const wysiwyg = { diff --git a/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx new file mode 100644 index 00000000000..e7e21ca839c --- /dev/null +++ b/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx @@ -0,0 +1,152 @@ +/* +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 "@testing-library/jest-dom"; +import React from "react"; +import { render, screen } from "@testing-library/react"; +import { InputEventProcessor, Wysiwyg, WysiwygProps } from "@matrix-org/matrix-wysiwyg"; + +import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext"; +import { IRoomState } from "../../../../../../src/components/structures/RoomView"; +import { createTestClient, getRoomContext, mkEvent, mkStubRoom } from "../../../../../test-utils"; +import RoomContext from "../../../../../../src/contexts/RoomContext"; +import { WysiwygComposer } + from "../../../../../../src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer"; +import SettingsStore from "../../../../../../src/settings/SettingsStore"; + +// Work around missing ClipboardEvent type +class MyClipboardEvent {} +window.ClipboardEvent = MyClipboardEvent as any; + +let inputEventProcessor: InputEventProcessor | null = null; + +// The wysiwyg fetch wasm bytes and a specific workaround is needed to make it works in a node (jest) environnement +// See https://github.com/matrix-org/matrix-wysiwyg/blob/main/platforms/web/test.setup.ts +jest.mock("@matrix-org/matrix-wysiwyg", () => ({ + useWysiwyg: (props: WysiwygProps) => { + inputEventProcessor = props.inputEventProcessor ?? null; + return { + ref: { current: null }, + content: 'html', + isWysiwygReady: true, + wysiwyg: { clear: () => void 0 }, + formattingStates: { + bold: 'enabled', + italic: 'enabled', + underline: 'enabled', + strikeThrough: 'enabled', + }, + }; + }, +})); + +describe('WysiwygComposer', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + const mockClient = createTestClient(); + const mockEvent = mkEvent({ + type: "m.room.message", + room: 'myfakeroom', + user: 'myfakeuser', + content: { "msgtype": "m.text", "body": "Replying to this" }, + event: true, + }); + const mockRoom = mkStubRoom('myfakeroom', 'myfakeroom', mockClient) as any; + mockRoom.findEventById = jest.fn(eventId => { + return eventId === mockEvent.getId() ? mockEvent : null; + }); + + const defaultRoomContext: IRoomState = getRoomContext(mockRoom, {}); + + const customRender = (onChange = (_content: string) => void 0, onSend = () => void 0, disabled = false) => { + return render( + + + + + , + ); + }; + + it('Should have contentEditable at false when disabled', () => { + // When + customRender(jest.fn(), jest.fn(), true); + + // Then + expect(screen.getByRole('textbox')).toHaveAttribute('contentEditable', "false"); + }); + + it('Should call onChange handler', (done) => { + const html = 'html'; + customRender((content) => { + expect(content).toBe((html)); + done(); + }, jest.fn()); + }); + + it('Should call onSend when Enter is pressed ', () => { + //When + const onSend = jest.fn(); + customRender(jest.fn(), onSend); + + // When we tell its inputEventProcesser that the user pressed Enter + const event = new InputEvent("insertParagraph", { inputType: "insertParagraph" }); + const wysiwyg = { actions: { clear: () => {} } } as Wysiwyg; + inputEventProcessor(event, wysiwyg); + + // Then it sends a message + expect(onSend).toBeCalledTimes(1); + }); + + describe('When settings require Ctrl+Enter to send', () => { + beforeEach(() => { + jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => { + if (name === "MessageComposerInput.ctrlEnterToSend") return true; + }); + }); + + it('Should not call onSend when Enter is pressed', async () => { + // Given a composer + const onSend = jest.fn(); + customRender(() => {}, onSend, false); + + // When we tell its inputEventProcesser that the user pressed Enter + const event = new InputEvent("input", { inputType: "insertParagraph" }); + const wysiwyg = { actions: { clear: () => {} } } as Wysiwyg; + inputEventProcessor(event, wysiwyg); + + // Then it does not send a message + expect(onSend).toBeCalledTimes(0); + }); + + it('Should send a message when Ctrl+Enter is pressed', async () => { + // Given a composer + const onSend = jest.fn(); + customRender(() => {}, onSend, false); + + // When we tell its inputEventProcesser that the user pressed Ctrl+Enter + const event = new InputEvent("input", { inputType: "sendMessage" }); + const wysiwyg = { actions: { clear: () => {} } } as Wysiwyg; + inputEventProcessor(event, wysiwyg); + + // Then it sends a message + expect(onSend).toBeCalledTimes(1); + }); + }); +}); + diff --git a/test/components/views/rooms/wysiwyg_composer/utils/createMessageContent-test.ts b/test/components/views/rooms/wysiwyg_composer/utils/createMessageContent-test.ts new file mode 100644 index 00000000000..a4335b2bf10 --- /dev/null +++ b/test/components/views/rooms/wysiwyg_composer/utils/createMessageContent-test.ts @@ -0,0 +1,133 @@ +/* +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 { mkEvent } from "../../../../../test-utils"; +import { RoomPermalinkCreator } from "../../../../../../src/utils/permalinks/Permalinks"; +import { createMessageContent } + from "../../../../../../src/components/views/rooms/wysiwyg_composer/utils/createMessageContent"; + +describe('createMessageContent', () => { + const permalinkCreator = { + forEvent(eventId: string): string { + return "$$permalink$$"; + }, + } as RoomPermalinkCreator; + const message = 'hello world'; + const mockEvent = mkEvent({ + type: "m.room.message", + room: 'myfakeroom', + user: 'myfakeuser', + content: { "msgtype": "m.text", "body": "Replying to this" }, + event: true, + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it("Should create html message", () => { + // When + const content = createMessageContent(message, { permalinkCreator }); + + // Then + expect(content).toEqual({ + "body": message, + "format": "org.matrix.custom.html", + "formatted_body": message, + "msgtype": "m.text", + }); + }); + + it('Should add reply to message content', () => { + // When + const content = createMessageContent(message, { permalinkCreator, replyToEvent: mockEvent }); + + // Then + expect(content).toEqual({ + "body": "> Replying to this\n\nhello world", + "format": "org.matrix.custom.html", + "formatted_body": "
In reply to" + + " myfakeuser"+ + "
Replying to this
hello world", + "msgtype": "m.text", + "m.relates_to": { + "m.in_reply_to": { + "event_id": mockEvent.getId(), + }, + }, + }); + }); + + it("Should add relation to message", () => { + // When + const relation = { + rel_type: "m.thread", + event_id: "myFakeThreadId", + }; + const content = createMessageContent(message, { permalinkCreator, relation }); + + // Then + expect(content).toEqual({ + "body": message, + "format": "org.matrix.custom.html", + "formatted_body": message, + "msgtype": "m.text", + "m.relates_to": { + "event_id": "myFakeThreadId", + "rel_type": "m.thread", + }, + }); + }); + + it('Should add fields related to edition', () => { + // When + const editedEvent = mkEvent({ + type: "m.room.message", + room: 'myfakeroom', + user: 'myfakeuser2', + content: { + "msgtype": "m.text", + "body": "First message", + "formatted_body": "First Message", + "m.relates_to": { + "m.in_reply_to": { + "event_id": 'eventId', + }, + } }, + event: true, + }); + const content = + createMessageContent(message, { permalinkCreator, editedEvent }); + + // Then + expect(content).toEqual({ + "body": message, + "format": "org.matrix.custom.html", + "formatted_body": ` * ${message}`, + "msgtype": "m.text", + "m.new_content": { + "body": message, + "format": "org.matrix.custom.html", + "formatted_body": message, + "msgtype": "m.text", + }, + "m.relates_to": { + "event_id": editedEvent.getId(), + "rel_type": "m.replace", + }, + }); + }); +}); diff --git a/test/components/views/rooms/wysiwyg_composer/message-test.ts b/test/components/views/rooms/wysiwyg_composer/utils/message-test.ts similarity index 53% rename from test/components/views/rooms/wysiwyg_composer/message-test.ts rename to test/components/views/rooms/wysiwyg_composer/utils/message-test.ts index e672d2639b0..9d13f281760 100644 --- a/test/components/views/rooms/wysiwyg_composer/message-test.ts +++ b/test/components/views/rooms/wysiwyg_composer/utils/message-test.ts @@ -14,15 +14,19 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { IRoomState } from "../../../../../src/components/structures/RoomView"; -import { createMessageContent, sendMessage } from "../../../../../src/components/views/rooms/wysiwyg_composer/utils/message"; -import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext"; -import { Layout } from "../../../../../src/settings/enums/Layout"; -import { createTestClient, mkEvent, mkStubRoom } from "../../../../test-utils"; -import defaultDispatcher from "../../../../../src/dispatcher/dispatcher"; -import SettingsStore from "../../../../../src/settings/SettingsStore"; -import { SettingLevel } from "../../../../../src/settings/SettingLevel"; -import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks"; +import { EventStatus } from "matrix-js-sdk/src/matrix"; + +import { IRoomState } from "../../../../../../src/components/structures/RoomView"; +import { editMessage, sendMessage } + from "../../../../../../src/components/views/rooms/wysiwyg_composer/utils/message"; +import { createTestClient, getRoomContext, mkEvent, mkStubRoom } from "../../../../../test-utils"; +import defaultDispatcher from "../../../../../../src/dispatcher/dispatcher"; +import SettingsStore from "../../../../../../src/settings/SettingsStore"; +import { SettingLevel } from "../../../../../../src/settings/SettingLevel"; +import { RoomPermalinkCreator } from "../../../../../../src/utils/permalinks/Permalinks"; +import EditorStateTransfer from "../../../../../../src/utils/EditorStateTransfer"; +import * as ConfirmRedactDialog + from "../../../../../../src/components/views/dialogs/ConfirmRedactDialog"; describe('message', () => { const permalinkCreator = { @@ -35,117 +39,30 @@ describe('message', () => { type: "m.room.message", room: 'myfakeroom', user: 'myfakeuser', - content: { "msgtype": "m.text", "body": "Replying to this" }, + content: { + "msgtype": "m.text", + "body": "Replying to this", + "format": 'org.matrix.custom.html', + "formatted_body": 'Replying to this', + }, event: true, }); - afterEach(() => { - jest.resetAllMocks(); + const mockClient = createTestClient(); + const mockRoom = mkStubRoom('myfakeroom', 'myfakeroom', mockClient) as any; + mockRoom.findEventById = jest.fn(eventId => { + return eventId === mockEvent.getId() ? mockEvent : null; }); - describe('createMessageContent', () => { - it("Should create html message", () => { - // When - const content = createMessageContent(message, { permalinkCreator }); + const defaultRoomContext: IRoomState = getRoomContext(mockRoom, {}); - // Then - expect(content).toEqual({ - "body": message, - "format": "org.matrix.custom.html", - "formatted_body": message, - "msgtype": "m.text", - }); - }); + const spyDispatcher = jest.spyOn(defaultDispatcher, "dispatch"); - it('Should add reply to message content', () => { - // When - const content = createMessageContent(message, { permalinkCreator, replyToEvent: mockEvent }); - - // Then - expect(content).toEqual({ - "body": "> Replying to this\n\nhello world", - "format": "org.matrix.custom.html", - "formatted_body": "
In reply to" + - " myfakeuser"+ - "
Replying to this
hello world", - "msgtype": "m.text", - "m.relates_to": { - "m.in_reply_to": { - "event_id": mockEvent.getId(), - }, - }, - }); - }); - - it("Should add relation to message", () => { - // When - const relation = { - rel_type: "m.thread", - event_id: "myFakeThreadId", - }; - const content = createMessageContent(message, { permalinkCreator, relation }); - - // Then - expect(content).toEqual({ - "body": message, - "format": "org.matrix.custom.html", - "formatted_body": message, - "msgtype": "m.text", - "m.relates_to": { - "event_id": "myFakeThreadId", - "rel_type": "m.thread", - }, - }); - }); + afterEach(() => { + jest.resetAllMocks(); }); describe('sendMessage', () => { - const mockClient = createTestClient(); - const mockRoom = mkStubRoom('myfakeroom', 'myfakeroom', mockClient) as any; - mockRoom.findEventById = jest.fn(eventId => { - return eventId === mockEvent.getId() ? mockEvent : null; - }); - - const defaultRoomContext: IRoomState = { - room: mockRoom, - roomLoading: true, - peekLoading: false, - shouldPeek: true, - membersLoaded: false, - numUnreadMessages: 0, - canPeek: false, - showApps: false, - isPeeking: false, - showRightPanel: true, - joining: false, - atEndOfLiveTimeline: true, - showTopUnreadMessagesBar: false, - statusBarVisible: false, - canReact: false, - canSendMessages: false, - layout: Layout.Group, - lowBandwidth: false, - alwaysShowTimestamps: false, - showTwelveHourTimestamps: false, - readMarkerInViewThresholdMs: 3000, - readMarkerOutOfViewThresholdMs: 30000, - showHiddenEvents: false, - showReadReceipts: true, - showRedactions: true, - showJoinLeaves: true, - showAvatarChanges: true, - showDisplaynameChanges: true, - matrixClientIsReady: false, - timelineRenderingType: TimelineRenderingType.Room, - liveTimeline: undefined, - canSelfRedact: false, - resizing: false, - narrow: false, - activeCall: null, - }; - - const spyDispatcher = jest.spyOn(defaultDispatcher, "dispatch"); - it('Should not send empty html message', async () => { // When await sendMessage('', { roomContext: defaultRoomContext, mxClient: mockClient, permalinkCreator }); @@ -231,4 +148,78 @@ describe('message', () => { ); }); }); + + describe('editMessage', () => { + const editorStateTransfer = new EditorStateTransfer(mockEvent); + + it('Should cancel editing and ask for event removal when message is empty', async () => { + // When + const mockCreateRedactEventDialog = jest.spyOn(ConfirmRedactDialog, 'createRedactEventDialog'); + + const mockEvent = mkEvent({ + type: "m.room.message", + room: 'myfakeroom', + user: 'myfakeuser', + content: { "msgtype": "m.text", "body": "Replying to this" }, + event: true, + }); + const replacingEvent = mkEvent({ + type: "m.room.message", + room: 'myfakeroom', + user: 'myfakeuser', + content: { "msgtype": "m.text", "body": "ReplacingEvent" }, + event: true, + }); + replacingEvent.setStatus(EventStatus.QUEUED); + mockEvent.makeReplaced(replacingEvent); + const editorStateTransfer = new EditorStateTransfer(mockEvent); + + await editMessage('', { roomContext: defaultRoomContext, mxClient: mockClient, editorStateTransfer }); + + // Then + expect(mockClient.sendMessage).toBeCalledTimes(0); + expect(mockClient.cancelPendingEvent).toBeCalledTimes(1); + expect(mockCreateRedactEventDialog).toBeCalledTimes(1); + expect(spyDispatcher).toBeCalledTimes(0); + }); + + it('Should do nothing if the content is unmodified', async () => { + // When + await editMessage( + mockEvent.getContent().body, + { roomContext: defaultRoomContext, mxClient: mockClient, editorStateTransfer }); + + // Then + expect(mockClient.sendMessage).toBeCalledTimes(0); + }); + + it('Should send a message when the content is modified', async () => { + // When + const newMessage = `${mockEvent.getContent().body} new content`; + await editMessage( + newMessage, + { roomContext: defaultRoomContext, mxClient: mockClient, editorStateTransfer }); + + // Then + const { msgtype, format } = mockEvent.getContent(); + const expectedContent = { + "body": newMessage, + "formatted_body": ` * ${newMessage}`, + "m.new_content": { + "body": "Replying to this new content", + "format": "org.matrix.custom.html", + "formatted_body": "Replying to this new content", + "msgtype": "m.text", + }, + "m.relates_to": { + "event_id": mockEvent.getId(), + "rel_type": "m.replace", + }, + msgtype, + format, + }; + expect(mockClient.sendMessage).toBeCalledWith(mockEvent.getRoomId(), null, expectedContent); + expect(spyDispatcher).toBeCalledWith({ action: 'message_sent' }); + }); + }); }); diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index 4549190600d..4cc2d1e0dee 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -178,6 +178,7 @@ export function createTestClient(): MatrixClient { sendToDevice: jest.fn().mockResolvedValue(undefined), queueToDevice: jest.fn().mockResolvedValue(undefined), encryptAndSendToDevices: jest.fn().mockResolvedValue(undefined), + cancelPendingEvent: jest.fn(), getMediaHandler: jest.fn().mockReturnValue({ setVideoInput: jest.fn(), From edecc451ddc727ba93f426898c860eef1480b3e8 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Mon, 24 Oct 2022 14:41:11 +0200 Subject: [PATCH 11/14] Fix MessageCompoer-test --- test/components/views/rooms/MessageComposer-test.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/components/views/rooms/MessageComposer-test.tsx b/test/components/views/rooms/MessageComposer-test.tsx index 26156987600..066d97e683c 100644 --- a/test/components/views/rooms/MessageComposer-test.tsx +++ b/test/components/views/rooms/MessageComposer-test.tsx @@ -39,7 +39,7 @@ import { SendMessageComposer } from "../../../../src/components/views/rooms/Send import { E2EStatus } from "../../../../src/utils/ShieldUtils"; import { addTextToComposer } from "../../../test-utils/composer"; import UIStore, { UI_EVENTS } from "../../../../src/stores/UIStore"; -import { WysiwygComposer } from "../../../../src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer"; +import { SendWysiwygComposer } from "../../../../src/components/views/rooms/wysiwyg_composer"; // The wysiwyg fetch wasm bytes and a specific workaround is needed to make it works in a node (jest) environnement // See https://github.com/matrix-org/matrix-wysiwyg/blob/main/platforms/web/test.setup.ts @@ -346,14 +346,14 @@ describe("MessageComposer", () => { }); }); - it('should render WysiwygComposer', () => { + it('should render SendWysiwygComposer', () => { const room = mkStubRoom("!roomId:server", "Room 1", cli); SettingsStore.setValue("feature_wysiwyg_composer", null, SettingLevel.DEVICE, true); const wrapper = wrapAndRender({ room }); SettingsStore.setValue("feature_wysiwyg_composer", null, SettingLevel.DEVICE, false); - expect(wrapper.find(WysiwygComposer)).toBeTruthy(); + expect(wrapper.find(SendWysiwygComposer)).toBeTruthy(); }); }); From fda4dabf6d79ac5badfd736cd652b56cd59dd710 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Mon, 24 Oct 2022 15:03:18 +0200 Subject: [PATCH 12/14] Fix MessageComposer test --- src/components/views/rooms/MessageComposer.tsx | 2 +- test/components/views/rooms/MessageComposer-test.tsx | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index 60bc5ee6410..674635d8961 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -102,7 +102,7 @@ interface IState { showVoiceBroadcastButton: boolean; } -class MessageComposer extends React.Component { +export class MessageComposer extends React.Component { private dispatcherRef?: string; private messageComposerInput = createRef(); private voiceRecordingButton = createRef(); diff --git a/test/components/views/rooms/MessageComposer-test.tsx b/test/components/views/rooms/MessageComposer-test.tsx index 066d97e683c..debeb7b5e63 100644 --- a/test/components/views/rooms/MessageComposer-test.tsx +++ b/test/components/views/rooms/MessageComposer-test.tsx @@ -21,7 +21,8 @@ import { MatrixEvent, MsgType, RoomMember } from "matrix-js-sdk/src/matrix"; import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread"; import { createTestClient, mkEvent, mkStubRoom, stubClient } from "../../../test-utils"; -import MessageComposer from "../../../../src/components/views/rooms/MessageComposer"; +import MessageComposer, { MessageComposer as MessageComposerClass } + from "../../../../src/components/views/rooms/MessageComposer"; import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; import RoomContext from "../../../../src/contexts/RoomContext"; @@ -106,7 +107,7 @@ describe("MessageComposer", () => { it("should call notifyTimelineHeightChanged() for the same context", () => { dis.dispatch({ action: "reply_to_event", - context: (wrapper.instance as unknown as MessageComposer).context, + context: (wrapper.instance as unknown as MessageComposerClass).context, }); wrapper.update(); @@ -207,7 +208,7 @@ describe("MessageComposer", () => { let stateBefore: any; beforeEach(() => { - wrapper = wrapAndRender({ room }); + wrapper = wrapAndRender({ room }).children(); stateBefore = { ...wrapper.instance().state }; resizeCallback("test", {}); wrapper.update(); @@ -220,7 +221,8 @@ describe("MessageComposer", () => { describe("when a resize to narrow event occurred in UIStore", () => { beforeEach(() => { - wrapper = wrapAndRender({ room }, true, true); + wrapper = wrapAndRender({ room }, true, true).children(); + wrapper.setState({ isMenuOpen: true, isStickerPickerOpen: true, @@ -240,7 +242,7 @@ describe("MessageComposer", () => { describe("when a resize to non-narrow event occurred in UIStore", () => { beforeEach(() => { - wrapper = wrapAndRender({ room }, true, false); + wrapper = wrapAndRender({ room }, true, false).children(); wrapper.setState({ isMenuOpen: true, isStickerPickerOpen: true, From 3aead6f9442b5b3805ea2b7665a93eb9045cb8f4 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Mon, 24 Oct 2022 15:25:03 +0200 Subject: [PATCH 13/14] Update @matrix.org/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 f0ab2c266b4..3973d22fb8b 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "dependencies": { "@babel/runtime": "^7.12.5", "@matrix-org/analytics-events": "^0.2.0", - "@matrix-org/matrix-wysiwyg": "^0.3.0", + "@matrix-org/matrix-wysiwyg": "^0.3.2", "@matrix-org/react-sdk-module-api": "^0.0.3", "@sentry/browser": "^6.11.0", "@sentry/tracing": "^6.11.0", diff --git a/yarn.lock b/yarn.lock index add14d4c3e5..d1e87319af0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1660,10 +1660,10 @@ resolved "https://registry.yarnpkg.com/@matrix-org/analytics-events/-/analytics-events-0.2.0.tgz#453925c939ecdd5ca6c797d293deb8cf0933f1b8" integrity sha512-+0/Sydm4MNOcqd8iySJmojVPB74Axba4BXlwTsiKmL5fgYqdUkwmqkO39K7Pn8i+a+8pg11oNvBPkpWs3O5Qww== -"@matrix-org/matrix-wysiwyg@^0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-wysiwyg/-/matrix-wysiwyg-0.3.0.tgz#9a0b996c47fbb63fb235a0810b678158b253f721" - integrity sha512-m33qOo64VIZRqzMZ5vJ9m2gYns+sCaFFy3R5Nn9JfDnldQ1oh+ra611I9keFmO/Ls6548ZN8hUkv+49Ua3iBHA== +"@matrix-org/matrix-wysiwyg@^0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-wysiwyg/-/matrix-wysiwyg-0.3.2.tgz#586f3ad2f4a7bf39d8e2063630c52294c877bcd6" + integrity sha512-Q6Ntj2q1/7rVUlro94snn9eZy/3EbrGqaq5nqNMbttXcnFzYtgligDV1avViB4Um6ZRdDOxnQEPkMca/SqYSmw== "@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 4d9d07e7fd53a4e2a5fbcffd8b1b36b3b4e5b076 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Mon, 24 Oct 2022 15:27:34 +0200 Subject: [PATCH 14/14] Update date in header --- res/css/views/rooms/wysiwyg_composer/_EditWysiwygComposer.pcss | 3 +-- res/css/views/rooms/wysiwyg_composer/components/_Editor.pcss | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/res/css/views/rooms/wysiwyg_composer/_EditWysiwygComposer.pcss b/res/css/views/rooms/wysiwyg_composer/_EditWysiwygComposer.pcss index 8c245bc90f9..73e5fef6e9a 100644 --- a/res/css/views/rooms/wysiwyg_composer/_EditWysiwygComposer.pcss +++ b/res/css/views/rooms/wysiwyg_composer/_EditWysiwygComposer.pcss @@ -1,6 +1,5 @@ /* -Copyright 2019 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +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. diff --git a/res/css/views/rooms/wysiwyg_composer/components/_Editor.pcss b/res/css/views/rooms/wysiwyg_composer/components/_Editor.pcss index 88fa080e23d..6a6b68af7c6 100644 --- a/res/css/views/rooms/wysiwyg_composer/components/_Editor.pcss +++ b/res/css/views/rooms/wysiwyg_composer/components/_Editor.pcss @@ -1,6 +1,5 @@ /* -Copyright 2019 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +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.