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,71 @@
import { mockAppRoot } from '@rocket.chat/mock-providers';
import { renderHook, waitFor } from '@testing-library/react';
import { useCustomEmoji } from './useCustomEmoji';
import { emoji } from '../../../../lib/emoji';
jest.mock('../../../../lib/customEmoji', () => ({ customRender: jest.fn((html: string) => html) }));
const pristinePackages = { ...emoji.packages };
beforeEach(() => {
emoji.list = {};
emoji.packages = { ...pristinePackages };
});
afterEach(() => {
jest.restoreAllMocks();
});
const createCustomEmoji = () => ({
_id: 'emojiId',
_updatedAt: new Date().toISOString(),
name: 'my-emoji',
aliases: ['my-alias'],
extension: 'png',
});
it('should call emoji-custom.list without a query parameter', async () => {
const getCustomEmojis = jest.fn().mockReturnValue({ emojis: { update: [], remove: [] } });
renderHook(() => useCustomEmoji(), {
wrapper: mockAppRoot().withEndpoint('GET', '/v1/emoji-custom.list', getCustomEmojis).build(),
});
await waitFor(() => expect(getCustomEmojis).toHaveBeenCalledWith({}));
});
it('should populate emoji.packages.emojiCustom with the emojis and aliases returned by the endpoint', async () => {
const customEmoji = createCustomEmoji();
const dispatchUpdateSpy = jest.spyOn(emoji, 'dispatchUpdate');
renderHook(() => useCustomEmoji(), {
wrapper: mockAppRoot()
.withEndpoint('GET', '/v1/emoji-custom.list', () => ({ emojis: { update: [customEmoji], remove: [] } }))
.build(),
});
await waitFor(() => expect(emoji.packages.emojiCustom.emojisByCategory.rocket).toContain('my-emoji'));
expect(emoji.packages.emojiCustom.list).toEqual(expect.arrayContaining([':my-emoji:', ':my-alias:']));
expect(emoji.list[':my-emoji:']).toMatchObject({ name: 'my-emoji', emojiPackage: 'emojiCustom' });
expect(emoji.list[':my-alias:']).toEqual({ emojiPackage: 'emojiCustom', aliasOf: 'my-emoji' });
expect(dispatchUpdateSpy).toHaveBeenCalled();
});
it('should reset emoji.packages.emojiCustom to an empty list when the request fails', async () => {
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
renderHook(() => useCustomEmoji(), {
wrapper: mockAppRoot()
.withEndpoint('GET', '/v1/emoji-custom.list', () => {
throw new Error('unexpected error');
})
.build(),
});
await waitFor(() => expect(consoleErrorSpy).toHaveBeenCalledWith('Error getting custom emoji ', expect.any(Error)));
expect(emoji.packages.emojiCustom.list).toEqual([]);
expect(emoji.packages.emojiCustom.emojisByCategory.rocket).toEqual([]);
});

View File

@@ -0,0 +1,55 @@
import { useEndpoint } from '@rocket.chat/ui-contexts';
import { useQuery } from '@tanstack/react-query';
import { useEffect } from 'react';
import { customRender } from '../../../../lib/customEmoji';
import { emoji } from '../../../../lib/emoji';
export const useCustomEmoji = () => {
const getCustomEmojis = useEndpoint('GET', '/v1/emoji-custom.list');
const result = useQuery({
queryKey: ['emoji-custom.list'],
queryFn: () => getCustomEmojis({}),
});
useEffect(() => {
emoji.packages.emojiCustom = {
emojiCategories: [{ key: 'rocket', i18n: 'Custom' }],
categoryIndex: 1,
toneList: {},
list: [],
_regexpSignature: null,
_regexp: null,
emojisByCategory: { rocket: [] },
render: customRender,
renderPicker: customRender,
};
if (result.isError) {
console.error('Error getting custom emoji ', result.error);
}
if (result.isSuccess) {
const {
emojis: { update: customEmojis },
} = result.data;
const addCustomEmojis = () => {
for (const currentEmoji of customEmojis) {
emoji.packages.emojiCustom.emojisByCategory.rocket.push(currentEmoji.name);
emoji.packages.emojiCustom.list?.push(`:${currentEmoji.name}:`);
emoji.list[`:${currentEmoji.name}:`] = { ...currentEmoji, emojiPackage: 'emojiCustom' } as any;
for (const alias of currentEmoji.aliases) {
emoji.packages.emojiCustom.list?.push(`:${alias}:`);
emoji.list[`:${alias}:`] = {
emojiPackage: 'emojiCustom',
aliasOf: currentEmoji.name,
};
}
}
emoji.dispatchUpdate();
};
addCustomEmojis();
}
}, [result.data, result.error, result.isError, result.isSuccess]);
};

