diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 093e20b8b6..c7e252d667 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -467,16 +467,17 @@ class TimelinePanel extends React.Component { break; } - case "insert_mention": { + case "composer_insert": { + // re-dispatch to the correct composer if (this.state.editState) { dis.dispatch({ ...payload, - action: "insert_mention_edit_composer", + action: "edit_composer_insert", }); } else { dis.dispatch({ ...payload, - action: "insert_mention_send_composer", + action: "send_composer_insert", }); } break; diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index 56f070ba36..8c23906c68 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -233,7 +233,7 @@ export default class MessageContextMenu extends React.Component { onQuoteClick = () => { dis.dispatch({ - action: 'quote', + action: "composer_insert", event: this.props.mxEvent, }); this.closeMenu(); diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index 353f40b6a9..83f6c80937 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -390,8 +390,8 @@ export default class TextualBody extends React.Component { onEmoteSenderClick = event => { const mxEvent = this.props.mxEvent; dis.dispatch({ - action: 'insert_mention', - user_id: mxEvent.getSender(), + action: "composer_insert", + userId: mxEvent.getSender(), }); }; diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index b862cc84d4..61eaa54639 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -360,8 +360,8 @@ const UserOptionsSection: React.FC<{ const onInsertPillButton = function() { dis.dispatch({ - action: 'insert_mention', - user_id: member.userId, + action: "composer_insert", + userId: member.userId, }); }; diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index b8ebde75cc..ebf97a1d09 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -18,6 +18,7 @@ limitations under the License. import classNames from 'classnames'; import React, {createRef, ClipboardEvent} from 'react'; import {Room} from 'matrix-js-sdk/src/models/room'; +import {MatrixEvent} from 'matrix-js-sdk/src/models/event'; import EMOTICON_REGEX from 'emojibase-regex/emoticon'; import EditorModel from '../../../editor/model'; @@ -32,7 +33,7 @@ import { import {getCaretOffsetAndText, getRangeForSelection} from '../../../editor/dom'; import Autocomplete, {generateCompletionDomId} from '../rooms/Autocomplete'; import {getAutoCompleteCreator} from '../../../editor/parts'; -import {parsePlainTextMessage} from '../../../editor/deserialize'; +import {parseEvent, parsePlainTextMessage} from '../../../editor/deserialize'; import {renderModel} from '../../../editor/render'; import TypingStore from "../../../stores/TypingStore"; import SettingsStore from "../../../settings/SettingsStore"; @@ -732,4 +733,30 @@ export default class BasicMessageEditor extends React.Component // refocus on composer, as we just clicked "Mention" this.focus(); } + + public insertQuotedMessage(event: MatrixEvent) { + const {model} = this.props; + const {partCreator} = model; + const quoteParts = parseEvent(event, partCreator, {isQuotedMessage: true}); + // add two newlines + quoteParts.push(partCreator.newline()); + quoteParts.push(partCreator.newline()); + model.transform(() => { + const addedLen = model.insert(quoteParts, model.positionForOffset(0)); + return model.positionForOffset(addedLen, true); + }); + // refocus on composer, as we just clicked "Quote" + this.focus(); + } + + public insertPlaintext(text: string) { + const {model} = this.props; + const {partCreator} = model; + const caret = this.getCaret(); + const position = model.positionForOffset(caret.offset, caret.atNodeEnd); + model.transform(() => { + const addedLen = model.insert([partCreator.plain(text)], position); + return model.positionForOffset(caret.offset + addedLen, true); + }); + } } diff --git a/src/components/views/rooms/EditMessageComposer.js b/src/components/views/rooms/EditMessageComposer.js index 1c2b3aed1c..39fafe486c 100644 --- a/src/components/views/rooms/EditMessageComposer.js +++ b/src/components/views/rooms/EditMessageComposer.js @@ -281,8 +281,14 @@ export default class EditMessageComposer extends React.Component { }; onAction = payload => { - if (payload.action === "insert_mention_edit_composer" && this._editorRef) { - this._editorRef.insertMention(payload.user_id); + if (payload.action === "edit_composer_insert" && this._editorRef) { + if (payload.user_id) { + this._editorRef.insertMention(payload.userId); + } else if (payload.event) { + this._editorRef.insertQuotedMessage(payload.event); + } else if (payload.text) { + this._editorRef.insertPlaintext(payload.text); + } } }; diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index d51f4c00f1..fe4fd95d24 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -644,8 +644,8 @@ export default class EventTile extends React.Component { onSenderProfileClick = event => { const mxEvent = this.props.mxEvent; dis.dispatch({ - action: 'insert_mention', - user_id: mxEvent.getSender(), + action: "composer_insert", + userId: mxEvent.getSender(), }); }; diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index b7078766fb..09b9fb4a36 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -312,8 +312,8 @@ export default class MessageComposer extends React.Component { addEmoji(emoji) { dis.dispatch({ - action: "insert_emoji", - emoji, + action: "composer_insert", + text: emoji, }); } diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index aaeadffae1..ab6aac8be8 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -482,44 +482,18 @@ export default class SendMessageComposer extends React.Component { case Action.FocusComposer: this._editorRef && this._editorRef.focus(); break; - case 'insert_mention_send_composer': - this._editorRef && this._editorRef.insertMention(payload.user_id); - break; - case 'quote': - this._insertQuotedMessage(payload.event); - break; - case 'insert_emoji': - this._insertEmoji(payload.emoji); + case "send_composer_insert": + if (payload.userId) { + this._editorRef && this._editorRef.insertMention(payload.userId); + } else if (payload.event) { + this._editorRef && this._editorRef.insertQuotedMessage(payload.event); + } else if (payload.text) { + this._editorRef && this._editorRef.insertPlaintext(payload.emoji); + } break; } }; - _insertQuotedMessage(event) { - const {model} = this; - const {partCreator} = model; - const quoteParts = parseEvent(event, partCreator, {isQuotedMessage: true}); - // add two newlines - quoteParts.push(partCreator.newline()); - quoteParts.push(partCreator.newline()); - model.transform(() => { - const addedLen = model.insert(quoteParts, model.positionForOffset(0)); - return model.positionForOffset(addedLen, true); - }); - // refocus on composer, as we just clicked "Quote" - this._editorRef && this._editorRef.focus(); - } - - _insertEmoji = (emoji) => { - const {model} = this; - const {partCreator} = model; - const caret = this._editorRef.getCaret(); - const position = model.positionForOffset(caret.offset, caret.atNodeEnd); - model.transform(() => { - const addedLen = model.insert([partCreator.plain(emoji)], position); - return model.positionForOffset(caret.offset + addedLen, true); - }); - }; - _onPaste = (event) => { const {clipboardData} = event; // Prioritize text on the clipboard over files as Office on macOS puts a bitmap diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts index cd32c3743f..0a01e02b9a 100644 --- a/src/dispatcher/actions.ts +++ b/src/dispatcher/actions.ts @@ -138,4 +138,9 @@ export enum Action { * Fired when an upload is cancelled by the user. Should be used with UploadCanceledPayload. */ UploadCanceled = "upload_canceled", + + /** + * Inserts content into the active composer. Should be used with ComposerInsertPayload + */ + ComposerInsert = "composer_insert", } diff --git a/src/dispatcher/payloads/ComposerInsertPayload.ts b/src/dispatcher/payloads/ComposerInsertPayload.ts new file mode 100644 index 0000000000..9702855432 --- /dev/null +++ b/src/dispatcher/payloads/ComposerInsertPayload.ts @@ -0,0 +1,42 @@ +/* +Copyright 2021 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 { MatrixEvent } from "matrix-js-sdk/src/models/event"; + +import { ActionPayload } from "../payloads"; +import { Action } from "../actions"; + +interface IBaseComposerInsertPayload extends ActionPayload { + action: Action.ComposerInsert, +} + +interface IComposerInsertMentionPayload extends IBaseComposerInsertPayload { + userId: string; +} + +interface IComposerInsertQuotePayload extends IBaseComposerInsertPayload { + event: MatrixEvent; +} + +interface IComposerInsertPlaintextPayload extends IBaseComposerInsertPayload { + text: string; +} + +export type ComposerInsertPayload = + IComposerInsertMentionPayload | + IComposerInsertQuotePayload | + IComposerInsertPlaintextPayload; + diff --git a/src/editor/model.ts b/src/editor/model.ts index 2e70b872e9..8c4a2dbd0d 100644 --- a/src/editor/model.ts +++ b/src/editor/model.ts @@ -390,7 +390,7 @@ export default class EditorModel { return addLen; } - positionForOffset(totalOffset: number, atPartEnd: boolean) { + positionForOffset(totalOffset: number, atPartEnd = false) { let currentOffset = 0; const index = this._parts.findIndex(part => { const partLen = part.text.length;