diff --git a/apps/meteor/client/components/message/toolbar/MessageToolbarActionMenu.tsx b/apps/meteor/client/components/message/toolbar/MessageToolbarActionMenu.tsx
index 47f7ac67c2c17..76c71994006d2 100644
--- a/apps/meteor/client/components/message/toolbar/MessageToolbarActionMenu.tsx
+++ b/apps/meteor/client/components/message/toolbar/MessageToolbarActionMenu.tsx
@@ -50,14 +50,18 @@ const MessageToolbarActionMenu = ({ message, context, room, subscription, onChan
usePinMessageAction(message, { room, subscription }),
useStarMessageAction(message, { room }),
useUnstarMessageAction(message, { room }),
- usePermalinkAction(message, { id: 'permalink-star', context: ['starred'], order: 10 }),
- usePermalinkAction(message, { id: 'permalink-pinned', context: ['pinned'], order: 5 }),
- usePermalinkAction(message, {
- id: 'permalink',
- context: ['message', 'message-mobile', 'threads', 'federated', 'videoconf', 'videoconf-threads'],
- type: 'duplication',
- order: 5,
- }),
+ usePermalinkAction(message, { id: 'permalink-star', context: ['starred'], order: 10 }, { room }),
+ usePermalinkAction(message, { id: 'permalink-pinned', context: ['pinned'], order: 5 }, { room }),
+ usePermalinkAction(
+ message,
+ {
+ id: 'permalink',
+ context: ['message', 'message-mobile', 'threads', 'federated', 'videoconf', 'videoconf-threads'],
+ type: 'duplication',
+ order: 5,
+ },
+ { room },
+ ),
useFollowMessageAction(message, { room, context }),
useUnFollowMessageAction(message, { room, context }),
useMarkAsUnreadMessageAction(message, { room, subscription }),
diff --git a/apps/meteor/client/components/message/toolbar/items/DefaultItems.tsx b/apps/meteor/client/components/message/toolbar/items/DefaultItems.tsx
index 28d536c6be9c2..bcf15c1fdfd28 100644
--- a/apps/meteor/client/components/message/toolbar/items/DefaultItems.tsx
+++ b/apps/meteor/client/components/message/toolbar/items/DefaultItems.tsx
@@ -17,7 +17,7 @@ const DefaultItems = ({ message, room, subscription }: DefaultItemsProps) => {
-
+
>
);
};
diff --git a/apps/meteor/client/components/message/toolbar/items/MobileItems.tsx b/apps/meteor/client/components/message/toolbar/items/MobileItems.tsx
index 462f356998ea5..c39e4c144ce2f 100644
--- a/apps/meteor/client/components/message/toolbar/items/MobileItems.tsx
+++ b/apps/meteor/client/components/message/toolbar/items/MobileItems.tsx
@@ -18,7 +18,7 @@ const MobileItems = ({ message, room, subscription }: MobileItemsProps) => {
-
+
>
);
diff --git a/apps/meteor/client/components/message/toolbar/items/ThreadsItems.tsx b/apps/meteor/client/components/message/toolbar/items/ThreadsItems.tsx
index 56f05cb13d681..dea0956505302 100644
--- a/apps/meteor/client/components/message/toolbar/items/ThreadsItems.tsx
+++ b/apps/meteor/client/components/message/toolbar/items/ThreadsItems.tsx
@@ -16,7 +16,7 @@ const ThreadsItems = ({ message, room, subscription }: ThreadsItemsProps) => {
<>
-
+
>
);
diff --git a/apps/meteor/client/components/message/toolbar/items/actions/ForwardMessageAction.spec.tsx b/apps/meteor/client/components/message/toolbar/items/actions/ForwardMessageAction.spec.tsx
new file mode 100644
index 0000000000000..81873898db7ae
--- /dev/null
+++ b/apps/meteor/client/components/message/toolbar/items/actions/ForwardMessageAction.spec.tsx
@@ -0,0 +1,154 @@
+import { mockAppRoot } from '@rocket.chat/mock-providers';
+import { render, screen } from '@testing-library/react';
+import { axe } from 'jest-axe';
+
+import ForwardMessageAction from './ForwardMessageAction';
+import FakeRoomProvider from '../../../../../../tests/mocks/client/FakeRoomProvider';
+import { createFakeRoom } from '../../../../../../tests/mocks/data';
+
+// Mock the getPermaLink function
+jest.mock('../../../../../lib/getPermaLink', () => ({
+ getPermaLink: jest.fn(() => Promise.resolve(null)),
+}));
+
+jest.mock('../../../../../views/room/modals/ForwardMessageModal', () => ({
+ getPermaLink: jest.fn(() => null),
+}));
+
+const appRoot = mockAppRoot()
+ .withTranslations('en', 'core', {
+ Forward_message: 'Forward message',
+ Action_not_available_encrypted_content: 'Action not available for encrypted content',
+ })
+ .build();
+
+const createMockMessage = (overrides: any = {}) => ({
+ _id: 'message-id',
+ rid: 'room-id',
+ msg: 'Test message',
+ ts: new Date(),
+ u: { _id: 'user-id', username: 'testuser' },
+ ...overrides,
+});
+
+describe('ForwardMessageAction', () => {
+ it('should render the forward action for normal messages', () => {
+ const message = createMockMessage();
+ const room = createFakeRoom();
+
+ render(
+
+
+ ,
+ { wrapper: appRoot },
+ );
+
+ expect(screen.getByRole('button', { name: 'Forward message' })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: 'Forward message' })).not.toBeDisabled();
+ });
+
+ it('should be disabled for encrypted messages', () => {
+ const message = createMockMessage({
+ t: 'e2e',
+ e2e: 'encrypted',
+ });
+ const room = createFakeRoom();
+
+ render(
+
+
+ ,
+ { wrapper: appRoot },
+ );
+
+ const button = screen.getByRole('button', { name: 'Action not available for encrypted content' });
+ expect(button).toBeDisabled();
+ });
+
+ it('should be disabled for ABAC rooms', () => {
+ const message = createMockMessage();
+ const room = createFakeRoom({
+ // @ts-expect-error - abacAttributes is not yet implemented in IRoom type
+ abacAttributes: { someAttribute: 'value' },
+ });
+
+ render(
+
+
+ ,
+ { wrapper: appRoot },
+ );
+
+ const button = screen.getByRole('button', { name: 'Not_available_for_ABAC_enabled_rooms' });
+ expect(button).toBeDisabled();
+ });
+
+ it('should be disabled for both encrypted messages and ABAC rooms', () => {
+ const message = createMockMessage({
+ t: 'e2e',
+ e2e: 'encrypted',
+ });
+ const room = createFakeRoom({
+ // @ts-expect-error - abacAttributes is not yet implemented in IRoom type
+ abacAttributes: { someAttribute: 'value' },
+ });
+
+ render(
+
+
+ ,
+ { wrapper: appRoot },
+ );
+
+ const button = screen.getByRole('button', { name: 'Action not available for encrypted content' });
+ expect(button).toBeDisabled();
+ });
+
+ it('should have no accessibility violations for normal messages', async () => {
+ const message = createMockMessage();
+ const room = createFakeRoom();
+
+ const { container } = render(
+
+
+ ,
+ { wrapper: appRoot },
+ );
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
+
+ it('should have no accessibility violations for encrypted messages', async () => {
+ const message = createMockMessage({
+ t: 'e2e',
+ e2e: 'encrypted',
+ });
+ const room = createFakeRoom();
+
+ const { container } = render(
+
+
+ ,
+ { wrapper: appRoot },
+ );
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
+
+ it('should have no accessibility violations for ABAC rooms', async () => {
+ const message = createMockMessage();
+ const room = createFakeRoom({
+ // @ts-expect-error - abacAttributes is not yet implemented in IRoom type
+ abacAttributes: { someAttribute: 'value' },
+ });
+
+ const { container } = render(
+
+
+ ,
+ { wrapper: appRoot },
+ );
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
+});
diff --git a/apps/meteor/client/components/message/toolbar/items/actions/ForwardMessageAction.tsx b/apps/meteor/client/components/message/toolbar/items/actions/ForwardMessageAction.tsx
index 1e92189fb879a..161edabde7dc8 100644
--- a/apps/meteor/client/components/message/toolbar/items/actions/ForwardMessageAction.tsx
+++ b/apps/meteor/client/components/message/toolbar/items/actions/ForwardMessageAction.tsx
@@ -1,5 +1,7 @@
-import { type IMessage, isE2EEMessage } from '@rocket.chat/core-typings';
+import { isE2EEMessage } from '@rocket.chat/core-typings';
+import type { IRoom, IMessage } from '@rocket.chat/core-typings';
import { useSetModal } from '@rocket.chat/ui-contexts';
+import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { getPermaLink } from '../../../../../lib/getPermaLink';
@@ -8,21 +10,34 @@ import MessageToolbarItem from '../../MessageToolbarItem';
type ForwardMessageActionProps = {
message: IMessage;
+ room: IRoom;
};
-const ForwardMessageAction = ({ message }: ForwardMessageActionProps) => {
+const ForwardMessageAction = ({ message, room }: ForwardMessageActionProps) => {
const setModal = useSetModal();
const { t } = useTranslation();
const encrypted = isE2EEMessage(message);
+ // @ts-expect-error to be implemented
+ const isABACEnabled = !!room.abacAttributes;
+
+ const getTitle = useMemo(() => {
+ if (encrypted) {
+ return t('Action_not_available_encrypted_content', { action: t('Forward_message') });
+ }
+ if (isABACEnabled) {
+ return t('Not_available_for_ABAC_enabled_rooms');
+ }
+ return t('Forward_message');
+ }, [encrypted, isABACEnabled, t]);
return (
{
const permalink = await getPermaLink(message._id);
setModal(
diff --git a/apps/meteor/client/components/message/toolbar/usePermalinkAction.spec.ts b/apps/meteor/client/components/message/toolbar/usePermalinkAction.spec.ts
new file mode 100644
index 0000000000000..ef5911c2c2395
--- /dev/null
+++ b/apps/meteor/client/components/message/toolbar/usePermalinkAction.spec.ts
@@ -0,0 +1,171 @@
+import type { MessageActionContext } from '@rocket.chat/apps-engine/definition/ui';
+import type { IMessage, IRoom } from '@rocket.chat/core-typings';
+import { mockAppRoot } from '@rocket.chat/mock-providers';
+import { renderHook } from '@testing-library/react';
+
+import { usePermalinkAction } from './usePermalinkAction';
+import type { MessageActionConfig } from '../../../../app/ui-utils/client/lib/MessageAction';
+import { createFakeUser } from '../../../../tests/mocks/data';
+
+// Mock the getPermaLink function
+jest.mock('../../../lib/getPermaLink', () => ({
+ getPermaLink: jest.fn(() => Promise.resolve('https://example.com/permalink')),
+}));
+
+const user = createFakeUser({
+ _id: 'current-user-id',
+ username: 'currentuser',
+ active: true,
+ roles: ['admin'],
+ type: 'user',
+});
+
+const appRoot = mockAppRoot()
+ .withUser(user)
+ .withTranslations('en', 'core', {
+ Copy_link: 'Copy link',
+ Copied: 'Copied',
+ })
+ .build();
+
+const createMockMessage = (overrides: Partial = {}): IMessage => ({
+ _id: 'message-id',
+ rid: 'room-id',
+ msg: 'Test message',
+ ts: new Date(),
+ u: { _id: 'user-id', username: 'testuser' },
+ _updatedAt: new Date(),
+ channels: [],
+ file: { _id: 'file-id', name: 'file.txt', type: 'text/plain', size: 100, format: 'text/plain' },
+ mentions: [],
+ reactions: {},
+ starred: [],
+ ...overrides,
+});
+
+const createMockRoom = (overrides: Partial = {}): IRoom => ({
+ _id: 'room-id',
+ t: 'c' as const,
+ name: 'test-room',
+ msgs: 0,
+ u: { _id: 'user-id', username: 'testuser' },
+ usersCount: 1,
+ _updatedAt: new Date(),
+ ...overrides,
+});
+
+describe('usePermalinkAction', () => {
+ it('should be enabled for normal messages', () => {
+ const message = createMockMessage();
+ const room = createMockRoom();
+ const config = {
+ id: 'permalink',
+ context: ['message', 'message-mobile'],
+ type: 'communication',
+ order: 0,
+ } as { context: MessageActionContext[]; order: number } & Pick;
+
+ const { result } = renderHook(() => usePermalinkAction(message, config, { room }), { wrapper: appRoot });
+
+ expect(result.current).toEqual({
+ id: 'permalink',
+ icon: 'permalink',
+ label: 'Copy_link',
+ context: ['message', 'message-mobile'],
+ type: 'communication',
+ action: expect.any(Function),
+ order: 0,
+ group: 'menu',
+ disabled: false,
+ });
+ });
+ it('should be disabled for encrypted messages', () => {
+ const message = createMockMessage({
+ t: 'e2e',
+ e2e: 'done',
+ });
+ const room = createMockRoom();
+ const config = {
+ id: 'permalink',
+ context: ['message', 'message-mobile'],
+ type: 'communication',
+ order: 0,
+ } as { context: MessageActionContext[]; order: number } & Pick;
+
+ const { result } = renderHook(() => usePermalinkAction(message, config, { room }), { wrapper: appRoot });
+
+ expect(result.current).toEqual({
+ id: 'permalink',
+ icon: 'permalink',
+ label: 'Copy_link',
+ context: ['message', 'message-mobile'],
+ type: 'communication',
+ action: expect.any(Function),
+ order: 0,
+ group: 'menu',
+ disabled: true,
+ tooltip: 'Action_not_available_encrypted_content',
+ });
+ });
+
+ it('should be disabled for ABAC rooms', () => {
+ const message = createMockMessage();
+ const room = createMockRoom({
+ // @ts-expect-error to be implemented
+ abacAttributes: { someAttribute: 'value' },
+ });
+ const config = {
+ id: 'permalink',
+ context: ['message', 'message-mobile'],
+ type: 'communication',
+ order: 0,
+ } as { context: MessageActionContext[]; order: number } & Pick;
+
+ const { result } = renderHook(() => usePermalinkAction(message, config, { room }), { wrapper: appRoot });
+
+ expect(result.current).toEqual({
+ id: 'permalink',
+ icon: 'permalink',
+ label: 'Copy_link',
+ context: ['message', 'message-mobile'],
+ type: 'communication',
+ action: expect.any(Function),
+ order: 0,
+ group: 'menu',
+ disabled: true,
+ tooltip: 'Not_available_for_ABAC_enabled_rooms',
+ });
+ });
+
+ it('should be disabled for both encrypted messages and ABAC rooms', () => {
+ const message = createMockMessage({
+ t: 'e2e',
+ e2e: 'done',
+ });
+ const room = createMockRoom({
+ // @ts-expect-error to be implemented
+ abacAttributes: { someAttribute: 'value' },
+ });
+ const config = {
+ id: 'permalink',
+ context: ['message', 'message-mobile'],
+ type: 'communication',
+ order: 0,
+ } as { context: MessageActionContext[]; order: number } & Pick;
+
+ const { result } = renderHook(() => usePermalinkAction(message, config, { room }), { wrapper: appRoot });
+
+ expect(result.current).toEqual({
+ id: 'permalink',
+ icon: 'permalink',
+ label: 'Copy_link',
+ context: ['message', 'message-mobile'],
+ type: 'communication',
+ action: expect.any(Function),
+ order: 0,
+ group: 'menu',
+ disabled: true,
+ tooltip: 'Action_not_available_encrypted_content',
+ });
+ });
+});
diff --git a/apps/meteor/client/components/message/toolbar/usePermalinkAction.ts b/apps/meteor/client/components/message/toolbar/usePermalinkAction.ts
index 6ddc0a13139a2..f9b59abca1d8c 100644
--- a/apps/meteor/client/components/message/toolbar/usePermalinkAction.ts
+++ b/apps/meteor/client/components/message/toolbar/usePermalinkAction.ts
@@ -1,6 +1,7 @@
-import type { IMessage } from '@rocket.chat/core-typings';
+import type { IMessage, IRoom } from '@rocket.chat/core-typings';
import { isE2EEMessage } from '@rocket.chat/core-typings';
import { useToastMessageDispatch } from '@rocket.chat/ui-contexts';
+import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import type { MessageActionConfig, MessageActionContext } from '../../../../app/ui-utils/client/lib/MessageAction';
@@ -9,12 +10,24 @@ import { getPermaLink } from '../../../lib/getPermaLink';
export const usePermalinkAction = (
message: IMessage,
{ id, context, type, order }: { context: MessageActionContext[]; order: number } & Pick,
+ { room }: { room: IRoom },
): MessageActionConfig | null => {
const { t } = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
+ // @ts-expect-error - to be implemented
+ const isABACEnabled = !!room.abacAttributes;
const encrypted = isE2EEMessage(message);
+ const tooltip = useMemo(() => {
+ if (encrypted) {
+ return t('Action_not_available_encrypted_content', { action: t('Copy_link') });
+ }
+ if (isABACEnabled) {
+ return t('Not_available_for_ABAC_enabled_rooms');
+ }
+ return null;
+ }, [encrypted, isABACEnabled, t]);
return {
id,
@@ -33,7 +46,7 @@ export const usePermalinkAction = (
},
order,
group: 'menu',
- disabled: encrypted,
- ...(encrypted && { tooltip: t('Action_not_available_encrypted_content', { action: t('Copy_link') }) }),
+ disabled: encrypted || isABACEnabled,
+ ...(tooltip && { tooltip }),
};
};
diff --git a/apps/meteor/client/components/message/toolbar/useReplyInDMAction.ts b/apps/meteor/client/components/message/toolbar/useReplyInDMAction.ts
index 98532cc6a1ddd..33536db3deefc 100644
--- a/apps/meteor/client/components/message/toolbar/useReplyInDMAction.ts
+++ b/apps/meteor/client/components/message/toolbar/useReplyInDMAction.ts
@@ -16,6 +16,8 @@ export const useReplyInDMAction = (
const user = useUser();
const router = useRouter();
const encrypted = isE2EEMessage(message);
+ // @ts-expect-error - abacAttributes is not yet implemented in IRoom type
+ const isABACEnabled = !!room.abacAttributes;
const canCreateDM = usePermission('create-d');
const isLayoutEmbedded = useEmbeddedLayout();
const { t } = useTranslation();
@@ -37,6 +39,16 @@ export const useReplyInDMAction = (
);
const dmSubs = Subscriptions.use(useShallow((state) => state.find(subsPredicate)));
+ const tooltip = useMemo(() => {
+ if (encrypted) {
+ return t('Action_not_available_encrypted_content', { action: t('Reply_in_direct_message') });
+ }
+ if (isABACEnabled) {
+ return t('Not_available_for_ABAC_enabled_rooms');
+ }
+ return null;
+ }, [encrypted, isABACEnabled, t]);
+
const canReplyInDM = useMemo(() => {
if (!subscription || room.t === 'd' || room.t === 'l' || isLayoutEmbedded) {
return false;
@@ -71,7 +83,7 @@ export const useReplyInDMAction = (
},
order: 0,
group: 'menu',
- disabled: encrypted,
- ...(encrypted && { tooltip: t('Action_not_available_encrypted_content', { action: t('Reply_in_direct_message') }) }),
+ disabled: encrypted || isABACEnabled,
+ ...(tooltip && { tooltip }),
};
};
diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json
index b6115227e137a..dcdbe01b31e34 100644
--- a/packages/i18n/src/locales/en.i18n.json
+++ b/packages/i18n/src/locales/en.i18n.json
@@ -3739,6 +3739,7 @@
"Nonprofit": "Nonprofit",
"Normal": "Normal",
"Not_Available": "Not Available",
+ "Not_available_for_ABAC_enabled_rooms": "Not available in ABAC-managed rooms",
"Not_Following": "Not Following",
"Not_Imported_Messages_Title": "The following messages were not imported successfully",
"Not_Visible_To_Workspace": "Not visible to workspace",