[NEW] Show Omnichannel room icon based on source definition (#23912)
* feat: adding sidebar icon definition * fix: fixing unit tests and object logic * Fix review * feat: adding source on the create livechat room * A few refactors * Update Apps-Engine version * Added arcane logic to handle icons * chore: removing unused var * Fix merge develop * Update client/hooks/useRoomIcon.tsx * Update Apps-Engine * Change source attribution * Fix prettier error Co-authored-by: Guilherme Gazzo <guilhermegazzo@gmail.com> Co-authored-by: Douglas Gubert <douglas.gubert@gmail.com>pull/23875/head^2
parent
1e2e0257d8
commit
0e2c91bbff
@ -0,0 +1,37 @@ |
||||
import { Icon, Box } from '@rocket.chat/fuselage'; |
||||
import React, { ComponentProps, ReactElement } from 'react'; |
||||
|
||||
import { IOmnichannelRoomFromAppSource } from '../../../../definition/IRoom'; |
||||
import { AsyncStatePhase } from '../../../lib/asyncState/AsyncStatePhase'; |
||||
import { useOmnichannelRoomIcon } from './context/OmnichannelRoomIconContext'; |
||||
|
||||
export const colors = { |
||||
busy: 'danger-500', |
||||
away: 'warning-600', |
||||
online: 'success-500', |
||||
offline: 'neutral-600', |
||||
}; |
||||
|
||||
export const OmnichannelAppSourceRoomIcon = ({ |
||||
room, |
||||
size = 'x16', |
||||
placement = 'default', |
||||
}: { |
||||
room: IOmnichannelRoomFromAppSource; |
||||
size: ComponentProps<typeof Icon>['size']; |
||||
placement: 'sidebar' | 'default'; |
||||
}): ReactElement => { |
||||
const color = colors[room.v.status || 'offline']; |
||||
const icon = (placement === 'sidebar' && room.source.sidebarIcon) || room.source.defaultIcon; |
||||
const { phase, value } = useOmnichannelRoomIcon(room.source.id, icon || ''); |
||||
if ([AsyncStatePhase.REJECTED, AsyncStatePhase.LOADING].includes(phase)) { |
||||
return <Icon name='headset' size={size} color={color} />; |
||||
} |
||||
return ( |
||||
<Box color={color}> |
||||
<svg className='rc-icon rc-input__icon-svg rc-input__icon-svg--plus' aria-hidden='true'> |
||||
<use href={`#${value}`} /> |
||||
</svg> |
||||
</Box> |
||||
); |
||||
}; |
@ -0,0 +1,31 @@ |
||||
import { Icon } from '@rocket.chat/fuselage'; |
||||
import React, { ComponentProps, ReactElement } from 'react'; |
||||
|
||||
import { IOmnichannelRoom } from '../../../../definition/IRoom'; |
||||
|
||||
const colors = { |
||||
busy: 'danger-500', |
||||
away: 'warning-600', |
||||
online: 'success-500', |
||||
offline: 'neutral-600', |
||||
}; |
||||
|
||||
const iconMap = { |
||||
widget: 'livechat', |
||||
email: 'mail', |
||||
sms: 'sms', |
||||
app: 'headset', |
||||
api: 'headset', |
||||
other: 'headset', |
||||
}; |
||||
|
||||
export const OmnichannelCoreSourceRoomIcon = ({ |
||||
room, |
||||
size = 'x16', |
||||
}: { |
||||
room: IOmnichannelRoom; |
||||
size: ComponentProps<typeof Icon>['size']; |
||||
}): ReactElement => { |
||||
const icon = iconMap[room.source.type] || 'headset'; |
||||
return <Icon name={icon} size={size} color={colors[room.v.status || 'offline']} />; |
||||
}; |
@ -0,0 +1,21 @@ |
||||
import { Icon } from '@rocket.chat/fuselage'; |
||||
import React, { ComponentProps, ReactElement } from 'react'; |
||||
|
||||
import { IOmnichannelRoom, isOmnichannelRoomFromAppSource } from '../../../../definition/IRoom'; |
||||
import { OmnichannelAppSourceRoomIcon } from './OmnichannelAppSourceRoomIcon'; |
||||
import { OmnichannelCoreSourceRoomIcon } from './OmnichannelCoreSourceRoomIcon'; |
||||
|
||||
export const OmnichannelRoomIcon = ({ |
||||
room, |
||||
size = 'x16', |
||||
placement = 'default', |
||||
}: { |
||||
room: IOmnichannelRoom; |
||||
size: ComponentProps<typeof Icon>['size']; |
||||
placement: 'sidebar' | 'default'; |
||||
}): ReactElement => { |
||||
if (isOmnichannelRoomFromAppSource(room)) { |
||||
return <OmnichannelAppSourceRoomIcon placement={placement} room={room} size={size} />; |
||||
} |
||||
return <OmnichannelCoreSourceRoomIcon room={room} size={size} />; |
||||
}; |
@ -0,0 +1,31 @@ |
||||
import { createContext, useMemo, useContext } from 'react'; |
||||
import { useSubscription, Unsubscribe } from 'use-subscription'; |
||||
|
||||
import { AsyncState } from '../../../../lib/asyncState/AsyncState'; |
||||
import { AsyncStatePhase } from '../../../../lib/asyncState/AsyncStatePhase'; |
||||
|
||||
type IOmnichannelRoomIconContext = { |
||||
queryIcon( |
||||
app: string, |
||||
icon: string, |
||||
): { |
||||
getCurrentValue: () => AsyncState<string>; |
||||
subscribe: (callback: () => void) => Unsubscribe; |
||||
}; |
||||
}; |
||||
|
||||
export const OmnichannelRoomIconContext = createContext<IOmnichannelRoomIconContext>({ |
||||
queryIcon: () => ({ |
||||
getCurrentValue: (): AsyncState<string> => ({ |
||||
phase: AsyncStatePhase.LOADING, |
||||
value: undefined, |
||||
error: undefined, |
||||
}), |
||||
subscribe: (): Unsubscribe => (): void => undefined, |
||||
}), |
||||
}); |
||||
|
||||
export const useOmnichannelRoomIcon = (app: string, icon: string): AsyncState<string> => { |
||||
const { queryIcon } = useContext(OmnichannelRoomIconContext); |
||||
return useSubscription(useMemo(() => queryIcon(app, icon), [app, queryIcon, icon])); |
||||
}; |
@ -0,0 +1 @@ |
||||
export * from './OmnichannelRoomIcon'; |
@ -0,0 +1,37 @@ |
||||
import { Emitter } from '@rocket.chat/emitter'; |
||||
import DOMPurify from 'dompurify'; |
||||
|
||||
import { APIClient } from '../../../../../app/utils/client'; |
||||
|
||||
const OmnichannelRoomIcon = new (class extends Emitter { |
||||
icons = new Map<string, string>(); |
||||
|
||||
constructor() { |
||||
super(); |
||||
} |
||||
|
||||
public get(appId: string, icon: string): string | undefined { |
||||
if (!appId || !icon) { |
||||
return; |
||||
} |
||||
if (this.icons.has(`${appId}-${icon}`)) { |
||||
return `${appId}-${icon}`; |
||||
} |
||||
APIClient.get(`apps/public/${appId}/get-sidebar-icon`, { icon }).then((response) => { |
||||
this.icons.set( |
||||
`${appId}-${icon}`, |
||||
DOMPurify.sanitize(response, { |
||||
FORBID_ATTR: ['id'], |
||||
NAMESPACE: 'http://www.w3.org/2000/svg', |
||||
USE_PROFILES: { svg: true, svgFilters: true }, |
||||
}) |
||||
.replace(`<svg`, `<symbol id="${appId}-${icon}"`) |
||||
.replace(`</svg>`, '</symbol>'), |
||||
); |
||||
this.emit('change'); |
||||
this.emit(`${appId}-${icon}`); |
||||
}); |
||||
} |
||||
})(); |
||||
|
||||
export default OmnichannelRoomIcon; |
@ -0,0 +1,62 @@ |
||||
import React, { FC, useMemo } from 'react'; |
||||
import { createPortal } from 'react-dom'; |
||||
import { useSubscription, Subscription } from 'use-subscription'; |
||||
|
||||
import { AsyncState } from '../../../../lib/asyncState/AsyncState'; |
||||
import { AsyncStatePhase } from '../../../../lib/asyncState/AsyncStatePhase'; |
||||
import { OmnichannelRoomIconContext } from '../context/OmnichannelRoomIconContext'; |
||||
import OmnichannelRoomIcon from '../lib/OmnichannelRoomIcon'; |
||||
|
||||
export const OmnichannelRoomIconProvider: FC = ({ children }) => { |
||||
const svgIcons = useSubscription( |
||||
useMemo( |
||||
() => ({ |
||||
getCurrentValue: (): string[] => Array.from(OmnichannelRoomIcon.icons.values()), |
||||
subscribe: (callback): (() => void) => OmnichannelRoomIcon.on('change', callback), |
||||
}), |
||||
[], |
||||
), |
||||
); |
||||
return ( |
||||
<OmnichannelRoomIconContext.Provider |
||||
value={useMemo( |
||||
() => ({ |
||||
queryIcon: (app: string, iconName: string): Subscription<AsyncState<string>> => ({ |
||||
getCurrentValue: (): AsyncState<string> => { |
||||
const icon = OmnichannelRoomIcon.get(app, iconName); |
||||
|
||||
if (!icon) { |
||||
return { |
||||
phase: AsyncStatePhase.LOADING, |
||||
value: undefined, |
||||
error: undefined, |
||||
}; |
||||
} |
||||
|
||||
return { |
||||
phase: AsyncStatePhase.RESOLVED, |
||||
value: icon, |
||||
error: undefined, |
||||
}; |
||||
}, |
||||
subscribe: (callback): (() => void) => |
||||
OmnichannelRoomIcon.on(`${app}-${iconName}`, callback), |
||||
}), |
||||
}), |
||||
[], |
||||
)} |
||||
> |
||||
{createPortal( |
||||
<svg |
||||
xmlns='http://www.w3.org/2000/svg' |
||||
xmlnsXlink='http://www.w3.org/1999/xlink' |
||||
style={{ display: 'none' }} |
||||
dangerouslySetInnerHTML={{ __html: svgIcons.join('') }} |
||||
/>, |
||||
document.body, |
||||
'custom-icons', |
||||
)} |
||||
{children} |
||||
</OmnichannelRoomIconContext.Provider> |
||||
); |
||||
}; |
@ -0,0 +1,45 @@ |
||||
import { Icon } from '@rocket.chat/fuselage'; |
||||
import React, { ComponentProps, FC } from 'react'; |
||||
|
||||
import { IRoom, isDirectMessageRoom, isOmnichannelRoom } from '../../../definition/IRoom'; |
||||
import { ReactiveUserStatus } from '../UserStatus'; |
||||
import { OmnichannelRoomIcon } from './OmnichannelRoomIcon'; |
||||
|
||||
export const RoomIcon: FC<{ |
||||
room: IRoom; |
||||
size: ComponentProps<typeof Icon>['size']; |
||||
highlighted?: boolean; |
||||
placement: 'sidebar' | 'default'; |
||||
}> = ({ room, size = 'x16', placement }) => { |
||||
if (room.prid) { |
||||
return <Icon name='baloons' size={size} />; |
||||
} |
||||
|
||||
if (room.teamMain) { |
||||
return <Icon name={room.t === 'p' ? 'team-lock' : 'team'} size={size} />; |
||||
} |
||||
|
||||
if (isOmnichannelRoom(room)) { |
||||
return <OmnichannelRoomIcon placement={placement} room={room} size={size} />; |
||||
} |
||||
if (isDirectMessageRoom(room)) { |
||||
if (room.uids && room.uids.length > 2) { |
||||
return <Icon name='balloon' size={size} />; |
||||
} |
||||
if (room.uids && room.uids.length > 0) { |
||||
return ( |
||||
<ReactiveUserStatus uid={room.uids.filter((uid) => uid !== room.u._id)[0] || room.u._id} /> |
||||
); |
||||
} |
||||
return <Icon name='at' size={size} />; |
||||
} |
||||
|
||||
switch (room.t) { |
||||
case 'p': |
||||
return <Icon name='hashtag-lock' size={size} />; |
||||
case 'c': |
||||
return <Icon name='hash' size={size} />; |
||||
default: |
||||
return null; |
||||
} |
||||
}; |
@ -0,0 +1 @@ |
||||
export * from './RoomIcon'; |
@ -1,16 +0,0 @@ |
||||
import { Sidebar } from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
|
||||
import { useRoomIcon } from '../../hooks/useRoomIcon'; |
||||
|
||||
const SidebarIcon = ({ room, highlighted }) => { |
||||
const icon = useRoomIcon(room); |
||||
|
||||
return ( |
||||
<Sidebar.Item.Icon highlighted={highlighted} {...(icon.name && icon)}> |
||||
{!icon.name && icon} |
||||
</Sidebar.Item.Icon> |
||||
); |
||||
}; |
||||
|
||||
export default SidebarIcon; |
Loading…
Reference in new issue