Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/shy-dolphins-share.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rocket.chat/meteor": patch
---

Fixes intermittent error "Cannot read properties of undefined" when editing messages
102 changes: 27 additions & 75 deletions apps/meteor/app/ui/client/lib/ChatMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings';
import { isVideoConfMessage } from '@rocket.chat/core-typings';
import type { IActionManager } from '@rocket.chat/ui-contexts';

import { CurrentEditingMessage } from './CurrentEditingMessage';
import { UserAction } from './UserAction';
import type { ChatAPI, ComposerAPI, DataAPI, UploadsAPI } from '../../../../client/lib/chats/ChatAPI';
import { createDataAPI } from '../../../../client/lib/chats/data';
Expand All @@ -15,10 +16,7 @@ import { sendMessage } from '../../../../client/lib/chats/flows/sendMessage';
import { uploadFiles } from '../../../../client/lib/chats/flows/uploadFiles';
import { ReadStateManager } from '../../../../client/lib/chats/readStateManager';
import { createUploadsAPI } from '../../../../client/lib/chats/uploads';
import {
setHighlightMessage,
clearHighlightMessage,
} from '../../../../client/views/room/MessageList/providers/messageHighlightSubscription';
import { setHighlightMessage } from '../../../../client/views/room/MessageList/providers/messageHighlightSubscription';