View File

@@ -0,0 +1,121 @@
import { isE2EEPinnedMessage, type IRoom, type IMessage } from '@rocket.chat/core-typings';
import { useUserId, useSetting, useRouter, useLayout, useUser } from '@rocket.chat/ui-contexts';
import { useEffect, useRef } from 'react';
import { MentionsParser } from '../../../../../app/mentions/lib/MentionsParser';
import { e2e } from '../../../../lib/e2ee';
import { onClientBeforeSendMessage } from '../../../../lib/onClientBeforeSendMessage';
import { onClientMessageReceived } from '../../../../lib/onClientMessageReceived';
import { Rooms } from '../../../../stores';
import { useE2EEState } from '../../../room/hooks/useE2EEState';
export const useE2EEncryption = () => {
const userId = useUserId();
const enabled = useSetting('E2E_Enable', false);
const router = useRouter();
// we don't care about the reactivity of this boolean
const adminEmbedded = useLayout().isEmbedded && router.getLocationPathname().startsWith('/admin');
useEffect(() => {
if (!userId) {
return;
}
if (enabled && !adminEmbedded) {
e2e.startClient(userId);
} else {
e2e.setState('DISABLED');
e2e.closeAlert();
}
}, [adminEmbedded, enabled, userId]);
const state = useE2EEState();
const ready = state === 'READY' || state === 'SAVE_PASSWORD';
const listenersAttachedRef = useRef(false);
const mentionsEnabled = useSetting('E2E_Enabled_Mentions', true);
const me = useUser()?.username || '';
const pattern = useSetting('UTF8_User_Names_Validation', '[0-9a-zA-Z-_.]+');
const useRealName = useSetting('UI_Use_Real_Name', false);
useEffect(() => {
if (!ready) {
return;
}
if (listenersAttachedRef.current) {
return;
}
const offClientMessageReceived = onClientMessageReceived.use(async (msg) => {
const e2eRoom = await e2e.getInstanceByRoomId(msg.rid);
if (!e2eRoom?.shouldConvertReceivedMessages()) {
return msg;
}
if (isE2EEPinnedMessage(msg)) {
return e2e.decryptPinnedMessage(msg);
}
return e2e.decryptMessage(msg);
});
// Encrypt messages before sending
const offClientBeforeSendMessage = onClientBeforeSendMessage.use(async (message) => {
const e2eRoom = await e2e.getInstanceByRoomId(message.rid);
if (!e2eRoom) {
return message;
}
// e2e.getInstanceByRoomId already waits for the room to be available which means this logic needs to be
// refactored to avoid waiting for the room again
const subscription = await new Promise<IRoom>((resolve) => {
const room = Rooms.state.get(message.rid);
if (room) resolve(room);
const unsubscribe = Rooms.use.subscribe((state) => {
const room = state.get(message.rid);
if (room) {
unsubscribe();
resolve(room);
}
});
});
subscription.encrypted ? e2eRoom.resume() : e2eRoom.pause();
const shouldConvertSentMessages = await e2eRoom.shouldConvertSentMessages(message);
if (!shouldConvertSentMessages) return message;
if (mentionsEnabled) {
const mentions = new MentionsParser({
pattern: () => pattern,
useRealName: () => useRealName,
me: () => me,
});
const e2eMentions: IMessage['e2eMentions'] = {
e2eUserMentions: mentions.getUserMentions(message.msg),
e2eChannelMentions: mentions.getChannelMentions(message.msg),
};
message.e2eMentions = e2eMentions;
}
// Should encrypt this message.
const encryptedMessage = await e2eRoom.encryptMessage(message);
return encryptedMessage;
});
listenersAttachedRef.current = true;
return () => {
offClientMessageReceived();
offClientBeforeSendMessage();
listenersAttachedRef.current = false;
};
}, [me, mentionsEnabled, pattern, ready, useRealName]);
};

