From 65772e49a06a4d9360a187b1c60a303d45733d14 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Mon, 15 Jan 2024 13:28:02 -0300 Subject: [PATCH] refactor(client): Move components from `app/` to `client/` (#31234) --- .../client/lib/{userCard.tsx => userCard.ts} | 22 ++--- .../UserAvatarEditor/UserAvatarEditor.tsx | 83 ++++++++++--------- .../UserAvatarEditor/UserAvatarSuggestion.ts | 6 ++ .../UserAvatarSuggestions.tsx | 48 ++++------- .../UserAvatarEditor/readFileAsDataURL.ts | 16 ++++ .../useUserAvatarSuggestions.ts | 12 +++ .../useOmnichannelExternalFrameRoomAction.ts | 2 +- .../client/hooks/useAvatarSuggestions.ts | 8 -- .../client/lib/portals/portalsSubscription.ts | 8 +- .../omnichannel}/ExternalFrameContainer.tsx | 10 +-- .../client/views/room/UserCardHolder.tsx | 22 +++++ .../views/room/composer}/ComposerBoxPopup.tsx | 6 +- .../ComposerBoxPopupCannedResponse.tsx | 6 +- .../room/composer}/ComposerBoxPopupEmoji.tsx | 8 +- .../composer}/ComposerBoxPopupPreview.tsx | 33 ++++---- .../room/composer}/ComposerBoxPopupRoom.tsx | 6 +- .../ComposerBoxPopupSlashCommand.tsx | 6 +- .../room/composer}/ComposerBoxPopupUser.tsx | 8 +- .../composer}/hooks/useComposerBoxPopup.ts | 4 +- .../hooks/useComposerBoxPopupQueries.ts | 4 +- .../composer}/hooks/useEnablePopupPreview.ts | 2 +- .../room/composer/messageBox/MessageBox.tsx | 8 +- .../room/providers/ComposerPopupProvider.tsx | 26 +++--- .../server/methods/getAvatarSuggestion.ts | 18 ++-- package.json | 2 +- packages/rest-typings/src/v1/users.ts | 20 ++--- yarn.lock | 60 +++++++------- 27 files changed, 241 insertions(+), 213 deletions(-) rename apps/meteor/app/ui/client/lib/{userCard.tsx => userCard.ts} (70%) create mode 100644 apps/meteor/client/components/avatar/UserAvatarEditor/UserAvatarSuggestion.ts create mode 100644 apps/meteor/client/components/avatar/UserAvatarEditor/readFileAsDataURL.ts create mode 100644 apps/meteor/client/components/avatar/UserAvatarEditor/useUserAvatarSuggestions.ts delete mode 100644 apps/meteor/client/hooks/useAvatarSuggestions.ts rename apps/meteor/{app/livechat/client/externalFrame => client/views/omnichannel}/ExternalFrameContainer.tsx (83%) create mode 100644 apps/meteor/client/views/room/UserCardHolder.tsx rename apps/meteor/{app/ui-message/client/popup => client/views/room/composer}/ComposerBoxPopup.tsx (97%) rename apps/meteor/{app/ui-message/client/popup/components/composerBoxPopup => client/views/room/composer}/ComposerBoxPopupCannedResponse.tsx (69%) rename apps/meteor/{app/ui-message/client/popup/components/composerBoxPopup => client/views/room/composer}/ComposerBoxPopupEmoji.tsx (63%) rename apps/meteor/{app/ui-message/client/popup/components/composerBoxPopupPreview => client/views/room/composer}/ComposerBoxPopupPreview.tsx (86%) rename apps/meteor/{app/ui-message/client/popup/components/composerBoxPopup => client/views/room/composer}/ComposerBoxPopupRoom.tsx (74%) rename apps/meteor/{app/ui-message/client/popup/components/composerBoxPopup => client/views/room/composer}/ComposerBoxPopupSlashCommand.tsx (74%) rename apps/meteor/{app/ui-message/client/popup/components/composerBoxPopup => client/views/room/composer}/ComposerBoxPopupUser.tsx (84%) rename apps/meteor/{app/ui-message/client/popup => client/views/room/composer}/hooks/useComposerBoxPopup.ts (97%) rename apps/meteor/{app/ui-message/client/popup => client/views/room/composer}/hooks/useComposerBoxPopupQueries.ts (89%) rename apps/meteor/{app/ui-message/client/popup => client/views/room/composer}/hooks/useEnablePopupPreview.ts (71%) diff --git a/apps/meteor/app/ui/client/lib/userCard.tsx b/apps/meteor/app/ui/client/lib/userCard.ts similarity index 70% rename from apps/meteor/app/ui/client/lib/userCard.tsx rename to apps/meteor/app/ui/client/lib/userCard.ts index e4fd5b343140..90b3d4ec5201 100644 --- a/apps/meteor/app/ui/client/lib/userCard.tsx +++ b/apps/meteor/app/ui/client/lib/userCard.ts @@ -1,14 +1,12 @@ import type { ComponentProps } from 'react'; -import React, { Suspense, createElement, lazy } from 'react'; +import { createElement } from 'react'; import { createPortal } from 'react-dom'; -import { useSyncExternalStore } from 'use-sync-external-store/shim'; import { registerPortal } from '../../../../client/lib/portals/portalsSubscription'; import { queueMicrotask } from '../../../../client/lib/utils/queueMicrotask'; +import UserCardHolder from '../../../../client/views/room/UserCardHolder'; -const UserCard = lazy(() => import('../../../../client/views/room/UserCard')); - -type UserCardProps = ComponentProps; +type UserCardProps = ReturnType['getProps']>; let props: UserCardProps; @@ -29,16 +27,6 @@ const subscribeToProps = (callback: () => void) => { }; }; -const UserCardWithProps = () => { - const props = useSyncExternalStore(subscribeToProps, getProps); - - return ( - - - - ); -}; - const createContainer = () => { const container = document.createElement('div'); container.id = 'react-user-card'; @@ -67,8 +55,8 @@ export const openUserCard = (params: Omit) => { } if (!unregisterPortal) { - const children = createElement(UserCardWithProps); - const portal = <>{createPortal(children, container)}; + const children = createElement(UserCardHolder, { getProps, subscribeToProps }); + const portal = createPortal(children, container); unregisterPortal = registerPortal(container, portal); } diff --git a/apps/meteor/client/components/avatar/UserAvatarEditor/UserAvatarEditor.tsx b/apps/meteor/client/components/avatar/UserAvatarEditor/UserAvatarEditor.tsx index ae03418be755..aec9961e8631 100644 --- a/apps/meteor/client/components/avatar/UserAvatarEditor/UserAvatarEditor.tsx +++ b/apps/meteor/client/components/avatar/UserAvatarEditor/UserAvatarEditor.tsx @@ -1,5 +1,5 @@ import type { IUser, AvatarObject } from '@rocket.chat/core-typings'; -import { Box, Button, TextInput, Margins, Avatar, IconButton } from '@rocket.chat/fuselage'; +import { Box, Button, TextInput, Avatar, IconButton } from '@rocket.chat/fuselage'; import { useToastMessageDispatch, useSetting, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement, ChangeEvent } from 'react'; import React, { useState, useCallback } from 'react'; @@ -7,17 +7,11 @@ import React, { useState, useCallback } from 'react'; import { useSingleFileInput } from '../../../hooks/useSingleFileInput'; import { isValidImageFormat } from '../../../lib/utils/isValidImageFormat'; import UserAvatar from '../UserAvatar'; +import type { UserAvatarSuggestion } from './UserAvatarSuggestion'; import UserAvatarSuggestions from './UserAvatarSuggestions'; +import { readFileAsDataURL } from './readFileAsDataURL'; -const toDataURL = (file: File, callback: (result: FileReader['result']) => void): void => { - const reader = new FileReader(); - reader.onloadend = function (e): void { - callback(e?.target?.result || null); - }; - reader.readAsDataURL(file); -}; - -type UserAvatarEditorType = { +type UserAvatarEditorProps = { currentUsername: IUser['username']; username: IUser['username']; setAvatarObj: (obj: AvatarObject) => void; @@ -25,7 +19,7 @@ type UserAvatarEditorType = { etag: IUser['avatarETag']; }; -function UserAvatarEditor({ currentUsername, username, setAvatarObj, disabled, etag }: UserAvatarEditorType): ReactElement { +function UserAvatarEditor({ currentUsername, username, setAvatarObj, disabled, etag }: UserAvatarEditorProps): ReactElement { const t = useTranslation(); const rotateImages = useSetting('FileUpload_RotateImages'); const [avatarFromUrl, setAvatarFromUrl] = useState(''); @@ -35,14 +29,15 @@ function UserAvatarEditor({ currentUsername, username, setAvatarObj, disabled, e const setUploadedPreview = useCallback( async (file, avatarObj) => { setAvatarObj(avatarObj); - toDataURL(file, async (dataURL) => { - if (typeof dataURL === 'string' && (await isValidImageFormat(dataURL))) { + try { + const dataURL = await readFileAsDataURL(file); + + if (await isValidImageFormat(dataURL)) { setNewAvatarSource(dataURL); - return; } - + } catch (error) { dispatchToastMessage({ type: 'error', message: t('Avatar_format_invalid') }); - }); + } }, [setAvatarObj, t, dispatchToastMessage], ); @@ -65,6 +60,14 @@ function UserAvatarEditor({ currentUsername, username, setAvatarObj, disabled, e setAvatarFromUrl(event.currentTarget.value); }; + const handleSelectSuggestion = useCallback( + (suggestion: UserAvatarSuggestion) => { + setAvatarObj(suggestion as unknown as AvatarObject); + setNewAvatarSource(suggestion.blob); + }, + [setAvatarObj, setNewAvatarSource], + ); + return ( {t('Profile_picture')} @@ -81,32 +84,30 @@ function UserAvatarEditor({ currentUsername, username, setAvatarObj, disabled, e /> - - - - - - - - - {t('Use_url_for_avatar')} - + + + + - + + + {t('Use_url_for_avatar')} + diff --git a/apps/meteor/client/components/avatar/UserAvatarEditor/UserAvatarSuggestion.ts b/apps/meteor/client/components/avatar/UserAvatarEditor/UserAvatarSuggestion.ts new file mode 100644 index 000000000000..e2ac26c6d0f3 --- /dev/null +++ b/apps/meteor/client/components/avatar/UserAvatarEditor/UserAvatarSuggestion.ts @@ -0,0 +1,6 @@ +export type UserAvatarSuggestion = { + blob: string; + contentType: string; + service: string; + url: string; +}; diff --git a/apps/meteor/client/components/avatar/UserAvatarEditor/UserAvatarSuggestions.tsx b/apps/meteor/client/components/avatar/UserAvatarEditor/UserAvatarSuggestions.tsx index 04b0c92acd95..4ccb7d304683 100644 --- a/apps/meteor/client/components/avatar/UserAvatarEditor/UserAvatarSuggestions.tsx +++ b/apps/meteor/client/components/avatar/UserAvatarEditor/UserAvatarSuggestions.tsx @@ -1,43 +1,31 @@ -import type { AvatarObject } from '@rocket.chat/core-typings'; -import { Box, Button, Margins, Avatar } from '@rocket.chat/fuselage'; +import { Button, Avatar } from '@rocket.chat/fuselage'; import React, { useCallback } from 'react'; -import { useAvatarSuggestions } from '../../../hooks/useAvatarSuggestions'; +import type { UserAvatarSuggestion } from './UserAvatarSuggestion'; +import { useUserAvatarSuggestions } from './useUserAvatarSuggestions'; type UserAvatarSuggestionsProps = { - setAvatarObj: (obj: AvatarObject) => void; - setNewAvatarSource: (source: string) => void; disabled?: boolean; + onSelectOne?: (suggestion: UserAvatarSuggestion) => void; }; -const UserAvatarSuggestions = ({ setAvatarObj, setNewAvatarSource, disabled }: UserAvatarSuggestionsProps) => { - const handleClick = useCallback( - (suggestion) => () => { - setAvatarObj(suggestion); - setNewAvatarSource(suggestion.blob); - }, - [setAvatarObj, setNewAvatarSource], - ); +function UserAvatarSuggestions({ disabled, onSelectOne }: UserAvatarSuggestionsProps) { + const { data: suggestions = [] } = useUserAvatarSuggestions(); - const { data } = useAvatarSuggestions(); - const suggestions = Object.values(data?.suggestions || {}); + const handleClick = useCallback((suggestion: UserAvatarSuggestion) => () => onSelectOne?.(suggestion), [onSelectOne]); return ( - - {suggestions && - suggestions.length > 0 && - suggestions.map( - (suggestion) => - suggestion.blob && ( - - ), - )} - + <> + {suggestions.map( + (suggestion) => + suggestion.blob && ( + + ), + )} + ); -}; +} export default UserAvatarSuggestions; diff --git a/apps/meteor/client/components/avatar/UserAvatarEditor/readFileAsDataURL.ts b/apps/meteor/client/components/avatar/UserAvatarEditor/readFileAsDataURL.ts new file mode 100644 index 000000000000..d2bb3f7beed1 --- /dev/null +++ b/apps/meteor/client/components/avatar/UserAvatarEditor/readFileAsDataURL.ts @@ -0,0 +1,16 @@ +export const readFileAsDataURL = (file: File) => + new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onloadend = (event) => { + const result = event.target?.result; + if (typeof result === 'string') { + resolve(result); + return; + } + reject(new Error('Failed to read file')); + }; + reader.onerror = (event) => { + reject(new Error(`Failed to read file: ${event}`)); + }; + reader.readAsDataURL(file); + }); diff --git a/apps/meteor/client/components/avatar/UserAvatarEditor/useUserAvatarSuggestions.ts b/apps/meteor/client/components/avatar/UserAvatarEditor/useUserAvatarSuggestions.ts new file mode 100644 index 000000000000..42bb09406d3b --- /dev/null +++ b/apps/meteor/client/components/avatar/UserAvatarEditor/useUserAvatarSuggestions.ts @@ -0,0 +1,12 @@ +import { useEndpoint } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; + +export const useUserAvatarSuggestions = () => { + const getAvatarSuggestions = useEndpoint('GET', '/v1/users.getAvatarSuggestion'); + + return useQuery({ + queryKey: ['account', 'profile', 'avatar-suggestions'], + queryFn: async () => getAvatarSuggestions(), + select: (data) => Object.values(data.suggestions), + }); +}; diff --git a/apps/meteor/client/hooks/roomActions/useOmnichannelExternalFrameRoomAction.ts b/apps/meteor/client/hooks/roomActions/useOmnichannelExternalFrameRoomAction.ts index 7f03b369b449..fe1e704f0b1c 100644 --- a/apps/meteor/client/hooks/roomActions/useOmnichannelExternalFrameRoomAction.ts +++ b/apps/meteor/client/hooks/roomActions/useOmnichannelExternalFrameRoomAction.ts @@ -3,7 +3,7 @@ import { lazy, useMemo } from 'react'; import type { RoomToolboxActionConfig } from '../../views/room/contexts/RoomToolboxContext'; -const ExternalFrameContainer = lazy(() => import('../../../app/livechat/client/externalFrame/ExternalFrameContainer')); +const ExternalFrameContainer = lazy(() => import('../../views/omnichannel/ExternalFrameContainer')); export const useOmnichannelExternalFrameRoomAction = () => { const enabled = useSetting('Omnichannel_External_Frame_Enabled', false); diff --git a/apps/meteor/client/hooks/useAvatarSuggestions.ts b/apps/meteor/client/hooks/useAvatarSuggestions.ts deleted file mode 100644 index 223cab8ca4b4..000000000000 --- a/apps/meteor/client/hooks/useAvatarSuggestions.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { useEndpoint } from '@rocket.chat/ui-contexts'; -import { useQuery } from '@tanstack/react-query'; - -export const useAvatarSuggestions = () => { - const getAvatarSuggestions = useEndpoint('GET', '/v1/users.getAvatarSuggestion'); - - return useQuery(['getAvatarSuggestion'], async () => getAvatarSuggestions()); -}; diff --git a/apps/meteor/client/lib/portals/portalsSubscription.ts b/apps/meteor/client/lib/portals/portalsSubscription.ts index 24295d624936..513393eb983a 100644 --- a/apps/meteor/client/lib/portals/portalsSubscription.ts +++ b/apps/meteor/client/lib/portals/portalsSubscription.ts @@ -1,9 +1,9 @@ import { Emitter } from '@rocket.chat/emitter'; import { Random } from '@rocket.chat/random'; -import type { ReactElement } from 'react'; +import type { ReactPortal } from 'react'; type SubscribedPortal = { - portal: ReactElement; + portal: ReactPortal; key: string; }; @@ -11,7 +11,7 @@ type PortalsSubscription = { subscribe: (callback: () => void) => () => void; getSnapshot: () => SubscribedPortal[]; has: (key: unknown) => boolean; - set: (key: unknown, portal: ReactElement) => void; + set: (key: unknown, portal: ReactPortal) => void; delete: (key: unknown) => void; }; @@ -43,7 +43,7 @@ export const unregisterPortal = (key: unknown): void => { portalsSubscription.delete(key); }; -export const registerPortal = (key: unknown, portal: ReactElement): (() => void) => { +export const registerPortal = (key: unknown, portal: ReactPortal): (() => void) => { portalsSubscription.set(key, portal); return (): void => { unregisterPortal(key); diff --git a/apps/meteor/app/livechat/client/externalFrame/ExternalFrameContainer.tsx b/apps/meteor/client/views/omnichannel/ExternalFrameContainer.tsx similarity index 83% rename from apps/meteor/app/livechat/client/externalFrame/ExternalFrameContainer.tsx rename to apps/meteor/client/views/omnichannel/ExternalFrameContainer.tsx index 5b37ee8de2d2..e339d48af103 100644 --- a/apps/meteor/app/livechat/client/externalFrame/ExternalFrameContainer.tsx +++ b/apps/meteor/client/views/omnichannel/ExternalFrameContainer.tsx @@ -2,11 +2,11 @@ import { useSetting, useUserId } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import React, { useMemo } from 'react'; -import { useRoom } from '../../../../client/views/room/contexts/RoomContext'; -import { sdk } from '../../../utils/client/lib/SDKClient'; -import { encrypt, getKeyFromString } from './crypto'; +import { encrypt, getKeyFromString } from '../../../app/livechat/client/externalFrame/crypto'; +import { sdk } from '../../../app/utils/client/lib/SDKClient'; +import { useRoom } from '../room/contexts/RoomContext'; -const ExternalFrameContainer = () => { +function ExternalFrameContainer() { const uid = useUserId(); const room = useRoom(); const { 'X-Auth-Token': authToken } = sdk.rest.getCredentials() || {}; @@ -42,6 +42,6 @@ const ExternalFrameContainer = () => {