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
Code scanning - action / CodeQL-Build (push) Failing after 2s
Auto-close duplicate issues / auto-close-duplicates (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,30 @@
import type { DistributiveOmit } from '@rocket.chat/core-typings';
import type * as UiKit from '@rocket.chat/ui-kit';
import { createContext } from 'react';
/**
* An action manager is responsible for handling interactions with the UiKit.
*/
export interface IActionManager {
on(viewId: string, listener: (data: any) => void): void;
on(eventName: 'busy', listener: ({ busy }: { busy: boolean }) => void): void;
off(viewId: string, listener: (data: any) => any): void;
off(eventName: 'busy', listener: ({ busy }: { busy: boolean }) => void): void;
notifyBusy(): void;
notifyIdle(): void;
generateTriggerId(appId: string | undefined): string;
emitInteraction(appId: string, userInteraction: DistributiveOmit<UiKit.UserInteraction, 'triggerId'>): Promise<void>;
handleServerInteraction(interaction: UiKit.ServerInteraction): UiKit.ServerInteraction['type'] | undefined;
getInteractionPayloadByViewId(viewId: UiKit.ContextualBarView['id']):
| {
view: UiKit.ContextualBarView;
}
| undefined;
openView(surface: 'modal', view: UiKit.ModalView): void;
openView(surface: 'banner', view: UiKit.BannerView): void;
openView(surface: 'contextual_bar', view: UiKit.ContextualBarView): void;
// eslint-disable-next-line @typescript-eslint/no-duplicate-type-constituents
disposeView(viewId: UiKit.ModalView['id'] | UiKit.BannerView['viewId'] | UiKit.ContextualBarView['id']): void;
}
export const ActionManagerContext = createContext<IActionManager | undefined>(undefined);

View File

@@ -0,0 +1,21 @@
import { createContext } from 'react';
export type AttachmentContextValue = {
getURL: (url: string) => string;
dimensions: {
width: number;
height: number;
};
collapsedByDefault: boolean;
autoLoadEmbedMedias: boolean;
};
export const AttachmentContext = createContext<AttachmentContextValue>({
getURL: (url: string) => url,
dimensions: {
width: 368,
height: 368,
},
collapsedByDefault: false,
autoLoadEmbedMedias: true,
});

View File

@@ -0,0 +1,51 @@
import type { LoginServiceConfiguration } from '@rocket.chat/core-typings';
import { createContext } from 'react';
export type LoginService = LoginServiceConfiguration & {
icon?: string;
title?: string;
};
export type AuthenticationContextValue = {
readonly isLoggingIn: boolean;
loginWithPassword: (user: string | { username: string } | { email: string } | { id: string }, password: string) => Promise<void>;
loginWithToken: (user: string, callback?: (error: Error | null | undefined) => void) => Promise<void>;
loginWithService<T extends LoginServiceConfiguration>(service: T): () => Promise<true>;
loginWithCustomOauth: (service: string, options: { redirectUrl: string }, callback?: (response: unknown) => void) => void;
loginWithIframe: (token: string, callback?: (error: Error | null | undefined) => void) => Promise<void>;
loginWithTokenRoute: (token: string, callback?: (error: Error | null | undefined) => void) => Promise<void>;
getLoginToken: () => string | null;
unstoreLoginToken: (callback: () => void) => () => void;
wipeLocalAuth: () => void;
queryLoginServices: {
getCurrentValue: () => LoginService[];
subscribe: (onStoreChange: () => void) => () => void;
};
};
export const AuthenticationContext = createContext<AuthenticationContextValue>({
isLoggingIn: false,
loginWithService: () => () => Promise.reject(new Error('loginWithService not implemented')),
loginWithCustomOauth: () => {
throw new Error('loginWithCustomOauth not implemented');
},
loginWithPassword: async () => Promise.reject(new Error('loginWithPassword not implemented')),
loginWithToken: async () => Promise.reject(new Error('loginWithToken not implemented')),
loginWithIframe: async () => Promise.reject(new Error('loginWithIframe not implemented')),
loginWithTokenRoute: async () => Promise.reject(new Error('loginWithTokenRoute not implemented')),
getLoginToken: () => {
throw new Error('getLoginToken not implemented');
},
unstoreLoginToken: () => {
throw new Error('unstoreLoginToken not implemented');
},
wipeLocalAuth: () => {
throw new Error('wipeLocalAuth not implemented');
},
queryLoginServices: {
getCurrentValue: () => [],
subscribe: (_: () => void) => {
throw new Error('queryLoginServices not implemented');
},
},
});

View File

@@ -0,0 +1,38 @@
import type { IRole, IRoom } from '@rocket.chat/core-typings';
import type { ObjectId } from 'mongodb';
import { createContext } from 'react';
export type AuthorizationContextValue = {
queryPermission(
permission: string | ObjectId,
scope?: string | ObjectId,
scopedRoles?: IRole['_id'][],
): [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => boolean];
queryAtLeastOnePermission(
permission: (string | ObjectId)[],
scope?: string | ObjectId,
scopedRoles?: IRole['_id'][],
): [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => boolean];
queryAllPermissions(
permission: (string | ObjectId)[],
scope?: string | ObjectId,
scopedRoles?: IRole['_id'][],
): [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => boolean];
queryRole(
role: string | ObjectId,
scope?: IRoom['_id'],
): [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => boolean];
getRoles(): ReadonlyMap<IRole['_id'], IRole>;
subscribeToRoles(callback: () => void): () => void;
};
const dummyRolesMap: ReadonlyMap<IRole['_id'], IRole> = new Map();
export const AuthorizationContext = createContext<AuthorizationContextValue>({
queryPermission: () => [() => (): void => undefined, (): boolean => false],
queryAtLeastOnePermission: () => [() => (): void => undefined, (): boolean => false],
queryAllPermissions: () => [() => (): void => undefined, (): boolean => false],
queryRole: () => [() => (): void => undefined, (): boolean => false],
getRoles: (): ReadonlyMap<IRole['_id'], IRole> => dummyRolesMap,
subscribeToRoles: (): (() => void) => (): void => undefined,
});

View File

@@ -0,0 +1,17 @@
import { createContext } from 'react';
const dummy = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQYV2Oora39DwAFaQJ3y3rKeAAAAABJRU5ErkJggg==';
export type AvatarUrlContextValue = {
getUserPathAvatar: {
(username: string, etag?: string | null): string;
(params: { userId: string; etag?: string | null }): string;
(params: { username: string; etag?: string | null }): string;
};
getRoomPathAvatar: (...args: any) => string;
};
export const AvatarUrlContext = createContext<AvatarUrlContextValue>({
getUserPathAvatar: () => dummy,
getRoomPathAvatar: () => dummy,
});

View File

@@ -0,0 +1,68 @@
import type { ICustomSound } from '@rocket.chat/core-typings';
import { createContext } from 'react';
export type CustomSoundContextValue = {
play: (
soundId: string,
options?: {
volume?: number | undefined;
loop?: boolean | undefined;
},
) => void;
pause: (sound: ICustomSound['_id']) => void;
stop: (sound: ICustomSound['_id']) => void;
callSounds: {
playRinger: () => void;
playDialer: () => void;
stopRinger: () => void;
stopDialer: () => void;
};
voipSounds: {
playRinger: () => void;
playDialer: () => void;
playCallEnded: () => void;
stopRinger: () => void;
stopDialer: () => void;
stopCallEnded: () => void;
stopAll: () => void;
};
notificationSounds: {
playNewRoom: () => void;
playNewRoomLoop: () => void;
playNewMessage: () => void;
stopNewRoom: () => void;
stopNewMessage: () => void;
playNewMessageCustom: (soundId: ICustomSound['_id']) => void;
};
list: Omit<ICustomSound, '_updatedAt'>[];
};
export const CustomSoundContext = createContext<CustomSoundContextValue>({
play: () => undefined,
pause: () => undefined,
stop: () => undefined,
callSounds: {
playRinger: () => undefined,
playDialer: () => undefined,
stopRinger: () => undefined,
stopDialer: () => undefined,
},
voipSounds: {
playRinger: () => undefined,
playDialer: () => undefined,
playCallEnded: () => undefined,
stopRinger: () => undefined,
stopDialer: () => undefined,
stopCallEnded: () => undefined,
stopAll: () => undefined,
},
notificationSounds: {
playNewRoom: () => undefined,
playNewRoomLoop: () => undefined,
playNewMessage: () => undefined,
stopNewRoom: () => undefined,
stopNewMessage: () => undefined,
playNewMessageCustom: () => undefined,
},
list: [],
});

View File

@@ -0,0 +1,34 @@
import { createContext } from 'react';
export type Device = {
id: string;
label: string;
type: string;
};
type EnabledDeviceContextValue = {
enabled: true;
availableAudioOutputDevices: Device[];
availableAudioInputDevices: Device[];
// availableVideoInputDevices: Device[]
selectedAudioOutputDevice?: Device;
selectedAudioInputDevice?: Device;
// selectedVideoInputDevice?: Device;
setAudioOutputDevice: (data: { outputDevice: Device; HTMLAudioElement: HTMLAudioElement }) => void;
setAudioInputDevice: (device: Device) => void;
// setVideoInputDevice: (device: Device) => void;
permissionStatus: PermissionStatus | undefined;
};
type DisabledDeviceContextValue = {
enabled: false;
};
export type DeviceContextValue = EnabledDeviceContextValue | DisabledDeviceContextValue;
export const isDeviceContextEnabled = (context: DeviceContextValue): context is EnabledDeviceContextValue =>
(context as EnabledDeviceContextValue).enabled;
export const DeviceContext = createContext<DeviceContextValue>({
enabled: false,
});

View File

@@ -0,0 +1,83 @@
import { createContext } from 'react';
export type SizeLayout = {
sidebar: string;
contextualBar: string;
};
export type LayoutContextValue = {
isEmbedded: boolean;
showTopNavbarEmbeddedLayout: boolean;
isTablet: boolean;
isMobile: boolean;
roomToolboxExpanded: boolean;
sidebar: {
overlayed: boolean;
setOverlayed: (value: boolean) => void;
isCollapsed: boolean;
shouldToggle: boolean;
toggle: () => void;
collapse: () => void;
expand: () => void;
close: () => void;
};
sidePanel: {
displaySidePanel: boolean;
closeSidePanel: () => void;
openSidePanel: () => void;
};
navbar: {
searchExpanded: boolean;
expandSearch?: () => void;
collapseSearch?: () => void;
};
size: SizeLayout;
contextualBarExpanded: boolean;
contextualBarPosition: 'absolute' | 'relative' | 'fixed';
hiddenActions: {
roomToolbox: Array<string>;
messageToolbox: Array<string>;
composerToolbox: Array<string>;
userToolbox: Array<string>;
};
};
export const LayoutContext = createContext<LayoutContextValue>({
isEmbedded: false,
showTopNavbarEmbeddedLayout: false,
isTablet: false,
isMobile: false,
roomToolboxExpanded: true,
navbar: {
searchExpanded: false,
expandSearch: () => undefined,
collapseSearch: () => undefined,
},
sidebar: {
overlayed: false,
setOverlayed: () => undefined,
isCollapsed: false,
shouldToggle: false,
toggle: () => undefined,
collapse: () => undefined,
expand: () => undefined,
close: () => undefined,
},
sidePanel: {
displaySidePanel: true,
closeSidePanel: () => undefined,
openSidePanel: () => undefined,
},
size: {
sidebar: '240px',
contextualBar: '380px',
},
contextualBarPosition: 'relative',
contextualBarExpanded: false,
hiddenActions: {
roomToolbox: [],
messageToolbox: [],
composerToolbox: [],
userToolbox: [],
},
});

View File

@@ -0,0 +1,12 @@
import type { ReactNode } from 'react';
import { createContext } from 'react';
export type ModalContextValue = {
modal: {
setModal(modal?: ReactNode): void;
};
currentModal: { component: ReactNode; region?: symbol };
region?: symbol;
};
export const ModalContext = createContext<ModalContextValue | undefined>(undefined);

View File

@@ -0,0 +1,46 @@
import type { Box } from '@rocket.chat/fuselage';
import type { Keys as IconName } from '@rocket.chat/icons';
import { createContext } from 'react';
import type { ReactNode, ComponentProps, ComponentType } from 'react';
import type { TranslationKey } from './TranslationContext';
export type RenderToolboxItemParams = RoomToolboxActionConfig & {
className?: ComponentProps<typeof Box>['className'];
toolbox: RoomToolboxContextValue;
};
export type RoomToolboxActionConfig = {
id: string;
icon: IconName;
title: TranslationKey;
anonymous?: boolean;
tooltip?: string;
disabled?: boolean;
full?: true;
order?: number;
groups: Array<'group' | 'channel' | 'live' | 'direct' | 'direct_multiple' | 'team'>;
hotkey?: string;
action?: () => void;
featured?: boolean;
renderToolboxItem?: (params: RenderToolboxItemParams) => ReactNode;
tabComponent?: ComponentType<{
onClickBack?: () => void;
}>;
type?: 'organization' | 'communication' | 'customization' | 'apps' | 'moderation';
variant?: 'danger';
};
export type RoomToolboxContextValue = {
actions: RoomToolboxActionConfig[];
tab?: RoomToolboxActionConfig;
context?: string;
openTab: (actionId: string, context?: string) => void;
closeTab: () => void;
};
export const RoomToolboxContext = createContext<RoomToolboxContextValue>({
actions: [],
openTab: () => undefined,
closeTab: () => undefined,
});

View File

@@ -0,0 +1,112 @@
import type { RoomType, RoomRouteData, DirectRoomRouteData, OmnichannelRoomRouteData, ChannelRouteData } from '@rocket.chat/core-typings';
import type { ReactNode } from 'react';
import { createContext } from 'react';
export interface IRouterPaths {
index: {
pattern: '/';
pathname: '/';
};
home: {
pattern: '/home';
pathname: '/home';
};
}
export type LocationPathname = IRouterPaths[keyof IRouterPaths]['pathname'];
export type LocationSearch = string;
export type LocationHash = string;
export type RouteParameters = Record<string, string>;
export type SearchParameters = Record<string, string>;
export type RouteName = keyof IRouterPaths;
export type RouterPathPattern = IRouterPaths[keyof IRouterPaths]['pattern'];
export type To =
| LocationPathname
| {
name: RouteName;
params?: RouteParameters;
search?: SearchParameters;
}
| {
pattern: RouterPathPattern;
params?: RouteParameters;
search?: SearchParameters;
}
| {
pathname: LocationPathname;
search?: SearchParameters;
};
type RelativeRoutingType = 'route' | 'path';
export type RouteObject =
| {
path: RouterPathPattern;
id: RouteName;
element: ReactNode;
}
| {
path: '*';
id: 'not-found';
element: ReactNode;
};
export type RouterContextValue = {
subscribeToRouteChange(onRouteChange: () => void): () => void;
getLocationPathname(): LocationPathname;
getLocationSearch(): LocationSearch;
getLocationHash(): LocationHash;
getRouteParameters(): RouteParameters;
getSearchParameters(): SearchParameters;
getRouteName(): RouteName | undefined;
getPreviousRouteName(): RouteName | undefined;
buildRoutePath(to: To): LocationPathname | `${LocationPathname}?${LocationSearch}`;
navigate(to: To, options?: { replace?: boolean; state?: any; relative?: RelativeRoutingType }): void;
navigate(delta: number): void;
defineRoutes(routes: RouteObject[]): () => void;
getRoomRoute(roomType: 'd', routeData: DirectRoomRouteData): { path: LocationPathname };
getRoomRoute(roomType: 'l', routeData: OmnichannelRoomRouteData): { path: LocationPathname };
getRoomRoute(roomType: 'p' | 'c', routeData: ChannelRouteData): { path: LocationPathname };
getRoomRoute(
roomType: RoomType,
routeData: RoomRouteData,
): {
path: LocationPathname;
};
};
export const RouterContext = createContext<RouterContextValue>({
subscribeToRouteChange: () => () => undefined,
getLocationPathname: () => {
throw new Error('not implemented');
},
getRouteParameters: () => {
throw new Error('not implemented');
},
getLocationSearch: () => {
throw new Error('not implemented');
},
getLocationHash: () => {
throw new Error('not implemented');
},
getSearchParameters: () => {
throw new Error('not implemented');
},
getRouteName: () => {
throw new Error('not implemented');
},
getPreviousRouteName: () => {
throw new Error('not implemented');
},
buildRoutePath: () => {
throw new Error('not implemented');
},
navigate: () => undefined,
defineRoutes: () => () => undefined,
getRoomRoute: () => {
throw new Error('not implemented');
},
});

View File

@@ -0,0 +1,84 @@
import type { IServerInfo, Serialized } from '@rocket.chat/core-typings';
import type {
ServerMethodName,
ServerMethodParameters,
ServerMethodReturn,
StreamKeys,
StreamNames,
StreamerCallbackArgs,
StreamerEvents,
} from '@rocket.chat/ddp-client';
import type { Method, OperationParams, OperationResult, PathFor, PathPattern, UrlParams } from '@rocket.chat/rest-typings';
import { createContext } from 'react';
export type UploadResult = {
success: boolean;
status: string;
[key: string]: unknown;
};
export type ServerContextValue = {
connected: boolean;
status: 'connected' | 'connecting' | 'failed' | 'waiting' | 'offline';
retryCount: number;
retryTime?: number | undefined;
info?: IServerInfo;
absoluteUrl: (path: string) => string;
callMethod?: <MethodName extends ServerMethodName>(
methodName: MethodName,
...args: ServerMethodParameters<MethodName>
) => Promise<ServerMethodReturn<MethodName>>;
callEndpoint: <TMethod extends Method, TPathPattern extends PathPattern>(args: {
method: TMethod;
pathPattern: TPathPattern;
keys: UrlParams<TPathPattern>;
params: OperationParams<TMethod, TPathPattern>;
signal?: AbortSignal;
}) => Promise<Serialized<OperationResult<TMethod, TPathPattern>>>;
uploadToEndpoint: (
endpoint: PathFor<'POST'>,
formData: any,
) =>
| Promise<UploadResult>
| {
promise: Promise<UploadResult>;
};
getStream: <N extends StreamNames, K extends StreamKeys<N>>(
streamName: N,
_options?: {
retransmit?: boolean | undefined;
retransmitToSelf?: boolean | undefined;
},
) => (eventName: K, callback: (...args: StreamerCallbackArgs<N, K>) => void) => () => void;
getStreamAll: <N extends StreamNames>(
streamName: N,
) => (callback: (eventName: string, args: StreamerEvents[N][number]['args']) => void) => () => void;
writeStream: <N extends StreamNames, K extends StreamKeys<N>>(streamName: N, eventName: K, ...args: StreamerCallbackArgs<N, K>) => void;
disconnect: () => void;
reconnect: () => void;
};
export const ServerContext = createContext<ServerContextValue>({
connected: true,
status: 'connected',
retryCount: 0,
info: undefined,
absoluteUrl: (path) => path,
callEndpoint: () => {
throw new Error('not implemented');
},
uploadToEndpoint: async () => {
throw new Error('not implemented');
},
getStream: () => () => (): void => undefined,
getStreamAll: () => () => (): void => undefined,
writeStream: () => {
throw new Error('not implemented');
},
disconnect: () => {
throw new Error('not implemented');
},
reconnect: () => {
throw new Error('not implemented');
},
});

View File

@@ -0,0 +1,11 @@
import { createContext } from 'react';
export type SessionContextValue = {
query: (name: string) => [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => unknown];
dispatch: (name: string, value: unknown) => void;
};
export const SessionContext = createContext<SessionContextValue>({
query: () => [() => (): void => undefined, (): undefined => undefined],
dispatch: (): void => undefined,
});

View File

@@ -0,0 +1,29 @@
import type { ISetting } from '@rocket.chat/core-typings';
import { createContext } from 'react';
export type SettingsContextQuery = {
readonly _id?: ISetting['_id'][] | RegExp;
readonly group?: ISetting['_id'];
readonly section?: string;
readonly tab?: ISetting['_id'];
readonly skip?: number;
readonly limit?: number;
};
export type SettingsContextValue = {
readonly hasPrivateAccess: boolean;
readonly querySetting: (
_id: ISetting['_id'],
) => [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => ISetting | undefined];
readonly querySettings: (
query: SettingsContextQuery,
) => [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => ISetting[]];
readonly dispatch: (changes: Partial<ISetting>[], onSaved?: () => void) => Promise<void>;
};
export const SettingsContext = createContext<SettingsContextValue>({
hasPrivateAccess: false,
querySetting: () => [(): (() => void) => (): void => undefined, (): undefined => undefined],
querySettings: () => [(): (() => void) => (): void => undefined, (): ISetting[] => []],
dispatch: async () => undefined,
});

View File

@@ -0,0 +1,24 @@
import type { ReactNode } from 'react';
import { createContext } from 'react';
type ToastMessagePayload =
| {
type: 'success' | 'info' | 'warning';
message: ReactNode | string;
title?: string;
options?: object;
}
| {
type: 'error';
message: unknown;
title?: string;
options?: object;
};
export type ToastMessagesContextValue = {
dispatch: (payload: ToastMessagePayload) => void;
};
export const ToastMessagesContext = createContext<ToastMessagesContextValue>({
dispatch: () => undefined,
});

View File

@@ -0,0 +1,12 @@
import type { ReactNode } from 'react';
import { createContext } from 'react';
export type TooltipContextValue = {
open: (payload: ReactNode, anchor: HTMLElement) => void;
close: () => void;
};
export const TooltipContext = createContext<TooltipContextValue>({
open: () => undefined,
close: () => undefined,
});

View File

@@ -0,0 +1,51 @@
import type { RocketchatI18nKeys } from '@rocket.chat/i18n';
import { createContext } from 'react';
export type TranslationLanguage = {
en: string;
name: string;
ogName: string;
key: string;
};
type KeysWithoutSuffix = {
[
K in RocketchatI18nKeys as K extends `${infer T extends string}_${'one' | 'other' | 'zero' | 'few' | 'many' | 'two' | 'three' | 'four'}`
? T
: K
]: never;
};
export type TranslationKey = keyof KeysWithoutSuffix | `app-${string}.${string}`;
export type TranslationContextValue = {
languages: TranslationLanguage[];
language: TranslationLanguage['key'];
loadLanguage: (language: TranslationLanguage['key']) => Promise<void>;
translate: {
(key: TranslationKey, options?: unknown): string;
(key: TranslationKey, ...options: unknown[]): string;
has: (
key: string,
options?: {
lng?: string;
},
) => key is TranslationKey;
};
};
export const TranslationContext = createContext<TranslationContextValue>({
languages: [
{
name: 'Default',
en: 'Default',
ogName: 'Default',
key: '',
},
],
language: '',
loadLanguage: async () => console.warn('TranslationContext: loadLanguage not implemented'),
translate: Object.assign((key: string) => key, {
has: (key: string): key is TranslationKey => Boolean(key),
}),
});

View File

@@ -0,0 +1,20 @@
import type { AriaButtonProps } from '@react-aria/button';
import type { OverlayTriggerState } from '@react-stately/overlays';
import type { MutableRefObject, UIEvent } from 'react';
import { createContext } from 'react';
export type UserCardContextValue = {
openUserCard: (e: UIEvent, username: string) => void;
closeUserCard: () => void;
triggerProps: AriaButtonProps<'button'>;
triggerRef: MutableRefObject<Element | null>;
state: OverlayTriggerState;
};
export const UserCardContext = createContext<UserCardContextValue>({
openUserCard: () => undefined,
closeUserCard: () => undefined,
triggerProps: {},
triggerRef: { current: null },
state: { isOpen: false, setOpen: () => undefined, open: () => undefined, close: () => undefined, toggle: () => undefined },
});

View File

@@ -0,0 +1,63 @@
import type { IRoom, ISubscription, IUser } from '@rocket.chat/core-typings';
import type { OffCallbackHandler } from '@rocket.chat/emitter';
import type { ObjectId, Filter, FindOptions as MongoFindOptions, Document } from 'mongodb';
import { createContext } from 'react';
import type { SubscriptionWithRoom } from './types/SubscriptionWithRoom';
export type SubscriptionQuery =
| {
rid: string | ObjectId;
}
| {
name: string;
}
| {
open: boolean;
}
| object;
export type Fields<TSchema extends Document = Document> = Exclude<MongoFindOptions<TSchema>['projection'], undefined>;
export type Sort<TSchema extends Document = Document> = Exclude<MongoFindOptions<TSchema>['sort'], undefined>;
export type FindOptions<TSchema extends Document = Document> = {
fields?: Fields<TSchema>;
sort?: Sort<TSchema>;
skip?: number;
limit?: number;
};
export type UserContextValue = {
userId: string | undefined;
user: IUser | null;
queryPreference: <T>(
key: string | ObjectId,
defaultValue?: T,
) => [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => T | undefined];
querySubscription: (
query: Filter<Pick<ISubscription, 'rid' | 'name'>>,
) => [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => ISubscription | undefined];
queryRoom: (
query: Filter<Pick<IRoom, '_id'>>,
fields?: Fields,
sort?: Sort,
) => [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => IRoom | undefined];
querySubscriptions: (
query: SubscriptionQuery,
options?: FindOptions,
) => [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => SubscriptionWithRoom[]];
logout: () => Promise<void>;
onLogout: (callback: () => void) => OffCallbackHandler;
};
export const UserContext = createContext<UserContextValue>({
userId: undefined,
user: null,
queryPreference: () => [() => (): void => undefined, (): undefined => undefined],
querySubscription: () => [() => (): void => undefined, (): undefined => undefined],
queryRoom: () => [() => (): void => undefined, (): undefined => undefined],
querySubscriptions: () => [() => (): void => undefined, (): [] => []],
logout: () => Promise.resolve(),
onLogout: () => (): void => undefined,
});

View File

@@ -0,0 +1,13 @@
import type { UserPresence } from '@rocket.chat/core-typings';
import { createContext } from 'react';
export type UserPresenceContextValue = {
queryUserData: (uid: string | undefined) => {
get(): UserPresence | undefined;
subscribe(callback: () => void): () => void;
};
};
export const UserPresenceContext = createContext<UserPresenceContextValue>({
queryUserData: () => ({ get: () => undefined, subscribe: () => () => undefined }),
});

View File

@@ -0,0 +1,5 @@
import { useContext } from 'react';
import { ServerContext } from '../ServerContext';
export const useAbsoluteUrl = (): ((path: string) => string) => useContext(ServerContext).absoluteUrl;

View File

@@ -0,0 +1,24 @@
import type { CustomFieldMetadata } from '@rocket.chat/core-typings';
import { useMemo } from 'react';
import { useSetting } from './useSetting';
export const useAccountsCustomFields = () => {
const accountsCustomFieldsJSON = useSetting('Accounts_CustomFields');
return useMemo((): CustomFieldMetadata[] => {
if (typeof accountsCustomFieldsJSON !== 'string' || accountsCustomFieldsJSON.trim() === '') {
return [];
}
try {
return Object.entries<Omit<CustomFieldMetadata, 'name'>>(JSON.parse(accountsCustomFieldsJSON)).map(
([fieldName, fieldData]): CustomFieldMetadata => {
return { ...fieldData, name: fieldName };
},
);
} catch {
console.error('Invalid JSON for Accounts_CustomFields');
}
return [];
}, [accountsCustomFieldsJSON]);
};

View File

@@ -0,0 +1,10 @@
import type { ObjectId } from 'mongodb';
import { useContext, useMemo, useSyncExternalStore } from 'react';
import { AuthorizationContext } from '../AuthorizationContext';
export const useAllPermissions = (permissions: (string | ObjectId)[], scope?: string | ObjectId): boolean => {
const { queryAllPermissions } = useContext(AuthorizationContext);
const [subscribe, getSnapshot] = useMemo(() => queryAllPermissions(permissions, scope), [queryAllPermissions, permissions, scope]);
return useSyncExternalStore(subscribe, getSnapshot);
};

View File

@@ -0,0 +1,11 @@
import type { IRocketChatAssets, ISettingAsset } from '@rocket.chat/core-typings';
import { useAbsoluteUrl } from './useAbsoluteUrl';
import { useSetting } from './useSetting';
export const useAssetPath = (assetId: keyof IRocketChatAssets): string | undefined => {
const asset = useSetting<ISettingAsset>(`Assets_${assetId}`);
const absoluteUrl = useAbsoluteUrl();
return (asset?.url ?? asset?.defaultUrl) && absoluteUrl(asset?.url ?? asset?.defaultUrl ?? '');
};

View File

@@ -0,0 +1,15 @@
import type { IRocketChatAssets } from '@rocket.chat/core-typings';
import { useDarkMode } from '@rocket.chat/fuselage-hooks';
import { useAssetPath } from './useAssetPath';
import { useUserPreference } from './useUserPreference';
export const useAssetWithDarkModePath = <A extends keyof IRocketChatAssets>(
assetId: A extends `${infer T}_dark` ? T : never,
): string | undefined => {
const userThemePreference = useUserPreference('themeAppearence') || 'auto';
const theme = useDarkMode(userThemePreference === 'auto' ? undefined : userThemePreference === 'dark') ? 'dark' : 'light';
const themeAssetId = theme === 'dark' ? (`${assetId}_dark` as const) : assetId;
return useAssetPath(themeAssetId);
};

View File

@@ -0,0 +1,13 @@
import type { ObjectId } from 'mongodb';
import { useContext, useMemo, useSyncExternalStore } from 'react';
import { AuthorizationContext } from '../AuthorizationContext';
export const useAtLeastOnePermission = (permissions: (string | ObjectId)[], scope?: string | ObjectId): boolean => {
const { queryAtLeastOnePermission } = useContext(AuthorizationContext);
const [subscribe, getSnapshot] = useMemo(
() => queryAtLeastOnePermission(permissions, scope),
[queryAtLeastOnePermission, permissions, scope],
);
return useSyncExternalStore(subscribe, getSnapshot);
};

View File

@@ -0,0 +1,5 @@
import { useContext } from 'react';
import { AttachmentContext } from '../AttachmentContext';
export const useAttachmentAutoLoadEmbedMedia = (): boolean => useContext(AttachmentContext).autoLoadEmbedMedias;

View File

@@ -0,0 +1,8 @@
import { useContext } from 'react';
import { AttachmentContext } from '../AttachmentContext';
export const useAttachmentDimensions = (): {
width: number;
height: number;
} => useContext(AttachmentContext).dimensions;

View File

@@ -0,0 +1,5 @@
import { useContext } from 'react';
import { AttachmentContext } from '../AttachmentContext';
export const useAttachmentIsCollapsedByDefault = (): boolean => useContext(AttachmentContext).collapsedByDefault;

View File

@@ -0,0 +1,25 @@
import { useContext } from 'react';
import type { Device } from '../DeviceContext';
import { DeviceContext, isDeviceContextEnabled } from '../DeviceContext';
type AvailableDevices = {
audioInput?: Device[];
audioOutput?: Device[];
};
export const useAvailableDevices = (): AvailableDevices | null => {
const context = useContext(DeviceContext);
if (!isDeviceContextEnabled(context)) {
console.warn(
'Device Management is disabled on unsecure contexts, see https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts/features_restricted_to_secure_contexts',
);
return null;
}
return {
audioInput: context.availableAudioInputDevices,
audioOutput: context.availableAudioOutputDevices,
};
};

View File

@@ -0,0 +1,8 @@
import { useContext } from 'react';
import { ServerContext } from '../ServerContext';
export const useConnectionStatus = () => {
const { connected, retryTime, status, reconnect, disconnect } = useContext(ServerContext);
return { connected, retryTime, status, reconnect, disconnect };
};

View File

@@ -0,0 +1,21 @@
import { useContext } from 'react';
import type { ModalContextValue } from '../ModalContext';
import { ModalContext } from '../ModalContext';
/**
* Similar to useModal this hook return the current modal from the context value
*/
export const useCurrentModal = (): ModalContextValue['currentModal']['component'] => {
const context = useContext(ModalContext);
if (!context) {
throw new Error('useCurrentModal must be used inside Modal Context');
}
if (context.currentModal?.region !== context.region) {
return null;
}
return context.currentModal.component;
};

View File

@@ -0,0 +1,20 @@
import { useCallback, useContext, useSyncExternalStore } from 'react';
import { RouterContext } from '../RouterContext';
export const useCurrentRoutePath = () => {
const router = useContext(RouterContext);
const getSnapshot = useCallback(() => {
const name = router.getRouteName();
return name
? router.buildRoutePath({
name,
params: router.getRouteParameters(),
search: router.getSearchParameters(),
})
: undefined;
}, [router]);
return useSyncExternalStore(router.subscribeToRouteChange, getSnapshot);
};

View File

@@ -0,0 +1,6 @@
import { useContext } from 'react';
import type { CustomSoundContextValue } from '../CustomSoundContext';
import { CustomSoundContext } from '../CustomSoundContext';
export const useCustomSound = (): CustomSoundContextValue => useContext(CustomSoundContext);

View File

@@ -0,0 +1,17 @@
import { useContext } from 'react';
import { DeviceContext, isDeviceContextEnabled } from '../DeviceContext';
export const useDeviceConstraints = (): MediaStreamConstraints | null => {
const context = useContext(DeviceContext);
if (!isDeviceContextEnabled(context)) {
console.warn(
'Device Management is disabled on unsecure contexts, see https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts/features_restricted_to_secure_contexts',
);
return null;
}
const selectedAudioInputDeviceId = context.selectedAudioInputDevice?.id;
return { audio: selectedAudioInputDeviceId === 'default' ? true : { deviceId: { exact: selectedAudioInputDeviceId } } };
};

View File

@@ -0,0 +1,42 @@
import type { Serialized } from '@rocket.chat/core-typings';
import type { Method, OperationParams, OperationResult, PathPattern, UrlParams } from '@rocket.chat/rest-typings';
import { useCallback, useContext, useRef } from 'react';
import { ServerContext } from '../ServerContext';
type EndpointOptions = {
signal?: AbortSignal;
};
export type EndpointFunction<TMethod extends Method, TPathPattern extends PathPattern> =
undefined extends OperationParams<TMethod, TPathPattern>
? (
params?: OperationParams<TMethod, TPathPattern>,
options?: EndpointOptions,
) => Promise<Serialized<OperationResult<TMethod, TPathPattern>>>
: (
params: OperationParams<TMethod, TPathPattern>,
options?: EndpointOptions,
) => Promise<Serialized<OperationResult<TMethod, TPathPattern>>>;
export function useEndpoint<TMethod extends Method, TPathPattern extends PathPattern>(
method: TMethod,
pathPattern: TPathPattern,
...[keys]: NoInfer<undefined extends UrlParams<TPathPattern> ? [keys?: UrlParams<TPathPattern>] : [keys: UrlParams<TPathPattern>]>
): EndpointFunction<TMethod, TPathPattern> {
const { callEndpoint } = useContext(ServerContext);
const keysRef = useRef(keys);
keysRef.current = keys;
return useCallback(
(params: OperationParams<TMethod, TPathPattern> | undefined, options?: EndpointOptions) =>
callEndpoint({
method,
pathPattern,
keys: keysRef.current as UrlParams<TPathPattern>,
params: params as OperationParams<TMethod, TPathPattern>,
signal: options?.signal,
}),
[callEndpoint, pathPattern, method],
);
}

View File

@@ -0,0 +1,5 @@
import { useContext } from 'react';
import { DeviceContext } from '../DeviceContext';
export const useIsDeviceManagementEnabled = (): boolean => useContext(DeviceContext).enabled;

View File

@@ -0,0 +1,8 @@
import { useContext } from 'react';
import { AuthenticationContext } from '../AuthenticationContext';
export const useIsLoggingIn = () => {
const { isLoggingIn } = useContext(AuthenticationContext);
return isLoggingIn;
};

View File

@@ -0,0 +1,5 @@
import { useContext } from 'react';
import { SettingsContext } from '../SettingsContext';
export const useIsPrivilegedSettingsContext = (): boolean => useContext(SettingsContext).hasPrivateAccess;

View File

@@ -0,0 +1,6 @@
import { useContext } from 'react';
import type { TranslationContextValue } from '../TranslationContext';
import { TranslationContext } from '../TranslationContext';
export const useLanguage = (): TranslationContextValue['language'] => useContext(TranslationContext).language;

View File

@@ -0,0 +1,6 @@
import { useContext } from 'react';
import type { TranslationContextValue } from '../TranslationContext';
import { TranslationContext } from '../TranslationContext';
export const useLanguages = (): TranslationContextValue['languages'] => useContext(TranslationContext).languages;

View File

@@ -0,0 +1,6 @@
import { useContext } from 'react';
import type { LayoutContextValue } from '../LayoutContext';
import { LayoutContext } from '../LayoutContext';
export const useLayout = (): LayoutContextValue => useContext(LayoutContext);

View File

@@ -0,0 +1,5 @@
import { useContext } from 'react';
import { LayoutContext } from '../LayoutContext';
export const useLayoutContextualBarExpanded = (): boolean => useContext(LayoutContext).contextualBarExpanded;

View File

@@ -0,0 +1,7 @@
import { useContext } from 'react';
import type { LayoutContextValue } from '../LayoutContext';
import { LayoutContext } from '../LayoutContext';
export const useLayoutContextualBarPosition = (): LayoutContextValue['contextualBarPosition'] =>
useContext(LayoutContext).contextualBarPosition;

View File

@@ -0,0 +1,6 @@
import { useContext } from 'react';
import type { LayoutContextValue } from '../LayoutContext';
import { LayoutContext } from '../LayoutContext';
export const useLayoutHiddenActions = (): LayoutContextValue['hiddenActions'] => useContext(LayoutContext).hiddenActions;

View File

@@ -0,0 +1,6 @@
import { useContext } from 'react';
import type { LayoutContextValue } from '../LayoutContext';
import { LayoutContext } from '../LayoutContext';
export const useLayoutSizes = (): LayoutContextValue['size'] => useContext(LayoutContext).size;

View File

@@ -0,0 +1,6 @@
import { useContext } from 'react';
import type { TranslationContextValue } from '../TranslationContext';
import { TranslationContext } from '../TranslationContext';
export const useLoadLanguage = (): TranslationContextValue['loadLanguage'] => useContext(TranslationContext).loadLanguage;

View File

@@ -0,0 +1,8 @@
import { useContext, useSyncExternalStore } from 'react';
import { RouterContext } from '../RouterContext';
export const useLocationHash = () => {
const { getLocationHash, subscribeToRouteChange } = useContext(RouterContext);
return useSyncExternalStore(subscribeToRouteChange, getLocationHash);
};

View File

@@ -0,0 +1,9 @@
import { useContext, useSyncExternalStore } from 'react';
import { AuthenticationContext } from '../AuthenticationContext';
export const useLoginServices = () => {
const { queryLoginServices } = useContext(AuthenticationContext);
return useSyncExternalStore(queryLoginServices.subscribe, queryLoginServices.getCurrentValue);
};

View File

@@ -0,0 +1,5 @@
import { useContext } from 'react';
import { AuthenticationContext } from '../AuthenticationContext';
export const useLoginToken = (): string | null => useContext(AuthenticationContext).getLoginToken();

View File

@@ -0,0 +1,5 @@
import { useContext } from 'react';
import { AuthenticationContext } from '../AuthenticationContext';
export const useLoginWithCustomOauth = () => useContext(AuthenticationContext).loginWithCustomOauth;

View File

@@ -0,0 +1,6 @@
import { useContext } from 'react';
import { AuthenticationContext } from '../AuthenticationContext';
export const useLoginWithIframe = (): ((token: string, callback?: (error: Error | null | undefined) => void) => Promise<void>) =>
useContext(AuthenticationContext).loginWithIframe;

View File

@@ -0,0 +1,5 @@
import { useContext } from 'react';
import { AuthenticationContext } from '../AuthenticationContext';
export const useLoginWithPassword = () => useContext(AuthenticationContext).loginWithPassword;

View File

@@ -0,0 +1,12 @@
import type { LoginServiceConfiguration } from '@rocket.chat/core-typings';
import { useContext, useMemo } from 'react';
import { AuthenticationContext } from '../AuthenticationContext';
export const useLoginWithService = <T extends LoginServiceConfiguration>(service: T): (() => Promise<true>) => {
const { loginWithService } = useContext(AuthenticationContext);
return useMemo(() => {
return loginWithService(service);
}, [loginWithService, service]);
};

View File

@@ -0,0 +1,5 @@
import { useContext } from 'react';
import { AuthenticationContext } from '../AuthenticationContext';
export const useLoginWithToken = () => useContext(AuthenticationContext).loginWithToken;

View File

@@ -0,0 +1,6 @@
import { useContext } from 'react';
import { AuthenticationContext } from '../AuthenticationContext';
export const useLoginWithTokenRoute = (): ((token: string, callback?: (error: Error | null | undefined) => void) => Promise<void>) =>
useContext(AuthenticationContext).loginWithTokenRoute;

View File

@@ -0,0 +1,15 @@
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
import { useContext } from 'react';
import { UserContext } from '../UserContext';
import { useRouter } from './useRouter';
export const useLogout = (): (() => void) => {
const router = useRouter();
const { logout } = useContext(UserContext);
return useStableCallback(() => {
void logout();
router.navigate('/');
});
};

View File

@@ -0,0 +1,68 @@
import { renderHook } from '@testing-library/react';
import { useMediaDeviceMicrophonePermission } from './useMediaDevicePermission';
import { DeviceContext } from '../DeviceContext';
const states = [
{ expectedState: 'granted', state: 'granted', requestDevice: 'function' },
{ expectedState: 'denied', state: 'denied', requestDevice: 'undefined' },
{ expectedState: 'prompt', state: 'prompt', requestDevice: 'function' },
];
const getWrapper =
(state: PermissionState | undefined, availableAudioInputDevices: any[] = [], enabled = true) =>
({ children }: { children: any }) => {
return (
<DeviceContext.Provider
value={{
enabled,
selectedAudioOutputDevice: undefined,
selectedAudioInputDevice: undefined,
availableAudioOutputDevices: [],
availableAudioInputDevices,
permissionStatus: state ? ({ state } as PermissionStatus) : undefined,
setAudioOutputDevice: () => undefined,
setAudioInputDevice: () => undefined,
}}
>
{children}
</DeviceContext.Provider>
);
};
describe('useMediaDeviceMicrophonePermission', () => {
it('Should return permission state denied and requestDevice is undefined if context is disabled', async () => {
const { result } = renderHook(() => useMediaDeviceMicrophonePermission(), {
wrapper: getWrapper(undefined, ['device1', 'device2'], false),
});
expect(result.current.state).toBe('denied');
expect(result.current.requestDevice).toBeUndefined();
});
it.each(states)('Should return permission state $state and requestDevice is $requestDevice', async ({ state, requestDevice }) => {
const { result } = renderHook(() => useMediaDeviceMicrophonePermission(), {
wrapper: getWrapper(state as PermissionState),
});
expect(result.current.state).toBe(state);
expect(typeof result.current.requestDevice).toBe(requestDevice);
});
it('Should return permission state granted and requestDevice is function if permissionStatus is undefined and availableAudioInputDevices has records', async () => {
const { result } = renderHook(() => useMediaDeviceMicrophonePermission(), {
wrapper: getWrapper(undefined, ['device1', 'device2']),
});
expect(result.current.state).toBe('granted');
expect(typeof result.current.requestDevice).toBe('function');
});
it('Should return permission state prompt and requestDevice is function if permissionStatus is undefined and availableAudioInputDevices is empty', async () => {
const { result } = renderHook(() => useMediaDeviceMicrophonePermission(), {
wrapper: getWrapper(undefined),
});
expect(result.current.state).toBe('prompt');
expect(typeof result.current.requestDevice).toBe('function');
});
});

View File

@@ -0,0 +1,61 @@
import { useContext } from 'react';
import { DeviceContext, isDeviceContextEnabled } from '../DeviceContext';
const requestDevice = async ({
onAccept,
onReject,
constraints = { audio: true },
}: {
onAccept?: (stream: MediaStream) => void;
onReject?: (error: DOMException) => void;
constraints?: MediaStreamConstraints;
}): Promise<void> => {
if (!navigator.mediaDevices) {
return;
}
await navigator.mediaDevices.getUserMedia(constraints).then(onAccept, onReject);
};
const isPermissionDenied = (state: PermissionState): state is 'denied' => {
return state === 'denied';
};
type DeniedReturn = { state: 'denied'; requestDevice?: never };
type PromptOrGrantedReturn = { state: 'prompt' | 'granted'; requestDevice: typeof requestDevice };
/**
* @description Hook to check if the microphone permission is granted. If the permission is denied, or the permission is not requested, the hook will return a function to request the permission. Right now just the microphone permission is handled with this hook, since DeviceContext is only used for audio input and output.
* @returns { state: 'granted' } if the permission is granted
* @returns { state: 'denied' } if the permission is denied
* @returns { state: 'prompt', requestPrompt: function ({onAccept, onReject}) {} } if the permission is in prompt state.
*/
export const useMediaDeviceMicrophonePermission = (): DeniedReturn | PromptOrGrantedReturn => {
const context = useContext(DeviceContext);
if (!isDeviceContextEnabled(context)) {
return {
state: 'denied',
};
}
const { permissionStatus, availableAudioInputDevices } = context;
if (permissionStatus) {
if (isPermissionDenied(permissionStatus.state)) {
return { state: permissionStatus.state };
}
return { state: permissionStatus.state, requestDevice };
}
if (availableAudioInputDevices.length > 0) {
return { state: 'granted', requestDevice };
}
return {
state: 'prompt',
requestDevice,
};
};

View File

@@ -0,0 +1,8 @@
import { useContext } from 'react';
import { AttachmentContext } from '../AttachmentContext';
export const useMediaUrl = (): ((path: string) => string) => {
const { getURL } = useContext(AttachmentContext);
return getURL;
};

View File

@@ -0,0 +1,24 @@
import type { ServerMethodName, ServerMethodParameters, ServerMethodReturn, ServerMethods } from '@rocket.chat/ddp-client';
import { useCallback, useContext } from 'react';
import { ServerContext } from '../ServerContext';
type ServerMethodFunction<MethodName extends ServerMethodName> = (
...args: ServerMethodParameters<MethodName>
) => Promise<ServerMethodReturn<MethodName>>;
/** @deprecated prefer the use of api endpoints (useEndpoint) */
export const useMethod = <MethodName extends keyof ServerMethods>(methodName: MethodName): ServerMethodFunction<MethodName> => {
const { callMethod } = useContext(ServerContext);
return useCallback(
(...args: ServerMethodParameters<MethodName>) => {
if (!callMethod) {
throw new Error(`cannot use useMethod(${methodName}) hook without a wrapping ServerContext`);
}
return callMethod(methodName, ...args);
},
[callMethod, methodName],
);
};

View File

@@ -0,0 +1,17 @@
import { useContext } from 'react';
import type { ModalContextValue } from '../ModalContext';
import { ModalContext } from '../ModalContext';
/**
* Consider using useCurrentModal to get the current modal
*/
export const useModal = (): ModalContextValue['modal'] => {
const context = useContext(ModalContext);
if (!context) {
throw new Error('useModal must be used inside Modal Context');
}
return context.modal;
};

View File

@@ -0,0 +1,6 @@
import type { OffCallbackHandler } from '@rocket.chat/emitter';
import { useContext } from 'react';
import { UserContext } from '../UserContext';
export const useOnLogout = (): ((callback: () => void) => OffCallbackHandler) => useContext(UserContext).onLogout;

View File

@@ -0,0 +1,28 @@
import { PasswordPolicy, type PasswordPolicyOptions, type PasswordPolicyValidation } from '@rocket.chat/password-policies';
import { useMemo, useCallback } from 'react';
export type { PasswordPolicyValidation };
export type UsePasswordPolicyResult = {
validations: PasswordPolicyValidation[];
valid: boolean;
};
export type UsePasswordPolicyReturn = (password: string) => UsePasswordPolicyResult;
export type UsePasswordPolicy = (options: PasswordPolicyOptions) => UsePasswordPolicyReturn;
export const usePasswordPolicy: UsePasswordPolicy = (options) => {
const policy = useMemo(() => new PasswordPolicy(options), [options]);
return useCallback(
(password: string) => {
const validations = policy.sendValidationMessage(password);
return {
validations,
valid: validations.every(({ isValid }) => isValid),
};
},
[policy],
);
};

View File

@@ -0,0 +1,28 @@
import type { PasswordPolicyOptions } from '@rocket.chat/password-policies';
import { useSetting } from './useSetting';
export const usePasswordPolicyOptions = (): PasswordPolicyOptions => {
const enabled = useSetting('Accounts_Password_Policy_Enabled', true);
const minLength = useSetting('Accounts_Password_Policy_MinLength', 14);
const maxLength = useSetting('Accounts_Password_Policy_MaxLength', -1);
const forbidRepeatingCharacters = useSetting('Accounts_Password_Policy_ForbidRepeatingCharacters', true);
const forbidRepeatingCharactersCount = useSetting('Accounts_Password_Policy_ForbidRepeatingCharactersCount', 3);
const mustContainAtLeastOneLowercase = useSetting('Accounts_Password_Policy_AtLeastOneLowercase', true);
const mustContainAtLeastOneUppercase = useSetting('Accounts_Password_Policy_AtLeastOneUppercase', true);
const mustContainAtLeastOneNumber = useSetting('Accounts_Password_Policy_AtLeastOneNumber', true);
const mustContainAtLeastOneSpecialCharacter = useSetting('Accounts_Password_Policy_AtLeastOneSpecialCharacter', true);
return {
enabled,
minLength,
maxLength,
forbidRepeatingCharacters,
forbidRepeatingCharactersCount,
mustContainAtLeastOneLowercase,
mustContainAtLeastOneUppercase,
mustContainAtLeastOneNumber,
mustContainAtLeastOneSpecialCharacter,
throwError: false,
};
};

View File

@@ -0,0 +1,10 @@
import type { ObjectId } from 'mongodb';
import { useContext, useMemo, useSyncExternalStore } from 'react';
import { AuthorizationContext } from '../AuthorizationContext';
export const usePermission = (permission: string | ObjectId, scope?: string | ObjectId): boolean => {
const { queryPermission } = useContext(AuthorizationContext);
const [subscribe, getSnapshot] = useMemo(() => queryPermission(permission, scope), [queryPermission, permission, scope]);
return useSyncExternalStore(subscribe, getSnapshot);
};

View File

@@ -0,0 +1,20 @@
import type { IRole } from '@rocket.chat/core-typings';
import { useContext, useMemo, useSyncExternalStore } from 'react';
import { AuthorizationContext } from '../AuthorizationContext';
/**
* Check if the user has the permission considering the user have the scoped roles
* @param permission The permission to check
* @param scopedRoles The roles to be also considered
* @returns boolean
*/
export const usePermissionWithScopedRoles = (permission: string, scopedRoles: IRole['_id'][]): boolean => {
const { queryPermission } = useContext(AuthorizationContext);
const [subscribe, getSnapshot] = useMemo(
() => queryPermission(permission, undefined, scopedRoles),
[queryPermission, permission, scopedRoles],
);
return useSyncExternalStore(subscribe, getSnapshot);
};

View File

@@ -0,0 +1,10 @@
import type { ObjectId } from 'mongodb';
import { useContext, useMemo, useSyncExternalStore } from 'react';
import { AuthorizationContext } from '../AuthorizationContext';
export const useRole = (role: string | ObjectId): boolean => {
const { queryRole } = useContext(AuthorizationContext);
const [subscribe, getSnapshot] = useMemo(() => queryRole(role), [queryRole, role]);
return useSyncExternalStore(subscribe, getSnapshot);
};

View File

@@ -0,0 +1,40 @@
import type { IRole } from '@rocket.chat/core-typings';
import { useCallback, useContext, useMemo, useSyncExternalStore } from 'react';
import { AuthorizationContext } from '../AuthorizationContext';
/**
* Returns a function that maps a role reference to a role id.
*
* Authorization checks such as `queryRole` match on `IRole._id`. The default roles
* are seeded with `_id === name`, but a custom role gets a random id, so its name
* never matches. Use this resolver whenever the caller only knows the role name —
* an app that declares an action button filter, for example.
*
* A known id wins over a name, so an existing caller that already passes an id is
* unaffected. An unknown value passes through unchanged, which keeps the failure
* mode of the check it feeds.
*/
export const useRoleIdResolver = (): ((role: string) => IRole['_id']) => {
const { getRoles, subscribeToRoles } = useContext(AuthorizationContext);
const roles = useSyncExternalStore(subscribeToRoles, getRoles);
const idsByName = useMemo(() => {
const index = new Map<IRole['name'], IRole['_id']>();
for (const role of roles.values()) {
// `roles.create` and `roles.update` reject a name already taken by another role, so a
// name maps to at most one role. Should duplicates still exist (e.g. written straight
// to the database), the first one iterated wins instead of the last, so adding another
// duplicate later does not silently repoint every check that names it.
if (index.has(role.name)) {
continue;
}
index.set(role.name, role._id);
}
return index;
}, [roles]);
return useCallback((role: string) => (roles.has(role) ? role : (idsByName.get(role) ?? role)), [idsByName, roles]);
};

View File

@@ -0,0 +1,11 @@
import { useCallback, useContext, useSyncExternalStore } from 'react';
import { AuthorizationContext } from '../AuthorizationContext';
export const useRolesDescription = () => {
const { getRoles, subscribeToRoles } = useContext(AuthorizationContext);
const roles = useSyncExternalStore(subscribeToRoles, getRoles);
return useCallback((ids: string[]) => ids.map((role) => roles.get(role)?.description || roles.get(role)?.name || role), [roles]);
};

View File

@@ -0,0 +1,5 @@
import { useContext } from 'react';
import { AvatarUrlContext } from '../AvatarUrlContext';
export const useRoomAvatarPath = (): ((...args: any) => string) => useContext(AvatarUrlContext).getRoomPathAvatar;

View File

@@ -0,0 +1,6 @@
import { useContext } from 'react';
import type { RoomToolboxContextValue } from '../RoomToolboxContext';
import { RoomToolboxContext } from '../RoomToolboxContext';
export const useRoomToolbox = (): RoomToolboxContextValue => useContext(RoomToolboxContext);

View File

@@ -0,0 +1,36 @@
import { useContext, useMemo } from 'react';
import type { RouteName, RouteParameters } from '../RouterContext';
import { RouterContext } from '../RouterContext';
type Route = {
push: (
parameters?: RouteParameters,
queryStringParameters?: ((prev: Record<string, string>) => Record<string, string>) | Record<string, string>,
) => void;
replace: (
parameters?: RouteParameters,
queryStringParameters?: ((prev: Record<string, string>) => Record<string, string>) | Record<string, string>,
) => void;
};
/** @deprecated prefer `useRouter` */
export const useRoute = (name: RouteName): Route => {
const router = useContext(RouterContext);
return useMemo<Route>(
() => ({
push: (params, queryStringParameters) => {
const search =
typeof queryStringParameters === 'function' ? queryStringParameters(router.getSearchParameters()) : queryStringParameters;
router.navigate({ name, params, search }, { replace: false });
},
replace: (params, queryStringParameters) => {
const search =
typeof queryStringParameters === 'function' ? queryStringParameters(router.getSearchParameters()) : queryStringParameters;
router.navigate({ name, params, search }, { replace: true });
},
}),
[name, router],
);
};

View File

@@ -0,0 +1,13 @@
import { useCallback, useContext, useSyncExternalStore } from 'react';
import { RouterContext } from '../RouterContext';
export const useRouteParameter = (name: string): string | undefined => {
const router = useContext(RouterContext);
const getSnapshot = useCallback(() => {
return router.getRouteParameters()[name];
}, [router, name]);
return useSyncExternalStore(router.subscribeToRouteChange, getSnapshot);
};

View File

@@ -0,0 +1,5 @@
import { useContext } from 'react';
import { RouterContext } from '../RouterContext';
export const useRouter = () => useContext(RouterContext);

View File

@@ -0,0 +1,14 @@
import { useCallback, useContext, useSyncExternalStore } from 'react';
import { RouterContext } from '../RouterContext';
export const useSearchParameter = (name: string): string | undefined => {
const { getSearchParameters, subscribeToRouteChange } = useContext(RouterContext);
const getSnapshot = useCallback(() => {
const searchParameters = getSearchParameters();
return searchParameters[name];
}, [getSearchParameters, name]);
return useSyncExternalStore(subscribeToRouteChange, getSnapshot);
};

View File

@@ -0,0 +1,8 @@
import { useContext, useSyncExternalStore } from 'react';
import { RouterContext } from '../RouterContext';
export const useSearchParameters = () => {
const { getSearchParameters, subscribeToRouteChange } = useContext(RouterContext);
return useSyncExternalStore(subscribeToRouteChange, getSearchParameters);
};

View File

@@ -0,0 +1,25 @@
import { useContext } from 'react';
import type { Device } from '../DeviceContext';
import { DeviceContext, isDeviceContextEnabled } from '../DeviceContext';
type SelectedDevices = {
audioInput?: Device;
audioOutput?: Device;
};
export const useSelectedDevices = (): SelectedDevices | null => {
const context = useContext(DeviceContext);
if (!isDeviceContextEnabled(context)) {
console.warn(
'Device Management is disabled on unsecure contexts, see https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts/features_restricted_to_secure_contexts',
);
return null;
}
return {
audioInput: context.selectedAudioInputDevice,
audioOutput: context.selectedAudioOutputDevice,
};
};

View File

@@ -0,0 +1,12 @@
import type { IServerInfo } from '@rocket.chat/core-typings';
import { useContext } from 'react';
import { ServerContext } from '../ServerContext';
export const useServerInformation = (): IServerInfo => {
const { info } = useContext(ServerContext);
if (!info) {
throw new Error('useServerInformation: no info available');
}
return info;
};

View File

@@ -0,0 +1,9 @@
import { useContext, useMemo, useSyncExternalStore } from 'react';
import { SessionContext } from '../SessionContext';
export const useSession = (name: string): unknown => {
const { query } = useContext(SessionContext);
const [subscribe, getSnapshot] = useMemo(() => query(name), [query, name]);
return useSyncExternalStore(subscribe, getSnapshot);
};

View File

@@ -0,0 +1,8 @@
import { useCallback, useContext } from 'react';
import { SessionContext } from '../SessionContext';
export const useSessionDispatch = (name: string) => {
const { dispatch } = useContext(SessionContext);
return useCallback((value: unknown) => dispatch(name, value), [dispatch, name]);
};

View File

@@ -0,0 +1,18 @@
import { useContext } from 'react';
import type { Device } from '../DeviceContext';
import { DeviceContext, isDeviceContextEnabled } from '../DeviceContext';
type setInputMediaDevice = (inputDevice: Device) => void;
export const useSetInputMediaDevice = (): setInputMediaDevice => {
const context = useContext(DeviceContext);
if (!isDeviceContextEnabled(context)) {
console.warn(
'Device Management is disabled on unsecure contexts, see https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts/features_restricted_to_secure_contexts',
);
return () => undefined;
}
return context.setAudioInputDevice;
};

View File

@@ -0,0 +1,5 @@
import type { ReactNode } from 'react';
import { useModal } from './useModal';
export const useSetModal = (): ((modal?: ReactNode) => void) => useModal().setModal;

View File

@@ -0,0 +1,21 @@
import { useContext } from 'react';
import type { Device } from '../DeviceContext';
import { DeviceContext, isDeviceContextEnabled } from '../DeviceContext';
// This allows different places to set the output device by providing a HTMLAudioElement
type setOutputMediaDevice = ({ outputDevice, HTMLAudioElement }: { outputDevice: Device; HTMLAudioElement: HTMLAudioElement }) => void;
export const useSetOutputMediaDevice = (): setOutputMediaDevice => {
const context = useContext(DeviceContext);
if (!isDeviceContextEnabled(context)) {
console.warn(
'Device Management is disabled on unsecure contexts, see https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts/features_restricted_to_secure_contexts',
);
return () => undefined;
}
return context.setAudioOutputDevice;
};

View File

@@ -0,0 +1,13 @@
import type { ISetting } from '@rocket.chat/core-typings';
import { useSettingStructure } from './useSettingStructure';
export function useSetting<TSetting extends ISetting>(settingId: TSetting['_id']): TSetting['value'] | undefined;
export function useSetting(settingId: ISetting['_id'], fallbackValue: string): string;
export function useSetting(settingId: ISetting['_id'], fallbackValue: boolean): boolean;
export function useSetting(settingId: ISetting['_id'], fallbackValue: number): number;
export function useSetting<TValue>(settingId: ISetting['_id'], fallbackValue: TValue): TValue;
export function useSetting<TValue>(settingId: ISetting['_id'], fallbackValue?: TValue): TValue | undefined {
const setting = useSettingStructure(settingId);
return (setting?.value as TValue | undefined) ?? fallbackValue;
}

View File

@@ -0,0 +1,9 @@
import type { ISetting } from '@rocket.chat/core-typings';
import { useCallback } from 'react';
import { useSettingsDispatch } from './useSettingsDispatch';
export const useSettingSetValue = <T extends ISetting['value']>(_id: ISetting['_id']): ((value: T) => Promise<void>) => {
const dispatch = useSettingsDispatch();
return useCallback((value: T) => dispatch([{ _id, value }]), [dispatch, _id]);
};

View File

@@ -0,0 +1,10 @@
import type { ISetting } from '@rocket.chat/core-typings';
import { useContext, useMemo, useSyncExternalStore } from 'react';
import { SettingsContext } from '../SettingsContext';
export const useSettingStructure = (_id: ISetting['_id']): ISetting | undefined => {
const { querySetting } = useContext(SettingsContext);
const [subscribe, getSnapshot] = useMemo(() => querySetting(_id), [querySetting, _id]);
return useSyncExternalStore(subscribe, getSnapshot);
};

View File

@@ -0,0 +1,11 @@
import type { ISetting } from '@rocket.chat/core-typings';
import { useContext, useMemo, useSyncExternalStore } from 'react';
import type { SettingsContextQuery } from '../SettingsContext';
import { SettingsContext } from '../SettingsContext';
export const useSettings = (query?: SettingsContextQuery): ISetting[] => {
const { querySettings } = useContext(SettingsContext);
const [subscribe, getSnapshot] = useMemo(() => querySettings(query ?? {}), [querySettings, query]);
return useSyncExternalStore(subscribe, getSnapshot);
};

View File

@@ -0,0 +1,6 @@
import { useContext } from 'react';
import type { SettingsContextValue } from '../SettingsContext';
import { SettingsContext } from '../SettingsContext';
export const useSettingsDispatch = (): SettingsContextValue['dispatch'] => useContext(SettingsContext).dispatch;

View File

@@ -0,0 +1,30 @@
import type { StreamNames, StreamerEvents, StreamKeys, StreamerCallbackArgs } from '@rocket.chat/ddp-client';
import { useContext, useMemo } from 'react';
import { ServerContext } from '../ServerContext';
// Define a type for a function that can be used to unsubscribe from a stream
export type ServerStreamFunction<N extends StreamNames, K extends StreamKeys<N>> = (
args: K,
callback: (...args: StreamerCallbackArgs<N, K>) => void,
) => () => void;
// Define a type for the configuration of a stream, based on its name
type StreamsConfigs<N extends StreamNames> = StreamerEvents[N][number];
// Define a type for the callback function that will be returned by useStream/useSingleStream
// This type will depend on the type of the stream it subscribes to
type StreamerCallback<S extends StreamNames, N extends StreamsConfigs<S> = StreamsConfigs<S>> = <E extends N['key']>(
eventName: E,
cb: (...event: StreamerCallbackArgs<S, E>) => void,
) => () => void;
export function useStream<N extends StreamNames>(
streamName: N,
options?: {
retransmit?: boolean;
retransmitToSelf?: boolean;
},
): StreamerCallback<N> {
const { getStream } = useContext(ServerContext);
return useMemo(() => getStream(streamName, options), [getStream, streamName, options]);
}

View File

@@ -0,0 +1,13 @@
import type { StreamNames, StreamerEvents } from '@rocket.chat/ddp-client';
import { useContext, useMemo } from 'react';
import { ServerContext } from '../ServerContext';
export type StreamAllSubscriber<N extends StreamNames> = (
callback: (eventName: string, args: StreamerEvents[N][number]['args']) => void,
) => () => void;
export function useStreamAll<N extends StreamNames>(streamName: N): StreamAllSubscriber<N> {
const { getStreamAll } = useContext(ServerContext);
return useMemo(() => getStreamAll(streamName), [getStreamAll, streamName]);
}

View File

@@ -0,0 +1,6 @@
import { useContext } from 'react';
import type { ToastMessagesContextValue } from '../ToastMessagesContext';
import { ToastMessagesContext } from '../ToastMessagesContext';
export const useToastMessageDispatch = (): ToastMessagesContextValue['dispatch'] => useContext(ToastMessagesContext).dispatch;

View File

@@ -0,0 +1,6 @@
import { useContext } from 'react';
import type { TooltipContextValue } from '../TooltipContext';
import { TooltipContext } from '../TooltipContext';
export const useTooltipClose = (): TooltipContextValue['close'] => useContext(TooltipContext).close;

View File

@@ -0,0 +1,6 @@
import { useContext } from 'react';
import type { TooltipContextValue } from '../TooltipContext';
import { TooltipContext } from '../TooltipContext';
export const useTooltipOpen = (): TooltipContextValue['open'] => useContext(TooltipContext).open;

View File

@@ -0,0 +1,7 @@
import { useContext } from 'react';
import type { TranslationContextValue } from '../TranslationContext';
import { TranslationContext } from '../TranslationContext';
/** @deprecated prefer `useTranslation` from `react-i18next` */
export const useTranslation = (): TranslationContextValue['translate'] => useContext(TranslationContext).translate;

View File

@@ -0,0 +1,5 @@
import { useContext } from 'react';
import { AuthenticationContext } from '../AuthenticationContext';
export const useUnstoreLoginToken = (): ((callback: () => void) => () => void) => useContext(AuthenticationContext).unstoreLoginToken;

View File

@@ -0,0 +1,10 @@
import type { PathFor } from '@rocket.chat/rest-typings';
import { useCallback, useContext } from 'react';
import type { UploadResult } from '../ServerContext';
import { ServerContext } from '../ServerContext';
export const useUpload = (endpoint: PathFor<'POST'>): ((formData: any) => Promise<UploadResult> | { promise: Promise<UploadResult> }) => {
const { uploadToEndpoint } = useContext(ServerContext);
return useCallback((formData: any) => uploadToEndpoint(endpoint, formData), [endpoint, uploadToEndpoint]);
};

View File

@@ -0,0 +1,6 @@
import type { IUser } from '@rocket.chat/core-typings';
import { useContext } from 'react';
import { UserContext } from '../UserContext';
export const useUser = (): IUser | null => useContext(UserContext).user;

View File

@@ -0,0 +1,5 @@
import { useContext } from 'react';
import { AvatarUrlContext, type AvatarUrlContextValue } from '../AvatarUrlContext';
export const useUserAvatarPath = (): AvatarUrlContextValue['getUserPathAvatar'] => useContext(AvatarUrlContext).getUserPathAvatar;

View File

@@ -0,0 +1,5 @@
import { useContext } from 'react';
import { UserCardContext } from '../UserCardContext';
export const useUserCard = () => useContext(UserCardContext);

Some files were not shown because too many files have changed in this diff Show More