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

This commit is contained in:
upstream-mirror
2026-09-16 01:21:02 +05:00
commit 30810eecd7
10161 changed files with 987110 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1 @@
export * from './OmnichannelRoomIcon';

View File

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

View File

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

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

View File

@@ -0,0 +1 @@
export * from './RoomIcon';