View File

@@ -0,0 +1,98 @@
import { useEndpoint, useRole, useSetModal, useSetting, useToastMessageDispatch } from '@rocket.chat/ui-contexts';
import { useMutation } from '@tanstack/react-query';
import { useEffect, useReducer } from 'react';
import { useTranslation } from 'react-i18next';
import FingerprintChangeModal from '../../../../components/FingerprintChangeModal';
import FingerprintChangeModalConfirmation from '../../../../components/FingerprintChangeModalConfirmation';
const reducer = (
state: { openModal: boolean; openConfirmation: boolean; newWorkspace?: boolean },
action: { type: 'openModal' | 'openConfirmation' | 'closeModal'; newWorkspace?: boolean },
) => {
switch (action.type) {
case 'openModal':
return { openModal: true, openConfirmation: false, newWorkspace: undefined };
case 'openConfirmation':
return { openModal: false, openConfirmation: true, newWorkspace: action.newWorkspace };
case 'closeModal':
return { openModal: false, openConfirmation: false, newWorkspace: undefined };
default:
return state;
}
};
export const useFingerprintChange = () => {
const { t } = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
const isAdmin = useRole('admin');
const setModal = useSetModal();
const deploymentFingerPrintVerified = useSetting('Deployment_FingerPrint_Verified', true);
const fingerprintEndpoint = useEndpoint('POST', '/v1/fingerprint');
const [{ openConfirmation, openModal, newWorkspace }, dispatch] = useReducer(reducer, {
openModal: false,
openConfirmation: false,
newWorkspace: undefined,
});
const { mutate: fingerPrintMutation } = useMutation({
mutationKey: ['settings', 'Deployment_FingerPrint_Verified'],
mutationFn: async (setDeploymentAs: 'new-workspace' | 'updated-configuration') => {
const result = await fingerprintEndpoint({ setDeploymentAs });
return {
...result,
setDeploymentAs,
};
},
onSuccess: ({ setDeploymentAs }) => {
if (setDeploymentAs === 'new-workspace') {
return dispatchToastMessage({ type: 'success', message: t('New_workspace_confirmed') });
}
return dispatchToastMessage({ type: 'success', message: t('Configuration_update_confirmed') });
},
});
useEffect(() => {
if (!isAdmin) {
return;
}
if (deploymentFingerPrintVerified === null || deploymentFingerPrintVerified === true) {
return;
}
dispatch({ type: 'openModal' });
return () => {
dispatch({ type: 'closeModal' });
};
}, [deploymentFingerPrintVerified, isAdmin]);
useEffect(() => {
if (!openModal && !openConfirmation) {
setModal(null);
}
if (openModal) {
setModal(
<FingerprintChangeModal
onConfirm={() => dispatch({ type: 'openConfirmation', newWorkspace: false })}
onCancel={() => dispatch({ type: 'openConfirmation', newWorkspace: true })}
onClose={() => dispatch({ type: 'closeModal' })}
/>,
);
}
if (openConfirmation && newWorkspace !== undefined) {
setModal(
<FingerprintChangeModalConfirmation
onConfirm={() => {
fingerPrintMutation(newWorkspace ? 'new-workspace' : 'updated-configuration');
dispatch({ type: 'closeModal' });
}}
onCancel={() => dispatch({ type: 'openModal' })}
onClose={() => dispatch({ type: 'closeModal' })}
newWorkspace={newWorkspace}
/>,
);
}
}, [fingerPrintMutation, setModal, openConfirmation, openModal, newWorkspace]);
};

