Allow using room pills in slash commands (#7513)
This commit is contained in:
parent
31247a50ca
commit
b835588331
7 changed files with 193 additions and 249 deletions
|
@ -21,25 +21,22 @@ 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";
|
||||
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import EditorModel from '../../../editor/model';
|
||||
import { getCaretOffsetAndText } from '../../../editor/dom';
|
||||
import { htmlSerializeIfNeeded, textSerialize, containsEmote, stripEmoteCommand } from '../../../editor/serialize';
|
||||
import { findEditableEvent } from '../../../utils/EventUtils';
|
||||
import { parseEvent } from '../../../editor/deserialize';
|
||||
import { CommandPartCreator, Part, PartCreator, Type } from '../../../editor/parts';
|
||||
import { CommandPartCreator, Part, PartCreator } from '../../../editor/parts';
|
||||
import EditorStateTransfer from '../../../utils/EditorStateTransfer';
|
||||
import BasicMessageComposer, { REGEX_EMOTICON } from "./BasicMessageComposer";
|
||||
import { Command, CommandCategories, getCommand } from '../../../SlashCommands';
|
||||
import { CommandCategories } from '../../../SlashCommands';
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import { getKeyBindingsManager, MessageComposerAction } from '../../../KeyBindingsManager';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import SendHistoryManager from '../../../SendHistoryManager';
|
||||
import Modal from '../../../Modal';
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import { createRedactEventDialog } from '../dialogs/ConfirmRedactDialog';
|
||||
|
@ -47,6 +44,7 @@ import SettingsStore from "../../../settings/SettingsStore";
|
|||
import { withMatrixClientHOC, MatrixClientProps } from '../../../contexts/MatrixClientContext';
|
||||
import RoomContext from '../../../contexts/RoomContext';
|
||||
import { ComposerType } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||
import { getSlashCommand, isSlashCommand, runSlashCommand, shouldSendAnyway } from "../../../editor/commands";
|
||||
|
||||
function getHtmlReplyFallback(mxEvent: MatrixEvent): string {
|
||||
const html = mxEvent.getContent().formatted_body;
|
||||
|
@ -282,22 +280,6 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
|
|||
localStorage.setItem(this.editorStateKey, JSON.stringify(item));
|
||||
};
|
||||
|
||||
private isSlashCommand(): boolean {
|
||||
const parts = this.model.parts;
|
||||
const firstPart = parts[0];
|
||||
if (firstPart) {
|
||||
if (firstPart.type === Type.Command && firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")
|
||||
&& (firstPart.type === Type.Plain || firstPart.type === Type.PillCandidate)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private isContentModified(newContent: IContent): boolean {
|
||||
// if nothing has changed then bail
|
||||
const oldContent = this.props.editState.getEvent().getContent();
|
||||
|
@ -309,60 +291,6 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
|
|||
return true;
|
||||
}
|
||||
|
||||
private getSlashCommand(): [Command, string, string] {
|
||||
const commandText = this.model.parts.reduce((text, part) => {
|
||||
// use mxid to textify user pills in a command
|
||||
if (part.type === Type.UserPill) {
|
||||
return text + part.resourceId;
|
||||
}
|
||||
return text + part.text;
|
||||
}, "");
|
||||
const { cmd, args } = getCommand(commandText);
|
||||
return [cmd, args, commandText];
|
||||
}
|
||||
|
||||
private async runSlashCommand(cmd: Command, args: string, roomId: string): Promise<void> {
|
||||
const threadId = this.props.editState?.getEvent()?.getThread()?.id || null;
|
||||
|
||||
const result = cmd.run(roomId, threadId, args);
|
||||
let messageContent;
|
||||
let error = result.error;
|
||||
if (result.promise) {
|
||||
try {
|
||||
if (cmd.category === CommandCategories.messages) {
|
||||
messageContent = await result.promise;
|
||||
} else {
|
||||
await result.promise;
|
||||
}
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
}
|
||||
if (error) {
|
||||
logger.error("Command failure: %s", error);
|
||||
// assume the error is a server error when the command is async
|
||||
const isServerError = !!result.promise;
|
||||
const title = isServerError ? _td("Server error") : _td("Command error");
|
||||
|
||||
let errText;
|
||||
if (typeof error === 'string') {
|
||||
errText = error;
|
||||
} else if (error.message) {
|
||||
errText = error.message;
|
||||
} else {
|
||||
errText = _t("Server unavailable, overloaded, or something else went wrong.");
|
||||
}
|
||||
|
||||
Modal.createTrackedDialog(title, '', ErrorDialog, {
|
||||
title: _t(title),
|
||||
description: errText,
|
||||
});
|
||||
} else {
|
||||
logger.log("Command success.");
|
||||
if (messageContent) return messageContent;
|
||||
}
|
||||
}
|
||||
|
||||
private sendEdit = async (): Promise<void> => {
|
||||
const startTime = CountlyAnalytics.getTimestamp();
|
||||
const editedEvent = this.props.editState.getEvent();
|
||||
|
@ -389,40 +317,22 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
|
|||
// If content is modified then send an updated event into the room
|
||||
if (this.isContentModified(newContent)) {
|
||||
const roomId = editedEvent.getRoomId();
|
||||
if (!containsEmote(this.model) && this.isSlashCommand()) {
|
||||
const [cmd, args, commandText] = this.getSlashCommand();
|
||||
if (!containsEmote(this.model) && isSlashCommand(this.model)) {
|
||||
const [cmd, args, commandText] = getSlashCommand(this.model);
|
||||
if (cmd) {
|
||||
const threadId = this.props.editState?.getEvent()?.getThread()?.id || null;
|
||||
if (cmd.category === CommandCategories.messages) {
|
||||
editContent["m.new_content"] = await this.runSlashCommand(cmd, args, roomId);
|
||||
editContent["m.new_content"] = await runSlashCommand(cmd, args, roomId, threadId);
|
||||
if (!editContent["m.new_content"]) {
|
||||
return; // errored
|
||||
}
|
||||
} else {
|
||||
this.runSlashCommand(cmd, args, roomId);
|
||||
runSlashCommand(cmd, args, roomId, threadId);
|
||||
shouldSend = false;
|
||||
}
|
||||
} else {
|
||||
// ask the user if their unknown command should be sent as a message
|
||||
const { finished } = Modal.createTrackedDialog("Unknown command", "", QuestionDialog, {
|
||||
title: _t("Unknown Command"),
|
||||
description: <div>
|
||||
<p>
|
||||
{ _t("Unrecognised command: %(commandText)s", { commandText }) }
|
||||
</p>
|
||||
<p>
|
||||
{ _t("You can use <code>/help</code> to list available commands. " +
|
||||
"Did you mean to send this as a message?", {}, {
|
||||
code: t => <code>{ t }</code>,
|
||||
}) }
|
||||
</p>
|
||||
<p>
|
||||
{ _t("Hint: Begin your message with <code>//</code> to start it with a slash.", {}, {
|
||||
code: t => <code>{ t }</code>,
|
||||
}) }
|
||||
</p>
|
||||
</div>,
|
||||
button: _t('Send as message'),
|
||||
});
|
||||
const [sendAnyway] = await finished;
|
||||
} else if (!await shouldSendAnyway(commandText)) {
|
||||
// if !sendAnyway bail to let the user edit the composer and try again
|
||||
if (!sendAnyway) return;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (shouldSend) {
|
||||
|
|
|
@ -34,13 +34,11 @@ import {
|
|||
unescapeMessage,
|
||||
} from '../../../editor/serialize';
|
||||
import BasicMessageComposer, { REGEX_EMOTICON } from "./BasicMessageComposer";
|
||||
import { CommandPartCreator, Part, PartCreator, SerializedPart, Type } from '../../../editor/parts';
|
||||
import { CommandPartCreator, Part, PartCreator, SerializedPart } from '../../../editor/parts';
|
||||
import ReplyChain from "../elements/ReplyChain";
|
||||
import { findEditableEvent } from '../../../utils/EventUtils';
|
||||
import SendHistoryManager from "../../../SendHistoryManager";
|
||||
import { Command, CommandCategories, getCommand } from '../../../SlashCommands';
|
||||
import Modal from '../../../Modal';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import { CommandCategories } from '../../../SlashCommands';
|
||||
import ContentMessages from '../../../ContentMessages';
|
||||
import { withMatrixClientHOC, MatrixClientProps } from "../../../contexts/MatrixClientContext";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
|
@ -52,13 +50,12 @@ import { getKeyBindingsManager, MessageComposerAction } from '../../../KeyBindin
|
|||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
import { decorateStartSendingTime, sendRoundTripMetric } from "../../../sendTimePerformanceMetrics";
|
||||
import RoomContext, { TimelineRenderingType } from '../../../contexts/RoomContext';
|
||||
import DocumentPosition from "../../../editor/position";
|
||||
import { ComposerType } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||
import { getSlashCommand, isSlashCommand, runSlashCommand, shouldSendAnyway } from "../../../editor/commands";
|
||||
|
||||
function addReplyToMessageContent(
|
||||
content: IContent,
|
||||
|
@ -284,24 +281,6 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
|
|||
return true;
|
||||
}
|
||||
|
||||
private isSlashCommand(): boolean {
|
||||
const parts = this.model.parts;
|
||||
const firstPart = parts[0];
|
||||
if (firstPart) {
|
||||
if (firstPart.type === Type.Command && firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")) {
|
||||
return true;
|
||||
}
|
||||
// be extra resilient when somehow the AutocompleteWrapperModel or
|
||||
// CommandPartCreator fails to insert a command part, so we don't send
|
||||
// a command as a message
|
||||
if (firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")
|
||||
&& (firstPart.type === Type.Plain || firstPart.type === Type.PillCandidate)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private sendQuickReaction(): void {
|
||||
const timeline = this.context.liveTimeline;
|
||||
const events = timeline.getEvents();
|
||||
|
@ -337,66 +316,6 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
|
|||
}
|
||||
}
|
||||
|
||||
private getSlashCommand(): [Command, string, string] {
|
||||
const commandText = this.model.parts.reduce((text, part) => {
|
||||
// use mxid to textify user pills in a command
|
||||
if (part.type === "user-pill") {
|
||||
return text + part.resourceId;
|
||||
}
|
||||
return text + part.text;
|
||||
}, "");
|
||||
const { cmd, args } = getCommand(commandText);
|
||||
return [cmd, args, commandText];
|
||||
}
|
||||
|
||||
private async runSlashCommand(cmd: Command, args: string): Promise<void> {
|
||||
const threadId = this.props.relation?.rel_type === RelationType.Thread
|
||||
? this.props.relation?.event_id
|
||||
: null;
|
||||
|
||||
const result = cmd.run(this.props.room.roomId, threadId, args);
|
||||
let messageContent;
|
||||
let error = result.error;
|
||||
if (result.promise) {
|
||||
try {
|
||||
if (cmd.category === CommandCategories.messages) {
|
||||
// The command returns a modified message that we need to pass on
|
||||
messageContent = await result.promise;
|
||||
} else {
|
||||
await result.promise;
|
||||
}
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
}
|
||||
if (error) {
|
||||
logger.error("Command failure: %s", error);
|
||||
// assume the error is a server error when the command is async
|
||||
const isServerError = !!result.promise;
|
||||
const title = isServerError ? _td("Server error") : _td("Command error");
|
||||
|
||||
let errText;
|
||||
if (typeof error === 'string') {
|
||||
errText = error;
|
||||
} else if (error.translatedMessage) {
|
||||
// Check for translatable errors (newTranslatableError)
|
||||
errText = error.translatedMessage;
|
||||
} else if (error.message) {
|
||||
errText = error.message;
|
||||
} else {
|
||||
errText = _t("Server unavailable, overloaded, or something else went wrong.");
|
||||
}
|
||||
|
||||
Modal.createTrackedDialog(title, '', ErrorDialog, {
|
||||
title: _t(title),
|
||||
description: errText,
|
||||
});
|
||||
} else {
|
||||
logger.log("Command success.");
|
||||
if (messageContent) return messageContent;
|
||||
}
|
||||
}
|
||||
|
||||
public async sendMessage(): Promise<void> {
|
||||
const model = this.model;
|
||||
|
||||
|
@ -416,50 +335,32 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
|
|||
|
||||
const replyToEvent = this.props.replyToEvent;
|
||||
let shouldSend = true;
|
||||
let content;
|
||||
let content: IContent;
|
||||
|
||||
if (!containsEmote(model) && this.isSlashCommand()) {
|
||||
const [cmd, args, commandText] = this.getSlashCommand();
|
||||
if (!containsEmote(model) && isSlashCommand(this.model)) {
|
||||
const [cmd, args, commandText] = getSlashCommand(this.model);
|
||||
if (cmd) {
|
||||
const threadId = this.props.relation?.rel_type === RelationType.Thread
|
||||
? this.props.relation?.event_id
|
||||
: null;
|
||||
|
||||
if (cmd.category === CommandCategories.messages) {
|
||||
content = await this.runSlashCommand(cmd, args);
|
||||
content = await runSlashCommand(cmd, args, this.props.room.roomId, threadId);
|
||||
if (!content) {
|
||||
return; // errored
|
||||
}
|
||||
|
||||
if (replyToEvent) {
|
||||
addReplyToMessageContent(
|
||||
content,
|
||||
replyToEvent,
|
||||
this.props.permalinkCreator,
|
||||
);
|
||||
addReplyToMessageContent(content, replyToEvent, this.props.permalinkCreator);
|
||||
}
|
||||
attachRelation(content, this.props.relation);
|
||||
} else {
|
||||
this.runSlashCommand(cmd, args);
|
||||
runSlashCommand(cmd, args, this.props.room.roomId, threadId);
|
||||
shouldSend = false;
|
||||
}
|
||||
} else {
|
||||
// ask the user if their unknown command should be sent as a message
|
||||
const { finished } = Modal.createTrackedDialog("Unknown command", "", QuestionDialog, {
|
||||
title: _t("Unknown Command"),
|
||||
description: <div>
|
||||
<p>
|
||||
{ _t("Unrecognised command: %(commandText)s", { commandText }) }
|
||||
</p>
|
||||
<p>
|
||||
{ _t("You can use <code>/help</code> to list available commands. " +
|
||||
"Did you mean to send this as a message?", {}, {
|
||||
code: t => <code>{ t }</code>,
|
||||
}) }
|
||||
</p>
|
||||
<p>
|
||||
{ _t("Hint: Begin your message with <code>//</code> to start it with a slash.", {}, {
|
||||
code: t => <code>{ t }</code>,
|
||||
}) }
|
||||
</p>
|
||||
</div>,
|
||||
button: _t('Send as message'),
|
||||
});
|
||||
const [sendAnyway] = await finished;
|
||||
} else if (!await shouldSendAnyway(commandText)) {
|
||||
// if !sendAnyway bail to let the user edit the composer and try again
|
||||
if (!sendAnyway) return;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue