Skip to content

Commit

Permalink
refactor: Uses source instead of room to render the `OmnichannelR…
Browse files Browse the repository at this point in the history
…oomIcon` (#33118)
  • Loading branch information
dougfabris authored and abhinavkrin committed Oct 25, 2024
1 parent cdbca2a commit e17b625
Show file tree
Hide file tree
Showing 10 changed files with 92 additions and 112 deletions.
Original file line number Diff line number Diff line change
@@ -1,53 +1,29 @@
import { UserStatus, type IOmnichannelRoomFromAppSource } from '@rocket.chat/core-typings';
import { type IOmnichannelSourceFromApp } from '@rocket.chat/core-typings';
import { Icon, Box } from '@rocket.chat/fuselage';
import type { ComponentProps, ReactElement } from 'react';
import type { ComponentProps } from 'react';
import React from 'react';

import { AsyncStatePhase } from '../../../lib/asyncState/AsyncStatePhase';
import { useOmnichannelRoomIcon } from './context/OmnichannelRoomIconContext';

const colors = {
busy: 'status-font-on-danger',
away: 'status-font-on-warning',
online: 'status-font-on-success',
offline: 'annotation',
disabled: 'annotation',
type OmnichannelAppSourceRoomIconProps = {
source: IOmnichannelSourceFromApp;
color: ComponentProps<typeof Box>['color'];
size: ComponentProps<typeof Icon>['size'];
placement: 'sidebar' | 'default';
};

const convertBoxSizeToNumber = (boxSize: ComponentProps<typeof Icon>['size']): number => {
switch (boxSize) {
case 'x20': {
return 20;
}
case 'x24': {
return 24;
}
case 'x16':
default: {
return 16;
}
}
};
export const OmnichannelAppSourceRoomIcon = ({ source, color, size, placement }: OmnichannelAppSourceRoomIconProps) => {
const icon = (placement === 'sidebar' && source.sidebarIcon) || source.defaultIcon;
const { phase, value } = useOmnichannelRoomIcon(source.id, icon || '');

export const OmnichannelAppSourceRoomIcon = ({
room,
size = 16,
placement = 'default',
}: {
room: IOmnichannelRoomFromAppSource;
size: ComponentProps<typeof Icon>['size'];
placement: 'sidebar' | 'default';
}): ReactElement => {
const color = colors[room.v.status || UserStatus.OFFLINE];
const icon = (placement === 'sidebar' && room.source.sidebarIcon) || room.source.defaultIcon;
const { phase, value } = useOmnichannelRoomIcon(room.source.id, icon || '');
const fontSize = convertBoxSizeToNumber(size);
if ([AsyncStatePhase.REJECTED, AsyncStatePhase.LOADING].includes(phase)) {
return <Icon name='headset' size={size} color={color} />;
}

return (
<Box size={fontSize} color={color}>
<Box is='svg' size={fontSize} aria-hidden='true'>
<Box size={size} color={color}>
<Box is='svg' size={size} aria-hidden='true'>
<Box is='use' href={`#${value}`} />
</Box>
</Box>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,8 @@
import type { IOmnichannelRoom } from '@rocket.chat/core-typings';
import type { IOmnichannelSource } from '@rocket.chat/core-typings';
import { Icon } from '@rocket.chat/fuselage';
import type { ComponentProps, ReactElement } from 'react';
import type { ComponentProps } from 'react';
import React from 'react';

const colors = {
busy: 'status-font-on-danger',
away: 'status-font-on-warning',
online: 'status-font-on-success',
offline: 'annotation',
disabled: 'annotation',
};

const iconMap = {
widget: 'livechat',
email: 'mail',
Expand All @@ -20,13 +12,13 @@ const iconMap = {
other: 'headset',
} as const;

export const OmnichannelCoreSourceRoomIcon = ({
room,
size = 'x16',
}: {
room: IOmnichannelRoom;
type OmnichannelCoreSourceRoomIconProps = {
source: IOmnichannelSource;
color: ComponentProps<typeof Icon>['color'];
size: ComponentProps<typeof Icon>['size'];
}): ReactElement => {
const icon = iconMap[room.source?.type || 'other'] || 'headset';
return <Icon name={icon} size={size} color={colors[room.v?.status || 'offline']} />;
};

export const OmnichannelCoreSourceRoomIcon = ({ source, color, size }: OmnichannelCoreSourceRoomIconProps) => {
const icon = iconMap[source?.type || 'other'] || 'headset';
return <Icon name={icon} size={size} color={color} />;
};
Original file line number Diff line number Diff line change
@@ -1,23 +1,34 @@
import type { IOmnichannelRoom } from '@rocket.chat/core-typings';
import { isOmnichannelRoomFromAppSource } from '@rocket.chat/core-typings';
import type { IOmnichannelSource } from '@rocket.chat/core-typings';
import { UserStatus, isOmnichannelSourceFromApp } from '@rocket.chat/core-typings';
import type { Icon } from '@rocket.chat/fuselage';
import type { ComponentProps, ReactElement } from 'react';
import type { ComponentProps } from 'react';
import React from 'react';

import { OmnichannelAppSourceRoomIcon } from './OmnichannelAppSourceRoomIcon';
import { OmnichannelCoreSourceRoomIcon } from './OmnichannelCoreSourceRoomIcon';

export const OmnichannelRoomIcon = ({
room,
size,
placement = 'default',
}: {
room: IOmnichannelRoom;
const colors = {
busy: 'status-font-on-danger',
away: 'status-font-on-warning',
online: 'status-font-on-success',
offline: 'annotation',
disabled: 'annotation',
} as const;

type OmnichannelRoomIconProps = {
source: IOmnichannelSource;
color?: ComponentProps<typeof Icon>['color'];
status?: UserStatus;
size: ComponentProps<typeof Icon>['size'];
placement: 'sidebar' | 'default';
}): ReactElement => {
if (isOmnichannelRoomFromAppSource(room)) {
return <OmnichannelAppSourceRoomIcon placement={placement} room={room} size={size} />;
placement?: 'sidebar' | 'default';
};

export const OmnichannelRoomIcon = ({ source, color, status, size = 'x16', placement = 'default' }: OmnichannelRoomIconProps) => {
const iconColor = color ?? colors[status || UserStatus.OFFLINE];

if (isOmnichannelSourceFromApp(source)) {
return <OmnichannelAppSourceRoomIcon source={source} placement={placement} color={iconColor} size={size} />;
}
return <OmnichannelCoreSourceRoomIcon room={room} size={size} />;

return <OmnichannelCoreSourceRoomIcon source={source} color={iconColor} size={size} />;
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import DOMPurify from 'dompurify';

import { sdk } from '../../../../../app/utils/client/lib/SDKClient';

const OmnichannelRoomIcon = new (class extends Emitter {
const OmnichannelRoomIconManager = new (class extends Emitter {
icons = new Map<string, string>();

constructor() {
Expand All @@ -23,7 +23,7 @@ const OmnichannelRoomIcon = new (class extends Emitter {
sdk.rest
.send(`/apps/public/${appId}/get-sidebar-icon?icon=${icon}`, 'GET')
.then((response: any) => {
response.text().then((text: any) => {
response.text().then((text: string) => {
this.icons.set(
`${appId}-${icon}`,
DOMPurify.sanitize(text, {
Expand All @@ -44,4 +44,4 @@ const OmnichannelRoomIcon = new (class extends Emitter {
}
})();

export default OmnichannelRoomIcon;
export default OmnichannelRoomIconManager;
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import { useSyncExternalStore } from 'use-sync-external-store/shim';
import type { AsyncState } from '../../../../lib/asyncState/AsyncState';
import { AsyncStatePhase } from '../../../../lib/asyncState/AsyncStatePhase';
import { OmnichannelRoomIconContext } from '../context/OmnichannelRoomIconContext';
import OmnichannelRoomIcon from '../lib/OmnichannelRoomIcon';
import OmnichannelRoomIconManager from '../lib/OmnichannelRoomIconManager';

let icons = Array.from(OmnichannelRoomIcon.icons.values());
let icons = Array.from(OmnichannelRoomIconManager.icons.values());

type OmnichannelRoomIconProviderProps = {
children?: ReactNode;
Expand All @@ -18,8 +18,8 @@ export const OmnichannelRoomIconProvider = ({ children }: OmnichannelRoomIconPro
const svgIcons = useSyncExternalStore(
useCallback(
(callback): (() => void) =>
OmnichannelRoomIcon.on('change', () => {
icons = Array.from(OmnichannelRoomIcon.icons.values());
OmnichannelRoomIconManager.on('change', () => {
icons = Array.from(OmnichannelRoomIconManager.icons.values());
callback();
}),
[],
Expand All @@ -31,7 +31,7 @@ export const OmnichannelRoomIconProvider = ({ children }: OmnichannelRoomIconPro
<OmnichannelRoomIconContext.Provider
value={useMemo(() => {
const extractSnapshot = (app: string, iconName: string): AsyncState<string> => {
const icon = OmnichannelRoomIcon.get(app, iconName);
const icon = OmnichannelRoomIconManager.get(app, iconName);

if (icon) {
return {
Expand All @@ -57,7 +57,7 @@ export const OmnichannelRoomIconProvider = ({ children }: OmnichannelRoomIconPro
iconName: string,
): [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => AsyncState<string>] => [
(callback): (() => void) =>
OmnichannelRoomIcon.on(`${app}-${iconName}`, () => {
OmnichannelRoomIconManager.on(`${app}-${iconName}`, () => {
snapshots.set(`${app}-${iconName}`, extractSnapshot(app, iconName));

// Then we call the callback (onStoreChange), signaling React to re-render
Expand Down
6 changes: 3 additions & 3 deletions apps/meteor/client/components/RoomIcon/RoomIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { IOmnichannelRoom, IRoom } from '@rocket.chat/core-typings';
import type { IRoom } from '@rocket.chat/core-typings';
import { isOmnichannelRoom } from '@rocket.chat/core-typings';
import { Icon } from '@rocket.chat/fuselage';
import type { ComponentProps, ReactElement } from 'react';
Expand All @@ -24,8 +24,8 @@ export const RoomIcon = ({
return <Icon name='phone' size={size} />;
}

if (isOmnichannelRoom(room as IRoom)) {
return <OmnichannelRoomIcon placement={placement} room={room as IOmnichannelRoom} size={size} />;
if (isOmnichannelRoom(room)) {
return <OmnichannelRoomIcon placement={placement} source={room.source} status={room.v?.status} size={size} />;
}

if (isValidElement<any>(iconPropsOrReactNode)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const SourceField = ({ room }: SourceFieldProps) => {
<Label>{t('Channel')}</Label>
<Info>
<Box display='flex' alignItems='center'>
<OmnichannelRoomIcon room={room} size='x24' placement='default' />
<OmnichannelRoomIcon source={room.source} status={room.v.status} size='x24' />
<Label mi={8} mbe='0'>
{defaultTypesLabels[room.source.type] || roomSource}
</Label>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ type HeaderIconWithRoomProps = {
const HeaderIconWithRoom = ({ room }: HeaderIconWithRoomProps): ReactElement => {
const icon = useRoomIcon(room);
if (isOmnichannelRoom(room)) {
return <OmnichannelRoomIcon room={room} size='x20' placement='default' />;
return <OmnichannelRoomIcon source={room.source} status={room.v?.status} size='x20' />;
}

return <HeaderIcon icon={icon} />;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ type HeaderIconWithRoomProps = {
const HeaderIconWithRoom = ({ room }: HeaderIconWithRoomProps): ReactElement => {
const icon = useRoomIcon(room);
if (isOmnichannelRoom(room)) {
return <OmnichannelRoomIcon room={room} size='x20' placement='default' />;
return <OmnichannelRoomIcon source={room.source} status={room.v?.status} size='x20' />;
}

return <HeaderIcon icon={icon} />;
Expand Down
57 changes: 29 additions & 28 deletions packages/core-typings/src/IRoom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,31 @@ export enum OmnichannelSourceType {
OTHER = 'other', // catch-all source type
}

export interface IOmnichannelSource {
// TODO: looks like this is not so required as the definition suggests
// The source, or client, which created the Omnichannel room
type: OmnichannelSourceType;
// An optional identification of external sources, such as an App
id?: string;
// A human readable alias that goes with the ID, for post analytical purposes
alias?: string;
// A label to be shown in the room info
label?: string;
// The sidebar icon
sidebarIcon?: string;
// The default sidebar icon
defaultIcon?: string;
}

export interface IOmnichannelSourceFromApp extends IOmnichannelSource {
type: OmnichannelSourceType.APP;
id: string;
label: string;
sidebarIcon?: string;
defaultIcon?: string;
alias?: string;
}

export interface IOmnichannelGenericRoom extends Omit<IRoom, 'default' | 'featured' | 'broadcast'> {
t: 'l' | 'v';
v: Pick<ILivechatVisitor, '_id' | 'username' | 'status' | 'name' | 'token' | 'activity'> & {
Expand All @@ -176,21 +201,7 @@ export interface IOmnichannelGenericRoom extends Omit<IRoom, 'default' | 'featur
replyTo: string;
subject: string;
};
source: {
// TODO: looks like this is not so required as the definition suggests
// The source, or client, which created the Omnichannel room
type: OmnichannelSourceType;
// An optional identification of external sources, such as an App
id?: string;
// A human readable alias that goes with the ID, for post analytical purposes
alias?: string;
// A label to be shown in the room info
label?: string;
// The sidebar icon
sidebarIcon?: string;
// The default sidebar icon
defaultIcon?: string;
};
source: IOmnichannelSource;

// Note: this field is used only for email transcripts. For Pdf transcripts, we have a separate field.
transcriptRequest?: IRequestTranscript;
Expand Down Expand Up @@ -324,13 +335,7 @@ export interface IVoipRoom extends IOmnichannelGenericRoom {
}

export interface IOmnichannelRoomFromAppSource extends IOmnichannelRoom {
source: {
type: OmnichannelSourceType.APP;
id: string;
alias?: string;
sidebarIcon?: string;
defaultIcon?: string;
};
source: IOmnichannelSourceFromApp;
}

export type IVoipRoomClosingInfo = Pick<IOmnichannelGenericRoom, 'closer' | 'closedBy' | 'closedAt' | 'tags'> &
Expand All @@ -347,12 +352,8 @@ export const isOmnichannelRoom = (room: Pick<IRoom, 't'>): room is IOmnichannelR

export const isVoipRoom = (room: IRoom): room is IVoipRoom & IRoom => room.t === 'v';

export const isOmnichannelRoomFromAppSource = (room: IOmnichannelRoom): room is IOmnichannelRoomFromAppSource => {
if (!isOmnichannelRoom(room)) {
return false;
}

return room.source?.type === OmnichannelSourceType.APP;
export const isOmnichannelSourceFromApp = (source: IOmnichannelSource): source is IOmnichannelSourceFromApp => {
return source?.type === OmnichannelSourceType.APP;
};

export type RoomAdminFieldsType =
Expand Down

0 comments on commit e17b625

Please sign in to comment.