View File

@@ -0,0 +1,15 @@
import type { FontSize } from '@rocket.chat/rest-typings';
import { useUserPreference } from '@rocket.chat/ui-contexts';
import { useLayoutEffect } from 'react';
import { useCreateFontStyleElement } from '../../../account/accessibility/hooks/useCreateFontStyleElement';
export const useFontStylePreference = () => {
const fontSize = useUserPreference<FontSize>('fontSize');
const createFontStyleElement = useCreateFontStyleElement();
useLayoutEffect(() => {
// Handle the edge case where the font-size preference ceases to exist: set to 100% (default)
createFontStyleElement(fontSize || '100%');
}, [fontSize, createFontStyleElement]);
};

View File

@@ -0,0 +1,36 @@
import { useSessionDispatch, useStream, useWipeLocalAuth } from '@rocket.chat/ui-contexts';
import { useEffect } from 'react';
import { isSdkTransportEnabled } from '../../../../lib/sdk/sdkTransportEnabled';
const sdkTransportEnabled = isSdkTransportEnabled();
export const useForceLogout = (userId: string) => {
const getNotifyUserStream = useStream('notify-user');
const setForceLogout = useSessionDispatch('forceLogout');
const wipeLocalAuth = useWipeLocalAuth();
useEffect(() => {
setForceLogout(false);
const unsubscribe = getNotifyUserStream(`${userId}/force_logout`, () => {
setForceLogout(true);
if (!sdkTransportEnabled) {
// Flag off: develop's exact behaviour — only set the session flag.
// The legacy WS-reconnect → loginWithToken-fails → accounts-base
// chain handles the actual local cleanup.
return;
}
// With the SDK socket as the transport, that chain no longer fires
// reliably: DDPSDK auto-retries loginWithToken on every `connected`
// and swallows the rejection with `void`, so the navbar stays on
// Home with stale credentials. Wipe the stored login token + userId
// here so the router falls back to /login.
wipeLocalAuth();
});
return unsubscribe;
}, [getNotifyUserStream, setForceLogout, userId, wipeLocalAuth]);
};

View File

@@ -0,0 +1,20 @@
import { useOnLogout, useSetting } from '@rocket.chat/ui-contexts';
import { useEffect } from 'react';
import { useFireGlobalEvent } from '../../../../hooks/useFireGlobalEvent';
import { closeAllRooms } from '../closeAllRooms';
import { purgeAllDrafts } from '../purgeAllDrafts';
export const useLogoutCleanup = () => {
const onLogout = useOnLogout();
const logoutCustomScript = useSetting('Custom_Script_On_Logout');
const { mutate: fireLogoutCustomScript } = useFireGlobalEvent('Custom_Script_On_Logout');
useEffect(() => {
return onLogout(() => {
purgeAllDrafts();
closeAllRooms();
fireLogoutCustomScript(logoutCustomScript);
});
}, [fireLogoutCustomScript, logoutCustomScript, onLogout]);
};

View File

@@ -0,0 +1,58 @@
import type { ICalendarNotification, IUser } from '@rocket.chat/core-typings';
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
import { imperativeModal } from '@rocket.chat/ui-client';
import { useStream, useUserPreference } from '@rocket.chat/ui-contexts';
import { useEffect } from 'react';
import OutlookCalendarEventModal from '../../../outlookCalendar/OutlookCalendarEventModal';
export const useNotificationUserCalendar = (user: IUser) => {
const requireInteraction = useUserPreference('desktopNotificationRequireInteraction');
const notifyUserStream = useStream('notify-user');
const notifyUserCalendar = useStableCallback(async (notification: ICalendarNotification) => {
if (user.status === 'busy') {
return;
}
let body = notification.text;
if (notification.payload?.startTimeUtc) {
try {
const time = new Date(notification.payload.startTimeUtc);
const formattedTime = time.toLocaleTimeString(undefined, {
hour: 'numeric',
minute: 'numeric',
dayPeriod: 'narrow',
});
body = formattedTime;
} catch (error) {
console.error('Failed to format calendar notification time:', error);
body = notification.text;
}
}
const n = new Notification(notification.title, {
body,
tag: notification.payload._id,
silent: true,
requireInteraction,
} as NotificationOptions);
n.onclick = function () {
this.close();
window.focus();
imperativeModal.open({
component: OutlookCalendarEventModal,
props: { id: notification.payload._id, onClose: imperativeModal.close, onCancel: imperativeModal.close },
});
};
});
useEffect(() => {
if (!user?._id || !user.settings?.calendar?.outlook?.Enabled) {
return;
}
return notifyUserStream(`${user._id}/calendar`, notifyUserCalendar);
}, [notifyUserCalendar, notifyUserStream, user.settings?.calendar?.outlook?.Enabled, user?._id]);
};

