mirror of
https://github.com/element-hq/element-web
synced 2024-11-23 09:46:09 +03:00
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
|
@ -24,10 +24,11 @@ import { EventType } from "matrix-js-sdk/src/@types/event";
|
|||
import * as ContentHelpers from 'matrix-js-sdk/src/content-helpers';
|
||||
import { parseFragment as parseHtml, Element as ChildElement } from "parse5";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { IContent } from 'matrix-js-sdk/src/models/event';
|
||||
|
||||
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||
import dis from './dispatcher/dispatcher';
|
||||
import { _t, _td, newTranslatableError } from './languageHandler';
|
||||
import { _t, _td, newTranslatableError, ITranslatableError } from './languageHandler';
|
||||
import Modal from './Modal';
|
||||
import MultiInviter from './utils/MultiInviter';
|
||||
import { linkifyAndSanitizeHtml } from './HtmlUtils';
|
||||
|
@ -60,6 +61,7 @@ import SlashCommandHelpDialog from "./components/views/dialogs/SlashCommandHelpD
|
|||
import { shouldShowComponent } from "./customisations/helpers/UIComponents";
|
||||
import { TimelineRenderingType } from './contexts/RoomContext';
|
||||
import RoomViewStore from "./stores/RoomViewStore";
|
||||
import { XOR } from "./@types/common";
|
||||
|
||||
// XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816
|
||||
interface HTMLInputEvent extends Event {
|
||||
|
@ -94,7 +96,9 @@ export const CommandCategories = {
|
|||
"other": _td("Other"),
|
||||
};
|
||||
|
||||
type RunFn = ((roomId: string, args: string, cmd: string) => {error: any} | {promise: Promise<any>});
|
||||
export type RunResult = XOR<{ error: Error | ITranslatableError }, { promise: Promise<IContent | undefined> }>;
|
||||
|
||||
type RunFn = ((roomId: string, args: string, cmd: string) => RunResult);
|
||||
|
||||
interface ICommandOpts {
|
||||
command: string;
|
||||
|
@ -109,15 +113,15 @@ interface ICommandOpts {
|
|||
}
|
||||
|
||||
export class Command {
|
||||
command: string;
|
||||
aliases: string[];
|
||||
args: undefined | string;
|
||||
description: string;
|
||||
runFn: undefined | RunFn;
|
||||
category: string;
|
||||
hideCompletionAfterSpace: boolean;
|
||||
private _isEnabled?: () => boolean;
|
||||
public renderingTypes?: TimelineRenderingType[];
|
||||
public readonly command: string;
|
||||
public readonly aliases: string[];
|
||||
public readonly args: undefined | string;
|
||||
public readonly description: string;
|
||||
public readonly runFn: undefined | RunFn;
|
||||
public readonly category: string;
|
||||
public readonly hideCompletionAfterSpace: boolean;
|
||||
public readonly renderingTypes?: TimelineRenderingType[];
|
||||
private readonly _isEnabled?: () => boolean;
|
||||
|
||||
constructor(opts: ICommandOpts) {
|
||||
this.command = opts.command;
|
||||
|
@ -131,15 +135,15 @@ export class Command {
|
|||
this.renderingTypes = opts.renderingTypes;
|
||||
}
|
||||
|
||||
getCommand() {
|
||||
public getCommand() {
|
||||
return `/${this.command}`;
|
||||
}
|
||||
|
||||
getCommandWithArgs() {
|
||||
public getCommandWithArgs() {
|
||||
return this.getCommand() + " " + this.args;
|
||||
}
|
||||
|
||||
run(roomId: string, threadId: string, args: string) {
|
||||
public run(roomId: string, threadId: string, args: string): RunResult {
|
||||
// if it has no runFn then its an ignored/nop command (autocomplete only) e.g `/me`
|
||||
if (!this.runFn) {
|
||||
reject(
|
||||
|
@ -166,11 +170,11 @@ export class Command {
|
|||
return this.runFn.bind(this)(roomId, args);
|
||||
}
|
||||
|
||||
getUsage() {
|
||||
public getUsage() {
|
||||
return _t('Usage') + ': ' + this.getCommandWithArgs();
|
||||
}
|
||||
|
||||
isEnabled(): boolean {
|
||||
public isEnabled(): boolean {
|
||||
return this._isEnabled ? this._isEnabled() : true;
|
||||
}
|
||||
}
|
||||
|
@ -1289,7 +1293,6 @@ interface ICmd {
|
|||
|
||||
/**
|
||||
* Process the given text for /commands and return a bound method to perform them.
|
||||
* @param {string} roomId The room in which the command was performed.
|
||||
* @param {string} input The raw text input by the user.
|
||||
* @return {null|function(): Object} Function returning an object with the property 'error' if there was an error
|
||||
* processing the command, or 'promise' if a request was sent out.
|
||||
|
|
|
@ -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);
|
||||
} else {
|
||||
this.runSlashCommand(cmd, args, roomId);
|
||||
shouldSend = false;
|
||||
editContent["m.new_content"] = await runSlashCommand(cmd, args, roomId, threadId);
|
||||
if (!editContent["m.new_content"]) {
|
||||
return; // errored
|
||||
}
|
||||
} 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;
|
||||
runSlashCommand(cmd, args, roomId, threadId);
|
||||
shouldSend = false;
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
129
src/editor/commands.tsx
Normal file
129
src/editor/commands.tsx
Normal file
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
Copyright 2019 - 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 { logger } from "matrix-js-sdk/src/logger";
|
||||
import { IContent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
import EditorModel from "./model";
|
||||
import { Type } from "./parts";
|
||||
import { Command, CommandCategories, getCommand } from "../SlashCommands";
|
||||
import { ITranslatableError, _t, _td } from "../languageHandler";
|
||||
import Modal from "../Modal";
|
||||
import ErrorDialog from "../components/views/dialogs/ErrorDialog";
|
||||
import QuestionDialog from "../components/views/dialogs/QuestionDialog";
|
||||
|
||||
export function isSlashCommand(model: EditorModel): boolean {
|
||||
const parts = 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;
|
||||
}
|
||||
|
||||
export function getSlashCommand(model: EditorModel): [Command, string, string] {
|
||||
const commandText = model.parts.reduce((text, part) => {
|
||||
// use mxid to textify user pills in a command and room alias/id for room pills
|
||||
if (part.type === Type.UserPill || part.type === Type.RoomPill) {
|
||||
return text + part.resourceId;
|
||||
}
|
||||
return text + part.text;
|
||||
}, "");
|
||||
const { cmd, args } = getCommand(commandText);
|
||||
return [cmd, args, commandText];
|
||||
}
|
||||
|
||||
export async function runSlashCommand(
|
||||
cmd: Command,
|
||||
args: string,
|
||||
roomId: string,
|
||||
threadId: string | null,
|
||||
): Promise<IContent | null> {
|
||||
const result = cmd.run(roomId, threadId, args);
|
||||
let messageContent: IContent | null = null;
|
||||
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 as ITranslatableError).translatedMessage) {
|
||||
// Check for translatable errors (newTranslatableError)
|
||||
errText = (error as ITranslatableError).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;
|
||||
}
|
||||
}
|
||||
|
||||
export async function shouldSendAnyway(commandText: string): Promise<boolean> {
|
||||
// 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;
|
||||
return sendAnyway;
|
||||
}
|
|
@ -179,7 +179,7 @@ export function textSerialize(model: EditorModel): string {
|
|||
}
|
||||
|
||||
export function containsEmote(model: EditorModel): boolean {
|
||||
return startsWith(model, "/me ", false);
|
||||
return startsWith(model, "/me ", false) && model.parts[0]?.text?.length > 4;
|
||||
}
|
||||
|
||||
export function startsWith(model: EditorModel, prefix: string, caseSensitive = true): boolean {
|
||||
|
|
|
@ -976,6 +976,14 @@
|
|||
"sends snowfall": "sends snowfall",
|
||||
"Sends the given message with a space themed effect": "Sends the given message with a space themed effect",
|
||||
"sends space invaders": "sends space invaders",
|
||||
"Server error": "Server error",
|
||||
"Command error": "Command error",
|
||||
"Server unavailable, overloaded, or something else went wrong.": "Server unavailable, overloaded, or something else went wrong.",
|
||||
"Unknown Command": "Unknown Command",
|
||||
"Unrecognised command: %(commandText)s": "Unrecognised command: %(commandText)s",
|
||||
"You can use <code>/help</code> to list available commands. Did you mean to send this as a message?": "You can use <code>/help</code> to list available commands. Did you mean to send this as a message?",
|
||||
"Hint: Begin your message with <code>//</code> to start it with a slash.": "Hint: Begin your message with <code>//</code> to start it with a slash.",
|
||||
"Send as message": "Send as message",
|
||||
"unknown person": "unknown person",
|
||||
"Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>": "Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>",
|
||||
"You held the call <a>Switch</a>": "You held the call <a>Switch</a>",
|
||||
|
@ -1632,14 +1640,6 @@
|
|||
"Someone is using an unknown session": "Someone is using an unknown session",
|
||||
"This room is end-to-end encrypted": "This room is end-to-end encrypted",
|
||||
"Everyone in this room is verified": "Everyone in this room is verified",
|
||||
"Server error": "Server error",
|
||||
"Command error": "Command error",
|
||||
"Server unavailable, overloaded, or something else went wrong.": "Server unavailable, overloaded, or something else went wrong.",
|
||||
"Unknown Command": "Unknown Command",
|
||||
"Unrecognised command: %(commandText)s": "Unrecognised command: %(commandText)s",
|
||||
"You can use <code>/help</code> to list available commands. Did you mean to send this as a message?": "You can use <code>/help</code> to list available commands. Did you mean to send this as a message?",
|
||||
"Hint: Begin your message with <code>//</code> to start it with a slash.": "Hint: Begin your message with <code>//</code> to start it with a slash.",
|
||||
"Send as message": "Send as message",
|
||||
"Edit message": "Edit message",
|
||||
"Mod": "Mod",
|
||||
"%(count)s reply|other": "%(count)s replies",
|
||||
|
|
|
@ -43,7 +43,7 @@ counterpart.setSeparator('|');
|
|||
const FALLBACK_LOCALE = 'en';
|
||||
counterpart.setFallbackLocale(FALLBACK_LOCALE);
|
||||
|
||||
interface ITranslatableError extends Error {
|
||||
export interface ITranslatableError extends Error {
|
||||
translatedMessage: string;
|
||||
}
|
||||
|
||||
|
@ -51,6 +51,7 @@ interface ITranslatableError extends Error {
|
|||
* Helper function to create an error which has an English message
|
||||
* with a translatedMessage property for use by the consumer.
|
||||
* @param {string} message Message to translate.
|
||||
* @param {object} variables Variable substitutions, e.g { foo: 'bar' }
|
||||
* @returns {Error} The constructed error.
|
||||
*/
|
||||
export function newTranslatableError(message: string, variables?: IVariables): ITranslatableError {
|
||||
|
|
Loading…
Reference in a new issue