add support for emotes and running /commands

this does not yet include autocomplete for commands
This commit is contained in:
Bruno Windels 2019-08-21 11:26:21 +02:00
parent cc82353d8f
commit 88cc1c428d
3 changed files with 86 additions and 27 deletions

View file

@ -21,7 +21,7 @@ import PropTypes from 'prop-types';
import dis from '../../../dispatcher';
import EditorModel from '../../../editor/model';
import {getCaretOffsetAndText} from '../../../editor/dom';
import {htmlSerializeIfNeeded, textSerialize} from '../../../editor/serialize';
import {htmlSerializeIfNeeded, textSerialize, containsEmote, stripEmoteCommand} from '../../../editor/serialize';
import {findEditableEvent} from '../../../utils/EventUtils';
import {parseEvent} from '../../../editor/deserialize';
import {PartCreator} from '../../../editor/parts';
@ -56,17 +56,10 @@ function getTextReplyFallback(mxEvent) {
return "";
}
function _isEmote(model) {
const firstPart = model.parts[0];
return firstPart && firstPart.type === "plain" && firstPart.text.startsWith("/me ");
}
function createEditContent(model, editedEvent) {
const isEmote = _isEmote(model);
const isEmote = containsEmote(model);
if (isEmote) {
// trim "/me "
model = model.clone();
model.removeText({index: 0, offset: 0}, 4);
model = stripEmoteCommand(model);
}
const isReply = _isReply(editedEvent);
let plainPrefix = "";

View file

@ -19,7 +19,7 @@ import PropTypes from 'prop-types';
import dis from '../../../dispatcher';
import EditorModel from '../../../editor/model';
import {getCaretOffsetAndText} from '../../../editor/dom';
import {htmlSerializeIfNeeded, textSerialize} from '../../../editor/serialize';
import {htmlSerializeIfNeeded, textSerialize, containsEmote, stripEmoteCommand} from '../../../editor/serialize';
import {PartCreator} from '../../../editor/parts';
import {MatrixClient} from 'matrix-js-sdk';
import BasicMessageComposer from "./BasicMessageComposer";
@ -29,6 +29,10 @@ import ReplyThread from "../elements/ReplyThread";
import {parseEvent} from '../../../editor/deserialize';
import {findEditableEvent} from '../../../utils/EventUtils';
import ComposerHistoryManager from "../../../ComposerHistoryManager";
import {processCommandInput} from '../../../SlashCommands';
import sdk from '../../../index';
import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent);
@ -46,11 +50,15 @@ function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
}
function createMessageContent(model, permalinkCreator) {
const isEmote = containsEmote(model);
if (isEmote) {
model = stripEmoteCommand(model);
}
const repliedToEvent = RoomViewStore.getQuotingEvent();
const body = textSerialize(model);
const content = {
msgtype: "m.text",
msgtype: isEmote ? "m.emote" : "m.text",
body: body,
};
const formattedBody = htmlSerializeIfNeeded(model, {forceHTML: !!repliedToEvent});
@ -129,9 +137,10 @@ export default class SendMessageComposer extends React.Component {
}
}
// we keep sent messages/commands in a separate history (separate from undo history)
// so you can alt+up/down in them
selectSendHistory(up) {
const delta = up ? -1 : 1;
// True if we are not currently selecting history, but composing a message
if (this.sendHistoryManager.currentIndex === this.sendHistoryManager.history.length) {
// We can't go any further - there isn't any more history, so nop.
@ -152,24 +161,69 @@ export default class SendMessageComposer extends React.Component {
}
}
_isSlashCommand() {
const parts = this.model.parts;
const isPlain = parts.reduce((isPlain, part) => {
return isPlain && (part.type === "plain" || part.type === "newline");
}, true);
return isPlain && parts.length > 0 && parts[0].text.startsWith("/");
}
async _runSlashCommand() {
const commandText = this.model.parts.reduce((text, part) => {
return text + part.text;
}, "");
const cmd = processCommandInput(this.props.room.roomId, commandText);
if (cmd) {
let error = cmd.error;
if (cmd.promise) {
try {
await cmd.promise;
} catch (err) {
error = err;
}
}
if (error) {
console.error("Command failure: %s", error);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
// assume the error is a server error when the command is async
const isServerError = !!cmd.promise;
const title = isServerError ? "Server error" : "Command error";
Modal.createTrackedDialog(title, '', ErrorDialog, {
title: isServerError ? _t("Server error") : _t("Command error"),
description: error.message ? error.message : _t(
"Server unavailable, overloaded, or something else went wrong.",
),
});
} else {
console.log("Command success.");
}
}
}
_sendMessage() {
const isReply = !!RoomViewStore.getQuotingEvent();
const {roomId} = this.props.room;
const content = createMessageContent(this.model, this.props.permalinkCreator);
this.context.matrixClient.sendMessage(roomId, content);
if (!containsEmote(this.model) && this._isSlashCommand()) {
this._runSlashCommand();
} else {
const isReply = !!RoomViewStore.getQuotingEvent();
const {roomId} = this.props.room;
const content = createMessageContent(this.model, this.props.permalinkCreator);
this.context.matrixClient.sendMessage(roomId, content);
if (isReply) {
// 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,
});
}
}
this.sendHistoryManager.save(this.model);
// clear composer
this.model.reset([]);
this._editorRef.clearUndoHistory();
if (isReply) {
// 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,
});
}
dis.dispatch({action: 'focus_composer'});
this._editorRef.focus();
}
componentWillUnmount() {

View file

@ -56,3 +56,15 @@ export function textSerialize(model) {
}
}, "");
}
export function containsEmote(model) {
const firstPart = model.parts[0];
return firstPart && firstPart.type === "plain" && firstPart.text.startsWith("/me ");
}
export function stripEmoteCommand(model) {
// trim "/me "
model = model.clone();
model.removeText({index: 0, offset: 0}, 4);
return model;
}