View File

@@ -0,0 +1,76 @@
import type { AtLeast, INotificationDesktop, ISubscription, IUser } from '@rocket.chat/core-typings';
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
import { useEmbeddedLayout } from '@rocket.chat/ui-client';
import { useCustomSound, useRouter, useStream, useUserPreference } from '@rocket.chat/ui-contexts';
import { useEffect } from 'react';
import { useDesktopNotification } from '../../../../hooks/notification/useDesktopNotification';
import { useNewMessageNotification } from '../../../../hooks/notification/useNewMessageNotification';
import { RoomManager } from '../../../../lib/RoomManager';
import { fireGlobalEvent } from '../../../../lib/utils/fireGlobalEvent';
export const useNotifyUser = (user: IUser) => {
const router = useRouter();
const isLayoutEmbedded = useEmbeddedLayout();
const notifyUserStream = useStream('notify-user');
const muteFocusedConversations = useUserPreference('muteFocusedConversations');
const { notificationSounds } = useCustomSound();
const newMessageNotification = useNewMessageNotification();
const showDesktopNotification = useDesktopNotification();
const notifyNewRoom = useStableCallback(async (sub: AtLeast<ISubscription, 'rid'>): Promise<void> => {
if (user.status === 'busy') {
return;
}
if ((!router.getRouteParameters().name || router.getRouteParameters().name !== sub.name) && !sub.ls && sub.alert === true) {
notificationSounds.playNewRoom();
}
});
const notifyNewMessageAudioAndDesktop = useStableCallback((notification: INotificationDesktop) => {
const hasFocus = document.hasFocus();
const openedRoomId = ['channel', 'group', 'direct'].includes(router.getRouteName() || '') ? RoomManager.opened : undefined;
const { rid } = notification.payload;
const messageIsInOpenedRoom = openedRoomId === rid;
fireGlobalEvent('notification', {
notification,
fromOpenedRoom: messageIsInOpenedRoom,
hasFocus,
});
if (isLayoutEmbedded) {
if (!hasFocus && messageIsInOpenedRoom) {
// Play a notification sound
newMessageNotification(notification.payload);
showDesktopNotification(notification);
}
} else if (!hasFocus || !messageIsInOpenedRoom || !muteFocusedConversations) {
// Play a notification sound
newMessageNotification(notification.payload);
showDesktopNotification(notification);
}
});
useEffect(() => {
const unsubNotification = notifyUserStream(`${user._id}/notification`, notifyNewMessageAudioAndDesktop);
const unsubSubs = notifyUserStream(`${user._id}/subscriptions-changed`, (action, sub) => {
if (action !== 'inserted') {
return;
}
void notifyNewRoom(sub);
});
return () => {
unsubNotification();
unsubSubs();
};
}, [notifyNewMessageAudioAndDesktop, notifyNewRoom, notifyUserStream, router, user._id]);
useEffect(() => () => notificationSounds.stopNewRoom(), [notificationSounds]);
};

View File

