diff --git a/client/lib/isE2EEMessage.ts b/client/lib/isE2EEMessage.ts new file mode 100644 index 000000000000..906e3f0da08f --- /dev/null +++ b/client/lib/isE2EEMessage.ts @@ -0,0 +1,3 @@ +import { IMessage } from '../../definition/IMessage'; + +export const isE2EEMessage = (message: IMessage): boolean => message.t === 'e2e'; diff --git a/client/views/room/MessageList/components/Message.tsx b/client/views/room/MessageList/components/Message.tsx index 8b5012de8edf..550b95b82cde 100644 --- a/client/views/room/MessageList/components/Message.tsx +++ b/client/views/room/MessageList/components/Message.tsx @@ -1,21 +1,14 @@ /* eslint-disable complexity */ -import { css } from '@rocket.chat/css-in-js'; import { - Box, Message as MessageTemplate, MessageBody, MessageContainer, MessageHeader, MessageLeftContainer, MessageName, - MessageRole, - MessageRoles, MessageTimestamp, MessageUsername, - MessageReactions, MessageStatusPrivateIndicator, - MessageReactionAction, - Icon, } from '@rocket.chat/fuselage'; import React, { FC, memo } from 'react'; @@ -28,32 +21,27 @@ import Broadcast from '../../../../components/Message/Metrics/Broadcast'; import Discussion from '../../../../components/Message/Metrics/Discussion'; import Thread from '../../../../components/Message/Metrics/Thread'; import UserAvatar from '../../../../components/avatar/UserAvatar'; -import { useSetting } from '../../../../contexts/SettingsContext'; import { TranslationKey, useTranslation } from '../../../../contexts/TranslationContext'; import { useUserId } from '../../../../contexts/UserContext'; import { useUserData } from '../../../../hooks/useUserData'; +import { getUserDisplayName } from '../../../../lib/getUserDisplayName'; +import { isE2EEMessage } from '../../../../lib/isE2EEMessage'; import { UserPresence } from '../../../../lib/presence'; import MessageBlock from '../../../blocks/MessageBlock'; import MessageLocation from '../../../location/MessageLocation'; +import { useMessageActions, useMessageRunActionLink, useMessageOembedIsEnabled } from '../../contexts/MessageContext'; import { - useMessageActions, - useMessageOembedIsEnabled, - useMessageOembedMaxWidth, - useMessageRunActionLink, -} from '../../contexts/MessageContext'; -import { - useMessageListShowRoles, useMessageListShowUsername, useMessageListShowRealName, - useOpenEmojiPicker, - useReactionsFilter, - useReactToMessage, - useUserHasReacted, + useMessageListShowRoles, + useMessageListShowReadReceipt, } from '../contexts/MessageListContext'; import { MessageIndicators } from './MessageIndicators'; -import { MessageReaction } from './MessageReaction'; +import ReactionsList from './MessageReactionsList'; +import ReadReceipt from './MessageReadReceipt'; +import RolesList from './MessageRolesList'; import Toolbox from './Toolbox'; -import OEmbedList from './UrlPreview'; +import PreviewList from './UrlPreview'; const Message: FC<{ message: IMessage; sequential: boolean; subscription?: ISubscription }> = ({ message, @@ -70,23 +58,24 @@ const Message: FC<{ message: IMessage; sequential: boolean; subscription?: ISubs const runActionLink = useMessageRunActionLink(); - const hasReacted = useUserHasReacted(message); - const reactToMessage = useReactToMessage(message); - const filterReactions = useReactionsFilter(message); - const oembedIsEnabled = useMessageOembedIsEnabled(); - const oembedWidth = useMessageOembedMaxWidth(); - - const openEmojiPicker = useOpenEmojiPicker(message); - const isReadReceiptEnabled = useSetting('Message_Read_Receipt_Enabled'); + const shouldShowReadReceipt = useMessageListShowReadReceipt(); - const showRoles = useMessageListShowRoles(); const showRealName = useMessageListShowRealName(); const user: UserPresence = { ...message.u, roles: [], ...useUserData(message.u._id) }; const usernameAndRealNameAreSame = !user.name || user.username === user.name; const showUsername = useMessageListShowUsername() && showRealName && !usernameAndRealNameAreSame; + const isEncryptedMessage = isE2EEMessage(message); + const isEncryptedMessagePending = isEncryptedMessage && message.e2e === 'pending'; + const isMessageReady = !isEncryptedMessage || !isEncryptedMessagePending; + + const showRoles = useMessageListShowRoles(); + const shouldShowRolesList = !showRoles || !user.roles || !Array.isArray(user.roles) || user.roles.length < 1; + + const shouldShowReactionList = message.reactions && Object.keys(message.reactions).length; + const mineUid = useUserId(); return ( @@ -103,7 +92,7 @@ const Message: FC<{ message: IMessage; sequential: boolean; subscription?: ISubs data-username={user.username} onClick={user.username !== undefined ? openUserCard(user.username) : undefined} > - {(showRealName && user.name) || user.username} + {getUserDisplayName(user.name, user.username, showRealName)} {showUsername && ( )} - - {showRoles && Array.isArray(user.roles) && user.roles.length > 0 && ( - <> - {user.roles.map((role, index) => ( - {role} - ))} - - )} - {message.bot && {t('Bot')}} - + + {shouldShowRolesList && } + {formatters.messageHeader(message.ts)} {message.private && ( // The MessageStatusPrivateIndicator component should not have name prop, it should be fixed on fuselage @@ -131,22 +113,19 @@ const Message: FC<{ message: IMessage; sequential: boolean; subscription?: ISubs )} + - {message.e2e === 'pending' - ? t('E2E_message_encrypted_placeholder') - : message.e2e !== 'done' && - !message.blocks && - message.md && } - {!message.blocks && !message.md && message.msg} - {message.e2e === 'done' && message.msg} + {isEncryptedMessagePending && t('E2E_message_encrypted_placeholder')} + + {isMessageReady && !message.blocks && message.md && ( + + )} + + {isMessageReady && !message.blocks && !message.md && message.msg} {message.blocks && } {message.attachments && } - {/* {{#unless hideActionLinks}} - {{> MessageActions mid=msg._id actions=actionLinks runAction=(actions.runAction msg)}} - {{/unless}} */} - {message.actionLinks?.length && ( )} - {message.reactions && Object.keys(message.reactions).length > 0 && ( - - {Object.entries(message.reactions).map(([name, reactions]) => ( - - ))} - - - )} + + {shouldShowReactionList && } {isThreadMainMessage(message) && ( } {broadcast && user.username && } - {oembedIsEnabled && message.urls && ( - - - - )} + {oembedIsEnabled && message.urls && } - {isReadReceiptEnabled && ( - - - - )} + {shouldShowReadReceipt && } {!message.private && } diff --git a/client/views/room/MessageList/components/MessageIndicators.tsx b/client/views/room/MessageList/components/MessageIndicators.tsx index 992ac004cae9..7ff7cb669cc9 100644 --- a/client/views/room/MessageList/components/MessageIndicators.tsx +++ b/client/views/room/MessageList/components/MessageIndicators.tsx @@ -4,6 +4,7 @@ import React, { FC } from 'react'; import { IMessage, isEditedMessage } from '../../../../../definition/IMessage'; import { useTranslation } from '../../../../contexts/TranslationContext'; import { useUserId } from '../../../../contexts/UserContext'; +import { isE2EEMessage } from '../../../../lib/isE2EEMessage'; import { useMessageDateFormatter, useShowStarred, useShowTranslated, useShowFollowing } from '../contexts/MessageListContext'; // edited() { @@ -33,6 +34,7 @@ export const MessageIndicators: FC<{ const starred = useShowStarred({ message }); // TODO: useMessageStarred const following = useShowFollowing({ message }); // TODO: useMessageFollowing + const isEncryptedMessage = isE2EEMessage(message); const uid = useUserId(); const formatter = useMessageDateFormatter(); // TODO: useMessageDateFormatter @@ -60,6 +62,8 @@ export const MessageIndicators: FC<{ )} {message.pinned && } + {isEncryptedMessage && } + {starred && } ); diff --git a/client/views/room/MessageList/components/MessageReaction.tsx b/client/views/room/MessageList/components/MessageReaction.tsx index 7992be32506c..0ce85d54dc16 100644 --- a/client/views/room/MessageList/components/MessageReaction.tsx +++ b/client/views/room/MessageList/components/MessageReaction.tsx @@ -78,7 +78,7 @@ export const MessageReaction: FC<{ ref.current, ); }} - onMouseLeave={() => { + onMouseLeave={(): void => { closeTooltip(); }} {...props} diff --git a/client/views/room/MessageList/components/MessageReactionsList.tsx b/client/views/room/MessageList/components/MessageReactionsList.tsx new file mode 100644 index 000000000000..9797921ab59f --- /dev/null +++ b/client/views/room/MessageList/components/MessageReactionsList.tsx @@ -0,0 +1,32 @@ +import { MessageReactions, MessageReactionAction } from '@rocket.chat/fuselage'; +import React, { ReactElement } from 'react'; + +import { IMessage } from '../../../../../definition/IMessage'; +import { useOpenEmojiPicker, useReactionsFilter, useReactToMessage, useUserHasReacted } from '../contexts/MessageListContext'; +import { MessageReaction } from './MessageReaction'; + +const MessageReactionsList = ({ message }: { message: IMessage }): ReactElement | null => { + const hasReacted = useUserHasReacted(message); + const reactToMessage = useReactToMessage(message); + const filterReactions = useReactionsFilter(message); + const openEmojiPicker = useOpenEmojiPicker(message); + + return ( + + {message.reactions && + Object.entries(message.reactions).map(([name, reactions]) => ( + + ))} + + + ); +}; + +export default MessageReactionsList; diff --git a/client/views/room/MessageList/components/MessageReadReceipt.tsx b/client/views/room/MessageList/components/MessageReadReceipt.tsx new file mode 100644 index 000000000000..0f2c24ea658c --- /dev/null +++ b/client/views/room/MessageList/components/MessageReadReceipt.tsx @@ -0,0 +1,17 @@ +import { css } from '@rocket.chat/css-in-js'; +import { Box, Icon } from '@rocket.chat/fuselage'; +import React, { ReactElement } from 'react'; + +const MessageReadReceipt = (): ReactElement | null => ( + + + +); + +export default MessageReadReceipt; diff --git a/client/views/room/MessageList/components/MessageRolesList.tsx b/client/views/room/MessageList/components/MessageRolesList.tsx new file mode 100644 index 000000000000..19c67bfbc2b7 --- /dev/null +++ b/client/views/room/MessageList/components/MessageRolesList.tsx @@ -0,0 +1,25 @@ +import { MessageRole, MessageRoles } from '@rocket.chat/fuselage'; +import React, { ReactElement } from 'react'; + +import { useTranslation } from '../../../../contexts/TranslationContext'; +import { UserPresence } from '../../../../lib/presence'; + +type MessageRolesListProps = { + user: UserPresence; + isBot?: boolean; +}; + +const MessageRolesList = ({ user, isBot }: MessageRolesListProps): ReactElement | null => { + const t = useTranslation(); + + return ( + + {user.roles?.map((role, index) => ( + {role} + ))} + {isBot && {t('Bot')}} + + ); +}; + +export default MessageRolesList; diff --git a/client/views/room/MessageList/components/UrlPreview/PreviewList.tsx b/client/views/room/MessageList/components/UrlPreview/PreviewList.tsx index 8d32cc556311..427dbabfb0a5 100644 --- a/client/views/room/MessageList/components/UrlPreview/PreviewList.tsx +++ b/client/views/room/MessageList/components/UrlPreview/PreviewList.tsx @@ -1,5 +1,7 @@ +import { Box } from '@rocket.chat/fuselage'; import React, { ReactElement } from 'react'; +import { useMessageOembedIsEnabled, useMessageOembedMaxWidth } from '../../../contexts/MessageContext'; import OEmbedResolver from './OEmbedResolver'; import UrlPreview from './UrlPreview'; @@ -38,7 +40,7 @@ export type UrlPreview = { url: string; }; -type PreviewListProps = { urls: OembedUrlLegacy[] }; +type PreviewListProps = { urls: OembedUrlLegacy[] | undefined }; type PreviewTypes = 'headers' | 'oembed'; @@ -105,18 +107,25 @@ const isPreviewData = (data: PreviewData | false): data is PreviewData => !!data const isMetaPreview = (_data: PreviewData['data'], type: PreviewTypes): _data is PreviewMetadata => type === 'oembed'; -const PreviewList = ({ urls }: PreviewListProps): ReactElement => { +const PreviewList = ({ urls }: PreviewListProps): ReactElement | null => { + const oembedIsEnabled = useMessageOembedIsEnabled(); + const oembedWidth = useMessageOembedMaxWidth(); + + if (!oembedIsEnabled || !urls) { + return null; + } + const metaAndHeaders = urls.map(processMetaAndHeaders).filter(isPreviewData); return ( - <> + {metaAndHeaders.map(({ type, data }) => { if (isMetaPreview(data, type)) { return ; } return ; })} - + ); }; diff --git a/client/views/room/MessageList/contexts/MessageListContext.tsx b/client/views/room/MessageList/contexts/MessageListContext.tsx index 054eacca9318..b1a19a7b8346 100644 --- a/client/views/room/MessageList/contexts/MessageListContext.tsx +++ b/client/views/room/MessageList/contexts/MessageListContext.tsx @@ -14,6 +14,7 @@ export type MessageListContextValue = { showRoles: boolean; showRealName: boolean; showUsername: boolean; + showReadReceipt: boolean; }; export const MessageListContext = createContext({ @@ -34,6 +35,7 @@ export const MessageListContext = createContext({ showRoles: false, showRealName: false, showUsername: false, + showReadReceipt: false, }); export const useShowTranslated: MessageListContextValue['useShowTranslated'] = (...args) => @@ -47,6 +49,8 @@ export const useMessageDateFormatter: MessageListContextValue['useMessageDateFor export const useMessageListShowRoles = (): MessageListContextValue['showRoles'] => useContext(MessageListContext).showRoles; export const useMessageListShowRealName = (): MessageListContextValue['showRealName'] => useContext(MessageListContext).showRealName; export const useMessageListShowUsername = (): MessageListContextValue['showUsername'] => useContext(MessageListContext).showUsername; +export const useMessageListShowReadReceipt = (): MessageListContextValue['showReadReceipt'] => + useContext(MessageListContext).showReadReceipt; export const useUserHasReacted: MessageListContextValue['useUserHasReacted'] = (message: IMessage) => useContext(MessageListContext).useUserHasReacted(message); diff --git a/client/views/room/MessageList/providers/MessageListProvider.tsx b/client/views/room/MessageList/providers/MessageListProvider.tsx index 3ca3971954a5..cf90ae847f67 100644 --- a/client/views/room/MessageList/providers/MessageListProvider.tsx +++ b/client/views/room/MessageList/providers/MessageListProvider.tsx @@ -24,6 +24,7 @@ export const MessageListProvider: FC<{ const { isMobile } = useLayout(); const showRealName = Boolean(useSetting('UI_Use_Real_Name')) && !isMobile; + const showReadReceipt = Boolean(useSetting('Message_Read_Receipt_Enabled')); const autoTranslateEnabled = useSetting('AutoTranslate_Enabled'); const showRoles = Boolean(!useUserPreference('hideRoles') && !isMobile); const showUsername = Boolean(!useUserPreference('hideUsernames') && !isMobile); @@ -81,6 +82,7 @@ export const MessageListProvider: FC<{ showRoles, showRealName, showUsername, + showReadReceipt, useReactToMessage: uid ? (message) => @@ -99,7 +101,18 @@ export const MessageListProvider: FC<{ } : () => (): void => undefined, }), - [uid, autoTranslateEnabled, hasSubscription, autoTranslateLanguage, showRoles, username, reactToMessage, showRealName, showUsername], + [ + username, + uid, + autoTranslateEnabled, + hasSubscription, + autoTranslateLanguage, + showRoles, + showRealName, + showUsername, + showReadReceipt, + reactToMessage, + ], ); return ; diff --git a/client/views/room/components/BlazeTemplate.tsx b/client/views/room/components/BlazeTemplate.tsx index eab9f1c4a19d..3c8ac79ea887 100644 --- a/client/views/room/components/BlazeTemplate.tsx +++ b/client/views/room/components/BlazeTemplate.tsx @@ -8,6 +8,7 @@ const BlazeTemplate: FC< name: string; } & Record > = ({ name, flexShrink, overflow, onClick, ...props }) => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const ref = useRef(null!); useLayoutEffect(() => { if (!ref.current || !Template[name]) { @@ -15,7 +16,7 @@ const BlazeTemplate: FC< } const view = Blaze.renderWithData(Template[name], props, ref.current); - return () => { + return (): void => { view && Blaze.remove(view); }; // eslint-disable-next-line react-hooks/exhaustive-deps