Handle /me in rte (#10558)

* add /me handling

* use typeguards to avoid fighting TS

* improve clarity and use of typeguards

* add createMessageContent tests

* remove completed TODO

* improve comments

* remove duplication and renaming of argument
This commit is contained in:
alunturner 2023-04-11 09:23:03 +01:00 committed by GitHub
parent 7b5d1802b1
commit 4d5744008e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 45 additions and 18 deletions

View file

@ -21,6 +21,8 @@ import SettingsStore from "../../../../../settings/SettingsStore";
import { RoomPermalinkCreator } from "../../../../../utils/permalinks/Permalinks"; import { RoomPermalinkCreator } from "../../../../../utils/permalinks/Permalinks";
import { addReplyToMessageContent } from "../../../../../utils/Reply"; import { addReplyToMessageContent } from "../../../../../utils/Reply";
export const EMOTE_PREFIX = "/me ";
// Merges favouring the given relation // Merges favouring the given relation
function attachRelation(content: IContent, relation?: IEventRelation): void { function attachRelation(content: IContent, relation?: IEventRelation): void {
if (relation) { if (relation) {
@ -61,6 +63,8 @@ interface CreateMessageContentParams {
editedEvent?: MatrixEvent; editedEvent?: MatrixEvent;
} }
const isMatrixEvent = (e: MatrixEvent | undefined): e is MatrixEvent => e instanceof MatrixEvent;
export async function createMessageContent( export async function createMessageContent(
message: string, message: string,
isHTML: boolean, isHTML: boolean,
@ -72,22 +76,22 @@ export async function createMessageContent(
editedEvent, editedEvent,
}: CreateMessageContentParams, }: CreateMessageContentParams,
): Promise<IContent> { ): Promise<IContent> {
// TODO emote ? const isEditing = isMatrixEvent(editedEvent);
const isReply = isEditing ? Boolean(editedEvent.replyEventId) : isMatrixEvent(replyToEvent);
const isEditing = Boolean(editedEvent);
const isReply = isEditing ? Boolean(editedEvent?.replyEventId) : Boolean(replyToEvent);
const isReplyAndEditing = isEditing && isReply; const isReplyAndEditing = isEditing && isReply;
/*const isEmote = containsEmote(model); const isEmote = message.startsWith(EMOTE_PREFIX);
if (isEmote) { if (isEmote) {
model = stripEmoteCommand(model); // if we are dealing with an emote we want to remove the prefix so that `/me` does not
// appear after the `* <userName>` text in the timeline
message = message.slice(EMOTE_PREFIX.length);
} }
if (startsWith(model, "//")) { if (message.startsWith("//")) {
model = stripPrefix(model, "/"); // if user wants to enter a single slash at the start of a message, this
// is how they have to do it (due to it clashing with commands), so here we
// remove the first character to make sure //word displays as /word
message = message.slice(1);
} }
model = unescapeMessage(model);*/
// const body = textSerialize(model);
// if we're editing rich text, the message content is pure html // if we're editing rich text, the message content is pure html
// BUT if we're not, the message content will be plain text // BUT if we're not, the message content will be plain text
@ -96,8 +100,7 @@ export async function createMessageContent(
const formattedBodyPrefix = (isReplyAndEditing && getHtmlReplyFallback(editedEvent)) || ""; const formattedBodyPrefix = (isReplyAndEditing && getHtmlReplyFallback(editedEvent)) || "";
const content: IContent = { const content: IContent = {
// TODO emote msgtype: isEmote ? MsgType.Emote : MsgType.Text,
msgtype: MsgType.Text,
body: isEditing ? `${bodyPrefix} * ${body}` : body, body: isEditing ? `${bodyPrefix} * ${body}` : body,
}; };

View file

@ -31,7 +31,7 @@ import dis from "../../../../../dispatcher/dispatcher";
import { createRedactEventDialog } from "../../../dialogs/ConfirmRedactDialog"; import { createRedactEventDialog } from "../../../dialogs/ConfirmRedactDialog";
import { endEditing, cancelPreviousPendingEdit } from "./editing"; import { endEditing, cancelPreviousPendingEdit } from "./editing";
import EditorStateTransfer from "../../../../../utils/EditorStateTransfer"; import EditorStateTransfer from "../../../../../utils/EditorStateTransfer";
import { createMessageContent } from "./createMessageContent"; import { createMessageContent, EMOTE_PREFIX } from "./createMessageContent";
import { isContentModified } from "./isContentModified"; import { isContentModified } from "./isContentModified";
import { CommandCategories, getCommand } from "../../../../../SlashCommands"; import { CommandCategories, getCommand } from "../../../../../SlashCommands";
import { runSlashCommand, shouldSendAnyway } from "../../../../../editor/commands"; import { runSlashCommand, shouldSendAnyway } from "../../../../../editor/commands";
@ -78,11 +78,11 @@ export async function sendMessage(
let content: IContent | null = null; let content: IContent | null = null;
// Functionality here approximates what can be found in SendMessageComposer.sendMessage() // Slash command handling here approximates what can be found in SendMessageComposer.sendMessage()
if (message.startsWith("/") && !message.startsWith("//")) { // but note that the /me and // special cases are handled by the call to createMessageContent
if (message.startsWith("/") && !message.startsWith("//") && !message.startsWith(EMOTE_PREFIX)) {
const { cmd, args } = getCommand(message); const { cmd, args } = getCommand(message);
if (cmd) { if (cmd) {
// TODO handle /me special case separately, see end of SlashCommands.Commands
const threadId = relation?.rel_type === THREAD_RELATION_TYPE.name ? relation?.event_id : null; const threadId = relation?.rel_type === THREAD_RELATION_TYPE.name ? relation?.event_id : null;
let commandSuccessful: boolean; let commandSuccessful: boolean;
[content, commandSuccessful] = await runSlashCommand(cmd, args, roomId, threadId ?? null); [content, commandSuccessful] = await runSlashCommand(cmd, args, roomId, threadId ?? null);

View file

@ -13,10 +13,14 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { MsgType } from "matrix-js-sdk/src/matrix";
import { mkEvent } from "../../../../../test-utils"; import { mkEvent } from "../../../../../test-utils";
import { RoomPermalinkCreator } from "../../../../../../src/utils/permalinks/Permalinks"; import { RoomPermalinkCreator } from "../../../../../../src/utils/permalinks/Permalinks";
import { createMessageContent } from "../../../../../../src/components/views/rooms/wysiwyg_composer/utils/createMessageContent"; import {
createMessageContent,
EMOTE_PREFIX,
} from "../../../../../../src/components/views/rooms/wysiwyg_composer/utils/createMessageContent";
describe("createMessageContent", () => { describe("createMessageContent", () => {
const permalinkCreator = { const permalinkCreator = {
@ -130,4 +134,24 @@ describe("createMessageContent", () => {
}, },
}); });
}); });
it("Should strip the /me prefix from a message", async () => {
const textBody = "some body text";
const content = await createMessageContent(EMOTE_PREFIX + textBody, true, { permalinkCreator });
expect(content).toMatchObject({ body: textBody, formatted_body: textBody });
});
it("Should strip single / from message prefixed with //", async () => {
const content = await createMessageContent("//twoSlashes", true, { permalinkCreator });
expect(content).toMatchObject({ body: "/twoSlashes", formatted_body: "/twoSlashes" });
});
it("Should set the content type to MsgType.Emote when /me prefix is used", async () => {
const textBody = "some body text";
const content = await createMessageContent(EMOTE_PREFIX + textBody, true, { permalinkCreator });
expect(content).toMatchObject({ msgtype: MsgType.Emote });
});
}); });