@@ -0,0 +1,30 @@
import { useEndpoint } from '@rocket.chat/ui-contexts';
import { useQuery } from '@tanstack/react-query';
import { useEffect } from 'react';
import { AuthorizationUtils } from '../../../../../app/authorization/lib';
export const useRestrictedRoles = (): void => {
const getLicenseInfo = useEndpoint('GET', '/v1/licenses.info');
const { data: isEnterprise, isSuccess } = useQuery({
queryKey: ['isEnterprise'],
queryFn: async () => (await getLicenseInfo({})).license.hasValidLicense,
});
useEffect(() => {
if (!isSuccess) {
return;
}
if (isEnterprise) {
AuthorizationUtils.addRolePermissionWhiteList('guest', [
'view-d-room',
'view-joined-room',
'view-p-room',
'start-discussion',
'mobile-upload-file',
]);
}
}, [isEnterprise, isSuccess]);
};

View File

@@ -0,0 +1,63 @@
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
import { useRole, useSetModal, useSetting, useSettingSetValue, useToastMessageDispatch } from '@rocket.chat/ui-contexts';
import { useMutation } from '@tanstack/react-query';
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import UrlChangeModal from '../../../../components/UrlChangeModal';
import { getRootUrl, getRootUrlPathPrefix } from '../../../../lib/meteorRuntimeConfig';
export const useRootUrlChange = () => {
const { t } = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
const isAdmin = useRole('admin');
const setModal = useSetModal();
const closeModal = useStableCallback(() => setModal(null));
const currentUrl = location.origin + getRootUrlPathPrefix();
const siteUrl = useSetting('Site_Url', '');
const documentDomain = useSetting('Document_Domain', '');
const setSiteUrl = useSettingSetValue('Site_Url');
const {
mutate: siteUrlMutation,
isPending,
isSuccess,
} = useMutation({
mutationKey: ['settings', 'Site_Url'],
mutationFn: async (url: string) => {
await setSiteUrl(url);
return { url };
},
onSuccess: ({ url }) => dispatchToastMessage({ type: 'success', message: t('Saved_new_url_site_is__url__', { url }) }),
onError: () => dispatchToastMessage({ type: 'error', message: t('Something_went_wrong') }),
});
useEffect(() => {
if (!isAdmin) {
return;
}
if (!siteUrl) {
return;
}
if (isPending || isSuccess) {
return;
}
const rootUrl = getRootUrl();
if (rootUrl && rootUrl.replace(/\/$/, '') === currentUrl) {
return;
}
const onConfirm = () => {
closeModal();
siteUrlMutation(currentUrl);
};
setModal(<UrlChangeModal onClose={closeModal} onConfirm={onConfirm} siteUrl={siteUrl} currentUrl={currentUrl} />);
if (documentDomain) {
window.document.domain = documentDomain;
}
return closeModal;
}, [currentUrl, documentDomain, siteUrlMutation, siteUrl, isAdmin, isPending, isSuccess, setModal, closeModal]);
};

View File

@@ -0,0 +1,18 @@
import { useIsLoggingIn, useLoginToken } from '@rocket.chat/ui-contexts';
import { useEffect } from 'react';
export const useStoreCookiesOnLogin = (userId: string) => {
const isLoggingIn = useIsLoggingIn();
const loginToken = useLoginToken();
useEffect(() => {
// Check for isLoggingIn to be reactive and ensure it will process only after login finishes
// preventing race condition setting the rc_token as null forever
if (isLoggingIn === false) {
const secure = location.protocol === 'https:' ? '; secure' : '';
document.cookie = `rc_uid=${encodeURI(userId)}; path=/${secure}`;
document.cookie = `rc_token=${encodeURI(loginToken ?? '')}; path=/${secure}`;
}
}, [isLoggingIn, loginToken, userId]);
};

View File

@@ -0,0 +1,16 @@
import { useSetModal } from '@rocket.chat/ui-contexts';
import { useEffect } from 'react';
import { useRequire2faSetup } from '../../../hooks/useRequire2faSetup';
import TwoFactorRequiredModal from '../../MainLayout/TwoFactorRequiredModal';
export const useTwoFactorAuthSetupCheck = () => {
const setModal = useSetModal();
const require2faSetup = useRequire2faSetup();
useEffect(() => {
if (require2faSetup) {
setModal(<TwoFactorRequiredModal />);
}
}, [setModal, require2faSetup]);
};

