add support for emotes and running /commands
this does not yet include autocomplete for commands
This commit is contained in:
parent
cc82353d8f
commit
88cc1c428d
3 changed files with 86 additions and 27 deletions
|
@ -21,7 +21,7 @@ import PropTypes from 'prop-types';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import EditorModel from '../../../editor/model';
|
import EditorModel from '../../../editor/model';
|
||||||
import {getCaretOffsetAndText} from '../../../editor/dom';
|
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 {findEditableEvent} from '../../../utils/EventUtils';
|
||||||
import {parseEvent} from '../../../editor/deserialize';
|
import {parseEvent} from '../../../editor/deserialize';
|
||||||
import {PartCreator} from '../../../editor/parts';
|
import {PartCreator} from '../../../editor/parts';
|
||||||
|
@ -56,17 +56,10 @@ function getTextReplyFallback(mxEvent) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function _isEmote(model) {
|
|
||||||
const firstPart = model.parts[0];
|
|
||||||
return firstPart && firstPart.type === "plain" && firstPart.text.startsWith("/me ");
|
|
||||||
}
|
|
||||||
|
|
||||||
function createEditContent(model, editedEvent) {
|
function createEditContent(model, editedEvent) {
|
||||||
const isEmote = _isEmote(model);
|
const isEmote = containsEmote(model);
|
||||||
if (isEmote) {
|
if (isEmote) {
|
||||||
// trim "/me "
|
model = stripEmoteCommand(model);
|
||||||
model = model.clone();
|
|
||||||
model.removeText({index: 0, offset: 0}, 4);
|
|
||||||
}
|
}
|
||||||
const isReply = _isReply(editedEvent);
|
const isReply = _isReply(editedEvent);
|
||||||
let plainPrefix = "";
|
let plainPrefix = "";
|
||||||
|
|
|
@ -19,7 +19,7 @@ import PropTypes from 'prop-types';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import EditorModel from '../../../editor/model';
|
import EditorModel from '../../../editor/model';
|
||||||
import {getCaretOffsetAndText} from '../../../editor/dom';
|
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 {PartCreator} from '../../../editor/parts';
|
||||||
import {MatrixClient} from 'matrix-js-sdk';
|
import {MatrixClient} from 'matrix-js-sdk';
|
||||||
import BasicMessageComposer from "./BasicMessageComposer";
|
import BasicMessageComposer from "./BasicMessageComposer";
|
||||||
|
@ -29,6 +29,10 @@ import ReplyThread from "../elements/ReplyThread";
|
||||||
import {parseEvent} from '../../../editor/deserialize';
|
import {parseEvent} from '../../../editor/deserialize';
|
||||||
import {findEditableEvent} from '../../../utils/EventUtils';
|
import {findEditableEvent} from '../../../utils/EventUtils';
|
||||||
import ComposerHistoryManager from "../../../ComposerHistoryManager";
|
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) {
|
function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
|
||||||
const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent);
|
const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent);
|
||||||
|
@ -46,11 +50,15 @@ function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function createMessageContent(model, permalinkCreator) {
|
function createMessageContent(model, permalinkCreator) {
|
||||||
|
const isEmote = containsEmote(model);
|
||||||
|
if (isEmote) {
|
||||||
|
model = stripEmoteCommand(model);
|
||||||
|
}
|
||||||
const repliedToEvent = RoomViewStore.getQuotingEvent();
|
const repliedToEvent = RoomViewStore.getQuotingEvent();
|
||||||
|
|
||||||
const body = textSerialize(model);
|
const body = textSerialize(model);
|
||||||
const content = {
|
const content = {
|
||||||
msgtype: "m.text",
|
msgtype: isEmote ? "m.emote" : "m.text",
|
||||||
body: body,
|
body: body,
|
||||||
};
|
};
|
||||||
const formattedBody = htmlSerializeIfNeeded(model, {forceHTML: !!repliedToEvent});
|
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) {
|
selectSendHistory(up) {
|
||||||
const delta = up ? -1 : 1;
|
const delta = up ? -1 : 1;
|
||||||
|
|
||||||
// True if we are not currently selecting history, but composing a message
|
// True if we are not currently selecting history, but composing a message
|
||||||
if (this.sendHistoryManager.currentIndex === this.sendHistoryManager.history.length) {
|
if (this.sendHistoryManager.currentIndex === this.sendHistoryManager.history.length) {
|
||||||
// We can't go any further - there isn't any more history, so nop.
|
// We can't go any further - there isn't any more history, so nop.
|
||||||
|
@ -152,15 +161,55 @@ 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() {
|
_sendMessage() {
|
||||||
|
if (!containsEmote(this.model) && this._isSlashCommand()) {
|
||||||
|
this._runSlashCommand();
|
||||||
|
} else {
|
||||||
const isReply = !!RoomViewStore.getQuotingEvent();
|
const isReply = !!RoomViewStore.getQuotingEvent();
|
||||||
const {roomId} = this.props.room;
|
const {roomId} = this.props.room;
|
||||||
const content = createMessageContent(this.model, this.props.permalinkCreator);
|
const content = createMessageContent(this.model, this.props.permalinkCreator);
|
||||||
this.context.matrixClient.sendMessage(roomId, content);
|
this.context.matrixClient.sendMessage(roomId, content);
|
||||||
this.sendHistoryManager.save(this.model);
|
|
||||||
this.model.reset([]);
|
|
||||||
this._editorRef.clearUndoHistory();
|
|
||||||
|
|
||||||
if (isReply) {
|
if (isReply) {
|
||||||
// Clear reply_to_event as we put the message into the queue
|
// Clear reply_to_event as we put the message into the queue
|
||||||
// if the send fails, retry will handle resending.
|
// if the send fails, retry will handle resending.
|
||||||
|
@ -169,7 +218,12 @@ export default class SendMessageComposer extends React.Component {
|
||||||
event: null,
|
event: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
dis.dispatch({action: 'focus_composer'});
|
}
|
||||||
|
this.sendHistoryManager.save(this.model);
|
||||||
|
// clear composer
|
||||||
|
this.model.reset([]);
|
||||||
|
this._editorRef.clearUndoHistory();
|
||||||
|
this._editorRef.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue