Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: new E2EE composer hint #33283

Merged
merged 11 commits into from
Oct 14, 2024
11 changes: 11 additions & 0 deletions .changeset/many-files-turn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@rocket.chat/meteor": minor
"@rocket.chat/i18n": patch
"@rocket.chat/ui-composer": minor
---

Adds a warning to inform users they are about to send unencrypted messages in an E2E Encrypted room if they have the `Unencrypted messages in encrypted rooms` setting enabled.




Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const ComposerContainer = ({ children, ...props }: ComposerMessageProps): ReactE
}

if (isFederation) {
return <ComposerFederation room={room} {...props} />;
return <ComposerFederation {...props} />;
}

if (isAnonymous) {
Expand All @@ -68,7 +68,7 @@ const ComposerContainer = ({ children, ...props }: ComposerMessageProps): ReactE
return (
<>
{children}
<ComposerMessage readOnly={room.ro} {...props} />
<ComposerMessage {...props} />
</>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { IRoom } from '@rocket.chat/core-typings';
import { useSetting } from '@rocket.chat/ui-contexts';
import React from 'react';
import type { ReactElement } from 'react';
Expand All @@ -9,7 +8,7 @@ import ComposerMessage from '../ComposerMessage';
import ComposerFederationDisabled from './ComposerFederationDisabled';
import ComposerFederationJoinRoomDisabled from './ComposerFederationJoinRoomDisabled';

const ComposerFederation = ({ room, subscription, children, ...props }: ComposerMessageProps & { room: IRoom }): ReactElement => {
const ComposerFederation = ({ subscription, children, ...props }: ComposerMessageProps): ReactElement => {
const federationEnabled = useSetting('Federation_Matrix_enabled') === true;
const federationModuleEnabled = useHasLicenseModule('federation') === true;

Expand All @@ -24,7 +23,7 @@ const ComposerFederation = ({ room, subscription, children, ...props }: Composer
return (
<>
{children}
<ComposerMessage readOnly={room.ro} {...props} />
<ComposerMessage {...props} />
</>
);
};
Expand Down
5 changes: 2 additions & 3 deletions apps/meteor/client/views/room/composer/ComposerMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ export type ComposerMessageProps = {
tmid?: IMessage['_id'];
children?: ReactNode;
subscription?: ISubscription;
readOnly?: boolean;
tshow?: boolean;
previewUrls?: string[];
onResize?: () => void;
Expand All @@ -25,7 +24,7 @@ export type ComposerMessageProps = {
onUploadFiles?: (files: readonly File[]) => void;
};

const ComposerMessage = ({ tmid, readOnly, onSend, ...props }: ComposerMessageProps): ReactElement => {
const ComposerMessage = ({ tmid, onSend, ...props }: ComposerMessageProps): ReactElement => {
const chat = useChat();
const room = useRoom();
const dispatchToastMessage = useToastMessageDispatch();
Expand Down Expand Up @@ -89,7 +88,7 @@ const ComposerMessage = ({ tmid, readOnly, onSend, ...props }: ComposerMessagePr
return <ComposerSkeleton />;
}

return <MessageBox readOnly={readOnly ?? false} key={room._id} tmid={tmid} {...composerProps} showFormattingTips={true} {...props} />;
return <MessageBox key={room._id} tmid={tmid} {...composerProps} showFormattingTips={true} {...props} />;
};

export default memo(ComposerMessage);
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,12 @@ import {
MessageComposerToolbar,
MessageComposerActionsDivider,
MessageComposerToolbarSubmit,
MessageComposerHint,
MessageComposerButton,
} from '@rocket.chat/ui-composer';
import { useTranslation, useUserPreference, useLayout, useSetting } from '@rocket.chat/ui-contexts';
import { useMutation } from '@tanstack/react-query';
import type { ReactElement, MouseEventHandler, FormEvent, ClipboardEventHandler, MouseEvent } from 'react';
import React, { memo, useRef, useReducer, useCallback } from 'react';
import { Trans } from 'react-i18next';
import { useSubscription } from 'use-subscription';

import { createComposerAPI } from '../../../../../app/ui-message/client/messageBox/createComposerAPI';
Expand All @@ -42,6 +40,7 @@ import { useEnablePopupPreview } from '../hooks/useEnablePopupPreview';
import { useMessageComposerMergedRefs } from '../hooks/useMessageComposerMergedRefs';
import MessageBoxActionsToolbar from './MessageBoxActionsToolbar';
import MessageBoxFormattingToolbar from './MessageBoxFormattingToolbar';
import MessageBoxHint from './MessageBoxHint';
import MessageBoxReplies from './MessageBoxReplies';
import { useMessageBoxAutoFocus } from './hooks/useMessageBoxAutoFocus';
import { useMessageBoxPlaceholder } from './hooks/useMessageBoxPlaceholder';
Expand Down Expand Up @@ -79,7 +78,6 @@ const getEmptyArray = () => a;

type MessageBoxProps = {
tmid?: IMessage['_id'];
readOnly: boolean;
onSend?: (params: { value: string; tshow?: boolean; previewUrls?: string[]; isSlashCommandAllowed?: boolean }) => Promise<void>;
onJoin?: () => Promise<void>;
onResize?: () => void;
Expand All @@ -104,7 +102,6 @@ const MessageBox = ({
onUploadFiles,
onEscape,
onTyping,
readOnly,
tshow,
previewUrls,
}: MessageBoxProps): ReactElement => {
Expand Down Expand Up @@ -385,21 +382,12 @@ const MessageBox = ({
suspended={suspended}
/>
)}
{isEditing && (
<MessageComposerHint
icon='pencil'
helperText={
!isMobile ? (
<Trans i18nKey='Editing_message_hint'>
<strong>esc</strong> to cancel · <strong>enter</strong> to save
</Trans>
) : undefined
}
>
{t('Editing_message')}
</MessageComposerHint>
)}
{readOnly && !isEditing && <MessageComposerHint>{t('This_room_is_read_only')}</MessageComposerHint>}
<MessageBoxHint
isEditing={isEditing}
e2eEnabled={e2eEnabled}
unencryptedMessagesAllowed={unencryptedMessagesAllowed}
isMobile={isMobile}
/>
{isRecordingVideo && <VideoMessageRecorder reference={messageComposerRef} rid={room._id} tmid={tmid} />}
<MessageComposer ref={messageComposerRef} variant={isEditing ? 'editing' : undefined}>
{isRecordingAudio && <AudioMessageRecorder rid={room._id} isMicrophoneDenied={isMicrophoneDenied} />}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { mockAppRoot } from '@rocket.chat/mock-providers';
import { render, screen } from '@testing-library/react';
import React from 'react';

import { E2ERoomState } from '../../../../../app/e2e/client/E2ERoomState';
import { useRoom } from '../../contexts/RoomContext';
import { useE2EERoomState } from '../../hooks/useE2EERoomState';
import MessageBoxHint from './MessageBoxHint';

jest.mock('../../hooks/useE2EERoomState', () => ({
useE2EERoomState: jest.fn(),
}));

jest.mock('../../contexts/RoomContext', () => ({
useRoom: jest.fn(),
}));

const renderOptions = {
wrapper: mockAppRoot()
.withTranslations('en', 'core', {
Editing_message: 'Editing message',
Editing_message_hint: '<strong>esc</strong> to cancel · <strong>enter</strong> to save',
This_room_is_read_only: 'This room is read only',
E2EE_Composer_Unencrypted_Message: "You're sending an unencrypted message",
})
.build(),
legacyRoot: true,
};

describe('MessageBoxHint', () => {
beforeEach(() => {
(useRoom as jest.Mock).mockReturnValue({ _id: 'roomId', ro: false });
(useE2EERoomState as jest.Mock).mockReturnValue(E2ERoomState.WAITING_KEYS);
});

describe('Editing message', () => {
it('renders hint text when isEditing is true', () => {
render(<MessageBoxHint isEditing={true} isMobile={false} />, renderOptions);
expect(screen.getByText('Editing message')).toBeInTheDocument();
expect(screen.getByText('Editing message')).toBeInTheDocument();
});

it('renders helpText when isEditing is true and it is not mobile', () => {
render(<MessageBoxHint isEditing={true} isMobile={false} />, renderOptions);
expect(screen.getByText(/to save/)).toBeInTheDocument();
});

it('renders hint without helpText when isEditing is true and it is mobile', () => {
render(<MessageBoxHint isEditing={true} isMobile={true} />, renderOptions);
expect(screen.queryByText(/to save/)).not.toBeInTheDocument();
});
});

describe('Read only', () => {
beforeEach(() => {
(useRoom as jest.Mock).mockReturnValue({ _id: 'roomId', ro: true });
});

it('renders hint text when Read only is true', () => {
render(<MessageBoxHint />, renderOptions);
expect(screen.getByText('This room is read only')).toBeInTheDocument();
});
});

describe('Unencrypted message', () => {
it('renders hint text when E2EE room with unencrypted messages is true', () => {
render(<MessageBoxHint e2eEnabled={true} unencryptedMessagesAllowed={true} />, renderOptions);
expect(screen.getByText("You're sending an unencrypted message")).toBeInTheDocument();
});

it('renders "Read only" hint text when E2EE room with unencrypted messages is true', () => {
(useRoom as jest.Mock).mockReturnValue({ _id: 'roomId', ro: true });

render(<MessageBoxHint e2eEnabled={true} unencryptedMessagesAllowed={true} />, renderOptions);
expect(screen.getByText('This room is read only')).toBeInTheDocument();
});

it('renders "Editing message" hint text when isEditing is truem, E2EE is enabled and unencrypted messages is true', () => {
(useRoom as jest.Mock).mockReturnValue({ _id: 'roomId', ro: true });

render(<MessageBoxHint isEditing={true} e2eEnabled={true} unencryptedMessagesAllowed={true} />, renderOptions);
expect(screen.getByText('Editing message')).toBeInTheDocument();
});

it('does not renders hint text when E2ERoomState is READY', () => {
(useE2EERoomState as jest.Mock).mockReturnValue(E2ERoomState.READY);

render(<MessageBoxHint e2eEnabled={true} unencryptedMessagesAllowed={true} />, renderOptions);
expect(screen.queryByText("You're sending an unencrypted message")).not.toBeInTheDocument();
});

it('does not renders hint text when E2ERoomState is DISABLED', () => {
(useE2EERoomState as jest.Mock).mockReturnValue(E2ERoomState.DISABLED);

render(<MessageBoxHint e2eEnabled={true} unencryptedMessagesAllowed={true} />, renderOptions);
expect(screen.queryByText("You're sending an unencrypted message")).not.toBeInTheDocument();
});

it('does not renders hint text when unencrypted messages is true and E2EE is disabled', () => {
render(<MessageBoxHint e2eEnabled={false} unencryptedMessagesAllowed={true} />, renderOptions);
expect(screen.queryByText("You're sending an unencrypted message")).not.toBeInTheDocument();
});

it('does not renders hint text when unencrypted messages is false and E2EE is enabled', () => {
render(<MessageBoxHint e2eEnabled={true} unencryptedMessagesAllowed={false} />, renderOptions);
expect(screen.queryByText("You're sending an unencrypted message")).not.toBeInTheDocument();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { MessageComposerHint } from '@rocket.chat/ui-composer';
import type { ReactElement } from 'react';
import React, { memo } from 'react';
import { useTranslation, Trans } from 'react-i18next';

import { E2ERoomState } from '../../../../../app/e2e/client/E2ERoomState';
import { useRoom } from '../../contexts/RoomContext';
import { useE2EERoomState } from '../../hooks/useE2EERoomState';

type MessageBoxHintProps = {
isEditing?: boolean;
e2eEnabled?: boolean;
unencryptedMessagesAllowed?: boolean;
isMobile?: boolean;
};

const MessageBoxHint = ({ isEditing, e2eEnabled, unencryptedMessagesAllowed, isMobile }: MessageBoxHintProps): ReactElement | null => {
const room = useRoom();
const isReadOnly = room?.ro || false;
const { t } = useTranslation();

const e2eRoomState = useE2EERoomState(room._id);

const isUnencryptedHintVisible =
e2eEnabled &&
unencryptedMessagesAllowed &&
e2eRoomState &&
e2eRoomState !== E2ERoomState.READY &&
e2eRoomState !== E2ERoomState.DISABLED &&
!isEditing &&
!isReadOnly;

if (!isEditing && !isUnencryptedHintVisible && !isReadOnly) {
return null;
}

const renderHintText = (): string => {
if (isEditing) {
return t('Editing_message');
}
if (isReadOnly) {
return t('This_room_is_read_only');
}
if (isUnencryptedHintVisible) {
return t('E2EE_Composer_Unencrypted_Message');
}
return '';
};

return (
<MessageComposerHint
icon={isEditing ? 'pencil' : undefined}
helperText={isEditing && !isMobile ? <Trans i18nKey='Editing_message_hint' /> : undefined}
>
{renderHintText()}
</MessageComposerHint>
);
};

export default memo(MessageBoxHint);
1 change: 1 addition & 0 deletions packages/i18n/src/locales/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -1805,6 +1805,7 @@
"E2E_Encryption_enabled_for_room": "End-to-end encryption enabled for #{{roomName}}",
"E2E_Encryption_disabled_for_room": "End-to-end encryption disabled for #{{roomName}}",
"E2EE_not_available_OTR": "This room has OTR enabled, E2E encryption cannot work with OTR.",
"E2EE_Composer_Unencrypted_Message": "You're sending an unencrypted message",
"Markdown_Parser": "Markdown Parser",
"Markdown_SupportSchemesForLink": "Markdown Support Schemes for Link",
"E2E Encryption_Description": "Keep conversations private, ensuring only the sender and intended recipients are able to read them.",
Expand Down
Loading
Loading