View File

@@ -0,0 +1,69 @@
import { manageFavicon } from '@rocket.chat/favicon';
import { useSession, useSessionDispatch, useUserPreference, useUserSubscriptions } from '@rocket.chat/ui-contexts';
import { useEffect, useRef } from 'react';
import { useFireGlobalEvent } from '../../../../hooks/useFireGlobalEvent';
const query = { open: { $ne: false }, hideUnreadStatus: { $ne: true }, archived: { $ne: true } };
const options = { fields: { unread: 1, alert: 1, rid: 1, t: 1, name: 1, ls: 1, unreadAlert: 1, fname: 1, prid: 1 } };
const updateFavicon = manageFavicon();
type UnreadData = { unread: number; alert: boolean | undefined; unreadAlert: string | undefined };
export const useUnread = () => {
const unreadAlertEnabled = useUserPreference('unreadAlert');
const setUnread = useSessionDispatch('unread');
const unread = useSession('unread') as string | number | null | undefined;
const { mutate: fireEventUnreadChangedBySubscription } = useFireGlobalEvent('unread-changed-by-subscription');
const { mutate: fireEventUnreadChanged } = useFireGlobalEvent('unread-changed');
const subscriptions = useUserSubscriptions(query, options);
// We keep a lightweight snapshot of the last emitted per-subscription unread state so we only
// fire "unread-changed-by-subscription" for subscriptions whose unread-relevant fields changed.
// Previously we emitted one global event per subscription on ANY change, which scaled O(N)
// with the user subscription count (thousands) for every single message event, dominating CPU.
const prevSubsRef = useRef(new Map<string, UnreadData>());
useEffect(() => {
let badgeIndicator: false | '•' = false;
let unreadCount = 0;
const nextSnapshot = new Map<string, UnreadData>();
for (const subscription of subscriptions) {
const { rid, unread: unreadValue, alert, unreadAlert: subscriptionUnreadAlert } = subscription;
const prev = prevSubsRef.current.get(rid);
// Emit per-sub event only if something that influences unread UI changed.
if (!prev || prev.unread !== unreadValue || prev.alert !== alert || prev.unreadAlert !== subscriptionUnreadAlert) {
fireEventUnreadChangedBySubscription(subscription);
}
nextSnapshot.set(rid, { unread: unreadValue, alert, unreadAlert: subscriptionUnreadAlert });
if (alert || unreadValue > 0) {
if (alert === true && subscriptionUnreadAlert !== 'nothing') {
if (subscriptionUnreadAlert === 'all' || unreadAlertEnabled !== false) {
badgeIndicator = '•';
}
}
unreadCount += unreadValue;
}
}
prevSubsRef.current = nextSnapshot; // swap snapshot
if (unreadCount > 0) {
setUnread(unreadCount > 999 ? '999+' : unreadCount);
} else if (badgeIndicator !== false) {
setUnread(badgeIndicator);
} else {
setUnread('');
}
fireEventUnreadChanged(unreadCount);
}, [setUnread, subscriptions, unreadAlertEnabled, fireEventUnreadChangedBySubscription, fireEventUnreadChanged]);
useEffect(() => {
updateFavicon(unread);
}, [unread]);
};

View File

@@ -0,0 +1,16 @@
import { useEmbeddedLayout } from '@rocket.chat/ui-client';
import { useConnectionStatus, useIsLoggingIn } from '@rocket.chat/ui-contexts';
import { useEffect } from 'react';
import { VideoConfManager } from '../../../../lib/VideoConfManager';
export const useUpdateVideoConfUser = (userId: string) => {
const { connected } = useConnectionStatus();
const isLoggingIn = useIsLoggingIn();
const embeddedLayout = useEmbeddedLayout();
useEffect(() => {
// Videconf should not be available in embedded layout
VideoConfManager.updateUser(embeddedLayout ? null : userId, isLoggingIn, connected);
}, [userId, isLoggingIn, connected, embeddedLayout]);
};