snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
import type { IOmnichannelSourceFromApp } from '@rocket.chat/core-typings';
|
||||
import { Icon, Box } from '@rocket.chat/fuselage';
|
||||
import type { ComponentProps } from 'react';
|
||||
|
||||
import { useOmnichannelRoomIcon } from './context/OmnichannelRoomIconContext';
|
||||
|
||||
export type OmnichannelAppSourceRoomIconProps = {
|
||||
source: IOmnichannelSourceFromApp;
|
||||
color: ComponentProps<typeof Box>['color'];
|
||||
size: ComponentProps<typeof Icon>['size'];
|
||||
placement: 'sidebar' | 'default';
|
||||
};
|
||||
|
||||
export const OmnichannelAppSourceRoomIcon = ({ source, color, size, placement }: OmnichannelAppSourceRoomIconProps) => {
|
||||
const icon = (placement === 'sidebar' && source.sidebarIcon) || source.defaultIcon;
|
||||
const value = useOmnichannelRoomIcon(source.id, icon || '');
|
||||
|
||||
if (!value) {
|
||||
return <Icon name='headset' size={size} color={color} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box size={size} color={color}>
|
||||
<Box is='svg' size={size} aria-hidden='true'>
|
||||
<Box is='use' href={`#${value}`} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { IOmnichannelSource } from '@rocket.chat/core-typings';
|
||||
import { Icon } from '@rocket.chat/fuselage';
|
||||
import type { ComponentProps } from 'react';
|
||||
|
||||
const iconMap = {
|
||||
widget: 'livechat',
|
||||
email: 'mail',
|
||||
sms: 'sms',
|
||||
app: 'headset',
|
||||
api: 'headset',
|
||||
other: 'headset',
|
||||
} as const;
|
||||
|
||||
export type OmnichannelCoreSourceRoomIconProps = {
|
||||
source: IOmnichannelSource;
|
||||
color: ComponentProps<typeof Icon>['color'];
|
||||
size: ComponentProps<typeof Icon>['size'];
|
||||
};
|
||||
|
||||
export const OmnichannelCoreSourceRoomIcon = ({ source, color, size }: OmnichannelCoreSourceRoomIconProps) => {
|
||||
const icon = iconMap[source?.type || 'other'] || 'headset';
|
||||
return <Icon name={icon} size={size} color={color} />;
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
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 } from 'react';
|
||||
|
||||
import { OmnichannelAppSourceRoomIcon } from './OmnichannelAppSourceRoomIcon';
|
||||
import { OmnichannelCoreSourceRoomIcon } from './OmnichannelCoreSourceRoomIcon';
|
||||
|
||||
const colors = {
|
||||
busy: 'status-font-on-danger',
|
||||
away: 'status-font-on-warning',
|
||||
online: 'status-font-on-success',
|
||||
offline: 'annotation',
|
||||
disabled: 'annotation',
|
||||
} as const;
|
||||
|
||||
export type OmnichannelRoomIconProps = {
|
||||
source: IOmnichannelSource;
|
||||
color?: ComponentProps<typeof Icon>['color'];
|
||||
status?: UserStatus;
|
||||
size: ComponentProps<typeof Icon>['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 source={source} color={iconColor} size={size} />;
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import { createContext, useMemo, useContext, useSyncExternalStore } from 'react';
|
||||
|
||||
type IOmnichannelRoomIconContext = {
|
||||
queryIcon(app: string, icon: string): [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => string | undefined];
|
||||
};
|
||||
|
||||
export const OmnichannelRoomIconContext = createContext<IOmnichannelRoomIconContext>({
|
||||
queryIcon: () => [(): (() => void) => (): void => undefined, () => undefined],
|
||||
});
|
||||
|
||||
export const useOmnichannelRoomIcon = (app: string, icon: string): string | undefined => {
|
||||
const { queryIcon } = useContext(OmnichannelRoomIconContext);
|
||||
const [subscribe, getSnapshot] = useMemo(() => queryIcon(app, icon), [app, queryIcon, icon]);
|
||||
return useSyncExternalStore(subscribe, getSnapshot);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './OmnichannelRoomIcon';
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Emitter } from '@rocket.chat/emitter';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
import { sdk } from '../../../../lib/SDKClient';
|
||||
|
||||
const OmnichannelRoomIconManager = 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}`;
|
||||
}
|
||||
// TODO: update the apps icons to send JSON instead of a string. This will allow us to use APIClient.get()
|
||||
|
||||
sdk.rest
|
||||
.send(`/apps/public/${appId}/get-sidebar-icon?icon=${icon}`, 'GET')
|
||||
.then((response: any) => {
|
||||
response.text().then((text: string) => {
|
||||
this.icons.set(
|
||||
`${appId}-${icon}`,
|
||||
DOMPurify.sanitize(text, {
|
||||
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}`);
|
||||
});
|
||||
})
|
||||
.catch((error: any) => {
|
||||
console.error('error from get-sidebar-icon', error);
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
export default OmnichannelRoomIconManager;
|
||||
@@ -0,0 +1,81 @@
|
||||
import DOMPurify from 'dompurify';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useCallback, useMemo, useSyncExternalStore } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import { OmnichannelRoomIconContext } from '../context/OmnichannelRoomIconContext';
|
||||
import OmnichannelRoomIconManager from '../lib/OmnichannelRoomIconManager';
|
||||
|
||||
let icons = Array.from(OmnichannelRoomIconManager.icons.values());
|
||||
|
||||
export type OmnichannelRoomIconProviderProps = {
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export const OmnichannelRoomIconProvider = ({ children }: OmnichannelRoomIconProviderProps) => {
|
||||
const svgIcons = useSyncExternalStore(
|
||||
useCallback(
|
||||
(callback): (() => void) =>
|
||||
OmnichannelRoomIconManager.on('change', () => {
|
||||
icons = Array.from(OmnichannelRoomIconManager.icons.values());
|
||||
callback();
|
||||
}),
|
||||
[],
|
||||
),
|
||||
(): string[] => icons,
|
||||
);
|
||||
|
||||
return (
|
||||
<OmnichannelRoomIconContext.Provider
|
||||
value={useMemo(() => {
|
||||
const extractSnapshot = (app: string, iconName: string) => OmnichannelRoomIconManager.get(app, iconName);
|
||||
|
||||
// We cache all the icons here, so that we can use them in the OmnichannelRoomIcon component
|
||||
const snapshots = new Map<string, string | undefined>();
|
||||
|
||||
return {
|
||||
queryIcon: (
|
||||
app: string,
|
||||
iconName: string,
|
||||
): [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => string | undefined] => [
|
||||
(callback): (() => void) =>
|
||||
OmnichannelRoomIconManager.on(`${app}-${iconName}`, () => {
|
||||
snapshots.set(`${app}-${iconName}`, extractSnapshot(app, iconName));
|
||||
|
||||
// Then we call the callback (onStoreChange), signaling React to re-render
|
||||
callback();
|
||||
}),
|
||||
|
||||
// No problem here, because it's return value is a cached in the snapshots map on subsequent calls
|
||||
() => {
|
||||
let snapshot = snapshots.get(`${app}-${iconName}`);
|
||||
|
||||
if (!snapshot) {
|
||||
snapshot = extractSnapshot(app, iconName);
|
||||
snapshots.set(`${app}-${iconName}`, snapshot);
|
||||
}
|
||||
|
||||
return snapshot;
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [])}
|
||||
>
|
||||
{createPortal(
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
xmlnsXlink='http://www.w3.org/1999/xlink'
|
||||
style={{ display: 'none' }}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: DOMPurify.sanitize(`<svg>${svgIcons.join('')}</svg>`, {
|
||||
USE_PROFILES: { svg: true, svgFilters: true },
|
||||
}).slice(5, -6),
|
||||
}}
|
||||
/>,
|
||||
document.body,
|
||||
'custom-icons',
|
||||
)}
|
||||
{children}
|
||||
</OmnichannelRoomIconContext.Provider>
|
||||
);
|
||||
};
|
||||
40
apps/meteor/client/components/RoomIcon/RoomIcon.tsx
Normal file
40
apps/meteor/client/components/RoomIcon/RoomIcon.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { IRoom } from '@rocket.chat/core-typings';
|
||||
import { isOmnichannelRoom } from '@rocket.chat/core-typings';
|
||||
import { Icon } from '@rocket.chat/fuselage';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { isValidElement } from 'react';
|
||||
|
||||
import { OmnichannelRoomIcon } from './OmnichannelRoomIcon';
|
||||
import { useRoomIcon } from '../../hooks/useRoomIcon';
|
||||
|
||||
export const RoomIcon = ({
|
||||
room,
|
||||
size = 'x16',
|
||||
isIncomingCall,
|
||||
placement = 'default',
|
||||
}: {
|
||||
room: Pick<IRoom, 't' | 'prid' | 'teamMain' | 'uids' | 'u'>;
|
||||
size?: ComponentProps<typeof Icon>['size'];
|
||||
isIncomingCall?: boolean;
|
||||
placement?: 'sidebar' | 'default';
|
||||
}) => {
|
||||
const iconPropsOrReactNode = useRoomIcon(room);
|
||||
|
||||
if (isIncomingCall) {
|
||||
return <Icon name='phone' size={size} />;
|
||||
}
|
||||
|
||||
if (isOmnichannelRoom(room)) {
|
||||
return <OmnichannelRoomIcon placement={placement} source={room.source} status={room.v?.status} size={size} />;
|
||||
}
|
||||
|
||||
if (isValidElement<any>(iconPropsOrReactNode)) {
|
||||
return iconPropsOrReactNode;
|
||||
}
|
||||
|
||||
if (!iconPropsOrReactNode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Icon {...iconPropsOrReactNode} size={size} />;
|
||||
};
|
||||
1
apps/meteor/client/components/RoomIcon/index.tsx
Normal file
1
apps/meteor/client/components/RoomIcon/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export * from './RoomIcon';
|
||||
Reference in New Issue
Block a user