type DeepWritable<T> = T extends (...args: any) => any
? T
Expand All @@ -29,6 +27,8 @@ type DeepWritable<T> = T extends (...args: any) => any
export class ChatMessages implements ChatAPI {
public uid: string | null;

public tmid?: IMessage['_id'];

public composer: ComposerAPI | undefined;

public setComposerAPI = (composer?: ComposerAPI): void => {
Expand All @@ -38,6 +38,8 @@ export class ChatMessages implements ChatAPI {

public data: DataAPI;

public currentEditingMessage: CurrentEditingMessage;

public readStateManager: ReadStateManager;

public uploads: UploadsAPI;
Expand All @@ -55,15 +57,15 @@ export class ChatMessages implements ChatAPI {
performContinuously(action: 'recording' | 'uploading' | 'playing'): Promise<void> | void;
};

private currentEditingMID?: string;

public messageEditing: ChatAPI['messageEditing'] = {
toPreviousMessage: async () => {
if (!this.composer) {
return;
}

if (!this.currentEditing) {
const mid = this.currentEditingMessage.getMID();

if (!mid) {
let lastMessage = await this.data.findPreviousOwnMessage();

// Videoconf messages should not be edited
Expand All @@ -79,7 +81,7 @@ export class ChatMessages implements ChatAPI {
return;
}

const currentMessage = await this.data.findMessageByID(this.currentEditing.mid);
const currentMessage = await this.data.findMessageByID(mid);
let previousMessage = currentMessage ? await this.data.findPreviousOwnMessage(currentMessage) : undefined;

// Videoconf messages should not be edited
Expand All @@ -92,14 +94,17 @@ export class ChatMessages implements ChatAPI {
return;
}

await this.currentEditing.cancel();
await this.currentEditingMessage.cancel();
await this.currentEditingMessage.stop();
},
toNextMessage: async () => {
if (!this.composer || !this.currentEditing) {
const mid = this.currentEditingMessage.getMID();

if (!this.composer || !mid) {
return;
}

const currentMessage = await this.data.findMessageByID(this.currentEditing.mid);
const currentMessage = await this.data.findMessageByID(mid);
let nextMessage = currentMessage ? await this.data.findNextOwnMessage(currentMessage) : undefined;

// Videoconf messages should not be edited
Expand All @@ -112,18 +117,19 @@ export class ChatMessages implements ChatAPI {
return;
}

await this.currentEditing.cancel();
await this.currentEditingMessage.cancel();
await this.currentEditingMessage.stop();
},
editMessage: async (message: IMessage, { cursorAtStart = false }: { cursorAtStart?: boolean } = {}) => {
const text = (await this.data.getDraft(message._id)) || message.attachments?.[0]?.description || message.msg;

await this.currentEditing?.stop();
await this.currentEditingMessage.stop();

if (!this.composer || !(await this.data.canUpdateMessage(message))) {
return;
}

this.currentEditingMID = message._id;
this.currentEditingMessage.setMID(message._id);
setHighlightMessage(message._id);
this.composer.setEditingMode(true);

Expand All @@ -136,19 +142,14 @@ export class ChatMessages implements ChatAPI {

public flows: DeepWritable<ChatAPI['flows']>;

public constructor(
private params: {
rid: IRoom['_id'];
tmid?: IMessage['_id'];
uid: IUser['_id'] | null;
actionManager: IActionManager;
},
) {
public constructor(params: { rid: IRoom['_id']; tmid?: IMessage['_id']; uid: IUser['_id'] | null; actionManager: IActionManager }) {
const { rid, tmid } = params;
this.tmid = tmid;
this.uid = params.uid;
this.data = createDataAPI({ rid, tmid });
this.uploads = createUploadsAPI({ rid, tmid });
this.ActionManager = params.actionManager;
this.currentEditingMessage = new CurrentEditingMessage(this);

const unimplemented = () => {
throw new Error('Flow is not implemented');
Expand Down Expand Up @@ -185,60 +186,11 @@ export class ChatMessages implements ChatAPI {
};
}

public get currentEditing() {
if (!this.composer || !this.currentEditingMID) {
return undefined;
}

return {
mid: this.currentEditingMID,
reset: async (): Promise<boolean> => {
if (!this.composer || !this.currentEditingMID) {
return false;
}

const message = await this.data.findMessageByID(this.currentEditingMID);
if (this.composer.text !== message?.msg) {
this.composer.setText(message?.msg ?? '');
return true;
}

return false;
},
stop: async (): Promise<void> => {
if (!this.composer || !this.currentEditingMID) {
return;
}

const message = await this.data.findMessageByID(this.currentEditingMID);
const draft = this.composer.text;

if (draft === message?.msg) {
await this.data.discardDraft(this.currentEditingMID);
} else {
await this.data.saveDraft(this.currentEditingMID, (await this.data.getDraft(this.currentEditingMID)) || draft);
}

this.composer.setEditingMode(false);
this.currentEditingMID = undefined;
clearHighlightMessage();
},
cancel: async (): Promise<void> => {
if (!this.currentEditingMID) {
return;
}

await this.data.discardDraft(this.currentEditingMID);
await this.currentEditing?.stop();
this.composer?.setText((await this.data.getDraft(undefined)) ?? '');
},
};
}

public async release() {
if (this.currentEditing) {
if (!this.params.tmid) {
await this.currentEditing.cancel();
if (this.currentEditingMessage.getMID()) {
if (!this.tmid) {
await this.currentEditingMessage.cancel();
await this.currentEditingMessage.stop();
}
this.composer?.clear();
}
Expand Down
114 changes: 114 additions & 0 deletions apps/meteor/app/ui/client/lib/CurrentEditingMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import type { ChatAPI } from '../../../../client/lib/chats/ChatAPI';
import { clearHighlightMessage } from '../../../../client/views/room/MessageList/providers/messageHighlightSubscription';

export class CurrentEditingMessage {
private lock = false;

private mid?: string;

private queue: {
resolve: (release: () => void) => void;
}[] = [];

private chat: ChatAPI;

constructor(chat: ChatAPI) {
this.chat = chat;
}

public reset = async () => {
return this.runExclusive(async () => {
if (!this.chat.composer || !this.mid) {
return false;
}

const message = await this.chat.data.findMessageByID(this.mid);

if (this.chat.composer.text !== message?.msg) {
this.chat.composer.setText(message?.msg ?? '');
return true;
}

return false;
});
};

public stop = async () => {
await this.runExclusive(async () => {
if (!this.chat.composer || !this.mid) {
return;
}

const message = await this.chat.data.findMessageByID(this.mid);
const draft = this.chat.composer.text;

if (draft === message?.msg) {
await this.chat.data.discardDraft(this.mid);
} else {
await this.chat.data.saveDraft(this.mid, (await this.chat.data.getDraft(this.mid)) || draft);
}

this.chat.composer.setEditingMode(false);
this.mid = undefined;
clearHighlightMessage();
});
};

public cancel = async () => {
await this.runExclusive(async () => {
if (!this.mid) {
return;
}

await this.chat.data.discardDraft(this.mid);
this.chat.composer?.setText((await this.chat.data.getDraft(undefined)) ?? '');
});
};

private acquire = async () => {
return new Promise<() => void>((resolve) => {
this.queue.push({ resolve });
this.dispatch();
});
};

private dispatch() {
if (this.lock) {
return;
}

const next = this.queue.shift();

if (!next) {
return;
}

this.lock = true;
next.resolve(this.buildRelease());
}

private buildRelease = () => {
return async () => {
this.lock = false;
this.dispatch();
};
};

public getMID() {
return this.mid;
}

public setMID(mid: string) {
this.mid = mid;
}

private async runExclusive<T>(callback: () => Promise<T>) {
const release = await this.acquire();

try {
return await callback();
} finally {
release();
}
}
}
15 changes: 7 additions & 8 deletions apps/meteor/client/lib/chats/ChatAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,14 +118,13 @@ export type ChatAPI = {
editMessage(message: IMessage, options?: { cursorAtStart?: boolean }): Promise<void>;
};

readonly currentEditing:
| {
readonly mid: IMessage['_id'];
reset(): Promise<boolean>;
stop(): Promise<void>;
cancel(): Promise<void>;
}
| undefined;
readonly currentEditingMessage: {
setMID(mid: IMessage['_id']): void;
getMID(): string | undefined;
reset(): Promise<boolean>;
stop(): Promise<void>;
cancel(): Promise<void>;
};

readonly emojiPicker: {
open(el: Element, cb: (emoji: string) => void): void;
Expand Down
7 changes: 4 additions & 3 deletions apps/meteor/client/lib/chats/flows/processMessageEditing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ export const processMessageEditing = async (
message: Pick<IMessage, '_id' | 't'> & Partial<Omit<IMessage, '_id' | 't'>>,
previewUrls?: string[],
): Promise<boolean> => {
if (!chat.currentEditing) {
const mid = chat.currentEditingMessage.getMID();
if (!mid) {
return false;
}

Expand All @@ -22,12 +23,12 @@ export const processMessageEditing = async (
}

try {
await chat.data.updateMessage({ ...message, _id: chat.currentEditing.mid }, previewUrls);
await chat.data.updateMessage({ ...message, _id: mid }, previewUrls);
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
}

chat.currentEditing.stop();
chat.currentEditingMessage.stop();

return true;
};
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const processTooLongMessage = async (chat: ChatAPI, { msg }: Pick<IMessag
const fileUploadsEnabled = settings.get('FileUpload_Enabled');
const convertLongMessagesToAttachment = settings.get('Message_AllowConvertLongMessagesToAttachment');

if (chat.currentEditing || !fileUploadsEnabled || !convertLongMessagesToAttachment) {
if (chat.currentEditingMessage.getMID() || !fileUploadsEnabled || !convertLongMessagesToAttachment) {
dispatchToastMessage({ type: 'error', message: new Error(t('Message_too_long')) });
chat.composer?.setText(msg);
return true;
Expand Down
Loading
Loading