From 031c57ef54003726f898633cbaa8a514cdaac284 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Fri, 2 Feb 2024 12:02:13 -0300 Subject: [PATCH 1/2] chore: Normalize `UserCard` --- .../components/UserCard/UserCard.stories.tsx | 12 ++-- .../client/components/UserCard/UserCard.tsx | 64 ++++++------------- .../components/UserCard/UserCardAction.tsx | 2 +- .../components/UserCard/UserCardActions.tsx | 15 +++++ .../components/UserCard/UserCardContainer.tsx | 16 ++++- .../components/UserCard/UserCardSkeleton.tsx | 34 ++++++++++ .../client/components/UserCard/index.ts | 9 +-- .../client/components/UserInfo/UserInfo.tsx | 4 +- .../components/UserInfo/UserInfoUsername.tsx | 5 +- .../moderation/UserReports/UserReportInfo.tsx | 4 +- .../admin/users/AdminUserInfoWithData.tsx | 4 +- .../oauth/components/CurrentUserDisplay.tsx | 4 +- .../views/omnichannel/components/Info.tsx | 4 +- .../views/room/UserCard/UserCardWithData.tsx | 20 +++--- .../UserInfo/UserInfoWithData.tsx | 4 +- 15 files changed, 117 insertions(+), 84 deletions(-) create mode 100644 apps/meteor/client/components/UserCard/UserCardActions.tsx create mode 100644 apps/meteor/client/components/UserCard/UserCardSkeleton.tsx diff --git a/apps/meteor/client/components/UserCard/UserCard.stories.tsx b/apps/meteor/client/components/UserCard/UserCard.stories.tsx index b07d4a09cc7dc..e3174381d85d1 100644 --- a/apps/meteor/client/components/UserCard/UserCard.stories.tsx +++ b/apps/meteor/client/components/UserCard/UserCard.stories.tsx @@ -1,7 +1,7 @@ import type { ComponentMeta, ComponentStory } from '@storybook/react'; import React from 'react'; -import UserCard from '.'; +import { UserCard, UserCardRole, UserCardAction } from '.'; export default { title: 'Components/UserCard', @@ -14,16 +14,16 @@ export default { customStatus: '🛴 currently working on User Card', roles: ( <> - Admin - Rocket.Chat - Team + Admin + Rocket.Chat + Team ), bio: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla tempus, eros convallis vulputate cursus, nisi neque eleifend libero, eget lacinia justo purus nec est. In at sodales ipsum. Sed lacinia quis purus eget pulvinar. Aenean eu pretium nunc, at aliquam magna. Praesent dignissim, tortor sed volutpat mattis, mauris diam pulvinar leo, porta commodo risus est non purus. Mauris in justo vel lorem ullamcorper hendrerit. Nam est metus, viverra a pellentesque vitae, ornare eget odio. Morbi tempor feugiat mattis. Morbi non felis tempor, aliquam justo sed, sagittis nibh. Mauris consequat ex metus. Praesent sodales sit amet nibh a vulputate. Integer commodo, mi vel bibendum sollicitudin, urna lectus accumsan ante, eget faucibus augue ex id neque. Aenean consectetur, orci a pellentesque mattis, tortor tellus fringilla elit, non ullamcorper risus nunc feugiat risus. Fusce sit amet nisi dapibus turpis commodo placerat. In tortor ante, vehicula sit amet augue et, imperdiet porta sem.', actions: ( <> - - + + ), localTime: 'Local Time: 7:44 AM', diff --git a/apps/meteor/client/components/UserCard/UserCard.tsx b/apps/meteor/client/components/UserCard/UserCard.tsx index 5678f360af380..78dd9a720ac17 100644 --- a/apps/meteor/client/components/UserCard/UserCard.tsx +++ b/apps/meteor/client/components/UserCard/UserCard.tsx @@ -1,13 +1,14 @@ import { css } from '@rocket.chat/css-in-js'; -import { Box, Button, IconButton, Skeleton } from '@rocket.chat/fuselage'; +import { Box, Button, IconButton } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; -import type { ReactNode, ComponentProps, MouseEvent } from 'react'; +import type { ReactNode, ComponentProps } from 'react'; import React, { forwardRef } from 'react'; import { useEmbeddedLayout } from '../../hooks/useEmbeddedLayout'; import MarkdownText from '../MarkdownText'; import * as Status from '../UserStatus'; import UserAvatar from '../avatar/UserAvatar'; +import UserCardActions from './UserCardActions'; import UserCardContainer from './UserCardContainer'; import UserCardInfo from './UserCardInfo'; import UserCardRoles from './UserCardRoles'; @@ -22,9 +23,7 @@ const clampStyle = css` `; type UserCardProps = { - className?: string; - style?: ComponentProps['style']; - open?: (e: MouseEvent) => void; + onOpenUserInfo?: () => void; name?: string; username?: string; etag?: string; @@ -36,61 +35,40 @@ type UserCardProps = { localTime?: ReactNode; onClose?: () => void; nickname?: string; - isLoading?: boolean; -}; +} & ComponentProps; -const UserCard = forwardRef(function UserCard( +const UserCard = forwardRef(function UserCard( { - className, - style, - open, + onOpenUserInfo, name, username, etag, - customStatus = , - roles = ( - <> - - - - - ), - bio = , + customStatus, + roles, + bio, status = , actions, - localTime = , + localTime, onClose, nickname, - isLoading, - }: UserCardProps, + ...props + }, ref, ) { const t = useTranslation(); const isLayoutEmbedded = useEmbeddedLayout(); return ( - - - {!isLoading && username ? ( - - ) : ( - - )} + +
+ {username && } - {isLoading ? ( - <> - - - - - ) : ( - actions - )} + {actions} - +
- {isLoading ? : } + {nickname && ( ({nickname}) @@ -113,9 +91,9 @@ const UserCard = forwardRef(function UserCard( {typeof bio === 'string' ? : bio} )} - {!isLoading && open && !isLayoutEmbedded && ( + {onOpenUserInfo && !isLayoutEmbedded && (
-
diff --git a/apps/meteor/client/components/UserCard/UserCardAction.tsx b/apps/meteor/client/components/UserCard/UserCardAction.tsx index f13e71b601b52..c96e742be94e0 100644 --- a/apps/meteor/client/components/UserCard/UserCardAction.tsx +++ b/apps/meteor/client/components/UserCard/UserCardAction.tsx @@ -5,7 +5,7 @@ import React from 'react'; type UserCardActionProps = ComponentProps; const UserCardAction = ({ label, icon, ...props }: UserCardActionProps): ReactElement => ( - + ); export default UserCardAction; diff --git a/apps/meteor/client/components/UserCard/UserCardActions.tsx b/apps/meteor/client/components/UserCard/UserCardActions.tsx new file mode 100644 index 0000000000000..6a1258c029971 --- /dev/null +++ b/apps/meteor/client/components/UserCard/UserCardActions.tsx @@ -0,0 +1,15 @@ +import { useToolbar } from '@react-aria/toolbar'; +import { ButtonGroup } from '@rocket.chat/fuselage'; +import type { ReactElement, ComponentProps } from 'react'; +import React, { useRef } from 'react'; + +type UserCardActionsProps = ComponentProps; + +const UserCardActions = (props: UserCardActionsProps): ReactElement => { + const ref = useRef(null); + const { toolbarProps } = useToolbar(props, ref); + + return ; +}; + +export default UserCardActions; diff --git a/apps/meteor/client/components/UserCard/UserCardContainer.tsx b/apps/meteor/client/components/UserCard/UserCardContainer.tsx index c1279e4ff5131..2434077922e34 100644 --- a/apps/meteor/client/components/UserCard/UserCardContainer.tsx +++ b/apps/meteor/client/components/UserCard/UserCardContainer.tsx @@ -3,7 +3,21 @@ import type { ComponentProps } from 'react'; import React, { forwardRef } from 'react'; const UserCardContainer = forwardRef(function UserCardContainer(props: ComponentProps, ref) { - return ; + return ( + + ); }); export default UserCardContainer; diff --git a/apps/meteor/client/components/UserCard/UserCardSkeleton.tsx b/apps/meteor/client/components/UserCard/UserCardSkeleton.tsx new file mode 100644 index 0000000000000..9339eca8f6659 --- /dev/null +++ b/apps/meteor/client/components/UserCard/UserCardSkeleton.tsx @@ -0,0 +1,34 @@ +import { Box, Skeleton } from '@rocket.chat/fuselage'; +import type { ComponentProps } from 'react'; +import React, { forwardRef } from 'react'; + +import UserCardContainer from './UserCardContainer'; + +const UserCardSkeleton = forwardRef>(function UserCardSkeleton(props, ref) { + return ( + + + + + {Array.from({ length: 3 }).map((_, i) => ( + + ))} + + + + + + + + {Array.from({ length: 3 }).map((_, i) => ( + + ))} + {Array.from({ length: 2 }).map((_, i) => ( + + ))} + + + ); +}); + +export default UserCardSkeleton; diff --git a/apps/meteor/client/components/UserCard/index.ts b/apps/meteor/client/components/UserCard/index.ts index defc7140be156..75a5ad84cd6cd 100644 --- a/apps/meteor/client/components/UserCard/index.ts +++ b/apps/meteor/client/components/UserCard/index.ts @@ -3,12 +3,7 @@ import UserCardAction from './UserCardAction'; import UserCardInfo from './UserCardInfo'; import UserCardRole from './UserCardRole'; import UserCardRoles from './UserCardRoles'; +import UserCardSkeleton from './UserCardSkeleton'; import UserCardUsername from './UserCardUsername'; -export default Object.assign(UserCard, { - Action: UserCardAction, - Role: UserCardRole, - Roles: UserCardRoles, - Info: UserCardInfo, - Username: UserCardUsername, -}); +export { UserCard, UserCardAction, UserCardInfo, UserCardRole, UserCardRoles, UserCardUsername, UserCardSkeleton }; diff --git a/apps/meteor/client/components/UserInfo/UserInfo.tsx b/apps/meteor/client/components/UserInfo/UserInfo.tsx index bdce27d028ada..72722e8e91568 100644 --- a/apps/meteor/client/components/UserInfo/UserInfo.tsx +++ b/apps/meteor/client/components/UserInfo/UserInfo.tsx @@ -12,7 +12,7 @@ import { ContextualbarScrollableContent } from '../Contextualbar'; import InfoPanel from '../InfoPanel'; import MarkdownText from '../MarkdownText'; import UTCClock from '../UTCClock'; -import UserCard from '../UserCard'; +import { UserCardRoles } from '../UserCard'; import UserInfoAvatar from './UserInfoAvatar'; type UserInfoDataProps = Serialized< @@ -90,7 +90,7 @@ const UserInfo = ({ {roles.length !== 0 && ( {t('Roles')} - {roles} + {roles} )} diff --git a/apps/meteor/client/components/UserInfo/UserInfoUsername.tsx b/apps/meteor/client/components/UserInfo/UserInfoUsername.tsx index 75d2c87a82c01..171c5c68d8c42 100644 --- a/apps/meteor/client/components/UserInfo/UserInfoUsername.tsx +++ b/apps/meteor/client/components/UserInfo/UserInfoUsername.tsx @@ -3,16 +3,15 @@ import type { Box } from '@rocket.chat/fuselage'; import type { ReactElement, ComponentProps } from 'react'; import React from 'react'; -import UserCard from '../UserCard'; +import { UserCardUsername } from '../UserCard'; type UserInfoUsernameProps = { username: IUser['username']; status: ReactElement; } & ComponentProps; -// TODO: Remove UserCard.Username const UserInfoUsername = ({ username, status, ...props }: UserInfoUsernameProps): ReactElement => ( - + ); export default UserInfoUsername; diff --git a/apps/meteor/client/views/admin/moderation/UserReports/UserReportInfo.tsx b/apps/meteor/client/views/admin/moderation/UserReports/UserReportInfo.tsx index 5d1d99ba8f866..7b410f5e6705b 100644 --- a/apps/meteor/client/views/admin/moderation/UserReports/UserReportInfo.tsx +++ b/apps/meteor/client/views/admin/moderation/UserReports/UserReportInfo.tsx @@ -18,7 +18,7 @@ import React, { useMemo } from 'react'; import { ContextualbarScrollableContent } from '../../../../components/Contextualbar'; import GenericNoResults from '../../../../components/GenericNoResults'; -import UserCard from '../../../../components/UserCard'; +import { UserCardRole } from '../../../../components/UserCard'; import { useFormatDate } from '../../../../hooks/useFormatDate'; import ReportReason from '../helpers/ReportReason'; import UserColumn from '../helpers/UserColumn'; @@ -79,7 +79,7 @@ const UserReportInfo = ({ userId }: { userId: string }) => { {t('Roles')} {report.user.roles.map((role, index) => ( - {role} + {role} ))} diff --git a/apps/meteor/client/views/admin/users/AdminUserInfoWithData.tsx b/apps/meteor/client/views/admin/users/AdminUserInfoWithData.tsx index d0580b90c6428..f9b8bb01e1290 100644 --- a/apps/meteor/client/views/admin/users/AdminUserInfoWithData.tsx +++ b/apps/meteor/client/views/admin/users/AdminUserInfoWithData.tsx @@ -10,7 +10,7 @@ import React, { useMemo } from 'react'; import { getUserEmailAddress } from '../../../../lib/getUserEmailAddress'; import { ContextualbarContent } from '../../../components/Contextualbar'; import { FormSkeleton } from '../../../components/Skeleton'; -import UserCard from '../../../components/UserCard'; +import { UserCardRole } from '../../../components/UserCard'; import UserInfo from '../../../components/UserInfo'; import { UserStatus } from '../../../components/UserStatus'; import { getUserEmailVerified } from '../../../lib/utils/getUserEmailVerified'; @@ -76,7 +76,7 @@ const AdminUserInfoWithData = ({ uid, onReload }: AdminUserInfoWithDataProps): R name, username, lastLogin, - roles: getRoles(roles).map((role, index) => {role}), + roles: getRoles(roles).map((role, index) => {role}), bio, canViewAllInfo, phone, diff --git a/apps/meteor/client/views/oauth/components/CurrentUserDisplay.tsx b/apps/meteor/client/views/oauth/components/CurrentUserDisplay.tsx index e335dca527486..6e52570418ded 100644 --- a/apps/meteor/client/views/oauth/components/CurrentUserDisplay.tsx +++ b/apps/meteor/client/views/oauth/components/CurrentUserDisplay.tsx @@ -7,9 +7,7 @@ import { useTranslation } from 'react-i18next'; import LocalTime from '../../../components/LocalTime'; import MarkdownText from '../../../components/MarkdownText'; -import UserCard from '../../../components/UserCard'; -import UserCardInfo from '../../../components/UserCard/UserCardInfo'; -import UserCardRole from '../../../components/UserCard/UserCardRole'; +import { UserCard, UserCardInfo, UserCardRole } from '../../../components/UserCard'; const clampStyle = css` display: -webkit-box; diff --git a/apps/meteor/client/views/omnichannel/components/Info.tsx b/apps/meteor/client/views/omnichannel/components/Info.tsx index c7925af9c80ec..c8cdb9be42040 100644 --- a/apps/meteor/client/views/omnichannel/components/Info.tsx +++ b/apps/meteor/client/views/omnichannel/components/Info.tsx @@ -2,14 +2,14 @@ import { css } from '@rocket.chat/css-in-js'; import type { CSSProperties, FC } from 'react'; import React from 'react'; -import UserCard from '../../../components/UserCard'; +import { UserCardInfo } from '../../../components/UserCard'; const wordBreak = css` word-break: break-word; `; const Info: FC<{ className?: string; style?: CSSProperties }> = ({ className, ...props }) => ( - + ); export default Info; diff --git a/apps/meteor/client/views/room/UserCard/UserCardWithData.tsx b/apps/meteor/client/views/room/UserCard/UserCardWithData.tsx index 2c8668434985c..0337c794f2459 100644 --- a/apps/meteor/client/views/room/UserCard/UserCardWithData.tsx +++ b/apps/meteor/client/views/room/UserCard/UserCardWithData.tsx @@ -2,14 +2,14 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { PositionAnimated, AnimatedVisibility } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useSetting, useRolesDescription, useTranslation } from '@rocket.chat/ui-contexts'; -import type { ReactElement, UIEvent } from 'react'; +import type { ReactElement } from 'react'; import React, { useMemo, useRef } from 'react'; import { getUserDisplayName } from '../../../../lib/getUserDisplayName'; import { Backdrop } from '../../../components/Backdrop'; import GenericMenu from '../../../components/GenericMenu/GenericMenu'; import LocalTime from '../../../components/LocalTime'; -import UserCard from '../../../components/UserCard'; +import { UserCard, UserCardAction, UserCardRole, UserCardSkeleton } from '../../../components/UserCard'; import { ReactiveUserStatus } from '../../../components/UserStatus'; import { useUserInfoQuery } from '../../../hooks/useUserInfoQuery'; import { useUserInfoActions } from '../hooks/useUserInfoActions'; @@ -18,11 +18,11 @@ type UserCardWithDataProps = { username: string; target: Element; rid: IRoom['_id']; - open: (e: UIEvent) => void; + onOpenUserInfo: () => void; onClose: () => void; }; -const UserCardWithData = ({ username, target, rid, open, onClose }: UserCardWithDataProps): ReactElement => { +const UserCardWithData = ({ username, target, rid, onOpenUserInfo, onClose }: UserCardWithDataProps) => { const t = useTranslation(); const ref = useRef(target); const getRoles = useRolesDescription(); @@ -50,7 +50,7 @@ const UserCardWithData = ({ username, target, rid, open, onClose }: UserCardWith _id, name: getUserDisplayName(name, username, showRealNames), username, - roles: roles && getRoles(roles).map((role, index) => {role}), + roles: roles && getRoles(roles).map((role, index) => {role}), bio, etag: avatarETag, localTime: utcOffset && Number.isInteger(utcOffset) && , @@ -60,9 +60,9 @@ const UserCardWithData = ({ username, target, rid, open, onClose }: UserCardWith }; }, [data, username, showRealNames, isLoading, getRoles]); - const handleOpen = useMutableCallback((e: UIEvent) => { - open?.(e); - onClose?.(); + const handleOpenUserInfo = useMutableCallback(() => { + onOpenUserInfo(); + onClose(); }); const { actions: actionsDefinition, menuActions: menuOptions } = useUserInfoActions( @@ -80,7 +80,7 @@ const UserCardWithData = ({ username, target, rid, open, onClose }: UserCardWith const actions = useMemo(() => { const mapAction = ([key, { content, icon, onClick }]: any): ReactElement => ( - + ); return [...actionsDefinition.map(mapAction), menu].filter(Boolean); @@ -90,7 +90,7 @@ const UserCardWithData = ({ username, target, rid, open, onClose }: UserCardWith <> - + {isLoading ? : } ); diff --git a/apps/meteor/client/views/room/contextualBar/UserInfo/UserInfoWithData.tsx b/apps/meteor/client/views/room/contextualBar/UserInfo/UserInfoWithData.tsx index 47383259a0c43..d5e2f36625f4a 100644 --- a/apps/meteor/client/views/room/contextualBar/UserInfo/UserInfoWithData.tsx +++ b/apps/meteor/client/views/room/contextualBar/UserInfo/UserInfoWithData.tsx @@ -14,7 +14,7 @@ import { ContextualbarContent, } from '../../../../components/Contextualbar'; import { FormSkeleton } from '../../../../components/Skeleton'; -import UserCard from '../../../../components/UserCard'; +import { UserCardRole } from '../../../../components/UserCard'; import UserInfo from '../../../../components/UserInfo'; import { ReactiveUserStatus } from '../../../../components/UserStatus'; import { AsyncStatePhase } from '../../../../hooks/useAsyncState'; @@ -68,7 +68,7 @@ const UserInfoWithData = ({ uid, username, rid, onClose, onClickBack }: UserInfo name, username, lastLogin, - roles: roles && getRoles(roles).map((role, index) => {role}), + roles: roles && getRoles(roles).map((role, index) => {role}), bio, canViewAllInfo, phone, From 2b70bcc232c54bbfc70e757dd977602e5e98e547 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Fri, 2 Feb 2024 13:29:52 -0300 Subject: [PATCH 2/2] chore: introduce `UserCardProvider` --- .../client/lib/messageActionDefault.ts | 8 +-- apps/meteor/app/ui/client/lib/ChatMessages.ts | 9 ++- apps/meteor/app/ui/client/lib/userCard.ts | 64 ----------------- .../client/components/GazzodownText.tsx | 6 +- .../components/message/MessageHeader.tsx | 5 +- .../message/variants/RoomMessage.tsx | 2 +- .../message/variants/SystemMessage.tsx | 4 +- .../message/variants/ThreadMessage.tsx | 2 +- apps/meteor/client/lib/chats/ChatAPI.ts | 4 +- .../MessageList/ContactHistoryMessage.tsx | 4 +- .../client/views/room/UserCardHolder.tsx | 22 ------ .../client/views/room/body/RoomBody.tsx | 6 +- .../views/room/contexts/UserCardContext.ts | 14 ++++ .../actions/useCallAction.tsx | 5 +- .../ReactionListModal/ReactionListModal.tsx | 27 ++----- .../ReactionListModal/ReactionUserTag.tsx | 26 +++++-- .../modals/ReactionListModal/Reactions.tsx | 9 +-- .../views/room/providers/RoomProvider.tsx | 5 +- .../views/room/providers/UserCardProvider.tsx | 70 +++++++++++++++++++ .../hooks/useChatMessagesInstance.ts | 4 +- .../views/room/providers/hooks/useUserCard.ts | 56 --------------- 21 files changed, 150 insertions(+), 202 deletions(-) delete mode 100644 apps/meteor/app/ui/client/lib/userCard.ts delete mode 100644 apps/meteor/client/views/room/UserCardHolder.tsx create mode 100644 apps/meteor/client/views/room/contexts/UserCardContext.ts create mode 100644 apps/meteor/client/views/room/providers/UserCardProvider.tsx delete mode 100644 apps/meteor/client/views/room/providers/hooks/useUserCard.ts diff --git a/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts b/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts index 5807673188e53..834fd0bb05a2d 100644 --- a/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts +++ b/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts @@ -10,7 +10,7 @@ import { dispatchToastMessage } from '../../../../client/lib/toast'; import { messageArgs } from '../../../../client/lib/utils/messageArgs'; import { router } from '../../../../client/providers/RouterProvider'; import ForwardMessageModal from '../../../../client/views/room/modals/ForwardMessageModal/ForwardMessageModal'; -import ReactionList from '../../../../client/views/room/modals/ReactionListModal'; +import ReactionListModal from '../../../../client/views/room/modals/ReactionListModal'; import ReportMessageModal from '../../../../client/views/room/modals/ReportMessageModal'; import { hasAtLeastOnePermission, hasPermission } from '../../../authorization/client'; import { ChatRoom, Subscriptions } from '../../../models/client'; @@ -266,10 +266,10 @@ Meteor.startup(async () => { label: 'Reactions', context: ['message', 'message-mobile', 'threads', 'videoconf', 'videoconf-threads'], type: 'interaction', - action(this: unknown, _, { message: { reactions = {} } = messageArgs(this).msg }) { + action(this: unknown, _, { message: { reactions = {} } = messageArgs(this).msg, chat }) { imperativeModal.open({ - component: ReactionList, - props: { reactions, onClose: imperativeModal.close }, + component: ReactionListModal, + props: { reactions, onOpenUserCard: chat?.userCard.openUserCard, onClose: imperativeModal.close }, }); }, condition({ message: { reactions } }) { diff --git a/apps/meteor/app/ui/client/lib/ChatMessages.ts b/apps/meteor/app/ui/client/lib/ChatMessages.ts index 2e7c4a479e866..59efeff49af1a 100644 --- a/apps/meteor/app/ui/client/lib/ChatMessages.ts +++ b/apps/meteor/app/ui/client/lib/ChatMessages.ts @@ -45,7 +45,10 @@ export class ChatMessages implements ChatAPI { public ActionManager: any; - public userCard: { open(username: string): (event: UIEvent) => void; close(): void }; + public userCard: { + openUserCard(event: UIEvent, username: string): void; + closeUserCard(): void; + }; public emojiPicker: { open(el: Element, cb: (emoji: string) => void): void; @@ -160,8 +163,8 @@ export class ChatMessages implements ChatAPI { this.readStateManager = new ReadStateManager(rid); this.userCard = { - open: unimplemented, - close: unimplemented, + openUserCard: unimplemented, + closeUserCard: unimplemented, }; this.emojiPicker = { diff --git a/apps/meteor/app/ui/client/lib/userCard.ts b/apps/meteor/app/ui/client/lib/userCard.ts deleted file mode 100644 index 90b3d4ec52014..0000000000000 --- a/apps/meteor/app/ui/client/lib/userCard.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type { ComponentProps } from 'react'; -import { createElement } from 'react'; -import { createPortal } from 'react-dom'; - -import { registerPortal } from '../../../../client/lib/portals/portalsSubscription'; -import { queueMicrotask } from '../../../../client/lib/utils/queueMicrotask'; -import UserCardHolder from '../../../../client/views/room/UserCardHolder'; - -type UserCardProps = ReturnType['getProps']>; - -let props: UserCardProps; - -const subscribers = new Set<() => void>(); - -const updateProps = (newProps: Partial) => { - props = { ...props, ...newProps }; - subscribers.forEach((subscriber) => subscriber()); -}; - -const getProps = () => props; - -const subscribeToProps = (callback: () => void) => { - subscribers.add(callback); - - return () => { - subscribers.delete(callback); - }; -}; - -const createContainer = () => { - const container = document.createElement('div'); - container.id = 'react-user-card'; - document.body.appendChild(container); - - return container; -}; - -let container: HTMLDivElement | undefined; -let unregisterPortal: (() => void) | undefined; - -export const closeUserCard = () => { - queueMicrotask(() => { - if (unregisterPortal) { - unregisterPortal(); - unregisterPortal = undefined; - } - }); -}; - -export const openUserCard = (params: Omit) => { - updateProps({ ...params, onClose: closeUserCard }); - - if (!container) { - container = createContainer(); - } - - if (!unregisterPortal) { - const children = createElement(UserCardHolder, { getProps, subscribeToProps }); - const portal = createPortal(children, container); - unregisterPortal = registerPortal(container, portal); - } - - return closeUserCard; -}; diff --git a/apps/meteor/client/components/GazzodownText.tsx b/apps/meteor/client/components/GazzodownText.tsx index fe86cbd86fd1f..76232984a5944 100644 --- a/apps/meteor/client/components/GazzodownText.tsx +++ b/apps/meteor/client/components/GazzodownText.tsx @@ -25,7 +25,9 @@ type GazzodownTextProps = { }; const GazzodownText = ({ mentions, channels, searchText, children }: GazzodownTextProps) => { + const chat = useChat(); const highlights = useMessageListHighlights(); + const highlightRegex = useMemo(() => { if (!highlights?.length) { return; @@ -51,8 +53,6 @@ const GazzodownText = ({ mentions, channels, searchText, children }: GazzodownTe const ownUserId = useUserId(); const showMentionSymbol = Boolean(useUserPreference('mentionsWithSymbol')); - const chat = useChat(); - const resolveUserMention = useCallback( (mention: string) => { if (mention === 'all' || mention === 'here') { @@ -75,7 +75,7 @@ const GazzodownText = ({ mentions, channels, searchText, children }: GazzodownTe return (event: UIEvent): void => { event.stopPropagation(); - chat?.userCard.open(username)(event); + chat?.userCard.openUserCard(event, username); }; }, [chat?.userCard], diff --git a/apps/meteor/client/components/message/MessageHeader.tsx b/apps/meteor/client/components/message/MessageHeader.tsx index 3189aa2972bca..c12ee8c3ac207 100644 --- a/apps/meteor/client/components/message/MessageHeader.tsx +++ b/apps/meteor/client/components/message/MessageHeader.tsx @@ -52,7 +52,7 @@ const MessageHeader = ({ message }: MessageHeaderProps): ReactElement => { data-username={user.username} {...(user.username !== undefined && chat?.userCard && { - onClick: chat?.userCard.open(message.u.username), + onClick: (e) => chat?.userCard.openUserCard(e, message.u.username), style: { cursor: 'pointer' }, })} > @@ -66,7 +66,7 @@ const MessageHeader = ({ message }: MessageHeaderProps): ReactElement => { data-qa-type='username' {...(user.username !== undefined && chat?.userCard && { - onClick: chat?.userCard.open(message.u.username), + onClick: (e) => chat?.userCard.openUserCard(e, message.u.username), style: { cursor: 'pointer' }, })} > @@ -75,7 +75,6 @@ const MessageHeader = ({ message }: MessageHeaderProps): ReactElement => { )} - {shouldShowRolesList && } {formatTime(message.ts)} {message.private && {t('Only_you_can_see_this_message')}} diff --git a/apps/meteor/client/components/message/variants/RoomMessage.tsx b/apps/meteor/client/components/message/variants/RoomMessage.tsx index af8a6c08297cd..b38b7a08cff53 100644 --- a/apps/meteor/client/components/message/variants/RoomMessage.tsx +++ b/apps/meteor/client/components/message/variants/RoomMessage.tsx @@ -86,7 +86,7 @@ const RoomMessage = ({ username={message.u.username} size='x36' {...(chat?.userCard && { - onClick: chat?.userCard.open(message.u.username), + onClick: (e) => chat?.userCard.openUserCard(e, message.u.username), style: { cursor: 'pointer' }, })} /> diff --git a/apps/meteor/client/components/message/variants/SystemMessage.tsx b/apps/meteor/client/components/message/variants/SystemMessage.tsx index bef39112bb112..2936abbd1c390 100644 --- a/apps/meteor/client/components/message/variants/SystemMessage.tsx +++ b/apps/meteor/client/components/message/variants/SystemMessage.tsx @@ -75,7 +75,7 @@ const SystemMessage = ({ message, showUserAvatar }: SystemMessageProps): ReactEl user.username && chat?.userCard.openUserCard(e, user.username), style: { cursor: 'pointer' }, })} > @@ -88,7 +88,7 @@ const SystemMessage = ({ message, showUserAvatar }: SystemMessageProps): ReactEl data-username={user.username} {...(user.username !== undefined && chat?.userCard && { - onClick: chat?.userCard.open(user.username), + onClick: (e) => user.username && chat?.userCard.openUserCard(e, user.username), style: { cursor: 'pointer' }, })} > diff --git a/apps/meteor/client/components/message/variants/ThreadMessage.tsx b/apps/meteor/client/components/message/variants/ThreadMessage.tsx index 7c2b219b546f7..f9a4b385ac018 100644 --- a/apps/meteor/client/components/message/variants/ThreadMessage.tsx +++ b/apps/meteor/client/components/message/variants/ThreadMessage.tsx @@ -59,7 +59,7 @@ const ThreadMessage = ({ message, sequential, unread, showUserAvatar }: ThreadMe username={message.u.username} size='x36' {...(chat?.userCard && { - onClick: chat?.userCard.open(message.u.username), + onClick: (e) => chat?.userCard.openUserCard(e, message.u.username), style: { cursor: 'pointer' }, })} /> diff --git a/apps/meteor/client/lib/chats/ChatAPI.ts b/apps/meteor/client/lib/chats/ChatAPI.ts index 36f40f404a4f7..d2364cfcd72ca 100644 --- a/apps/meteor/client/lib/chats/ChatAPI.ts +++ b/apps/meteor/client/lib/chats/ChatAPI.ts @@ -127,8 +127,8 @@ export type ChatAPI = { | undefined; readonly userCard: { - open(username: string): (event: UIEvent) => void; - close(): void; + openUserCard(event: UIEvent, username: string): void; + closeUserCard(): void; }; readonly emojiPicker: { diff --git a/apps/meteor/client/views/omnichannel/contactHistory/MessageList/ContactHistoryMessage.tsx b/apps/meteor/client/views/omnichannel/contactHistory/MessageList/ContactHistoryMessage.tsx index c6ad907a14e14..f5dfa94adb2bd 100644 --- a/apps/meteor/client/views/omnichannel/contactHistory/MessageList/ContactHistoryMessage.tsx +++ b/apps/meteor/client/views/omnichannel/contactHistory/MessageList/ContactHistoryMessage.tsx @@ -52,7 +52,7 @@ const ContactHistoryMessage: FC<{ url={message.avatar} username={message.u.username} size='x18' - onClick={chat?.userCard.open(message.u.username)} + onClick={(e) => chat?.userCard.openUserCard(e, message.u.username)} style={{ cursor: 'pointer' }} /> )} @@ -80,7 +80,7 @@ const ContactHistoryMessage: FC<{ url={message.avatar} username={message.u.username} size='x36' - onClick={chat?.userCard.open(message.u.username)} + onClick={(e) => chat?.userCard.openUserCard(e, message.u.username)} style={{ cursor: 'pointer' }} /> )} diff --git a/apps/meteor/client/views/room/UserCardHolder.tsx b/apps/meteor/client/views/room/UserCardHolder.tsx deleted file mode 100644 index 0f9729217b4c8..0000000000000 --- a/apps/meteor/client/views/room/UserCardHolder.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import type { ComponentProps } from 'react'; -import React, { Suspense, lazy } from 'react'; -import { useSyncExternalStore } from 'use-sync-external-store/shim'; - -const UserCard = lazy(() => import('./UserCard')); - -type UserCardHolderProps = { - getProps: () => ComponentProps; - subscribeToProps: (callback: () => void) => () => void; -}; - -function UserCardHolder({ getProps, subscribeToProps }: UserCardHolderProps) { - const props = useSyncExternalStore(subscribeToProps, getProps); - - return ( - - - - ); -} - -export default UserCardHolder; diff --git a/apps/meteor/client/views/room/body/RoomBody.tsx b/apps/meteor/client/views/room/body/RoomBody.tsx index d2722200b2df9..1a6a11c07f793 100644 --- a/apps/meteor/client/views/room/body/RoomBody.tsx +++ b/apps/meteor/client/views/room/body/RoomBody.tsx @@ -174,13 +174,13 @@ const RoomBody = (): ReactElement => { }; }); - const handleOpenUserCardButtonClick = useCallback( + const handleOpenUserCard = useCallback( (event: UIEvent, username: IUser['username']) => { if (!username) { return; } - chat?.userCard.open(username)(event); + chat?.userCard.openUserCard(event, username); }, [chat?.userCard], ); @@ -585,7 +585,7 @@ const RoomBody = (): ReactElement => { username={roomLeader.username} name={roomLeader.name} visible={!hideLeaderHeader} - onAvatarClick={handleOpenUserCardButtonClick} + onAvatarClick={handleOpenUserCard} /> ) : null}
void; + closeUserCard: () => void; +}; + +export const UserCardContext = createContext({ + openUserCard: () => undefined, + closeUserCard: () => undefined, +}); + +export const useUserCard = () => useContext(UserCardContext); diff --git a/apps/meteor/client/views/room/hooks/useUserInfoActions/actions/useCallAction.tsx b/apps/meteor/client/views/room/hooks/useUserInfoActions/actions/useCallAction.tsx index 4889a50dbb7df..669062301f156 100644 --- a/apps/meteor/client/views/room/hooks/useUserInfoActions/actions/useCallAction.tsx +++ b/apps/meteor/client/views/room/hooks/useUserInfoActions/actions/useCallAction.tsx @@ -3,9 +3,9 @@ import { isRoomFederated } from '@rocket.chat/core-typings'; import { useTranslation, useUserRoom, useUserId, useUserSubscriptionByName } from '@rocket.chat/ui-contexts'; import { useMemo } from 'react'; -import { closeUserCard } from '../../../../../../app/ui/client/lib/userCard'; import { useVideoConfDispatchOutgoing, useVideoConfIsCalling, useVideoConfIsRinging } from '../../../../../contexts/VideoConfContext'; import { VideoConfManager } from '../../../../../lib/VideoConfManager'; +import { useUserCard } from '../../../contexts/UserCardContext'; import { useVideoConfWarning } from '../../../contextualBar/VideoConference/hooks/useVideoConfWarning'; import type { UserInfoAction, UserInfoActionType } from '../useUserInfoActions'; @@ -13,6 +13,7 @@ export const useCallAction = (user: Pick): UserInfoAc const t = useTranslation(); const usernameSubscription = useUserSubscriptionByName(user.username ?? ''); const room = useUserRoom(usernameSubscription?.rid || ''); + const { closeUserCard } = useUserCard(); const dispatchWarning = useVideoConfWarning(); const dispatchPopup = useVideoConfDispatchOutgoing(); @@ -43,7 +44,7 @@ export const useCallAction = (user: Pick): UserInfoAc type: 'communication' as UserInfoActionType, } : undefined; - }, [t, room, dispatchPopup, dispatchWarning, isCalling, isRinging, ownUserId, user._id]); + }, [t, room, dispatchPopup, closeUserCard, dispatchWarning, isCalling, isRinging, ownUserId, user._id]); return videoCallOption; }; diff --git a/apps/meteor/client/views/room/modals/ReactionListModal/ReactionListModal.tsx b/apps/meteor/client/views/room/modals/ReactionListModal/ReactionListModal.tsx index 162c8dd3071b5..596d74977b3d8 100644 --- a/apps/meteor/client/views/room/modals/ReactionListModal/ReactionListModal.tsx +++ b/apps/meteor/client/views/room/modals/ReactionListModal/ReactionListModal.tsx @@ -1,38 +1,25 @@ import type { IMessage } from '@rocket.chat/core-typings'; -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useTranslation } from '@rocket.chat/ui-contexts'; -import type { ReactElement } from 'react'; +import type { ReactElement, UIEvent } from 'react'; import React from 'react'; import GenericModal from '../../../../components/GenericModal'; -import { useChat } from '../../contexts/ChatContext'; import Reactions from './Reactions'; -type ReactionListProps = { +type ReactionListModalProps = { reactions: Required['reactions']; + onOpenUserCard?: (e: UIEvent, username: string) => void; onClose: () => void; }; -const ReactionList = ({ reactions, onClose }: ReactionListProps): ReactElement => { +const ReactionListModal = ({ reactions, onOpenUserCard, onClose }: ReactionListModalProps): ReactElement => { const t = useTranslation(); - const chat = useChat(); - - const onClick = useMutableCallback((e) => { - const { username } = e.currentTarget.dataset; - - if (!username) { - return; - } - - chat?.userCard.open(username)(e); - }); - return ( - - + + ); }; -export default ReactionList; +export default ReactionListModal; diff --git a/apps/meteor/client/views/room/modals/ReactionListModal/ReactionUserTag.tsx b/apps/meteor/client/views/room/modals/ReactionListModal/ReactionUserTag.tsx index 258751d18095e..4c13ac8af5321 100644 --- a/apps/meteor/client/views/room/modals/ReactionListModal/ReactionUserTag.tsx +++ b/apps/meteor/client/views/room/modals/ReactionListModal/ReactionUserTag.tsx @@ -1,18 +1,30 @@ import type { IUser } from '@rocket.chat/core-typings'; import { Box, Tag } from '@rocket.chat/fuselage'; -import type { ReactElement } from 'react'; +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import type { ReactElement, UIEvent } from 'react'; import React from 'react'; type ReactionUserTagProps = { username: IUser['username']; - onClick: (e: React.MouseEvent) => void; displayName: string; + onOpenUserCard?: (e: UIEvent, username: string) => void; }; -const ReactionUserTag = ({ username, onClick, displayName }: ReactionUserTagProps): ReactElement => ( - - {displayName} - -); +const ReactionUserTag = ({ username, displayName, onOpenUserCard }: ReactionUserTagProps): ReactElement => { + const handleOpenCard = useEffectEvent((e: UIEvent) => { + if (!username) { + return; + } + onOpenUserCard?.(e, username); + }); + + return ( + + + {displayName} + + + ); +}; export default ReactionUserTag; diff --git a/apps/meteor/client/views/room/modals/ReactionListModal/Reactions.tsx b/apps/meteor/client/views/room/modals/ReactionListModal/Reactions.tsx index 235cdfc0a269e..d766661de3e58 100644 --- a/apps/meteor/client/views/room/modals/ReactionListModal/Reactions.tsx +++ b/apps/meteor/client/views/room/modals/ReactionListModal/Reactions.tsx @@ -1,7 +1,7 @@ import type { IMessage } from '@rocket.chat/core-typings'; import { Box } from '@rocket.chat/fuselage'; import { useSetting } from '@rocket.chat/ui-contexts'; -import type { ReactElement } from 'react'; +import type { ReactElement, UIEvent } from 'react'; import React from 'react'; import Emoji from '../../../../components/Emoji'; @@ -9,11 +9,12 @@ import ReactionUserTag from './ReactionUserTag'; type ReactionsProps = { reactions: Required['reactions']; - onClick: (e: React.MouseEvent) => void; + onOpenUserCard?: (e: UIEvent, username: string) => void; }; -const Reactions = ({ reactions, onClick }: ReactionsProps): ReactElement => { +const Reactions = ({ reactions, onOpenUserCard }: ReactionsProps): ReactElement => { const useRealName = useSetting('UI_Use_Real_Name'); + return ( {Object.entries(reactions).map(([reaction, { names = [], usernames }]) => ( @@ -25,7 +26,7 @@ const Reactions = ({ reactions, onClick }: ReactionsProps): ReactElement => { key={username} displayName={useRealName ? names[i] || username : username} username={username} - onClick={onClick} + onOpenUserCard={onOpenUserCard} /> ))} diff --git a/apps/meteor/client/views/room/providers/RoomProvider.tsx b/apps/meteor/client/views/room/providers/RoomProvider.tsx index 88884d01439d3..65c83100b1c0c 100644 --- a/apps/meteor/client/views/room/providers/RoomProvider.tsx +++ b/apps/meteor/client/views/room/providers/RoomProvider.tsx @@ -17,6 +17,7 @@ import { useRoomRolesManagement } from '../body/hooks/useRoomRolesManagement'; import { RoomContext } from '../contexts/RoomContext'; import ComposerPopupProvider from './ComposerPopupProvider'; import RoomToolboxProvider from './RoomToolboxProvider'; +import UserCardProvider from './UserCardProvider'; import { useRedirectOnSettingsChanged } from './hooks/useRedirectOnSettingsChanged'; import { useRoomQuery } from './hooks/useRoomQuery'; import { useUsersNameChanged } from './hooks/useUsersNameChanged'; @@ -110,7 +111,9 @@ const RoomProvider = ({ rid, children }: RoomProviderProps): ReactElement => { - {children} + + {children} + diff --git a/apps/meteor/client/views/room/providers/UserCardProvider.tsx b/apps/meteor/client/views/room/providers/UserCardProvider.tsx new file mode 100644 index 0000000000000..47a9f1cd744ee --- /dev/null +++ b/apps/meteor/client/views/room/providers/UserCardProvider.tsx @@ -0,0 +1,70 @@ +import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import type { ComponentProps, ReactNode } from 'react'; +import React, { Suspense, lazy, useCallback, useMemo, useState } from 'react'; + +import { useRoom } from '../contexts/RoomContext'; +import { useRoomToolbox } from '../contexts/RoomToolboxContext'; +import { UserCardContext } from '../contexts/UserCardContext'; + +const UserCard = lazy(() => import('../UserCard')); + +const UserCardProvider = ({ children }: { children: ReactNode }) => { + const room = useRoom(); + const [userCardData, setUserCardData] = useState | null>(null); + + const { openTab } = useRoomToolbox(); + + const openUserInfo = useMutableCallback((username?: string) => { + switch (room.t) { + case 'l': + openTab('room-info', username); + break; + + case 'v': + openTab('voip-room-info', username); + break; + + case 'd': + (room.uids?.length ?? 0) > 2 ? openTab('user-info-group', username) : openTab('user-info', username); + break; + + default: + openTab('members-list', username); + break; + } + }); + + const handleSetUserCard = useCallback( + (e, username) => { + setUserCardData({ + username, + rid: room._id, + target: e.target, + onOpenUserInfo: () => openUserInfo(username), + onClose: () => setUserCardData(null), + }); + }, + [openUserInfo, room._id], + ); + + const contextValue = useMemo( + () => ({ + openUserCard: handleSetUserCard, + closeUserCard: () => setUserCardData(null), + }), + [handleSetUserCard], + ); + + return ( + + {children} + {userCardData && ( + + + + )} + + ); +}; + +export default UserCardProvider; diff --git a/apps/meteor/client/views/room/providers/hooks/useChatMessagesInstance.ts b/apps/meteor/client/views/room/providers/hooks/useChatMessagesInstance.ts index a468efd68fc68..023e99a99dd9f 100644 --- a/apps/meteor/client/views/room/providers/hooks/useChatMessagesInstance.ts +++ b/apps/meteor/client/views/room/providers/hooks/useChatMessagesInstance.ts @@ -7,8 +7,8 @@ import { useEmojiPicker } from '../../../../contexts/EmojiPickerContext'; import type { ChatAPI } from '../../../../lib/chats/ChatAPI'; import { useUiKitActionManager } from '../../../../uikit/hooks/useUiKitActionManager'; import { useRoomSubscription } from '../../contexts/RoomContext'; +import { useUserCard } from '../../contexts/UserCardContext'; import { useInstance } from './useInstance'; -import { useUserCard } from './useUserCard'; export function useChatMessagesInstance({ rid, tmid }: { rid: IRoom['_id']; tmid?: IMessage['_id'] }): ChatAPI { const uid = useUserId(); @@ -26,8 +26,8 @@ export function useChatMessagesInstance({ rid, tmid }: { rid: IRoom['_id']; tmid } }, [subscription, chatMessages?.readStateManager]); - chatMessages.userCard = useUserCard(); chatMessages.emojiPicker = useEmojiPicker(); + chatMessages.userCard = useUserCard(); return chatMessages; } diff --git a/apps/meteor/client/views/room/providers/hooks/useUserCard.ts b/apps/meteor/client/views/room/providers/hooks/useUserCard.ts deleted file mode 100644 index f04774df3da51..0000000000000 --- a/apps/meteor/client/views/room/providers/hooks/useUserCard.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import type { UIEvent } from 'react'; -import { useCallback, useEffect } from 'react'; - -import { openUserCard, closeUserCard } from '../../../../../app/ui/client/lib/userCard'; -import { useRoom } from '../../contexts/RoomContext'; -import { useRoomToolbox } from '../../contexts/RoomToolboxContext'; - -export const useUserCard = () => { - useEffect(() => { - return () => { - closeUserCard(); - }; - }, []); - - const room = useRoom(); - const { openTab } = useRoomToolbox(); - - const openUserInfo = useMutableCallback((username?: string) => { - switch (room.t) { - case 'l': - openTab('room-info', username); - break; - - case 'v': - openTab('voip-room-info', username); - break; - - case 'd': - (room.uids?.length ?? 0) > 2 ? openTab('user-info-group', username) : openTab('user-info', username); - break; - - default: - openTab('members-list', username); - break; - } - }); - - const open = useCallback( - (username: string) => (event: UIEvent) => { - event.preventDefault(); - openUserCard({ - username, - target: event.currentTarget, - rid: room._id, - open: (event: UIEvent) => { - event.preventDefault(); - openUserInfo(username); - }, - }); - }, - [openUserInfo, room._id], - ); - - return { open, close: closeUserCard }; -};