diff --git a/.changeset/hungry-games-smoke.md b/.changeset/hungry-games-smoke.md
new file mode 100644
index 0000000000000..350c33091aaaf
--- /dev/null
+++ b/.changeset/hungry-games-smoke.md
@@ -0,0 +1,5 @@
+---
+'@rocket.chat/meteor': patch
+---
+
+Blocks edit message action for Off-The-Record (OTR) messages.
diff --git a/apps/meteor/app/ui/client/lib/ChatMessages.ts b/apps/meteor/app/ui/client/lib/ChatMessages.ts
index 3745864061f42..a675c413fe18f 100644
--- a/apps/meteor/app/ui/client/lib/ChatMessages.ts
+++ b/apps/meteor/app/ui/client/lib/ChatMessages.ts
@@ -64,7 +64,7 @@ export class ChatMessages implements ChatAPI {
}
if (!this.currentEditing) {
- let lastMessage = await this.data.findLastOwnMessage();
+ let lastMessage = await this.data.findPreviousOwnMessage();
// Videoconf messages should not be edited
if (lastMessage && isVideoConfMessage(lastMessage)) {
diff --git a/apps/meteor/client/components/message/toolbar/useEditMessageAction.ts b/apps/meteor/client/components/message/toolbar/useEditMessageAction.ts
index 92df4f7cea35a..c19a2c7b8f5ec 100644
--- a/apps/meteor/client/components/message/toolbar/useEditMessageAction.ts
+++ b/apps/meteor/client/components/message/toolbar/useEditMessageAction.ts
@@ -1,4 +1,4 @@
-import { isRoomFederated } from '@rocket.chat/core-typings';
+import { isOTRAckMessage, isOTRMessage, isRoomFederated } from '@rocket.chat/core-typings';
import type { IRoom, IMessage, ISubscription } from '@rocket.chat/core-typings';
import { usePermission, useSetting, useUser } from '@rocket.chat/ui-contexts';
import moment from 'moment';
@@ -26,6 +26,10 @@ export const useEditMessageAction = (
return message.u._id === user?._id;
}
+ if (isOTRMessage(message) || isOTRAckMessage(message)) {
+ return false;
+ }
+
const editOwn = message.u && message.u._id === user?._id;
if (!canEditMessage && (!isEditAllowed || !editOwn)) {
return false;
diff --git a/apps/meteor/client/components/message/variants/RoomMessage.tsx b/apps/meteor/client/components/message/variants/RoomMessage.tsx
index 3832aa216163d..a939487015af0 100644
--- a/apps/meteor/client/components/message/variants/RoomMessage.tsx
+++ b/apps/meteor/client/components/message/variants/RoomMessage.tsx
@@ -1,4 +1,4 @@
-import type { IMessage } from '@rocket.chat/core-typings';
+import { type IMessage, isOTRAckMessage, isOTRMessage } from '@rocket.chat/core-typings';
import { Message, MessageLeftContainer, MessageContainer, CheckBox } from '@rocket.chat/fuselage';
import { useToggle } from '@rocket.chat/fuselage-hooks';
import { MessageAvatar } from '@rocket.chat/ui-avatar';
@@ -55,13 +55,12 @@ const RoomMessage = ({
const { openUserCard, triggerProps } = useUserCard();
const selecting = useIsSelecting();
- const isOTRMessage = message.t === 'otr' || message.t === 'otr-ack';
+ const isOTRMsg = isOTRMessage(message) || isOTRAckMessage(message);
const toggleSelected = useToggleSelect(message._id);
- const selected = useIsSelectedMessage(message._id, isOTRMessage);
+ const selected = useIsSelectedMessage(message._id, isOTRMsg);
useCountSelected();
-
const messageRef = useJumpToMessage(message._id);
return (
@@ -69,10 +68,10 @@ const RoomMessage = ({
ref={messageRef}
id={message._id}
role='listitem'
- aria-roledescription={t('message')}
+ aria-roledescription={isOTRMsg ? t('OTR_message') : t('message')}
tabIndex={0}
aria-labelledby={`${message._id}-displayName ${message._id}-time ${message._id}-content ${message._id}-read-status`}
- onClick={selecting && !isOTRMessage ? toggleSelected : undefined}
+ onClick={selecting && !isOTRMsg ? toggleSelected : undefined}
isSelected={selected}
isEditing={editing}
isPending={message.temp}
@@ -101,7 +100,7 @@ const RoomMessage = ({
{...triggerProps}
/>
)}
- {selecting && }
+ {selecting && }
{sequential && }
diff --git a/apps/meteor/client/components/message/variants/ThreadMessagePreview.tsx b/apps/meteor/client/components/message/variants/ThreadMessagePreview.tsx
index 9b905fd085c6b..af0aadefbf308 100644
--- a/apps/meteor/client/components/message/variants/ThreadMessagePreview.tsx
+++ b/apps/meteor/client/components/message/variants/ThreadMessagePreview.tsx
@@ -1,4 +1,4 @@
-import type { IThreadMessage } from '@rocket.chat/core-typings';
+import { isOTRAckMessage, isOTRMessage, type IThreadMessage } from '@rocket.chat/core-typings';
import {
Skeleton,
ThreadMessage,
@@ -45,10 +45,10 @@ const ThreadMessagePreview = ({ message, showUserAvatar, sequential, ...props }:
const { t } = useTranslation();
const isSelecting = useIsSelecting();
- const isOTRMessage = message.t === 'otr' || message.t === 'otr-ack';
+ const isOTRMsg = isOTRMessage(message) || isOTRAckMessage(message);
const toggleSelected = useToggleSelect(message._id);
- const isSelected = useIsSelectedMessage(message._id, isOTRMessage);
+ const isSelected = useIsSelectedMessage(message._id, isOTRMsg);
useCountSelected();
const messageType = parentMessage.isSuccess ? MessageTypes.getType(parentMessage.data) : null;
@@ -67,7 +67,7 @@ const ThreadMessagePreview = ({ message, showUserAvatar, sequential, ...props }:
return goToThread({ rid: message.rid, tmid: message.tmid, msg: message._id });
}
- if (isOTRMessage) {
+ if (isOTRMsg) {
return;
}
@@ -77,7 +77,7 @@ const ThreadMessagePreview = ({ message, showUserAvatar, sequential, ...props }:
return (
e.code === 'Enter' && handleThreadClick()}
@@ -123,7 +123,7 @@ const ThreadMessagePreview = ({ message, showUserAvatar, sequential, ...props }:
size='x18'
/>
)}
- {isSelecting && }
+ {isSelecting && }
diff --git a/apps/meteor/client/lib/chats/ChatAPI.ts b/apps/meteor/client/lib/chats/ChatAPI.ts
index 058870cfbd286..f41e9366ceee9 100644
--- a/apps/meteor/client/lib/chats/ChatAPI.ts
+++ b/apps/meteor/client/lib/chats/ChatAPI.ts
@@ -68,9 +68,7 @@ export type DataAPI = {
getMessageByID(mid: IMessage['_id']): Promise;
findLastMessage(): Promise;
getLastMessage(): Promise;
- findLastOwnMessage(): Promise;
- getLastOwnMessage(): Promise;
- findPreviousOwnMessage(message: IMessage): Promise;
+ findPreviousOwnMessage(message?: IMessage): Promise;
getPreviousOwnMessage(message: IMessage): Promise;
findNextOwnMessage(message: IMessage): Promise;
getNextOwnMessage(message: IMessage): Promise;
diff --git a/apps/meteor/client/lib/chats/data.ts b/apps/meteor/client/lib/chats/data.ts
index ffb5259983d7b..260db8da3f303 100644
--- a/apps/meteor/client/lib/chats/data.ts
+++ b/apps/meteor/client/lib/chats/data.ts
@@ -1,4 +1,11 @@
-import type { IEditedMessage, IMessage, IRoom, ISubscription } from '@rocket.chat/core-typings';
+import {
+ isOTRAckMessage,
+ isOTRMessage,
+ type IEditedMessage,
+ type IMessage,
+ type IRoom,
+ type ISubscription,
+} from '@rocket.chat/core-typings';
import { Random } from '@rocket.chat/random';
import moment from 'moment';
@@ -57,34 +64,15 @@ export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage
return message;
};
- const findLastOwnMessage = async (): Promise => {
- const uid = Meteor.userId();
-
- if (!uid) {
- return undefined;
- }
-
- return Messages.findOne(
- { rid, 'tmid': tmid ?? { $exists: false }, 'u._id': uid, '_hidden': { $ne: true } },
- { sort: { ts: -1 }, reactive: false },
- );
- };
-
- const getLastOwnMessage = async (): Promise => {
- const message = await findLastOwnMessage();
-
- if (!message) {
- throw new Error('Message not found');
- }
-
- return message;
- };
-
const canUpdateMessage = async (message: IMessage): Promise => {
if (MessageTypes.isSystemMessage(message)) {
return false;
}
+ if (isOTRMessage(message) || isOTRAckMessage(message)) {
+ return false;
+ }
+
const canEditMessage = hasAtLeastOnePermission('edit-message', message.rid);
const editAllowed = (settings.get('Message_AllowEditing') as boolean | undefined) ?? false;
const editOwn = message?.u && message.u._id === Meteor.userId();
@@ -104,7 +92,7 @@ export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage
return true;
};
- const findPreviousOwnMessage = async (message: IMessage): Promise => {
+ const findPreviousOwnMessage = async (message?: IMessage): Promise => {
const uid = Meteor.userId();
if (!uid) {
@@ -112,7 +100,7 @@ export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage
}
const msg = Messages.findOne(
- { rid, 'tmid': tmid ?? { $exists: false }, 'u._id': uid, '_hidden': { $ne: true }, 'ts': { $lt: message.ts } },
+ { rid, 'tmid': tmid ?? { $exists: false }, 'u._id': uid, '_hidden': { $ne: true }, 'ts': { ...(message && { $lt: message.ts }) } },
{ sort: { ts: -1 }, reactive: false },
);
@@ -308,8 +296,6 @@ export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage
getMessageByID,
findLastMessage,
getLastMessage,
- findLastOwnMessage,
- getLastOwnMessage,
findPreviousOwnMessage,
getPreviousOwnMessage,
findNextOwnMessage,
diff --git a/apps/meteor/client/lib/chats/flows/sendMessage.ts b/apps/meteor/client/lib/chats/flows/sendMessage.ts
index 3d372900fb299..0fb16296a8582 100644
--- a/apps/meteor/client/lib/chats/flows/sendMessage.ts
+++ b/apps/meteor/client/lib/chats/flows/sendMessage.ts
@@ -23,10 +23,11 @@ const process = async (chat: ChatAPI, message: IMessage, previewUrls?: string[],
return;
}
- message = (await onClientBeforeSendMessage(message)) as IMessage;
+ message = (await onClientBeforeSendMessage({ ...message, isEditing: !!chat.currentEditing })) as IMessage & { isEditing?: boolean };
// e2e should be a client property only
delete message.e2e;
+ delete (message as IMessage & { isEditing?: boolean }).isEditing;
if (await processMessageEditing(chat, message, previewUrls)) {
return;
diff --git a/apps/meteor/client/lib/onClientBeforeSendMessage.ts b/apps/meteor/client/lib/onClientBeforeSendMessage.ts
index e2e0404b9b301..e312b4f184f37 100644
--- a/apps/meteor/client/lib/onClientBeforeSendMessage.ts
+++ b/apps/meteor/client/lib/onClientBeforeSendMessage.ts
@@ -2,4 +2,4 @@ import type { AtLeast, IMessage } from '@rocket.chat/core-typings';
import { createAsyncTransformChain } from '../../lib/transforms';
-export const onClientBeforeSendMessage = createAsyncTransformChain>();
+export const onClientBeforeSendMessage = createAsyncTransformChain & { isEditing?: boolean }>();
diff --git a/apps/meteor/client/views/root/hooks/useOTRMessaging.ts b/apps/meteor/client/views/root/hooks/useOTRMessaging.ts
index b3195326fe8c6..8a6e778d15a25 100644
--- a/apps/meteor/client/views/root/hooks/useOTRMessaging.ts
+++ b/apps/meteor/client/views/root/hooks/useOTRMessaging.ts
@@ -24,14 +24,18 @@ export const useOTRMessaging = (uid: string) => {
};
const handleBeforeSendMessage = async (
- message: AtLeast,
+ message: AtLeast & { isEditing?: boolean },
): Promise> => {
if (!uid) {
return message;
}
- const otrRoom = OTR.getInstanceByRoomId(uid, message.rid);
+ if (message.isEditing) {
+ return (({ isEditing: _isEditing, ...rest }) => rest)(message);
+ }
+ delete message.isEditing;
+ const otrRoom = OTR.getInstanceByRoomId(uid, message.rid);
if (otrRoom && otrRoom.getState() === OtrRoomState.ESTABLISHED) {
const msg = await otrRoom.encrypt(message);
return { ...message, msg, t: 'otr' };
diff --git a/apps/meteor/tests/e2e/otr.spec.ts b/apps/meteor/tests/e2e/otr.spec.ts
index b6ff4789e0c27..e909673a4fb83 100644
--- a/apps/meteor/tests/e2e/otr.spec.ts
+++ b/apps/meteor/tests/e2e/otr.spec.ts
@@ -38,9 +38,72 @@ test.describe.serial('OTR', () => {
await poHomeChannel.content.sendMessage('hello OTR');
await poHomeChannel.tabs.kebab.click({ force: true });
await poHomeChannel.tabs.btnExportMessages.click();
- await poHomeChannel.content.getMessageByText('hello OTR').click();
+ await poHomeChannel.content.getOTRMessageByText('hello OTR').click();
await expect(poHomeChannel.content.btnClearSelection).toBeDisabled();
await user1Page.close();
});
+
+ test('should not allow edit OTR messages', async ({ browser, page }) => {
+ const user1Page = await browser.newPage({ storageState: Users.user1.state });
+ const user1Channel = new HomeChannel(user1Page);
+
+ await test.step('log in user1', async () => {
+ await user1Page.goto(`/direct/${Users.admin.data.username}`);
+ await user1Channel.content.waitForChannel();
+ });
+
+ await test.step('send normal messages', async () => {
+ await poHomeChannel.sidenav.openChat(Users.user1.data.username);
+ await poHomeChannel.content.sendMessage('Normal message');
+ await user1Channel.content.sendMessage('Normal message');
+ });
+
+ await test.step('invite OTR with user1', async () => {
+ await poHomeChannel.tabs.kebab.click({ force: true });
+ await poHomeChannel.tabs.btnEnableOTR.click({ force: true });
+ await poHomeChannel.tabs.otr.btnStartOTR.click();
+ });
+
+ await test.step('accept handshake with user1', async () => {
+ await user1Channel.tabs.otr.btnAcceptOTR.click();
+ });
+
+ await test.step('send OTR messages', async () => {
+ await poHomeChannel.content.sendMessage('hello user1');
+ await expect(poHomeChannel.content.lastUserMessage).toHaveAttribute('aria-roledescription', 'OTR message');
+
+ await user1Channel.content.sendMessage('hello admin');
+ await expect(user1Channel.content.lastUserMessage).toHaveAttribute('aria-roledescription', 'OTR message');
+ });
+
+ await test.step('not show edit message action', async () => {
+ await poHomeChannel.content.openLastMessageMenu();
+ await expect(poHomeChannel.content.btnOptionEditMessage).not.toBeVisible();
+
+ await user1Channel.content.openLastMessageMenu();
+ await expect(user1Channel.content.btnOptionEditMessage).not.toBeVisible();
+
+ await page.keyboard.press('Escape');
+ await user1Page.keyboard.press('Escape');
+ });
+
+ await test.step('keyboard edit action when using up arrow key should show normal messages', async () => {
+ await poHomeChannel.composer.focus();
+ await page.keyboard.press('ArrowUp');
+ await expect(poHomeChannel.composer).toHaveValue('Normal message');
+ await poHomeChannel.composer.fill('Normal message edited');
+ await page.keyboard.press('Enter');
+ await expect(poHomeChannel.content.nthMessage(0)).toContainText('Normal message edited');
+
+ await user1Channel.composer.focus();
+ await user1Page.keyboard.press('ArrowUp');
+ await expect(user1Channel.composer).toHaveValue('Normal message');
+ await user1Channel.composer.fill('Normal message edited');
+ await user1Page.keyboard.press('Enter');
+ await expect(user1Channel.content.nthMessage(0)).toContainText('Normal message edited');
+ });
+
+ await user1Page.close();
+ });
});
diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts
index 63e03a71e38aa..5fcb7a044fe87 100644
--- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts
+++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts
@@ -197,7 +197,7 @@ export class HomeContent {
}
get btnOptionEditMessage(): Locator {
- return this.page.locator('[data-qa-id="edit-message"]');
+ return this.page.locator('role=menu[name="More"] >> role=menuitem[name="Edit"]');
}
get btnOptionDeleteMessage(): Locator {
@@ -446,6 +446,10 @@ export class HomeContent {
return this.page.locator('[role="listitem"][aria-roledescription="message"]', { hasText: text });
}
+ getOTRMessageByText(text: string): Locator {
+ return this.page.locator('[role="listitem"][aria-roledescription="OTR message"]', { hasText: text });
+ }
+
getMessageById(id: string): Locator {
return this.page.locator(`[data-qa-type="message"][id="${id}"]`);
}
diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json
index 2173abe637a60..24b0fc2214c03 100644
--- a/packages/i18n/src/locales/en.i18n.json
+++ b/packages/i18n/src/locales/en.i18n.json
@@ -3689,8 +3689,9 @@
"OTR_Description": "Off-the-record chats are secure, private and disappear once ended.",
"OTR_Enable_Description": "Enable option to use off-the-record (OTR) messages in direct messages between 2 users. OTR messages are not recorded on the server and exchanged directly and encrypted between the 2 users.",
"OTR_Session_ended_other_user_went_offline": "OTR Session has ended. User {{username}} went offline",
+ "OTR_thread_message_preview": "OTR thread message preview",
"OTR_is_only_available_when_both_users_are_online": "OTR is only available when both users are online",
- "OTR_message": "OTR Message",
+ "OTR_message": "OTR message",
"OTR_messages_cannot_be_exported": "OTR messages cannot be exported",
"OTR_not_available": "OTR not available",
"OTR_not_available_e2ee": "This room has E2E encryption enabled, OTR cannot work with encrypted messages.",
@@ -6650,6 +6651,7 @@
"this_app_is_included_with_subscription": "This app is included with {{bundleName}} plans",
"thread": "thread",
"thread_message": "thread message",
+ "thread_message_preview": "thread message preview",
"threads_counter_one": "{{count}} unread threaded message",
"threads_counter_other": "{{count}} unread threaded messages",
"to_see_more_details_on_how_to_integrate": "to see more details on how to integrate.",
@@ -6831,4 +6833,4 @@
"__usernames__joined": "{{usernames}} joined",
"__usersCount__joined": "{{count}} joined",
"__usersCount__people_will_be_invited": "{{usersCount}} people will be invited"
-}
\ No newline at end of file
+}