2019-05-06 17:41:15 +02:00
|
|
|
/*
|
2021-06-30 13:01:26 +01:00
|
|
|
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
2019-05-06 17:41:15 +02:00
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
*/
|
2021-06-30 13:01:26 +01:00
|
|
|
|
|
|
|
|
import React, { createRef, KeyboardEvent } from "react";
|
|
|
|
|
import classNames from "classnames";
|
|
|
|
|
import { EventStatus, IContent, MatrixEvent } from "matrix-js-sdk/src/models/event";
|
2021-10-22 17:23:32 -05:00
|
|
|
import { MsgType } from "matrix-js-sdk/src/@types/event";
|
|
|
|
|
import { Room } from "matrix-js-sdk/src/models/room";
|
|
|
|
|
import { logger } from "matrix-js-sdk/src/logger";
|
2022-06-07 20:08:36 +01:00
|
|
|
import { Composer as ComposerEvent } from "@matrix-org/analytics-events/types/typescript/Composer";
|
2021-06-30 13:01:26 +01:00
|
|
|
|
2022-01-12 09:40:18 +00:00
|
|
|
import { _t } from "../../../languageHandler";
|
2020-05-13 20:41:41 -06:00
|
|
|
import dis from "../../../dispatcher/dispatcher";
|
2019-05-07 16:27:09 +02:00
|
|
|
import EditorModel from "../../../editor/model";
|
2021-05-26 14:14:55 +01:00
|
|
|
import { getCaretOffsetAndText } from "../../../editor/dom";
|
|
|
|
|
import { htmlSerializeIfNeeded, textSerialize, containsEmote, stripEmoteCommand } from "../../../editor/serialize";
|
|
|
|
|
import { findEditableEvent } from "../../../utils/EventUtils";
|
|
|
|
|
import { parseEvent } from "../../../editor/deserialize";
|
2022-01-12 09:40:18 +00:00
|
|
|
import { CommandPartCreator, Part, PartCreator } from "../../../editor/parts";
|
2019-06-12 18:52:34 +02:00
|
|
|
import EditorStateTransfer from "../../../utils/EditorStateTransfer";
|
2021-09-17 19:13:46 +02:00
|
|
|
import BasicMessageComposer, { REGEX_EMOTICON } from "./BasicMessageComposer";
|
2022-01-12 09:40:18 +00:00
|
|
|
import { CommandCategories } from "../../../SlashCommands";
|
2021-05-26 14:14:55 +01:00
|
|
|
import { Action } from "../../../dispatcher/actions";
|
2022-01-31 16:55:45 +01:00
|
|
|
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
2021-05-09 13:18:01 +05:30
|
|
|
import SendHistoryManager from "../../../SendHistoryManager";
|
2021-06-30 13:01:26 +01:00
|
|
|
import { ActionPayload } from "../../../dispatcher/payloads";
|
|
|
|
|
import AccessibleButton from "../elements/AccessibleButton";
|
2021-09-28 16:04:25 +02:00
|
|
|
import { createRedactEventDialog } from "../dialogs/ConfirmRedactDialog";
|
2021-09-17 19:13:46 +02:00
|
|
|
import SettingsStore from "../../../settings/SettingsStore";
|
2021-10-01 15:35:54 +02:00
|
|
|
import { withMatrixClientHOC, MatrixClientProps } from "../../../contexts/MatrixClientContext";
|
2021-10-14 16:57:02 +01:00
|
|
|
import RoomContext from "../../../contexts/RoomContext";
|
2021-10-25 13:43:54 +02:00
|
|
|
import { ComposerType } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
2022-01-12 09:40:18 +00:00
|
|
|
import { getSlashCommand, isSlashCommand, runSlashCommand, shouldSendAnyway } from "../../../editor/commands";
|
2022-01-31 16:55:45 +01:00
|
|
|
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
2022-02-09 14:42:08 +00:00
|
|
|
import { PosthogAnalytics } from "../../../PosthogAnalytics";
|
2022-03-29 13:33:11 +01:00
|
|
|
import { editorRoomKey, editorStateKey } from "../../../Editing";
|
2021-09-21 17:48:09 +02:00
|
|
|
|
2021-06-30 13:01:26 +01:00
|
|
|
function getHtmlReplyFallback(mxEvent: MatrixEvent): string {
|
2019-07-08 15:25:44 +02:00
|
|
|
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) || "";
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-30 13:01:26 +01:00
|
|
|
function getTextReplyFallback(mxEvent: MatrixEvent): string {
|
2019-07-08 15:25:44 +02:00
|
|
|
const body = mxEvent.getContent().body;
|
|
|
|
|
const lines = body.split("\n").map((l) => l.trim());
|
|
|
|
|
if (lines.length > 2 && lines[0].startsWith("> ") && lines[1].length === 0) {
|
|
|
|
|
return `${lines[0]}\n\n`;
|
|
|
|
|
}
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-30 14:27:23 +01:00
|
|
|
function createEditContent(model: EditorModel, editedEvent: MatrixEvent): IContent {
|
2019-08-21 11:26:21 +02:00
|
|
|
const isEmote = containsEmote(model);
|
2019-07-08 15:25:44 +02:00
|
|
|
if (isEmote) {
|
2019-08-21 11:26:21 +02:00
|
|
|
model = stripEmoteCommand(model);
|
2019-07-08 15:25:44 +02:00
|
|
|
}
|
2021-09-01 10:55:47 +01:00
|
|
|
const isReply = !!editedEvent.replyEventId;
|
2019-07-08 15:25:44 +02:00
|
|
|
let plainPrefix = "";
|
|
|
|
|
let htmlPrefix = "";
|
|
|
|
|
|
|
|
|
|
if (isReply) {
|
|
|
|
|
plainPrefix = getTextReplyFallback(editedEvent);
|
|
|
|
|
htmlPrefix = getHtmlReplyFallback(editedEvent);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const body = textSerialize(model);
|
|
|
|
|
|
2021-06-30 13:01:26 +01:00
|
|
|
const newContent: IContent = {
|
|
|
|
|
msgtype: isEmote ? MsgType.Emote : MsgType.Text,
|
2019-10-13 13:28:20 +03:00
|
|
|
body: body,
|
2019-07-08 15:25:44 +02:00
|
|
|
};
|
2021-06-30 13:01:26 +01:00
|
|
|
const contentBody: IContent = {
|
2019-07-08 15:25:44 +02:00
|
|
|
msgtype: newContent.msgtype,
|
|
|
|
|
body: `${plainPrefix} * ${body}`,
|
|
|
|
|
};
|
|
|
|
|
|
2022-04-19 15:53:59 +02:00
|
|
|
const formattedBody = htmlSerializeIfNeeded(model, {
|
|
|
|
|
forceHTML: isReply,
|
|
|
|
|
useMarkdown: SettingsStore.getValue("MessageComposerInput.useMarkdown"),
|
|
|
|
|
});
|
2019-07-08 15:25:44 +02:00
|
|
|
if (formattedBody) {
|
|
|
|
|
newContent.format = "org.matrix.custom.html";
|
2019-10-13 13:28:20 +03:00
|
|
|
newContent.formatted_body = formattedBody;
|
2019-07-08 15:25:44 +02:00
|
|
|
contentBody.format = newContent.format;
|
|
|
|
|
contentBody.formatted_body = `${htmlPrefix} * ${formattedBody}`;
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-30 14:27:23 +01:00
|
|
|
const relation = {
|
2019-07-08 15:25:44 +02:00
|
|
|
"m.new_content": newContent,
|
|
|
|
|
"m.relates_to": {
|
|
|
|
|
rel_type: "m.replace",
|
|
|
|
|
event_id: editedEvent.getId(),
|
|
|
|
|
},
|
2021-09-30 14:27:23 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return Object.assign(relation, contentBody);
|
2019-07-08 15:25:44 +02:00
|
|
|
}
|
|
|
|
|
|
2021-10-01 15:35:54 +02:00
|
|
|
interface IEditMessageComposerProps extends MatrixClientProps {
|
2021-06-30 13:01:26 +01:00
|
|
|
editState: EditorStateTransfer;
|
|
|
|
|
className?: string;
|
|
|
|
|
}
|
|
|
|
|
interface IState {
|
|
|
|
|
saveDisabled: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
2021-10-01 15:35:54 +02:00
|
|
|
class EditMessageComposer extends React.Component<IEditMessageComposerProps, IState> {
|
|
|
|
|
static contextType = RoomContext;
|
|
|
|
|
context!: React.ContextType<typeof RoomContext>;
|
2019-05-06 17:41:15 +02:00
|
|
|
|
2021-06-30 13:01:26 +01:00
|
|
|
private readonly editorRef = createRef<BasicMessageComposer>();
|
|
|
|
|
private readonly dispatcherRef: string;
|
|
|
|
|
private model: EditorModel = null;
|
|
|
|
|
|
2021-10-01 15:35:54 +02:00
|
|
|
constructor(props: IEditMessageComposerProps, context: React.ContextType<typeof RoomContext>) {
|
2021-06-30 13:45:43 +01:00
|
|
|
super(props);
|
|
|
|
|
this.context = context; // otherwise React will only set it prior to render due to type def above
|
2019-09-09 18:12:52 +01:00
|
|
|
|
2021-07-01 23:48:52 +01:00
|
|
|
const isRestored = this.createEditorModel();
|
|
|
|
|
const ev = this.props.editState.getEvent();
|
2021-09-30 14:27:23 +01:00
|
|
|
|
2021-10-14 16:57:02 +01:00
|
|
|
const editContent = createEditContent(this.model, ev);
|
2019-09-09 18:12:52 +01:00
|
|
|
this.state = {
|
2021-09-30 14:27:23 +01:00
|
|
|
saveDisabled: !isRestored || !this.isContentModified(editContent["m.new_content"]),
|
2019-09-09 18:12:52 +01:00
|
|
|
};
|
2021-06-30 13:01:26 +01:00
|
|
|
|
|
|
|
|
window.addEventListener("beforeunload", this.saveStoredEditorState);
|
2021-04-13 14:52:26 +01:00
|
|
|
this.dispatcherRef = dis.register(this.onAction);
|
2019-05-07 16:27:09 +02:00
|
|
|
}
|
|
|
|
|
|
2021-06-30 13:01:26 +01:00
|
|
|
private getRoom(): Room {
|
2021-10-01 15:35:54 +02:00
|
|
|
return this.props.mxClient.getRoom(this.props.editState.getEvent().getRoomId());
|
2019-05-24 14:42:33 +02:00
|
|
|
}
|
|
|
|
|
|
2021-06-30 13:01:26 +01:00
|
|
|
private onKeyDown = (event: KeyboardEvent): void => {
|
2019-09-25 10:33:52 +02:00
|
|
|
// ignore any keypress while doing IME compositions
|
2021-06-30 13:01:26 +01:00
|
|
|
if (this.editorRef.current?.isComposing(event)) {
|
2019-09-24 15:32:30 +02:00
|
|
|
return;
|
|
|
|
|
}
|
2021-03-01 21:43:00 +13:00
|
|
|
const action = getKeyBindingsManager().getMessageComposerAction(event);
|
2021-02-16 19:05:39 +13:00
|
|
|
switch (action) {
|
2022-01-31 16:55:45 +01:00
|
|
|
case KeyBindingAction.SendMessage:
|
2021-06-30 13:01:26 +01:00
|
|
|
this.sendEdit();
|
2021-12-15 16:21:32 +00:00
|
|
|
event.stopPropagation();
|
2019-05-24 14:42:33 +02:00
|
|
|
event.preventDefault();
|
2021-02-16 19:05:39 +13:00
|
|
|
break;
|
2022-01-31 16:55:45 +01:00
|
|
|
case KeyBindingAction.CancelReplyOrEdit:
|
2021-12-15 16:21:32 +00:00
|
|
|
event.stopPropagation();
|
2021-06-30 13:01:26 +01:00
|
|
|
this.cancelEdit();
|
2021-02-16 19:05:39 +13:00
|
|
|
break;
|
2022-01-31 16:55:45 +01:00
|
|
|
case KeyBindingAction.EditPrevMessage: {
|
2021-06-30 13:01:26 +01:00
|
|
|
if (this.editorRef.current?.isModified() || !this.editorRef.current?.isCaretAtStart()) {
|
2021-02-16 19:05:39 +13:00
|
|
|
return;
|
|
|
|
|
}
|
2021-10-01 15:35:54 +02:00
|
|
|
const previousEvent = findEditableEvent({
|
|
|
|
|
events: this.events,
|
|
|
|
|
isForward: false,
|
|
|
|
|
fromEventId: this.props.editState.getEvent().getId(),
|
|
|
|
|
});
|
2021-02-16 19:05:39 +13:00
|
|
|
if (previousEvent) {
|
2021-10-01 15:35:54 +02:00
|
|
|
dis.dispatch({
|
|
|
|
|
action: Action.EditEvent,
|
|
|
|
|
event: previousEvent,
|
|
|
|
|
timelineRenderingType: this.context.timelineRenderingType,
|
|
|
|
|
});
|
2021-02-16 19:05:39 +13:00
|
|
|
event.preventDefault();
|
|
|
|
|
}
|
|
|
|
|
break;
|
2019-05-24 14:42:33 +02:00
|
|
|
}
|
2022-01-31 16:55:45 +01:00
|
|
|
case KeyBindingAction.EditNextMessage: {
|
2021-06-30 13:01:26 +01:00
|
|
|
if (this.editorRef.current?.isModified() || !this.editorRef.current?.isCaretAtEnd()) {
|
2021-02-16 19:05:39 +13:00
|
|
|
return;
|
|
|
|
|
}
|
2021-10-01 15:35:54 +02:00
|
|
|
const nextEvent = findEditableEvent({
|
|
|
|
|
events: this.events,
|
|
|
|
|
isForward: true,
|
|
|
|
|
fromEventId: this.props.editState.getEvent().getId(),
|
|
|
|
|
});
|
2021-02-16 19:05:39 +13:00
|
|
|
if (nextEvent) {
|
2021-10-01 15:35:54 +02:00
|
|
|
dis.dispatch({
|
|
|
|
|
action: Action.EditEvent,
|
|
|
|
|
event: nextEvent,
|
|
|
|
|
timelineRenderingType: this.context.timelineRenderingType,
|
|
|
|
|
});
|
2021-02-16 19:05:39 +13:00
|
|
|
} else {
|
2022-03-23 10:54:40 +00:00
|
|
|
this.cancelEdit();
|
2021-02-16 19:05:39 +13:00
|
|
|
}
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
break;
|
2019-05-24 14:42:33 +02:00
|
|
|
}
|
2019-05-09 15:00:48 +02:00
|
|
|
}
|
2021-06-30 13:01:26 +01:00
|
|
|
};
|
|
|
|
|
|
2022-03-23 10:54:40 +00:00
|
|
|
private endEdit(): void {
|
2022-08-02 14:54:40 +01:00
|
|
|
localStorage.removeItem(this.editorRoomKey);
|
|
|
|
|
localStorage.removeItem(this.editorStateKey);
|
|
|
|
|
|
2022-03-23 10:54:40 +00:00
|
|
|
// close the event editing and focus composer
|
|
|
|
|
dis.dispatch({
|
|
|
|
|
action: Action.EditEvent,
|
|
|
|
|
event: null,
|
|
|
|
|
timelineRenderingType: this.context.timelineRenderingType,
|
|
|
|
|
});
|
|
|
|
|
dis.dispatch({
|
|
|
|
|
action: Action.FocusSendMessageComposer,
|
|
|
|
|
context: this.context.timelineRenderingType,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-30 13:01:26 +01:00
|
|
|
private get editorRoomKey(): string {
|
2022-03-29 13:33:11 +01:00
|
|
|
return editorRoomKey(this.props.editState.getEvent().getRoomId(), this.context.timelineRenderingType);
|
2019-05-06 17:41:15 +02:00
|
|
|
}
|
|
|
|
|
|
2021-06-30 13:01:26 +01:00
|
|
|
private get editorStateKey(): string {
|
2022-03-29 13:33:11 +01:00
|
|
|
return editorStateKey(this.props.editState.getEvent().getId());
|
2021-05-11 18:37:41 +05:30
|
|
|
}
|
|
|
|
|
|
2021-10-01 15:35:54 +02:00
|
|
|
private get events(): MatrixEvent[] {
|
|
|
|
|
const liveTimelineEvents = this.context.liveTimeline.getEvents();
|
|
|
|
|
const pendingEvents = this.getRoom().getPendingEvents();
|
|
|
|
|
const isInThread = Boolean(this.props.editState.getEvent().getThread());
|
|
|
|
|
return liveTimelineEvents.concat(isInThread ? [] : pendingEvents);
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-30 13:01:26 +01:00
|
|
|
private cancelEdit = (): void => {
|
2022-03-23 10:54:40 +00:00
|
|
|
this.endEdit();
|
2021-06-30 13:01:26 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private get shouldSaveStoredEditorState(): boolean {
|
|
|
|
|
return localStorage.getItem(this.editorRoomKey) !== null;
|
2019-05-06 17:41:15 +02:00
|
|
|
}
|
|
|
|
|
|
2021-06-30 13:01:26 +01:00
|
|
|
private restoreStoredEditorState(partCreator: PartCreator): Part[] {
|
|
|
|
|
const json = localStorage.getItem(this.editorStateKey);
|
2021-05-09 13:18:01 +05:30
|
|
|
if (json) {
|
|
|
|
|
try {
|
2021-06-29 13:11:58 +01:00
|
|
|
const { parts: serializedParts } = JSON.parse(json);
|
2021-06-30 13:01:26 +01:00
|
|
|
const parts: Part[] = serializedParts.map((p) => partCreator.deserializePart(p));
|
2021-05-09 13:18:01 +05:30
|
|
|
return parts;
|
|
|
|
|
} catch (e) {
|
2021-10-15 16:30:53 +02:00
|
|
|
logger.error("Error parsing editing state: ", e);
|
2021-05-09 13:18:01 +05:30
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-30 13:01:26 +01:00
|
|
|
private clearPreviousEdit(): void {
|
|
|
|
|
if (localStorage.getItem(this.editorRoomKey)) {
|
|
|
|
|
localStorage.removeItem(`mx_edit_state_${localStorage.getItem(this.editorRoomKey)}`);
|
2021-05-11 18:37:41 +05:30
|
|
|
}
|
2021-05-09 13:18:01 +05:30
|
|
|
}
|
|
|
|
|
|
2021-07-01 23:48:52 +01:00
|
|
|
private saveStoredEditorState = (): void => {
|
2021-05-09 13:18:01 +05:30
|
|
|
const item = SendHistoryManager.createItem(this.model);
|
2021-06-30 13:01:26 +01:00
|
|
|
this.clearPreviousEdit();
|
|
|
|
|
localStorage.setItem(this.editorRoomKey, this.props.editState.getEvent().getId());
|
|
|
|
|
localStorage.setItem(this.editorStateKey, JSON.stringify(item));
|
2021-07-01 23:48:52 +01:00
|
|
|
};
|
2021-05-09 13:18:01 +05:30
|
|
|
|
2021-06-30 13:01:26 +01:00
|
|
|
private isContentModified(newContent: IContent): boolean {
|
2019-06-13 22:56:32 +01:00
|
|
|
// if nothing has changed then bail
|
|
|
|
|
const oldContent = this.props.editState.getEvent().getContent();
|
2021-07-01 23:48:52 +01:00
|
|
|
if (
|
|
|
|
|
oldContent["msgtype"] === newContent["msgtype"] &&
|
|
|
|
|
oldContent["body"] === newContent["body"] &&
|
2019-06-13 22:56:32 +01:00
|
|
|
oldContent["format"] === newContent["format"] &&
|
2021-07-01 23:48:52 +01:00
|
|
|
oldContent["formatted_body"] === newContent["formatted_body"]
|
|
|
|
|
) {
|
2019-07-08 15:25:44 +02:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-30 13:01:26 +01:00
|
|
|
private sendEdit = async (): Promise<void> => {
|
2022-01-12 14:22:25 +00:00
|
|
|
if (this.state.saveDisabled) return;
|
|
|
|
|
|
2019-07-08 15:25:44 +02:00
|
|
|
const editedEvent = this.props.editState.getEvent();
|
2021-09-17 19:13:46 +02:00
|
|
|
|
2022-02-09 14:42:08 +00:00
|
|
|
PosthogAnalytics.instance.trackEvent<ComposerEvent>({
|
|
|
|
|
eventName: "Composer",
|
|
|
|
|
isEditing: true,
|
|
|
|
|
inThread: !!editedEvent?.getThread(),
|
|
|
|
|
isReply: !!editedEvent.replyEventId,
|
|
|
|
|
});
|
|
|
|
|
|
2021-09-17 19:13:46 +02:00
|
|
|
// 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);
|
|
|
|
|
}
|
2021-10-14 16:57:02 +01:00
|
|
|
const editContent = createEditContent(this.model, editedEvent);
|
2019-07-08 15:25:44 +02:00
|
|
|
const newContent = editContent["m.new_content"];
|
2019-09-09 18:12:52 +01:00
|
|
|
|
2021-04-14 16:01:42 +05:30
|
|
|
let shouldSend = true;
|
2019-09-09 18:12:52 +01:00
|
|
|
|
2021-09-28 16:04:25 +02:00
|
|
|
if (newContent?.body === "") {
|
|
|
|
|
this.cancelPreviousPendingEdit();
|
|
|
|
|
createRedactEventDialog({
|
|
|
|
|
mxEvent: editedEvent,
|
2022-03-29 13:33:11 +01:00
|
|
|
onCloseDialog: () => {
|
|
|
|
|
this.cancelEdit();
|
|
|
|
|
},
|
2021-09-28 16:04:25 +02:00
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-10 08:51:27 +01:00
|
|
|
// If content is modified then send an updated event into the room
|
2021-06-30 13:01:26 +01:00
|
|
|
if (this.isContentModified(newContent)) {
|
2019-09-09 18:12:52 +01:00
|
|
|
const roomId = editedEvent.getRoomId();
|
2022-01-12 09:40:18 +00:00
|
|
|
if (!containsEmote(this.model) && isSlashCommand(this.model)) {
|
|
|
|
|
const [cmd, args, commandText] = getSlashCommand(this.model);
|
2021-04-14 16:01:42 +05:30
|
|
|
if (cmd) {
|
2022-02-09 14:42:08 +00:00
|
|
|
const threadId = editedEvent?.getThread()?.id || null;
|
2022-06-10 17:16:31 +01:00
|
|
|
const [content, commandSuccessful] = await runSlashCommand(cmd, args, roomId, threadId);
|
|
|
|
|
if (!commandSuccessful) {
|
|
|
|
|
return; // errored
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-27 11:03:25 +01:00
|
|
|
if (cmd.category === CommandCategories.messages || cmd.category === CommandCategories.effects) {
|
2022-06-10 17:16:31 +01:00
|
|
|
editContent["m.new_content"] = content;
|
2021-04-14 16:01:42 +05:30
|
|
|
} else {
|
|
|
|
|
shouldSend = false;
|
|
|
|
|
}
|
2022-01-12 09:40:18 +00:00
|
|
|
} else if (!(await shouldSendAnyway(commandText))) {
|
2021-04-14 16:01:42 +05:30
|
|
|
// if !sendAnyway bail to let the user edit the composer and try again
|
2022-01-12 09:40:18 +00:00
|
|
|
return;
|
2021-04-14 16:01:42 +05:30
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (shouldSend) {
|
2021-06-30 13:01:26 +01:00
|
|
|
this.cancelPreviousPendingEdit();
|
2021-11-24 08:40:15 +00:00
|
|
|
|
|
|
|
|
const event = this.props.editState.getEvent();
|
|
|
|
|
const threadId = event.threadRootId || null;
|
|
|
|
|
|
2022-02-15 16:58:30 +00:00
|
|
|
this.props.mxClient.sendMessage(roomId, threadId, editContent);
|
2021-06-29 13:11:58 +01:00
|
|
|
dis.dispatch({ action: "message_sent" });
|
2021-04-14 16:01:42 +05:30
|
|
|
}
|
2019-06-13 22:56:32 +01:00
|
|
|
}
|
2019-05-14 10:37:57 +01:00
|
|
|
|
2022-03-23 10:54:40 +00:00
|
|
|
this.endEdit();
|
2019-09-09 18:12:52 +01:00
|
|
|
};
|
2019-05-14 10:37:57 +01:00
|
|
|
|
2021-06-30 13:01:26 +01:00
|
|
|
private cancelPreviousPendingEdit(): void {
|
2019-07-02 14:52:29 +02:00
|
|
|
const originalEvent = this.props.editState.getEvent();
|
|
|
|
|
const previousEdit = originalEvent.replacingEvent();
|
2019-07-02 18:30:57 +02:00
|
|
|
if (
|
|
|
|
|
previousEdit &&
|
|
|
|
|
(previousEdit.status === EventStatus.QUEUED || previousEdit.status === EventStatus.NOT_SENT)
|
|
|
|
|
) {
|
2021-10-01 15:35:54 +02:00
|
|
|
this.props.mxClient.cancelPendingEvent(previousEdit);
|
2019-07-02 14:52:29 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-12 18:52:34 +02:00
|
|
|
componentWillUnmount() {
|
2019-10-10 16:40:03 +02:00
|
|
|
// store caret and serialized parts in the
|
|
|
|
|
// editorstate so it can be restored when the remote echo event tile gets rendered
|
|
|
|
|
// in case we're currently editing a pending event
|
2019-06-12 18:52:34 +02:00
|
|
|
const sel = document.getSelection();
|
2019-10-10 16:40:03 +02:00
|
|
|
let caret;
|
|
|
|
|
if (sel.focusNode) {
|
2021-06-30 13:03:29 +01:00
|
|
|
caret = getCaretOffsetAndText(this.editorRef.current?.editorRef.current, sel).caret;
|
2019-10-10 16:40:03 +02:00
|
|
|
}
|
2019-06-12 18:52:34 +02:00
|
|
|
const parts = this.model.serializeParts();
|
2019-10-10 16:40:03 +02:00
|
|
|
// if caret is undefined because for some reason there isn't a valid selection,
|
|
|
|
|
// then when mounting the editor again with the same editor state,
|
|
|
|
|
// it will set the cursor at the end.
|
2019-06-12 18:52:34 +02:00
|
|
|
this.props.editState.setEditorState(caret, parts);
|
2021-06-30 13:01:26 +01:00
|
|
|
window.removeEventListener("beforeunload", this.saveStoredEditorState);
|
|
|
|
|
if (this.shouldSaveStoredEditorState) {
|
|
|
|
|
this.saveStoredEditorState();
|
2021-05-09 13:18:01 +05:30
|
|
|
}
|
2021-04-13 14:52:26 +01:00
|
|
|
dis.unregister(this.dispatcherRef);
|
2019-06-12 18:52:34 +02:00
|
|
|
}
|
|
|
|
|
|
2021-07-01 23:48:52 +01:00
|
|
|
private createEditorModel(): boolean {
|
2021-06-29 13:11:58 +01:00
|
|
|
const { editState } = this.props;
|
2021-06-30 13:01:26 +01:00
|
|
|
const room = this.getRoom();
|
2021-10-01 15:35:54 +02:00
|
|
|
const partCreator = new CommandPartCreator(room, this.props.mxClient);
|
2021-07-01 23:48:52 +01:00
|
|
|
|
2022-01-14 13:24:51 +00:00
|
|
|
let parts: Part[];
|
2021-07-01 23:48:52 +01:00
|
|
|
let isRestored = false;
|
2019-06-12 18:52:34 +02:00
|
|
|
if (editState.hasEditorState()) {
|
|
|
|
|
// if restoring state from a previous editor,
|
|
|
|
|
// restore serialized parts from the state
|
|
|
|
|
parts = editState.getSerializedParts().map((p) => partCreator.deserializePart(p));
|
|
|
|
|
} else {
|
2021-07-01 23:48:52 +01:00
|
|
|
// otherwise, either restore serialized parts from localStorage or parse the body of the event
|
|
|
|
|
const restoredParts = this.restoreStoredEditorState(partCreator);
|
2022-04-19 15:53:59 +02:00
|
|
|
parts =
|
|
|
|
|
restoredParts ||
|
|
|
|
|
parseEvent(editState.getEvent(), partCreator, {
|
|
|
|
|
shouldEscape: SettingsStore.getValue("MessageComposerInput.useMarkdown"),
|
|
|
|
|
});
|
2021-07-01 23:48:52 +01:00
|
|
|
isRestored = !!restoredParts;
|
2019-06-12 18:52:34 +02:00
|
|
|
}
|
2019-08-05 15:27:40 +02:00
|
|
|
this.model = new EditorModel(parts, partCreator);
|
2021-06-30 13:01:26 +01:00
|
|
|
this.saveStoredEditorState();
|
2019-06-12 18:52:34 +02:00
|
|
|
|
2021-07-01 23:48:52 +01:00
|
|
|
return isRestored;
|
2019-06-12 18:52:34 +02:00
|
|
|
}
|
|
|
|
|
|
2021-06-30 13:01:26 +01:00
|
|
|
private onChange = (): void => {
|
|
|
|
|
if (!this.state.saveDisabled || !this.editorRef.current?.isModified()) {
|
2019-09-09 18:12:52 +01:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.setState({
|
2019-09-11 17:29:52 +01:00
|
|
|
saveDisabled: false,
|
2019-09-09 18:12:52 +01:00
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2021-06-30 13:01:26 +01:00
|
|
|
private onAction = (payload: ActionPayload) => {
|
2021-10-25 13:43:54 +02:00
|
|
|
if (!this.editorRef.current) return;
|
|
|
|
|
|
|
|
|
|
if (payload.action === Action.ComposerInsert) {
|
|
|
|
|
if (payload.timelineRenderingType !== this.context.timelineRenderingType) return;
|
|
|
|
|
if (payload.composerType !== ComposerType.Edit) return;
|
|
|
|
|
|
2021-04-13 15:11:46 +01:00
|
|
|
if (payload.userId) {
|
2021-06-30 13:01:26 +01:00
|
|
|
this.editorRef.current?.insertMention(payload.userId);
|
2021-04-13 15:09:37 +01:00
|
|
|
} else if (payload.event) {
|
2021-06-30 13:01:26 +01:00
|
|
|
this.editorRef.current?.insertQuotedMessage(payload.event);
|
2021-04-13 15:09:37 +01:00
|
|
|
} else if (payload.text) {
|
2021-06-30 13:01:26 +01:00
|
|
|
this.editorRef.current?.insertPlaintext(payload.text);
|
2021-04-13 15:09:37 +01:00
|
|
|
}
|
2021-10-25 13:43:54 +02:00
|
|
|
} else if (payload.action === Action.FocusEditMessageComposer) {
|
2021-07-08 17:33:49 +02:00
|
|
|
this.editorRef.current.focus();
|
2021-04-13 14:52:26 +01:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2019-05-06 17:41:15 +02:00
|
|
|
render() {
|
2021-06-30 13:01:26 +01:00
|
|
|
return (
|
|
|
|
|
<div className={classNames("mx_EditMessageComposer", this.props.className)} onKeyDown={this.onKeyDown}>
|
2019-08-06 16:31:21 +02:00
|
|
|
<BasicMessageComposer
|
2021-06-30 13:01:26 +01:00
|
|
|
ref={this.editorRef}
|
2019-08-06 16:31:21 +02:00
|
|
|
model={this.model}
|
2021-06-30 13:01:26 +01:00
|
|
|
room={this.getRoom()}
|
2021-11-23 08:25:58 +00:00
|
|
|
threadId={this.props.editState?.getEvent()?.getThread()?.id}
|
2019-08-06 16:31:21 +02:00
|
|
|
initialCaret={this.props.editState.getCaret()}
|
2019-08-06 17:03:16 +02:00
|
|
|
label={_t("Edit message")}
|
2021-06-30 13:01:26 +01:00
|
|
|
onChange={this.onChange}
|
2019-08-06 16:31:21 +02:00
|
|
|
/>
|
|
|
|
|
<div className="mx_EditMessageComposer_buttons">
|
2021-06-30 13:01:26 +01:00
|
|
|
<AccessibleButton kind="secondary" onClick={this.cancelEdit}>
|
|
|
|
|
{_t("Cancel")}
|
|
|
|
|
</AccessibleButton>
|
|
|
|
|
<AccessibleButton kind="primary" onClick={this.sendEdit} disabled={this.state.saveDisabled}>
|
|
|
|
|
{_t("Save")}
|
2019-09-09 18:12:52 +01:00
|
|
|
</AccessibleButton>
|
2022-12-12 12:24:14 +01:00
|
|
|
</div>
|
2019-08-06 16:31:21 +02:00
|
|
|
</div>
|
|
|
|
|
);
|
2019-05-06 17:41:15 +02:00
|
|
|
}
|
|
|
|
|
}
|
2021-10-01 15:35:54 +02:00
|
|
|
|
|
|
|
|
const EditMessageComposerWithMatrixClient = withMatrixClientHOC(EditMessageComposer);
|
|
|
|
|
export default EditMessageComposerWithMatrixClient;
|