Skip to content

Commit

Permalink
refactor(client): Move components from app/ to client/ (RocketCha…
Browse files Browse the repository at this point in the history
  • Loading branch information
tassoevan authored Jan 15, 2024
1 parent 5deac49 commit 65772e4
Show file tree
Hide file tree
Showing 27 changed files with 241 additions and 213 deletions.
Original file line number Diff line number Diff line change
@@ -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<typeof UserCard>;
type UserCardProps = ReturnType<ComponentProps<typeof UserCardHolder>['getProps']>;

let props: UserCardProps;

Expand All @@ -29,16 +27,6 @@ const subscribeToProps = (callback: () => void) => {
};
};

const UserCardWithProps = () => {
const props = useSyncExternalStore(subscribeToProps, getProps);

return (
<Suspense fallback={null}>
<UserCard {...props} />
</Suspense>
);
};

const createContainer = () => {
const container = document.createElement('div');
container.id = 'react-user-card';
Expand Down Expand Up @@ -67,8 +55,8 @@ export const openUserCard = (params: Omit<UserCardProps, 'onClose'>) => {
}

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);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,25 @@
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';

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;
disabled?: boolean;
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('');
Expand All @@ -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],
);
Expand All @@ -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 (
<Box display='flex' flexDirection='column' fontScale='p2m' color='default'>
{t('Profile_picture')}
Expand All @@ -81,32 +84,30 @@ function UserAvatarEditor({ currentUsername, username, setAvatarObj, disabled, e
/>
<Box display='flex' flexDirection='column' flexGrow='1' justifyContent='space-between' mis={4}>
<Box display='flex' flexDirection='row' mbs='none'>
<Margins inline={4}>
<Button square mis='none' onClick={clickReset} disabled={disabled} mie={4} title={t('Accounts_SetDefaultAvatar')}>
<Avatar url={`/avatar/%40${username}`} />
</Button>
<IconButton icon='upload' secondary onClick={clickUpload} disabled={disabled} title={t('Upload')} />
<IconButton
data-qa-id='UserAvatarEditorSetAvatarLink'
icon='permalink'
secondary
onClick={clickUrl}
disabled={disabled || !avatarFromUrl}
title={t('Add_URL')}
/>
<UserAvatarSuggestions setAvatarObj={setAvatarObj} setNewAvatarSource={setNewAvatarSource} disabled={disabled} />
</Margins>
</Box>
<Margins inlineStart={4}>
<Box>{t('Use_url_for_avatar')}</Box>
<TextInput
data-qa-id='UserAvatarEditorLink'
flexGrow={0}
placeholder={t('Use_url_for_avatar')}
value={avatarFromUrl}
onChange={handleAvatarFromUrlChange}
<Button square disabled={disabled} mi={4} title={t('Accounts_SetDefaultAvatar')} onClick={clickReset}>
<Avatar url={`/avatar/%40${username}`} />
</Button>
<IconButton icon='upload' secondary disabled={disabled} title={t('Upload')} mi={4} onClick={clickUpload} />
<IconButton
icon='permalink'
secondary
disabled={disabled || !avatarFromUrl}
title={t('Add_URL')}
mi={4}
onClick={clickUrl}
data-qa-id='UserAvatarEditorSetAvatarLink'
/>
</Margins>
<UserAvatarSuggestions disabled={disabled} onSelectOne={handleSelectSuggestion} />
</Box>
<Box mis={4}>{t('Use_url_for_avatar')}</Box>
<TextInput
data-qa-id='UserAvatarEditorLink'
flexGrow={0}
placeholder={t('Use_url_for_avatar')}
value={avatarFromUrl}
mis={4}
onChange={handleAvatarFromUrlChange}
/>
</Box>
</Box>
</Box>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type UserAvatarSuggestion = {
blob: string;
contentType: string;
service: string;
url: string;
};
Original file line number Diff line number Diff line change
@@ -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 (
<Margins inline='x4'>
{suggestions &&
suggestions.length > 0 &&
suggestions.map(
(suggestion) =>
suggestion.blob && (
<Button key={suggestion.service} disabled={disabled} square onClick={handleClick(suggestion)}>
<Box mie={4}>
<Avatar title={suggestion.service} url={suggestion.blob as unknown as string} />
</Box>
</Button>
),
)}
</Margins>
<>
{suggestions.map(
(suggestion) =>
suggestion.blob && (
<Button key={suggestion.service} square disabled={disabled} mi={4} onClick={handleClick(suggestion)}>
<Avatar title={suggestion.service} url={suggestion.blob} />
</Button>
),
)}
</>
);
};
}

export default UserAvatarSuggestions;
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const readFileAsDataURL = (file: File) =>
new Promise<string>((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);
});
Original file line number Diff line number Diff line change
@@ -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),
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
8 changes: 0 additions & 8 deletions apps/meteor/client/hooks/useAvatarSuggestions.ts

This file was deleted.

8 changes: 4 additions & 4 deletions apps/meteor/client/lib/portals/portalsSubscription.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
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;
};

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;
};

Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() || {};
Expand Down Expand Up @@ -42,6 +42,6 @@ const ExternalFrameContainer = () => {
<iframe className='external-frame' src={externalFrameUrl} />
</div>
);
};
}

export default ExternalFrameContainer;
22 changes: 22 additions & 0 deletions apps/meteor/client/views/room/UserCardHolder.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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<typeof UserCard>;
subscribeToProps: (callback: () => void) => () => void;
};

function UserCardHolder({ getProps, subscribeToProps }: UserCardHolderProps) {
const props = useSyncExternalStore(subscribeToProps, getProps);

return (
<Suspense fallback={null}>
<UserCard {...props} />
</Suspense>
);
}

export default UserCardHolder;
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export type ComposerBoxPopupProps<
renderItem?: ({ item }: { item: T }) => ReactElement;
};

const ComposerBoxPopup = <
function ComposerBoxPopup<
T extends {
_id: string;
sort?: number;
Expand All @@ -29,7 +29,7 @@ const ComposerBoxPopup = <
focused,
select,
renderItem = ({ item }: { item: T }) => <>{JSON.stringify(item)}</>,
}: ComposerBoxPopupProps<T>): ReactElement | null => {
}: ComposerBoxPopupProps<T>): ReactElement | null {
const t = useTranslation();
const id = useUniqueId();
const composerBoxPopupRef = useRef<HTMLElement>(null);
Expand Down Expand Up @@ -105,6 +105,6 @@ const ComposerBoxPopup = <
</Tile>
</Box>
);
};
}

export default memo(ComposerBoxPopup);
Loading

0 comments on commit 65772e4

Please sign in to comment.