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 e4fd5b343140b..90b3d4ec52014 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 ae03418be7555..aec9961e8631d 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 0000000000000..e2ac26c6d0f37 --- /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 04b0c92acd957..4ccb7d304683a 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 0000000000000..d2bb3f7beed18 --- /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 0000000000000..42bb09406d3bc --- /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 7f03b369b449d..fe1e704f0b1ca 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 223cab8ca4b40..0000000000000 --- 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 24295d624936a..513393eb983a9 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 5b37ee8de2d20..e339d48af1034 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 = () => {