From bfb1638ff3c3d5dd412b95b3754c0a456b664951 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 5 Oct 2022 12:01:41 +0200 Subject: [PATCH 01/19] Add wysisyg composer (can only send message, enable behind a labs flag) --- package.json | 1 + .../views/rooms/MessageComposer.tsx | 55 +++-- .../wysiwyg_composer/WysiwygComposer.tsx | 58 ++++++ .../views/rooms/wysiwyg_composer/message.ts | 190 ++++++++++++++++++ .../rooms/wysiwyg_composer/useLocalStorage.ts | 33 +++ .../rooms/wysiwyg_composer/useMatrixClient.ts | 36 ++++ src/contexts/MatrixClientContext.tsx | 4 + src/contexts/RoomContext.ts | 5 +- src/i18n/strings/en_EN.json | 1 + src/settings/Settings.tsx | 7 + yarn.lock | 26 +++ 11 files changed, 401 insertions(+), 15 deletions(-) create mode 100644 src/components/views/rooms/wysiwyg_composer/WysiwygComposer.tsx create mode 100644 src/components/views/rooms/wysiwyg_composer/message.ts create mode 100644 src/components/views/rooms/wysiwyg_composer/useLocalStorage.ts create mode 100644 src/components/views/rooms/wysiwyg_composer/useMatrixClient.ts diff --git a/package.json b/package.json index 82b26a93f5..6016b0904d 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "matrix-events-sdk": "^0.0.1-beta.7", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", "matrix-widget-api": "^1.1.1", + "matrix-wysiwyg": "link:../matrix-wysiwyg/platforms/web", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", "pako": "^2.0.3", diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index cf0fe3fd6a..8d3de0f03b 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -58,6 +58,7 @@ import { startNewVoiceBroadcastRecording, VoiceBroadcastRecordingsStore, } from '../../../voice-broadcast'; +import { WysiwygComposer } from './wysiwyg_composer/WysiwygComposer'; let instanceCount = 0; @@ -105,6 +106,7 @@ export default class MessageComposer extends React.Component { private voiceRecordingButton = createRef(); private ref: React.RefObject = createRef(); private instanceId: number; + private composerSendMessage?: () => void; private _voiceRecording: Optional; @@ -313,6 +315,7 @@ export default class MessageComposer extends React.Component { } this.messageComposerInput.current?.sendMessage(); + this.composerSendMessage?.(); }; private onChange = (model: EditorModel) => { @@ -321,6 +324,13 @@ export default class MessageComposer extends React.Component { }); }; + private onWysiwygChange = (content: string) => { + console.log('content', content); + this.setState({ + isComposerEmpty: content?.length === 0, + }); + }; + private onVoiceStoreUpdate = () => { this.updateRecordingState(); }; @@ -394,20 +404,37 @@ export default class MessageComposer extends React.Component { const canSendMessages = this.context.canSendMessages && !this.context.tombstone; if (canSendMessages) { - controls.push( - , - ); + const isWysiwygComposerEnabled = SettingsStore.getValue("feature_wysiwyg_composer"); + + if (isWysiwygComposerEnabled) { + controls.push( + + { (sendMessage) => { + this.composerSendMessage = sendMessage; + } } + , + ); + } else { + controls.push( + , + ); + } controls.push( void; + relation: IEventRelation; + replyToEvent?: MatrixEvent; + permalinkCreator: RoomPermalinkCreator; + includeReplyLegacyFallback?: boolean; + children?: (sendMessage: () => void) => void; +} + +export function WysiwygComposer( + { disabled = false, onChange, children, ...props }: WysiwygProps, forwardRef, +) { + const roomContext = useRoomContext(); + const mxClient = useMatrixClientContext(); + + const [content, setContent] = useState(); + const { ref, isWysiwygReady } = useWysiwyg({ onChange: (_content) => { + setContent(_content); + onChange(_content); + } }); + + const memoizedSendMessage = useCallback(() => sendMessage(content, mxClient, { roomContext, ...props }), + [content, mxClient, roomContext, props], + ); + + return ( +
+
+ { children?.(memoizedSendMessage) } +
+ ); +} diff --git a/src/components/views/rooms/wysiwyg_composer/message.ts b/src/components/views/rooms/wysiwyg_composer/message.ts new file mode 100644 index 0000000000..a2113e4013 --- /dev/null +++ b/src/components/views/rooms/wysiwyg_composer/message.ts @@ -0,0 +1,190 @@ +/* +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 { 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 { 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 { attachRelation } from "../SendMessageComposer"; +import { addReplyToMessageContent } from "../../../../utils/Reply"; +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'; + +interface SendMessageParams { + relation: IEventRelation; + replyToEvent?: MatrixEvent; + roomContext: IRoomState; + permalinkCreator: RoomPermalinkCreator; + includeReplyLegacyFallback?: boolean; +} + +// exported for tests +export function createMessageContent( + message: string, + { relation, replyToEvent, permalinkCreator, includeReplyLegacyFallback = true }: + Omit, +): IContent { + const isEmote = false; + + // TODO do somethings about 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 = { + msgtype: isEmote ? "m.emote" : "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, + mxClient: MatrixClient, + { roomContext, ...params }: SendMessageParams, +) { + console.log('message', message); + const { relation, replyToEvent } = params; + const { room } = roomContext; + const { roomId } = room; + + const posthogEvent: ComposerEvent = { + eventName: "Composer", + isEditing: false, + isReply: Boolean(replyToEvent), + inThread: relation?.rel_type === THREAD_RELATION_TYPE.name, + }; + if (posthogEvent.inThread) { + const threadRoot = room.findEventById(relation.event_id); + posthogEvent.startsThread = threadRoot?.getThread()?.events.length === 1; + } + PosthogAnalytics.instance.trackEvent(posthogEvent); + + let content: IContent; + + // TODO slash comment + + // TODO replace emotion end of message ? + + // TODO quick reaction + + if (!content) { + content = createMessageContent( + message, + params, + ); + } + + // don't bother sending an empty message + if (!content.body.trim()) { + return; + } + + if (SettingsStore.getValue("Performance.addSendMessageTimingMetadata")) { + decorateStartSendingTime(content); + } + + const threadId = relation?.rel_type === THREAD_RELATION_TYPE.name + ? relation.event_id + : null; + + const prom = doMaybeLocalRoomAction( + roomId, + (actualRoomId: string) => mxClient.sendMessage(actualRoomId, threadId, content), + mxClient, + ); + if (replyToEvent) { + // Clear reply_to_event as we put the message into the queue + // if the send fails, retry will handle resending. + dis.dispatch({ + action: 'reply_to_event', + event: null, + context: roomContext.timelineRenderingType, + }); + } + dis.dispatch({ action: "message_sent" }); + CHAT_EFFECTS.forEach((effect) => { + if (containsEmoji(content, effect.emojis)) { + // For initial threads launch, chat effects are disabled + // see #19731 + const isNotThread = relation?.rel_type !== THREAD_RELATION_TYPE.name; + if (!SettingsStore.getValue("feature_thread") || isNotThread) { + dis.dispatch({ action: `effects.${effect.command}` }); + } + } + }); + if (SettingsStore.getValue("Performance.addSendMessageTimingMetadata")) { + prom.then(resp => { + sendRoundTripMetric(mxClient, roomId, resp.event_id); + }); + } + + // TODO save history + // TODO save local state + + // this.sendHistoryManager.save(model, replyToEvent); + // clear composer + // model.reset([]); + // this.editorRef.current?.clearUndoHistory(); + // this.editorRef.current?.focus(); + // this.clearStoredEditorState(); + //if (shouldSend && SettingsStore.getValue("scrollToBottomOnMessageSent")) { + if (SettingsStore.getValue("scrollToBottomOnMessageSent")) { + dis.dispatch({ + action: "scroll_to_bottom", + timelineRenderingType: roomContext.timelineRenderingType, + }); + } +} diff --git a/src/components/views/rooms/wysiwyg_composer/useLocalStorage.ts b/src/components/views/rooms/wysiwyg_composer/useLocalStorage.ts new file mode 100644 index 0000000000..315d0dbfef --- /dev/null +++ b/src/components/views/rooms/wysiwyg_composer/useLocalStorage.ts @@ -0,0 +1,33 @@ +/* +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 { IEventRelation, Room } from "matrix-js-sdk/src/matrix"; +import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread"; +import { useCallback, useMemo } from "react"; + +export function useWysiwygStoredState(room: Room, relation: IEventRelation) { + const editorStateKey = useMemo(() => { + let key = `mx_cider_state_${room.roomId}`; + if (relation?.rel_type === THREAD_RELATION_TYPE.name) { + key += `_${relation.event_id}`; + } + return key; + }, [room, relation]); + + const clearStoredEditorState = useCallback(() => localStorage.removeItem(editorStateKey), [editorStateKey]); + + return { clearStoredEditorState }; +} diff --git a/src/components/views/rooms/wysiwyg_composer/useMatrixClient.ts b/src/components/views/rooms/wysiwyg_composer/useMatrixClient.ts new file mode 100644 index 0000000000..9d26acdc9e --- /dev/null +++ b/src/components/views/rooms/wysiwyg_composer/useMatrixClient.ts @@ -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 { DebouncedFunc, throttle } from "lodash"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { useEffect, useState } from "react"; + +import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext"; + +export function useMatrixClient(room: Room) { + const mxClient = useMatrixClientContext(); + + const [prepareToEncrypt, setPrepareToEncrypt] = useState void>>(); + useEffect(() => { + if (mxClient.isCryptoEnabled() && mxClient.isRoomEncrypted(room.roomId)) { + setPrepareToEncrypt(throttle(() => { + mxClient.prepareToEncrypt(room); + }, 60000, { leading: true, trailing: false })); + } + }, [mxClient, room]); + + return { mxClient, prepareToEncrypt }; +} diff --git a/src/contexts/MatrixClientContext.tsx b/src/contexts/MatrixClientContext.tsx index 292c1e34d8..4b89bc3213 100644 --- a/src/contexts/MatrixClientContext.tsx +++ b/src/contexts/MatrixClientContext.tsx @@ -25,6 +25,10 @@ export interface MatrixClientProps { mxClient: MatrixClient; } +export function useMatrixClientContext() { + return useContext(MatrixClientContext); +} + const matrixHOC = ( ComposedComponent: ComponentClass, ) => { diff --git a/src/contexts/RoomContext.ts b/src/contexts/RoomContext.ts index a66749a0cd..80bc18e13b 100644 --- a/src/contexts/RoomContext.ts +++ b/src/contexts/RoomContext.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { createContext } from "react"; +import { createContext, useContext } from "react"; import { IRoomState } from "../components/structures/RoomView"; import { Layout } from "../settings/enums/Layout"; @@ -68,3 +68,6 @@ const RoomContext = createContext({ }); RoomContext.displayName = "RoomContext"; export default RoomContext; +export function useRoomContext() { + return useContext(RoomContext); +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 03d5517c84..498eaacc46 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -896,6 +896,7 @@ "How can I leave the beta?": "How can I leave the beta?", "To leave, return to this page and use the ā€œ%(leaveTheBeta)sā€ button.": "To leave, return to this page and use the ā€œ%(leaveTheBeta)sā€ button.", "Leave the beta": "Leave the beta", + "Wysiwyg composer (under active development)": "Wysiwyg composer (under active development)", "Render simple counters in room header": "Render simple counters in room header", "Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)", "Support adding custom themes": "Support adding custom themes", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 52538f7291..193e5b9647 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -303,6 +303,13 @@ export const SETTINGS: {[setting: string]: ISetting} = { }, }, + "feature_wysiwyg_composer": { + isFeature: true, + labsGroup: LabGroup.Messaging, + displayName: _td("Wysiwyg composer (under active development)"), + supportedLevels: LEVELS_FEATURE, + default: false, + }, "feature_state_counters": { isFeature: true, labsGroup: LabGroup.Rooms, diff --git a/yarn.lock b/yarn.lock index 965d66f09c..82c6636182 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6970,6 +6970,10 @@ matrix-widget-api@^1.1.1: "@types/events" "^3.0.0" events "^3.2.0" +"matrix-wysiwyg@link:../matrix-wysiwyg/platforms/web": + version "0.0.0" + uid "" + mdurl@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" @@ -7943,6 +7947,14 @@ react-dom@17.0.2: object-assign "^4.1.1" scheduler "^0.20.2" +react-dom@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" + integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== + dependencies: + loose-envify "^1.1.0" + scheduler "^0.23.0" + react-focus-lock@^2.5.1: version "2.9.1" resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-2.9.1.tgz#094cfc19b4f334122c73bb0bff65d77a0c92dd16" @@ -8018,6 +8030,13 @@ react@17.0.2: loose-envify "^1.1.0" object-assign "^4.1.1" +react@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" + integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== + dependencies: + loose-envify "^1.1.0" + read-pkg-up@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507" @@ -8403,6 +8422,13 @@ scheduler@^0.20.2: loose-envify "^1.1.0" object-assign "^4.1.1" +scheduler@^0.23.0: + version "0.23.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" + integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== + dependencies: + loose-envify "^1.1.0" + schema-utils@^3.0.0: version "3.1.1" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281" From 1d820cf83729f3f4eb9c6748c0401c44e8d0741f Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 5 Oct 2022 14:59:02 +0200 Subject: [PATCH 02/19] Remove console, unused variables... --- src/components/views/rooms/MessageComposer.tsx | 1 - .../views/rooms/wysiwyg_composer/WysiwygComposer.tsx | 8 ++++---- src/components/views/rooms/wysiwyg_composer/message.ts | 1 - 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index 8d3de0f03b..b82a991f2e 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -325,7 +325,6 @@ export default class MessageComposer extends React.Component { }; private onWysiwygChange = (content: string) => { - console.log('content', content); this.setState({ isComposerEmpty: content?.length === 0, }); diff --git a/src/components/views/rooms/wysiwyg_composer/WysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/WysiwygComposer.tsx index 55a81dc0c1..495f0c101e 100644 --- a/src/components/views/rooms/wysiwyg_composer/WysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/WysiwygComposer.tsx @@ -34,7 +34,7 @@ interface WysiwygProps { } export function WysiwygComposer( - { disabled = false, onChange, children, ...props }: WysiwygProps, forwardRef, + { disabled = false, onChange, children, ...props }: WysiwygProps, ) { const roomContext = useRoomContext(); const mxClient = useMatrixClientContext(); @@ -50,9 +50,9 @@ export function WysiwygComposer( ); return ( -
-
+ <> +
{ children?.(memoizedSendMessage) } -
+ ); } diff --git a/src/components/views/rooms/wysiwyg_composer/message.ts b/src/components/views/rooms/wysiwyg_composer/message.ts index a2113e4013..52c962e96c 100644 --- a/src/components/views/rooms/wysiwyg_composer/message.ts +++ b/src/components/views/rooms/wysiwyg_composer/message.ts @@ -95,7 +95,6 @@ export function sendMessage( mxClient: MatrixClient, { roomContext, ...params }: SendMessageParams, ) { - console.log('message', message); const { relation, replyToEvent } = params; const { room } = roomContext; const { roomId } = room; From a50329fb97db7dac9a1e55796211eafa80626258 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Fri, 7 Oct 2022 10:52:19 +0200 Subject: [PATCH 03/19] Focus and clear content after sending a message --- .../views/rooms/wysiwyg_composer/WysiwygComposer.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/views/rooms/wysiwyg_composer/WysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/WysiwygComposer.tsx index 495f0c101e..d5a9706989 100644 --- a/src/components/views/rooms/wysiwyg_composer/WysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/WysiwygComposer.tsx @@ -40,18 +40,20 @@ export function WysiwygComposer( const mxClient = useMatrixClientContext(); const [content, setContent] = useState(); - const { ref, isWysiwygReady } = useWysiwyg({ onChange: (_content) => { + const { ref, isWysiwygReady, wysiwyg } = useWysiwyg({ onChange: (_content) => { setContent(_content); onChange(_content); } }); - const memoizedSendMessage = useCallback(() => sendMessage(content, mxClient, { roomContext, ...props }), - [content, mxClient, roomContext, props], - ); + const memoizedSendMessage = useCallback(() => { + sendMessage(content, mxClient, { roomContext, ...props }); + wysiwyg.clear(); + ref.current?.focus(); + }, [content, mxClient, roomContext, wysiwyg, props, ref]); return ( <> -
+
{ children?.(memoizedSendMessage) } ); From 4938aa8f74dbdcdcc1628618f9a33886f65faa6d Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Fri, 7 Oct 2022 10:53:50 +0200 Subject: [PATCH 04/19] Remove unused code --- src/components/views/rooms/wysiwyg_composer/message.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/components/views/rooms/wysiwyg_composer/message.ts b/src/components/views/rooms/wysiwyg_composer/message.ts index 52c962e96c..61300b8611 100644 --- a/src/components/views/rooms/wysiwyg_composer/message.ts +++ b/src/components/views/rooms/wysiwyg_composer/message.ts @@ -173,12 +173,6 @@ export function sendMessage( // TODO save history // TODO save local state - // this.sendHistoryManager.save(model, replyToEvent); - // clear composer - // model.reset([]); - // this.editorRef.current?.clearUndoHistory(); - // this.editorRef.current?.focus(); - // this.clearStoredEditorState(); //if (shouldSend && SettingsStore.getValue("scrollToBottomOnMessageSent")) { if (SettingsStore.getValue("scrollToBottomOnMessageSent")) { dis.dispatch({ From 21677e67e826430dae827f265fa4491a57e11370 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Fri, 7 Oct 2022 10:56:50 +0200 Subject: [PATCH 05/19] Remove unused MatrixClient --- .../rooms/wysiwyg_composer/useMatrixClient.ts | 36 ------------------- 1 file changed, 36 deletions(-) delete mode 100644 src/components/views/rooms/wysiwyg_composer/useMatrixClient.ts diff --git a/src/components/views/rooms/wysiwyg_composer/useMatrixClient.ts b/src/components/views/rooms/wysiwyg_composer/useMatrixClient.ts deleted file mode 100644 index 9d26acdc9e..0000000000 --- a/src/components/views/rooms/wysiwyg_composer/useMatrixClient.ts +++ /dev/null @@ -1,36 +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 { DebouncedFunc, throttle } from "lodash"; -import { Room } from "matrix-js-sdk/src/models/room"; -import { useEffect, useState } from "react"; - -import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext"; - -export function useMatrixClient(room: Room) { - const mxClient = useMatrixClientContext(); - - const [prepareToEncrypt, setPrepareToEncrypt] = useState void>>(); - useEffect(() => { - if (mxClient.isCryptoEnabled() && mxClient.isRoomEncrypted(room.roomId)) { - setPrepareToEncrypt(throttle(() => { - mxClient.prepareToEncrypt(room); - }, 60000, { leading: true, trailing: false })); - } - }, [mxClient, room]); - - return { mxClient, prepareToEncrypt }; -} From c7f912529de66d66213c98c1444a590a2a6a5b16 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Fri, 7 Oct 2022 11:02:22 +0200 Subject: [PATCH 06/19] Move mxClient to object in sendMessage --- .../views/rooms/wysiwyg_composer/WysiwygComposer.tsx | 2 +- src/components/views/rooms/wysiwyg_composer/message.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/wysiwyg_composer/WysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/WysiwygComposer.tsx index d5a9706989..f22963e4e7 100644 --- a/src/components/views/rooms/wysiwyg_composer/WysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/WysiwygComposer.tsx @@ -46,7 +46,7 @@ export function WysiwygComposer( } }); const memoizedSendMessage = useCallback(() => { - sendMessage(content, mxClient, { roomContext, ...props }); + sendMessage(content, { mxClient, roomContext, ...props }); wysiwyg.clear(); ref.current?.focus(); }, [content, mxClient, roomContext, wysiwyg, props, ref]); diff --git a/src/components/views/rooms/wysiwyg_composer/message.ts b/src/components/views/rooms/wysiwyg_composer/message.ts index 61300b8611..e39f6b48c8 100644 --- a/src/components/views/rooms/wysiwyg_composer/message.ts +++ b/src/components/views/rooms/wysiwyg_composer/message.ts @@ -32,6 +32,7 @@ import { IRoomState } from "../../../structures/RoomView"; import dis from '../../../../dispatcher/dispatcher'; interface SendMessageParams { + mxClient: MatrixClient; relation: IEventRelation; replyToEvent?: MatrixEvent; roomContext: IRoomState; @@ -43,7 +44,7 @@ interface SendMessageParams { export function createMessageContent( message: string, { relation, replyToEvent, permalinkCreator, includeReplyLegacyFallback = true }: - Omit, + Omit, ): IContent { const isEmote = false; @@ -92,8 +93,7 @@ export function createMessageContent( export function sendMessage( message: string, - mxClient: MatrixClient, - { roomContext, ...params }: SendMessageParams, + { roomContext, mxClient, ...params }: SendMessageParams, ) { const { relation, replyToEvent } = params; const { room } = roomContext; From bcc53fc337bb3162363447927a574c25180cafc2 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Fri, 7 Oct 2022 11:46:57 +0200 Subject: [PATCH 07/19] Add style for WysiwygComposer --- res/css/_components.pcss | 1 + .../wysiwyg_composer/_WysiwygComposer.pcss | 53 +++++++++++++++++++ .../wysiwyg_composer/WysiwygComposer.tsx | 8 +-- 3 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 res/css/views/rooms/wysiwyg_composer/_WysiwygComposer.pcss diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 9161942d87..435ed9158b 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -295,6 +295,7 @@ @import "./views/rooms/_TopUnreadMessagesBar.pcss"; @import "./views/rooms/_VoiceRecordComposerTile.pcss"; @import "./views/rooms/_WhoIsTypingTile.pcss"; +@import "./views/rooms/wysiwyg_composer/_WysiwygComposer.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/_WysiwygComposer.pcss b/res/css/views/rooms/wysiwyg_composer/_WysiwygComposer.pcss new file mode 100644 index 0000000000..133b66388e --- /dev/null +++ b/res/css/views/rooms/wysiwyg_composer/_WysiwygComposer.pcss @@ -0,0 +1,53 @@ +/* +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. +*/ + +.mx_WysiwygComposer { + flex: 1; + display: flex; + flex-direction: column; + font-size: $font-14px; + /* fixed line height to prevent emoji from being taller than text */ + line-height: $font-18px; + justify-content: center; + margin-right: 6px; + /* don't grow wider than available space */ + min-width: 0; + + .mx_WysiwygComposer_container { + flex: 1; + display: flex; + flex-direction: column; + /* min-height at this level so the mx_BasicMessageComposer_input */ + /* still stays vertically centered when less than 55px. */ + /* We also set this to ensure the voice message recording widget */ + /* doesn't cause a jump. */ + min-height: 55px; + + .mx_WysiwygComposer_content { + border: 1px solid; + border-radius: 20px; + padding: 8px 10px; + /* this will center the contenteditable */ + /* in it's parent vertically */ + /* while keeping the autocomplete at the top */ + /* of the composer. The parent needs to be a flex container for this to work. */ + margin: auto 0; + /* max-height at this level so autocomplete doesn't get scrolled too */ + max-height: 140px; + overflow-y: auto; + } + } +} diff --git a/src/components/views/rooms/wysiwyg_composer/WysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/WysiwygComposer.tsx index f22963e4e7..3582b76ac9 100644 --- a/src/components/views/rooms/wysiwyg_composer/WysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/WysiwygComposer.tsx @@ -52,9 +52,11 @@ export function WysiwygComposer( }, [content, mxClient, roomContext, wysiwyg, props, ref]); return ( - <> -
+
+
+
+
{ children?.(memoizedSendMessage) } - +
); } From 67aab08759459c328f4c475f010ab4936e752d32 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Fri, 7 Oct 2022 15:05:29 +0200 Subject: [PATCH 08/19] Update yarn.lock --- yarn.lock | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/yarn.lock b/yarn.lock index 82c6636182..821bb0d90f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7947,14 +7947,6 @@ react-dom@17.0.2: object-assign "^4.1.1" scheduler "^0.20.2" -react-dom@^18.2.0: - version "18.2.0" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" - integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== - dependencies: - loose-envify "^1.1.0" - scheduler "^0.23.0" - react-focus-lock@^2.5.1: version "2.9.1" resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-2.9.1.tgz#094cfc19b4f334122c73bb0bff65d77a0c92dd16" @@ -8030,13 +8022,6 @@ react@17.0.2: loose-envify "^1.1.0" object-assign "^4.1.1" -react@^18.2.0: - version "18.2.0" - resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" - integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== - dependencies: - loose-envify "^1.1.0" - read-pkg-up@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507" @@ -8422,13 +8407,6 @@ scheduler@^0.20.2: loose-envify "^1.1.0" object-assign "^4.1.1" -scheduler@^0.23.0: - version "0.23.0" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" - integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== - dependencies: - loose-envify "^1.1.0" - schema-utils@^3.0.0: version "3.1.1" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281" From 667e8ef10f4047e1f5b8cf68a08113e954618ea2 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Mon, 10 Oct 2022 11:16:42 +0200 Subject: [PATCH 09/19] Add tests to message.ts --- .../views/rooms/wysiwyg_composer/message.ts | 19 ++- .../rooms/wysisyg_composer/message-test.ts | 148 ++++++++++++++++++ 2 files changed, 160 insertions(+), 7 deletions(-) create mode 100644 test/components/views/rooms/wysisyg_composer/message-test.ts diff --git a/src/components/views/rooms/wysiwyg_composer/message.ts b/src/components/views/rooms/wysiwyg_composer/message.ts index e39f6b48c8..9f0f47d128 100644 --- a/src/components/views/rooms/wysiwyg_composer/message.ts +++ b/src/components/views/rooms/wysiwyg_composer/message.ts @@ -23,7 +23,6 @@ import { PosthogAnalytics } from "../../../../PosthogAnalytics"; import SettingsStore from "../../../../settings/SettingsStore"; import { decorateStartSendingTime, sendRoundTripMetric } from "../../../../sendTimePerformanceMetrics"; import { attachRelation } from "../SendMessageComposer"; -import { addReplyToMessageContent } from "../../../../utils/Reply"; import { RoomPermalinkCreator } from "../../../../utils/permalinks/Permalinks"; import { doMaybeLocalRoomAction } from "../../../../utils/local-room"; import { CHAT_EFFECTS } from "../../../../effects"; @@ -33,7 +32,7 @@ import dis from '../../../../dispatcher/dispatcher'; interface SendMessageParams { mxClient: MatrixClient; - relation: IEventRelation; + relation?: IEventRelation; replyToEvent?: MatrixEvent; roomContext: IRoomState; permalinkCreator: RoomPermalinkCreator; @@ -81,12 +80,14 @@ export function createMessageContent( } attachRelation(content, relation); - if (replyToEvent) { + + // TODO reply + /*if (replyToEvent) { addReplyToMessageContent(content, replyToEvent, { permalinkCreator, includeLegacyFallback: includeReplyLegacyFallback, }); - } + }*/ return content; } @@ -106,7 +107,7 @@ export function sendMessage( inThread: relation?.rel_type === THREAD_RELATION_TYPE.name, }; if (posthogEvent.inThread) { - const threadRoot = room.findEventById(relation.event_id); + const threadRoot = room.findEventById(relation?.event_id); posthogEvent.startsThread = threadRoot?.getThread()?.events.length === 1; } PosthogAnalytics.instance.trackEvent(posthogEvent); @@ -144,7 +145,9 @@ export function sendMessage( (actualRoomId: string) => mxClient.sendMessage(actualRoomId, threadId, content), mxClient, ); - if (replyToEvent) { + + // TODO reply + /*if (replyToEvent) { // Clear reply_to_event as we put the message into the queue // if the send fails, retry will handle resending. dis.dispatch({ @@ -152,7 +155,7 @@ export function sendMessage( event: null, context: roomContext.timelineRenderingType, }); - } + }*/ dis.dispatch({ action: "message_sent" }); CHAT_EFFECTS.forEach((effect) => { if (containsEmoji(content, effect.emojis)) { @@ -180,4 +183,6 @@ export function sendMessage( timelineRenderingType: roomContext.timelineRenderingType, }); } + + return prom; } diff --git a/test/components/views/rooms/wysisyg_composer/message-test.ts b/test/components/views/rooms/wysisyg_composer/message-test.ts new file mode 100644 index 0000000000..fe4183f6d0 --- /dev/null +++ b/test/components/views/rooms/wysisyg_composer/message-test.ts @@ -0,0 +1,148 @@ +/* +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 { IRoomState } from "../../../../../src/components/structures/RoomView"; +import { createMessageContent, sendMessage } from "../../../../../src/components/views/rooms/wysiwyg_composer/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"; + +describe('message', () => { + const permalinkCreator = jest.fn() as any; + 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(); + }); + + describe('createMessageContent', () => { + 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", + }); + }); + }); + + 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, + canSendVoiceBroadcasts: 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, + }; + + const spyDispatcher = jest.spyOn(defaultDispatcher, "dispatch"); + + it('Should not send empty html message', async () => { + // When + await sendMessage(message, { roomContext: defaultRoomContext, mxClient: mockClient, permalinkCreator }); + + // Then + const expectedContent = { + "body": "hello world", + "format": "org.matrix.custom.html", + "formatted_body": "hello world", + "msgtype": "m.text", + }; + expect(mockClient.sendMessage).toBeCalledWith('myfakeroom', null, expectedContent); + expect(spyDispatcher).toBeCalledWith({ action: 'message_sent' }); + }); + + it('Should send html message', async () => { + // When + await sendMessage('', { roomContext: defaultRoomContext, mxClient: mockClient, permalinkCreator }); + + // Then + expect(mockClient.sendMessage).toBeCalledTimes(0); + expect(spyDispatcher).toBeCalledTimes(0); + }); + + it('Should scroll to bottom after sending a html message', async () => { + // When + SettingsStore.setValue("scrollToBottomOnMessageSent", null, SettingLevel.DEVICE, true); + await sendMessage(message, { roomContext: defaultRoomContext, mxClient: mockClient, permalinkCreator }); + + // Then + expect(spyDispatcher).toBeCalledWith( + { action: 'scroll_to_bottom', timelineRenderingType: defaultRoomContext.timelineRenderingType }, + ); + }); + + it('Should handle emojis', async () => { + // When + await sendMessage('šŸŽ‰', { roomContext: defaultRoomContext, mxClient: mockClient, permalinkCreator }); + + // Then + expect(spyDispatcher).toBeCalledWith( + { action: 'effects.confetti' }, + ); + }); + }); +}); From 3080d14c00f27b8758ff8745d5ea0be3ffb9f079 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Mon, 10 Oct 2022 11:17:26 +0200 Subject: [PATCH 10/19] Remove unused localstorage hook --- .../rooms/wysiwyg_composer/useLocalStorage.ts | 33 ------------------- 1 file changed, 33 deletions(-) delete mode 100644 src/components/views/rooms/wysiwyg_composer/useLocalStorage.ts diff --git a/src/components/views/rooms/wysiwyg_composer/useLocalStorage.ts b/src/components/views/rooms/wysiwyg_composer/useLocalStorage.ts deleted file mode 100644 index 315d0dbfef..0000000000 --- a/src/components/views/rooms/wysiwyg_composer/useLocalStorage.ts +++ /dev/null @@ -1,33 +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 { IEventRelation, Room } from "matrix-js-sdk/src/matrix"; -import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread"; -import { useCallback, useMemo } from "react"; - -export function useWysiwygStoredState(room: Room, relation: IEventRelation) { - const editorStateKey = useMemo(() => { - let key = `mx_cider_state_${room.roomId}`; - if (relation?.rel_type === THREAD_RELATION_TYPE.name) { - key += `_${relation.event_id}`; - } - return key; - }, [room, relation]); - - const clearStoredEditorState = useCallback(() => localStorage.removeItem(editorStateKey), [editorStateKey]); - - return { clearStoredEditorState }; -} From 200af78c2a3b913851061ebbb62b45657b907f62 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Mon, 10 Oct 2022 14:28:30 +0200 Subject: [PATCH 11/19] Use published matrix-wysisyg --- package.json | 2 +- .../views/rooms/wysiwyg_composer/WysiwygComposer.tsx | 2 +- yarn.lock | 9 +++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 6016b0904d..28fe47c38b 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "dependencies": { "@babel/runtime": "^7.12.5", "@matrix-org/analytics-events": "^0.2.0", + "@matrix-org/matrix-wysiwyg": "^0.0.1", "@matrix-org/react-sdk-module-api": "^0.0.3", "@sentry/browser": "^6.11.0", "@sentry/tracing": "^6.11.0", @@ -94,7 +95,6 @@ "matrix-events-sdk": "^0.0.1-beta.7", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", "matrix-widget-api": "^1.1.1", - "matrix-wysiwyg": "link:../matrix-wysiwyg/platforms/web", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", "pako": "^2.0.3", diff --git a/src/components/views/rooms/wysiwyg_composer/WysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/WysiwygComposer.tsx index 3582b76ac9..f1d63d9a62 100644 --- a/src/components/views/rooms/wysiwyg_composer/WysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/WysiwygComposer.tsx @@ -15,7 +15,7 @@ limitations under the License. */ import React, { useCallback, useState } from 'react'; -import { useWysiwyg } from "matrix-wysiwyg"; +import { useWysiwyg } from "@matrix-org/matrix-wysiwyg"; import { IEventRelation, MatrixEvent } from 'matrix-js-sdk/src/models/event'; import { useRoomContext } from '../../../../contexts/RoomContext'; diff --git a/yarn.lock b/yarn.lock index 821bb0d90f..9dace4cb50 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1549,6 +1549,11 @@ 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.0.1": + version "0.0.1" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-wysiwyg/-/matrix-wysiwyg-0.0.1.tgz#8a6dc546f5c2b44be9cf96eec2380071b70a03e3" + integrity sha512-sInK28oglwY8vgfwVO+SdDMbG3PgaBIkiFHPH+zAZjuSt28oWCLyLfhNKFy44l3cvFy8TUMRDftmimGaLJI7DQ== + "@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" resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz#8d53636d045e1776e2a2ec6613e57330dd9ce856" @@ -6970,10 +6975,6 @@ matrix-widget-api@^1.1.1: "@types/events" "^3.0.0" events "^3.2.0" -"matrix-wysiwyg@link:../matrix-wysiwyg/platforms/web": - version "0.0.0" - uid "" - mdurl@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" From a0377f09325af13777a7b9aac1e5fc2f10b87030 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Mon, 10 Oct 2022 14:43:04 +0200 Subject: [PATCH 12/19] Update wysiwig flag description --- src/i18n/strings/en_EN.json | 2 +- src/settings/Settings.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 498eaacc46..34be752afc 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -896,7 +896,7 @@ "How can I leave the beta?": "How can I leave the beta?", "To leave, return to this page and use the ā€œ%(leaveTheBeta)sā€ button.": "To leave, return to this page and use the ā€œ%(leaveTheBeta)sā€ button.", "Leave the beta": "Leave the beta", - "Wysiwyg composer (under active development)": "Wysiwyg composer (under active development)", + "Wysiwyg composer (plain text mode coming soon) (under active development)": "Wysiwyg composer (plain text mode coming soon) (under active development)", "Render simple counters in room header": "Render simple counters in room header", "Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)", "Support adding custom themes": "Support adding custom themes", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 193e5b9647..a4e55e6fcd 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -306,7 +306,7 @@ export const SETTINGS: {[setting: string]: ISetting} = { "feature_wysiwyg_composer": { isFeature: true, labsGroup: LabGroup.Messaging, - displayName: _td("Wysiwyg composer (under active development)"), + displayName: _td("Wysiwyg composer (plain text mode coming soon) (under active development)"), supportedLevels: LEVELS_FEATURE, default: false, }, From 101fd629dfc197c0c8f2297428b5fc0586d0ff3a Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Mon, 10 Oct 2022 16:50:36 +0200 Subject: [PATCH 13/19] Add WysiwygComposer test --- .../wysiwyg_composer/WysiwygComposer.tsx | 13 +- .../wysiwyg_composer/WysiwygComposer-test.tsx | 147 ++++++++++++++++++ .../message-test.ts | 0 3 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 test/components/views/rooms/wysiwyg_composer/WysiwygComposer-test.tsx rename test/components/views/rooms/{wysisyg_composer => wysiwyg_composer}/message-test.ts (100%) diff --git a/src/components/views/rooms/wysiwyg_composer/WysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/WysiwygComposer.tsx index f1d63d9a62..0c416f06fa 100644 --- a/src/components/views/rooms/wysiwyg_composer/WysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/WysiwygComposer.tsx @@ -26,7 +26,7 @@ import { useMatrixClientContext } from '../../../../contexts/MatrixClientContext interface WysiwygProps { disabled?: boolean; onChange: (content: string) => void; - relation: IEventRelation; + relation?: IEventRelation; replyToEvent?: MatrixEvent; permalinkCreator: RoomPermalinkCreator; includeReplyLegacyFallback?: boolean; @@ -54,7 +54,16 @@ export function WysiwygComposer( return (
-
+
{ children?.(memoizedSendMessage) }
diff --git a/test/components/views/rooms/wysiwyg_composer/WysiwygComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/WysiwygComposer-test.tsx new file mode 100644 index 0000000000..9745dc7f8c --- /dev/null +++ b/test/components/views/rooms/wysiwyg_composer/WysiwygComposer-test.tsx @@ -0,0 +1,147 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; +import { act, render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; + +import { IRoomState } from "../../../../../src/components/structures/RoomView"; +import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext"; +import { Layout } from "../../../../../src/settings/enums/Layout"; +import { createTestClient, mkEvent, mkStubRoom } from "../../../../test-utils"; +import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; +import { WysiwygComposer } from "../../../../../src/components/views/rooms/wysiwyg_composer/WysiwygComposer"; + +let callOnChange: (content: string) => void; + +// 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: ({ onChange }) => { + callOnChange = onChange; + return { ref: { current: null }, isWysiwygReady: true, wysiwyg: { clear: () => void 0 } }; + }, +})); + +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 = { + 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, + canSendVoiceBroadcasts: 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, + }; + + 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(() => callOnChange(html)); + }); + 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(); + }); +}); + diff --git a/test/components/views/rooms/wysisyg_composer/message-test.ts b/test/components/views/rooms/wysiwyg_composer/message-test.ts similarity index 100% rename from test/components/views/rooms/wysisyg_composer/message-test.ts rename to test/components/views/rooms/wysiwyg_composer/message-test.ts From 77005e2b7de432f09f9af6333edb5fed092009f6 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Mon, 10 Oct 2022 16:55:20 +0200 Subject: [PATCH 14/19] Update wysiwyg version --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 28fe47c38b..23228f9e4d 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.0.1", + "@matrix-org/matrix-wysiwyg": "^0.0.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 9dace4cb50..e72fc5de19 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1549,10 +1549,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.0.1": - version "0.0.1" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-wysiwyg/-/matrix-wysiwyg-0.0.1.tgz#8a6dc546f5c2b44be9cf96eec2380071b70a03e3" - integrity sha512-sInK28oglwY8vgfwVO+SdDMbG3PgaBIkiFHPH+zAZjuSt28oWCLyLfhNKFy44l3cvFy8TUMRDftmimGaLJI7DQ== +"@matrix-org/matrix-wysiwyg@^0.0.2": + version "0.0.2" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-wysiwyg/-/matrix-wysiwyg-0.0.2.tgz#c1a18f5f9ac061c4147a0fbbf9303a3c82e626e6" + integrity sha512-AY4sbmgcaFZhNxJfn3Va1SiKH4/gIdvWV9c/iehcIi3/xFB7lKCIwe7NNxzPpFOp+b+fEIbdHf3fhS5vJBi7xg== "@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 ec1140e274cd0e58b4e1af168dad8f65520827f4 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Mon, 10 Oct 2022 17:06:41 +0200 Subject: [PATCH 15/19] Fix type errors --- .../views/rooms/wysiwyg_composer/WysiwygComposer-test.tsx | 1 + test/components/views/rooms/wysiwyg_composer/message-test.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/test/components/views/rooms/wysiwyg_composer/WysiwygComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/WysiwygComposer-test.tsx index 9745dc7f8c..171455bfbc 100644 --- a/test/components/views/rooms/wysiwyg_composer/WysiwygComposer-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/WysiwygComposer-test.tsx @@ -91,6 +91,7 @@ describe('WysiwygComposer', () => { canSelfRedact: false, resizing: false, narrow: false, + activeCall: null, }; let sendMessage: () => void; diff --git a/test/components/views/rooms/wysiwyg_composer/message-test.ts b/test/components/views/rooms/wysiwyg_composer/message-test.ts index fe4183f6d0..605b3e35a7 100644 --- a/test/components/views/rooms/wysiwyg_composer/message-test.ts +++ b/test/components/views/rooms/wysiwyg_composer/message-test.ts @@ -96,6 +96,7 @@ describe('message', () => { canSelfRedact: false, resizing: false, narrow: false, + activeCall: null, }; const spyDispatcher = jest.spyOn(defaultDispatcher, "dispatch"); From 6c7158197c860f8a06994a62556f1605404ca6d9 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Mon, 10 Oct 2022 17:36:18 +0200 Subject: [PATCH 16/19] Add test in MessageComposer --- .../views/rooms/MessageComposer-test.tsx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/components/views/rooms/MessageComposer-test.tsx b/test/components/views/rooms/MessageComposer-test.tsx index 7f5e9715a6..0d4e733fa4 100644 --- a/test/components/views/rooms/MessageComposer-test.tsx +++ b/test/components/views/rooms/MessageComposer-test.tsx @@ -39,6 +39,15 @@ 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"; + +// 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: ({ onChange }) => { + return { ref: { current: null }, isWysiwygReady: true, wysiwyg: { clear: () => void 0 } }; + }, +})); describe("MessageComposer", () => { stubClient(); @@ -346,6 +355,14 @@ describe("MessageComposer", () => { expect(wrapper.find(MessageComposerButtons).props().showStickersButton).toBe(false); }); }); + + it('should render WysiwygComposer', () => { + const room = mkStubRoom("!roomId:server", "Room 1", cli); + + SettingsStore.setValue("feature_wysiwyg_composer", null, SettingLevel.DEVICE, true); + const wrapper = wrapAndRender({ room }); + expect(wrapper.find(WysiwygComposer)).toBeTruthy(); + }); }); function wrapAndRender( From 70f57797f5b727c92dc44f6369ce679c1c40a32c Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Mon, 10 Oct 2022 17:38:41 +0200 Subject: [PATCH 17/19] Add more todo in message.ts --- .../views/rooms/wysiwyg_composer/message.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/components/views/rooms/wysiwyg_composer/message.ts b/src/components/views/rooms/wysiwyg_composer/message.ts index 9f0f47d128..c1569324ef 100644 --- a/src/components/views/rooms/wysiwyg_composer/message.ts +++ b/src/components/views/rooms/wysiwyg_composer/message.ts @@ -45,9 +45,7 @@ export function createMessageContent( { relation, replyToEvent, permalinkCreator, includeReplyLegacyFallback = true }: Omit, ): IContent { - const isEmote = false; - - // TODO do somethings about emote ? + // TODO emote ? /*const isEmote = containsEmote(model); if (isEmote) { @@ -62,7 +60,9 @@ export function createMessageContent( const body = message; const content: IContent = { - msgtype: isEmote ? "m.emote" : "m.text", + // TODO emote + // msgtype: isEmote ? "m.emote" : "m.text", + msgtype: "m.text", body: body, }; @@ -106,10 +106,12 @@ export function sendMessage( isReply: Boolean(replyToEvent), inThread: relation?.rel_type === THREAD_RELATION_TYPE.name, }; - if (posthogEvent.inThread) { + + // TODO thread + /*if (posthogEvent.inThread) { const threadRoot = room.findEventById(relation?.event_id); posthogEvent.startsThread = threadRoot?.getThread()?.events.length === 1; - } + }*/ PosthogAnalytics.instance.trackEvent(posthogEvent); let content: IContent; From f8ec4ec1edc1712607f14277dd10a5c33b9b2057 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Mon, 10 Oct 2022 17:42:15 +0200 Subject: [PATCH 18/19] Disable wysiwyg at the end of the test --- test/components/views/rooms/MessageComposer-test.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/components/views/rooms/MessageComposer-test.tsx b/test/components/views/rooms/MessageComposer-test.tsx index 0d4e733fa4..b8ff024cd8 100644 --- a/test/components/views/rooms/MessageComposer-test.tsx +++ b/test/components/views/rooms/MessageComposer-test.tsx @@ -361,6 +361,8 @@ describe("MessageComposer", () => { 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(); }); }); From 0dd9aba47f58851abc3ef60b1acddf5a8ab6407a Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Tue, 11 Oct 2022 10:09:57 +0200 Subject: [PATCH 19/19] Fix aria-disabled --- src/components/views/rooms/wysiwyg_composer/WysiwygComposer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/wysiwyg_composer/WysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/WysiwygComposer.tsx index 0c416f06fa..e45324982d 100644 --- a/src/components/views/rooms/wysiwyg_composer/WysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/WysiwygComposer.tsx @@ -62,7 +62,7 @@ export function WysiwygComposer( aria-autocomplete="list" aria-haspopup="listbox" dir="auto" - aria-disabled={!disabled && isWysiwygReady} + aria-disabled={disabled || !isWysiwygReady} />
{ children?.(memoizedSendMessage) }