* Improve performance of RoomContext in RoomHeader This allows a component to subscribe to only part of the RoomContext so they do not need to re-render on every single change Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Prettier Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Add comment Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
242 lines
8.7 KiB
TypeScript
242 lines
8.7 KiB
TypeScript
/*
|
|
Copyright 2024 New Vector Ltd.
|
|
Copyright 2022, 2023 The Matrix.org Foundation C.I.C.
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
Please see LICENSE files in the repository root for full details.
|
|
*/
|
|
|
|
import { Composer as ComposerEvent } from "@matrix-org/analytics-events/types/typescript/Composer";
|
|
import {
|
|
IEventRelation,
|
|
MatrixEvent,
|
|
ISendEventResponse,
|
|
MatrixClient,
|
|
THREAD_RELATION_TYPE,
|
|
} from "matrix-js-sdk/src/matrix";
|
|
import { RoomMessageEventContent } from "matrix-js-sdk/src/types";
|
|
|
|
import { PosthogAnalytics } from "../../../../../PosthogAnalytics";
|
|
import SettingsStore from "../../../../../settings/SettingsStore";
|
|
import { decorateStartSendingTime, sendRoundTripMetric } from "../../../../../sendTimePerformanceMetrics";
|
|
import { doMaybeLocalRoomAction } from "../../../../../utils/local-room";
|
|
import { CHAT_EFFECTS } from "../../../../../effects";
|
|
import { containsEmoji } from "../../../../../effects/utils";
|
|
import { IRoomState } from "../../../../structures/RoomView";
|
|
import dis from "../../../../../dispatcher/dispatcher";
|
|
import { createRedactEventDialog } from "../../../dialogs/ConfirmRedactDialog";
|
|
import { endEditing, cancelPreviousPendingEdit } from "./editing";
|
|
import EditorStateTransfer from "../../../../../utils/EditorStateTransfer";
|
|
import { createMessageContent, EMOTE_PREFIX } from "./createMessageContent";
|
|
import { isContentModified } from "./isContentModified";
|
|
import { CommandCategories, getCommand } from "../../../../../SlashCommands";
|
|
import { runSlashCommand, shouldSendAnyway } from "../../../../../editor/commands";
|
|
import { Action } from "../../../../../dispatcher/actions";
|
|
import { addReplyToMessageContent } from "../../../../../utils/Reply";
|
|
import { attachRelation } from "../../SendMessageComposer";
|
|
|
|
export interface SendMessageParams {
|
|
mxClient: MatrixClient;
|
|
relation?: IEventRelation;
|
|
replyToEvent?: MatrixEvent;
|
|
roomContext: Pick<IRoomState, "timelineRenderingType" | "room">;
|
|
}
|
|
|
|
export async function sendMessage(
|
|
message: string,
|
|
isHTML: boolean,
|
|
{ roomContext, mxClient, ...params }: SendMessageParams,
|
|
): Promise<ISendEventResponse | undefined> {
|
|
const { relation, replyToEvent } = params;
|
|
const { room } = roomContext;
|
|
const roomId = room?.roomId;
|
|
|
|
if (!roomId) {
|
|
return;
|
|
}
|
|
|
|
const posthogEvent: ComposerEvent = {
|
|
eventName: "Composer",
|
|
isEditing: false,
|
|
messageType: "Text",
|
|
isReply: Boolean(replyToEvent),
|
|
// TODO thread
|
|
inThread: relation?.rel_type === THREAD_RELATION_TYPE.name,
|
|
};
|
|
|
|
// TODO thread
|
|
/*if (posthogEvent.inThread) {
|
|
const threadRoot = room.findEventById(relation?.event_id);
|
|
posthogEvent.startsThread = threadRoot?.getThread()?.events.length === 1;
|
|
}*/
|
|
PosthogAnalytics.instance.trackEvent<ComposerEvent>(posthogEvent);
|
|
|
|
let content: RoomMessageEventContent | null = null;
|
|
|
|
// Slash command handling here approximates what can be found in SendMessageComposer.sendMessage()
|
|
// 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);
|
|
if (cmd) {
|
|
const threadId = relation?.rel_type === THREAD_RELATION_TYPE.name ? relation?.event_id : null;
|
|
let commandSuccessful: boolean;
|
|
[content, commandSuccessful] = await runSlashCommand(mxClient, cmd, args, roomId, threadId ?? null);
|
|
|
|
if (!commandSuccessful) {
|
|
return; // errored
|
|
}
|
|
|
|
if (
|
|
content &&
|
|
(cmd.category === CommandCategories.messages || cmd.category === CommandCategories.effects)
|
|
) {
|
|
attachRelation(content, relation);
|
|
if (replyToEvent) {
|
|
addReplyToMessageContent(content, replyToEvent);
|
|
}
|
|
} else {
|
|
// instead of setting shouldSend to false as in SendMessageComposer, just return
|
|
return;
|
|
}
|
|
} else {
|
|
const sendAnyway = await shouldSendAnyway(message);
|
|
// re-focus the composer after QuestionDialog is closed
|
|
dis.dispatch({
|
|
action: Action.FocusAComposer,
|
|
context: roomContext.timelineRenderingType,
|
|
});
|
|
// if !sendAnyway bail to let the user edit the composer and try again
|
|
if (!sendAnyway) return;
|
|
}
|
|
}
|
|
|
|
// if content is null, we haven't done any slash command processing, so generate some content
|
|
content ??= await createMessageContent(message, isHTML, params);
|
|
|
|
// TODO replace emotion end of message ?
|
|
|
|
// TODO quick reaction
|
|
|
|
// don't bother sending an empty message
|
|
if (!content.body.trim()) {
|
|
return;
|
|
}
|
|
|
|
if (SettingsStore.getValue("Performance.addSendMessageTimingMetadata")) {
|
|
decorateStartSendingTime(content);
|
|
}
|
|
|
|
const threadId = relation?.event_id && relation?.rel_type === THREAD_RELATION_TYPE.name ? relation.event_id : null;
|
|
|
|
const prom = doMaybeLocalRoomAction(
|
|
roomId,
|
|
(actualRoomId: string) => mxClient.sendMessage(actualRoomId, threadId, content!),
|
|
mxClient,
|
|
);
|
|
|
|
if (replyToEvent) {
|
|
// 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,
|
|
context: roomContext.timelineRenderingType,
|
|
});
|
|
}
|
|
|
|
dis.dispatch({ action: "message_sent" });
|
|
CHAT_EFFECTS.forEach((effect) => {
|
|
if (content && containsEmoji(content, effect.emojis)) {
|
|
// For initial threads launch, chat effects are disabled
|
|
// see #19731
|
|
const isNotThread = relation?.rel_type !== THREAD_RELATION_TYPE.name;
|
|
if (isNotThread) {
|
|
dis.dispatch({ action: `effects.${effect.command}` });
|
|
}
|
|
}
|
|
});
|
|
if (SettingsStore.getValue("Performance.addSendMessageTimingMetadata")) {
|
|
prom.then((resp) => {
|
|
sendRoundTripMetric(mxClient, roomId, resp.event_id);
|
|
});
|
|
}
|
|
|
|
// TODO save history
|
|
// TODO save local state
|
|
|
|
//if (shouldSend && SettingsStore.getValue("scrollToBottomOnMessageSent")) {
|
|
if (SettingsStore.getValue("scrollToBottomOnMessageSent")) {
|
|
dis.dispatch({
|
|
action: "scroll_to_bottom",
|
|
timelineRenderingType: roomContext.timelineRenderingType,
|
|
});
|
|
}
|
|
|
|
return prom;
|
|
}
|
|
|
|
interface EditMessageParams {
|
|
mxClient: MatrixClient;
|
|
roomContext: Pick<IRoomState, "timelineRenderingType">;
|
|
editorStateTransfer: EditorStateTransfer;
|
|
}
|
|
|
|
export async function editMessage(
|
|
html: string,
|
|
{ roomContext, mxClient, editorStateTransfer }: EditMessageParams,
|
|
): Promise<ISendEventResponse | undefined> {
|
|
const editedEvent = editorStateTransfer.getEvent();
|
|
|
|
PosthogAnalytics.instance.trackEvent<ComposerEvent>({
|
|
eventName: "Composer",
|
|
isEditing: true,
|
|
messageType: "Text",
|
|
inThread: Boolean(editedEvent?.getThread()),
|
|
isReply: Boolean(editedEvent.replyEventId),
|
|
});
|
|
|
|
// TODO emoji
|
|
// Replace emoticon at the end of the message
|
|
/* if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) {
|
|
const caret = this.editorRef.current?.getCaret();
|
|
const position = this.model.positionForOffset(caret.offset, caret.atNodeEnd);
|
|
this.editorRef.current?.replaceEmoticon(position, REGEX_EMOTICON);
|
|
}*/
|
|
const editContent = await createMessageContent(html, true, { editedEvent });
|
|
const newContent = editContent["m.new_content"]!;
|
|
|
|
const shouldSend = true;
|
|
|
|
if (newContent?.body === "") {
|
|
cancelPreviousPendingEdit(mxClient, editorStateTransfer);
|
|
createRedactEventDialog({
|
|
mxEvent: editedEvent,
|
|
onCloseDialog: () => {
|
|
endEditing(roomContext);
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
let response: Promise<ISendEventResponse> | undefined;
|
|
|
|
const roomId = editedEvent.getRoomId();
|
|
|
|
// If content is modified then send an updated event into the room
|
|
if (isContentModified(newContent, editorStateTransfer) && roomId) {
|
|
// TODO Slash Commands
|
|
|
|
if (shouldSend) {
|
|
cancelPreviousPendingEdit(mxClient, editorStateTransfer);
|
|
|
|
const event = editorStateTransfer.getEvent();
|
|
const threadId = event.threadRootId || null;
|
|
|
|
response = mxClient.sendMessage(roomId, threadId, editContent);
|
|
dis.dispatch({ action: "message_sent" });
|
|
}
|
|
}
|
|
|
|
endEditing(roomContext);
|
|
return response;
|
|
}
|