{items.length === 0 && !isLoading &&
}
{items.length > 0 && (
-
+
{filterText ? t('Results') : t('Recent')}
)}
diff --git a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAuditMenu.spec.tsx b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAuditMenu.spec.tsx
index de197ab30f09f..aa66de600a502 100644
--- a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAuditMenu.spec.tsx
+++ b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAuditMenu.spec.tsx
@@ -129,3 +129,30 @@ it('should return audiLogs item if have license and can-audit-log permission', a
),
);
});
+
+it('should return auditSecurityLog item if have license and can-audit-log permission', async () => {
+ const { result } = renderHook(() => useAuditMenu(), {
+ wrapper: mockAppRoot()
+ .withEndpoint('GET', '/v1/licenses.info', () => ({
+ license: {
+ license: {
+ // @ts-expect-error: just for testing
+ grantedModules: [{ module: 'auditing' }],
+ },
+ // @ts-expect-error: just for testing
+ activeModules: ['auditing'],
+ },
+ }))
+ .withJohnDoe()
+ .withPermission('can-audit')
+ .build(),
+ });
+
+ await waitFor(() =>
+ expect(result.current.items[1]).toEqual(
+ expect.objectContaining({
+ id: 'auditSecurityLog',
+ }),
+ ),
+ );
+});
diff --git a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAuditMenu.tsx b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAuditMenu.tsx
index be412480c3007..744513c6316e0 100644
--- a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAuditMenu.tsx
+++ b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAuditMenu.tsx
@@ -25,8 +25,18 @@ export const useAuditMenu = () => {
onClick: () => router.navigate('/audit-log'),
};
+ const auditSecurityLogsItem: GenericMenuItemProps = {
+ id: 'auditSecurityLog',
+ content: t('Security_logs'),
+ onClick: () => router.navigate('/security-logs'),
+ };
+
return {
title: t('Audit'),
- items: [hasAuditPermission && auditMessageItem, hasAuditLogPermission && auditLogItem].filter(Boolean) as GenericMenuItemProps[],
+ items: [
+ hasAuditPermission && auditMessageItem,
+ hasAuditLogPermission && auditLogItem,
+ hasAuditPermission && auditSecurityLogsItem,
+ ].filter(Boolean) as GenericMenuItemProps[],
};
};
diff --git a/apps/meteor/client/apps/RealAppsEngineUIHost.ts b/apps/meteor/client/apps/RealAppsEngineUIHost.ts
index 85f024561de95..54bd4c8fa84da 100644
--- a/apps/meteor/client/apps/RealAppsEngineUIHost.ts
+++ b/apps/meteor/client/apps/RealAppsEngineUIHost.ts
@@ -2,11 +2,11 @@ import { AppsEngineUIHost } from '@rocket.chat/apps-engine/client/AppsEngineUIHo
import type { IExternalComponentRoomInfo, IExternalComponentUserInfo } from '@rocket.chat/apps-engine/client/definition';
import { Meteor } from 'meteor/meteor';
-import { Rooms } from '../../app/models/client';
import { getUserAvatarURL } from '../../app/utils/client/getUserAvatarURL';
import { sdk } from '../../app/utils/client/lib/SDKClient';
import { RoomManager } from '../lib/RoomManager';
import { baseURI } from '../lib/baseURI';
+import { Rooms } from '../stores';
// FIXME: replace non-null assertions with proper error handling
diff --git a/apps/meteor/client/cachedStores/PermissionsCachedStore.ts b/apps/meteor/client/cachedStores/PermissionsCachedStore.ts
new file mode 100644
index 0000000000000..f52f94ab3985d
--- /dev/null
+++ b/apps/meteor/client/cachedStores/PermissionsCachedStore.ts
@@ -0,0 +1,10 @@
+import type { IPermission } from '@rocket.chat/core-typings';
+
+import { PrivateCachedStore } from '../lib/cachedStores';
+import { Permissions } from '../stores';
+
+export const PermissionsCachedStore = new PrivateCachedStore({
+ name: 'permissions',
+ eventType: 'notify-logged',
+ store: Permissions.use,
+});
diff --git a/apps/meteor/client/cachedStores/PrivateSettingsCachedStore.ts b/apps/meteor/client/cachedStores/PrivateSettingsCachedStore.ts
new file mode 100644
index 0000000000000..4469b3f1f2bcb
--- /dev/null
+++ b/apps/meteor/client/cachedStores/PrivateSettingsCachedStore.ts
@@ -0,0 +1,31 @@
+import type { ISetting } from '@rocket.chat/core-typings';
+
+import { sdk } from '../../app/utils/client/lib/SDKClient';
+import { PrivateCachedStore } from '../lib/cachedStores';
+import { PrivateSettings } from '../stores';
+
+class PrivateSettingsCachedStore extends PrivateCachedStore {
+ constructor() {
+ super({
+ name: 'private-settings',
+ eventType: 'notify-logged',
+ store: PrivateSettings.use,
+ });
+ }
+
+ override setupListener() {
+ return sdk.stream('notify-logged', ['private-settings-changed'], async (t, setting) => {
+ this.log('record received', t, setting);
+ const { _id, ...fields } = setting;
+ this.store.getState().update(
+ (record) => record._id === _id,
+ (record) => ({ ...record, ...fields }),
+ );
+ this.sync();
+ });
+ }
+}
+
+const instance = new PrivateSettingsCachedStore();
+
+export { instance as PrivateSettingsCachedStore };
diff --git a/apps/meteor/client/cachedStores/PublicSettingsCachedStore.ts b/apps/meteor/client/cachedStores/PublicSettingsCachedStore.ts
new file mode 100644
index 0000000000000..095b15c2fa056
--- /dev/null
+++ b/apps/meteor/client/cachedStores/PublicSettingsCachedStore.ts
@@ -0,0 +1,18 @@
+import type { ISetting } from '@rocket.chat/core-typings';
+
+import { PublicCachedStore } from '../lib/cachedStores';
+import { PublicSettings } from '../stores';
+
+class PublicSettingsCachedStore extends PublicCachedStore {
+ constructor() {
+ super({
+ name: 'public-settings',
+ eventType: 'notify-all',
+ store: PublicSettings.use,
+ });
+ }
+}
+
+const instance = new PublicSettingsCachedStore();
+
+export { instance as PublicSettingsCachedStore };
diff --git a/apps/meteor/app/models/client/models/CachedChatRoom.ts b/apps/meteor/client/cachedStores/RoomsCachedStore.ts
similarity index 79%
rename from apps/meteor/app/models/client/models/CachedChatRoom.ts
rename to apps/meteor/client/cachedStores/RoomsCachedStore.ts
index 3071781758ef3..b965a56f0b1e5 100644
--- a/apps/meteor/app/models/client/models/CachedChatRoom.ts
+++ b/apps/meteor/client/cachedStores/RoomsCachedStore.ts
@@ -2,16 +2,15 @@ import type { IOmnichannelRoom, IRoom, IRoomWithRetentionPolicy } from '@rocket.
import { DEFAULT_SLA_CONFIG, LivechatPriorityWeight } from '@rocket.chat/core-typings';
import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts';
-import { CachedChatSubscription } from './CachedChatSubscription';
-import { PrivateCachedStore } from '../../../../client/lib/cachedCollections/CachedCollection';
-import { createDocumentMapStore } from '../../../../client/lib/cachedCollections/DocumentMapStore';
+import { PrivateCachedStore } from '../lib/cachedStores';
+import { Rooms, Subscriptions } from '../stores';
-class CachedChatRoom extends PrivateCachedStore {
+class RoomsCachedStore extends PrivateCachedStore {
constructor() {
super({
name: 'rooms',
eventType: 'notify-user',
- store: createDocumentMapStore(),
+ store: Rooms.use,
});
}
@@ -67,7 +66,7 @@ class CachedChatRoom extends PrivateCachedStore {
}
protected override handleLoadedFromServer(rooms: IRoom[]): void {
- const indexedSubscriptions = CachedChatSubscription.collection.state.indexBy('rid');
+ const indexedSubscriptions = Subscriptions.use.getState().indexBy('rid');
const subscriptionsWithRoom = rooms.flatMap((room) => {
const sub = indexedSubscriptions.get(room._id);
@@ -77,7 +76,7 @@ class CachedChatRoom extends PrivateCachedStore {
return this.merge(room, sub);
});
- CachedChatSubscription.collection.state.storeMany(subscriptionsWithRoom);
+ Subscriptions.use.getState().storeMany(subscriptionsWithRoom);
}
protected override async handleRecordEvent(action: 'removed' | 'changed', room: IRoom) {
@@ -85,7 +84,7 @@ class CachedChatRoom extends PrivateCachedStore {
if (action === 'removed') return;
- CachedChatSubscription.collection.state.update(
+ Subscriptions.use.getState().update(
(record) => record.rid === room._id,
(sub) => this.merge(room, sub),
);
@@ -94,7 +93,7 @@ class CachedChatRoom extends PrivateCachedStore {
protected override handleSyncEvent(action: 'removed' | 'changed', room: IRoom): void {
if (action === 'removed') return;
- CachedChatSubscription.collection.state.update(
+ Subscriptions.use.getState().update(
(record) => record.rid === room._id,
(sub) => this.merge(room, sub),
);
@@ -111,9 +110,6 @@ class CachedChatRoom extends PrivateCachedStore {
}
}
-const instance = new CachedChatRoom();
+const instance = new RoomsCachedStore();
-export {
- /** @deprecated new code refer to Minimongo collections like this one; prefer fetching data from the REST API, listening to changes via streamer events, and storing the state in a Tanstack Query */
- instance as CachedChatRoom,
-};
+export { instance as RoomsCachedStore };
diff --git a/apps/meteor/app/models/client/models/CachedChatSubscription.ts b/apps/meteor/client/cachedStores/SubscriptionsCachedStore.ts
similarity index 80%
rename from apps/meteor/app/models/client/models/CachedChatSubscription.ts
rename to apps/meteor/client/cachedStores/SubscriptionsCachedStore.ts
index 3a1440bf873cc..d3c2032201f7c 100644
--- a/apps/meteor/app/models/client/models/CachedChatSubscription.ts
+++ b/apps/meteor/client/cachedStores/SubscriptionsCachedStore.ts
@@ -2,26 +2,20 @@ import type { IOmnichannelRoom, IRoomWithRetentionPolicy, ISubscription } from '
import { DEFAULT_SLA_CONFIG, LivechatPriorityWeight } from '@rocket.chat/core-typings';
import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts';
-import { CachedChatRoom } from './CachedChatRoom';
-import { PrivateCachedCollection } from '../../../../client/lib/cachedCollections/CachedCollection';
+import { PrivateCachedStore } from '../lib/cachedStores';
+import { Rooms, Subscriptions } from '../stores';
-declare module '@rocket.chat/core-typings' {
- interface ISubscription {
- lowerCaseName: string;
- lowerCaseFName: string;
- }
-}
-
-class CachedChatSubscription extends PrivateCachedCollection {
+class SubscriptionsCachedStore extends PrivateCachedStore {
constructor() {
super({
name: 'subscriptions',
eventType: 'notify-user',
+ store: Subscriptions.use,
});
}
protected override mapRecord(subscription: ISubscription): SubscriptionWithRoom {
- const room = CachedChatRoom.store.getState().find((r) => r._id === subscription.rid);
+ const room = Rooms.use.getState().find((r) => r._id === subscription.rid);
const lastRoomUpdate = room?.lm || subscription.ts || room?.ts;
@@ -90,9 +84,6 @@ class CachedChatSubscription extends PrivateCachedCollection
({
- scrollbars: { autoHide: 'scroll' },
+ scrollbars: { autoHide: 'move' },
overflow: { x: overflowX ? 'scroll' : 'hidden' },
}) as const;
diff --git a/apps/meteor/client/components/FeaturePreviewSidePanelNavigation.tsx b/apps/meteor/client/components/FeaturePreviewSidePanelNavigation.tsx
deleted file mode 100644
index adffeea33cfda..0000000000000
--- a/apps/meteor/client/components/FeaturePreviewSidePanelNavigation.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import { FeaturePreview } from '@rocket.chat/ui-client';
-import type { ReactElement } from 'react';
-
-import { useSidePanelNavigationScreenSize } from '../hooks/useSidePanelNavigation';
-
-export const FeaturePreviewSidePanelNavigation = ({ children }: { children: ReactElement[] }) => {
- const disabled = !useSidePanelNavigationScreenSize();
- return ;
-};
diff --git a/apps/meteor/client/components/GazzodownText.spec.tsx b/apps/meteor/client/components/GazzodownText.spec.tsx
index 3e41da52b9c44..2bf9c746b5b0b 100644
--- a/apps/meteor/client/components/GazzodownText.spec.tsx
+++ b/apps/meteor/client/components/GazzodownText.spec.tsx
@@ -50,7 +50,7 @@ describe('GazzodownText highlights', () => {
,
- { legacyRoot: true, wrapper: wrapper.build() },
+ { wrapper: wrapper.build() },
);
// Expect that the highlighted element wraps exactly "тест"
expect(screen.getByTitle('Highlighted_chosen_word')).toHaveTextContent(/^тест$/i);
@@ -63,7 +63,7 @@ describe('GazzodownText highlights', () => {
,
- { legacyRoot: true, wrapper: wrapper.build() },
+ { wrapper: wrapper.build() },
);
expect(screen.getByTitle('Highlighted_chosen_word')).toHaveTextContent(/^тест$/i);
});
@@ -75,7 +75,7 @@ describe('GazzodownText highlights', () => {
,
- { legacyRoot: true, wrapper: wrapper.build() },
+ { wrapper: wrapper.build() },
);
expect(screen.queryByTitle('Highlighted_chosen_word')).not.toBeInTheDocument();
});
@@ -87,7 +87,7 @@ describe('GazzodownText highlights', () => {
,
- { legacyRoot: true, wrapper: wrapper.build() },
+ { wrapper: wrapper.build() },
);
expect(screen.getByTitle('Highlighted_chosen_word')).toHaveTextContent(/^тест$/i);
});
@@ -99,7 +99,7 @@ describe('GazzodownText highlights', () => {
,
- { legacyRoot: true, wrapper: wrapper.build() },
+ { wrapper: wrapper.build() },
);
expect(screen.getByTitle('Highlighted_chosen_word')).toHaveTextContent(/^test$/i);
});
@@ -111,7 +111,7 @@ describe('GazzodownText highlights', () => {
,
- { legacyRoot: true, wrapper: wrapper.build() },
+ { wrapper: wrapper.build() },
);
const highlightedElements = screen.getAllByTitle('Highlighted_chosen_word');
// Expect three separate highlights.
@@ -133,7 +133,7 @@ in it.`;
,
- { legacyRoot: true, wrapper: wrapper.build() },
+ { wrapper: wrapper.build() },
);
const highlightedElements = screen.getAllByTitle('Highlighted_chosen_word');
// At least two occurrences are expected: one for "Test" (capitalized) and one for "test"
@@ -151,7 +151,7 @@ in it.`;
,
- { legacyRoot: true, wrapper: wrapper.build() },
+ { wrapper: wrapper.build() },
);
// The highlighted element should contain only "test"
expect(screen.getByTitle('Highlighted_chosen_word')).toHaveTextContent(/^test$/i);
@@ -164,7 +164,7 @@ in it.`;
,
- { legacyRoot: true, wrapper: wrapper.build() },
+ { wrapper: wrapper.build() },
);
// The highlighted element should contain only "test"
expect(screen.getByTitle('Highlighted_chosen_word')).toHaveTextContent(/^test$/i);
@@ -177,7 +177,7 @@ in it.`;
,
- { legacyRoot: true, wrapper: wrapper.build() },
+ { wrapper: wrapper.build() },
);
// The highlighted element should contain only "test"
expect(screen.getByTitle('Highlighted_chosen_word')).toHaveTextContent(/^test$/i);
@@ -190,7 +190,7 @@ in it.`;
,
- { legacyRoot: true, wrapper: wrapper.build() },
+ { wrapper: wrapper.build() },
);
const highlightedElements = screen.getAllByTitle('Highlighted_chosen_word');
@@ -209,7 +209,7 @@ in it.`;
,
- { legacyRoot: true, wrapper: wrapper.build() },
+ { wrapper: wrapper.build() },
);
expect(screen.getByTitle('Highlighted_chosen_word')).toHaveTextContent(/^te-st_te\.st\/te=te!st:$/i);
@@ -222,7 +222,7 @@ in it.`;
,
- { legacyRoot: true, wrapper: wrapper.build() },
+ { wrapper: wrapper.build() },
);
expect(screen.getByTitle('Highlighted_chosen_word')).toHaveTextContent(/^test$/i);
});
diff --git a/apps/meteor/client/components/GenericNoResults/GenericNoResults.tsx b/apps/meteor/client/components/GenericNoResults/GenericNoResults.tsx
index 83e8d20a8537c..420137bcb46d0 100644
--- a/apps/meteor/client/components/GenericNoResults/GenericNoResults.tsx
+++ b/apps/meteor/client/components/GenericNoResults/GenericNoResults.tsx
@@ -6,10 +6,9 @@ type LinkProps = { linkText: string; linkHref: string } | { linkText?: never; li
type ButtonProps = { buttonTitle: string; buttonAction: () => void } | { buttonTitle?: never; buttonAction?: never };
type GenericNoResultsProps = {
- icon?: IconName;
+ icon?: IconName | null;
title?: string;
description?: string;
- buttonTitle?: string;
} & LinkProps &
ButtonProps;
@@ -27,7 +26,7 @@ const GenericNoResults = ({
return (
-
+ {icon && }
{title || t('No_results_found')}
{description && {description}}
{buttonTitle && buttonAction && (
diff --git a/apps/meteor/client/components/Omnichannel/modals/CloseChatModal.tsx b/apps/meteor/client/components/Omnichannel/modals/CloseChatModal.tsx
index 499a07588e743..fe476f047bae5 100644
--- a/apps/meteor/client/components/Omnichannel/modals/CloseChatModal.tsx
+++ b/apps/meteor/client/components/Omnichannel/modals/CloseChatModal.tsx
@@ -21,12 +21,12 @@ import {
ModalContent,
} from '@rocket.chat/fuselage';
import { GenericModal } from '@rocket.chat/ui-client';
-import { usePermission, useSetting, useTranslation, useUserPreference } from '@rocket.chat/ui-contexts';
+import { usePermission, useSetting, useUserPreference, useToastMessageDispatch } from '@rocket.chat/ui-contexts';
import { useCallback, useState, useEffect, useMemo } from 'react';
import { useForm } from 'react-hook-form';
+import { useTranslation } from 'react-i18next';
import { useHasLicenseModule } from '../../../hooks/useHasLicenseModule';
-import { dispatchToastMessage } from '../../../lib/toast';
import Tags from '../Tags';
type CloseChatModalFormData = {
@@ -50,7 +50,8 @@ type CloseChatModalProps = {
};
const CloseChatModal = ({ department, visitorEmail, onCancel, onConfirm }: CloseChatModalProps) => {
- const t = useTranslation();
+ const { t } = useTranslation();
+ const dispatchToastMessage = useToastMessageDispatch();
const {
formState: { errors },
@@ -147,11 +148,11 @@ const CloseChatModal = ({ department, visitorEmail, onCancel, onConfirm }: Close
}
setValue('subject', subject || customSubject || t('Transcript_of_your_livechat_conversation'));
}
- }, [transcriptEmail, setValue, visitorEmail, subject, t, customSubject]);
+ }, [transcriptEmail, setValue, visitorEmail, subject, t, customSubject, dispatchToastMessage]);
if (commentRequired || tagRequired || canSendTranscript) {
return (
- }>
+ }>
{t('Wrap_up_conversation')}
diff --git a/apps/meteor/client/components/Omnichannel/modals/EnterpriseDepartmentsModal.tsx b/apps/meteor/client/components/Omnichannel/modals/EnterpriseDepartmentsModal.tsx
index a4f18b9f54858..f7f02f76e3428 100644
--- a/apps/meteor/client/components/Omnichannel/modals/EnterpriseDepartmentsModal.tsx
+++ b/apps/meteor/client/components/Omnichannel/modals/EnterpriseDepartmentsModal.tsx
@@ -61,7 +61,7 @@ const EnterpriseDepartmentsModal = ({ closeModal }: { closeModal: () => void }):
-
diff --git a/apps/meteor/client/components/UserAndRoomAutoCompleteMultiple/UserAndRoomAutoCompleteMultiple.spec.tsx b/apps/meteor/client/components/UserAndRoomAutoCompleteMultiple/UserAndRoomAutoCompleteMultiple.spec.tsx
index f37e6a32c8c74..ff2a88d8093d6 100644
--- a/apps/meteor/client/components/UserAndRoomAutoCompleteMultiple/UserAndRoomAutoCompleteMultiple.spec.tsx
+++ b/apps/meteor/client/components/UserAndRoomAutoCompleteMultiple/UserAndRoomAutoCompleteMultiple.spec.tsx
@@ -1,91 +1,90 @@
-import { useUser, useUserSubscriptions, useRoomAvatarPath } from '@rocket.chat/ui-contexts';
+import { MockedAppRootBuilder } from '@rocket.chat/mock-providers/dist/MockedAppRootBuilder';
+import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import UserAndRoomAutoCompleteMultiple from './UserAndRoomAutoCompleteMultiple';
+import { createFakeSubscription, createFakeUser } from '../../../tests/mocks/data';
import { roomCoordinator } from '../../lib/rooms/roomCoordinator';
-// Mock dependencies
-jest.mock('@rocket.chat/ui-contexts', () => ({
- useUser: jest.fn(),
- useUserSubscriptions: jest.fn(),
- useRoomAvatarPath: jest.fn(),
-}));
+const user = createFakeUser({
+ active: true,
+ roles: ['admin'],
+ type: 'user',
+});
+
+const direct = createFakeSubscription({
+ t: 'd',
+ name: 'Direct',
+});
+
+const channel = createFakeSubscription({
+ t: 'c',
+ name: 'General',
+});
+
+const appRoot = new MockedAppRootBuilder()
+ .withSubscriptions([
+ { ...direct, ro: false },
+ { ...channel, ro: true },
+ ] as unknown as SubscriptionWithRoom[])
+ .withUser(user);
+
jest.mock('../../lib/rooms/roomCoordinator', () => ({
- roomCoordinator: { readOnly: jest.fn() },
+ roomCoordinator: {
+ readOnly: jest.fn(),
+ },
}));
-const mockUser = { _id: 'user1', username: 'testuser' };
+beforeEach(() => {
+ (roomCoordinator.readOnly as jest.Mock).mockReturnValue(false);
+});
-const mockRooms = [
- {
- rid: 'room1',
- fname: 'General',
- name: 'general',
- t: 'c',
- avatarETag: 'etag1',
- },
- {
- rid: 'room2',
- fname: 'Direct',
- name: 'direct',
- t: 'd',
- avatarETag: 'etag2',
- blocked: false,
- blocker: false,
- },
-];
-
-describe('UserAndRoomAutoCompleteMultiple', () => {
- beforeEach(() => {
- (useUser as jest.Mock).mockReturnValue(mockUser);
- (useUserSubscriptions as jest.Mock).mockReturnValue(mockRooms);
- (useRoomAvatarPath as jest.Mock).mockReturnValue((rid: string) => `/avatar/path/${rid}`);
- (roomCoordinator.readOnly as jest.Mock).mockReturnValue(false);
- });
+afterEach(() => jest.clearAllMocks());
- it('should render options based on user subscriptions', async () => {
- render();
+it('should render options based on user subscriptions', async () => {
+ render(, { wrapper: appRoot.build() });
- const input = screen.getByRole('textbox');
- await userEvent.click(input);
+ const input = screen.getByRole('textbox');
+ await userEvent.click(input);
- await waitFor(() => {
- expect(screen.getByText('General')).toBeInTheDocument();
- });
+ await waitFor(() => {
+ expect(screen.getByText('Direct')).toBeInTheDocument();
+ });
- await waitFor(() => {
- expect(screen.getByText('Direct')).toBeInTheDocument();
- });
+ await waitFor(() => {
+ expect(screen.getByText('General')).toBeInTheDocument();
});
+});
- it('should filter out read-only rooms', async () => {
- (roomCoordinator.readOnly as jest.Mock).mockImplementation((rid) => rid === 'room1');
- render();
+it('should filter out read-only rooms', async () => {
+ (roomCoordinator.readOnly as jest.Mock).mockReturnValueOnce(true);
- const input = screen.getByRole('textbox');
- await userEvent.click(input);
+ render(, { wrapper: appRoot.build() });
- await waitFor(() => {
- expect(screen.queryByText('General')).not.toBeInTheDocument();
- });
- await waitFor(() => {
- expect(screen.getByText('Direct')).toBeInTheDocument();
- });
+ const input = screen.getByRole('textbox');
+ await userEvent.click(input);
+
+ await waitFor(() => {
+ expect(screen.getByText('General')).toBeInTheDocument();
});
- it('should call onChange when selecting an option', async () => {
- const handleChange = jest.fn();
- render();
+ await waitFor(() => {
+ expect(screen.queryByText('Direct')).not.toBeInTheDocument();
+ });
+});
- const input = screen.getByRole('textbox');
- await userEvent.click(input);
+it('should call onChange when selecting an option', async () => {
+ const handleChange = jest.fn();
+ render(, { wrapper: appRoot.build() });
- await waitFor(() => {
- expect(screen.getByText('General')).toBeInTheDocument();
- });
+ const input = screen.getByRole('textbox');
+ await userEvent.click(input);
- await userEvent.click(screen.getByText('General'));
- expect(handleChange).toHaveBeenCalled();
+ await waitFor(() => {
+ expect(screen.getByText('General')).toBeInTheDocument();
});
+
+ await userEvent.click(screen.getByText('General'));
+ expect(handleChange).toHaveBeenCalled();
});
diff --git a/apps/meteor/client/components/UserAndRoomAutoCompleteMultiple/UserAndRoomAutoCompleteMultiple.tsx b/apps/meteor/client/components/UserAndRoomAutoCompleteMultiple/UserAndRoomAutoCompleteMultiple.tsx
index 509718bb1129d..804a0e5c72e0c 100644
--- a/apps/meteor/client/components/UserAndRoomAutoCompleteMultiple/UserAndRoomAutoCompleteMultiple.tsx
+++ b/apps/meteor/client/components/UserAndRoomAutoCompleteMultiple/UserAndRoomAutoCompleteMultiple.tsx
@@ -7,8 +7,8 @@ import { useUser, useUserSubscriptions } from '@rocket.chat/ui-contexts';
import type { ComponentProps } from 'react';
import { memo, useMemo, useState } from 'react';
-import { Rooms } from '../../../app/models/client';
import { roomCoordinator } from '../../lib/rooms/roomCoordinator';
+import { Rooms } from '../../stores';
type UserAndRoomAutoCompleteMultipleProps = Omit, 'filter'> & { limit?: number };
diff --git a/apps/meteor/client/components/Wizard/WizardActions.tsx b/apps/meteor/client/components/Wizard/WizardActions.tsx
new file mode 100644
index 0000000000000..82c3b22fb55ce
--- /dev/null
+++ b/apps/meteor/client/components/Wizard/WizardActions.tsx
@@ -0,0 +1,16 @@
+import { Box, ButtonGroup } from '@rocket.chat/fuselage';
+import type { ReactNode } from 'react';
+
+type WizardActionsProps = {
+ children: ReactNode;
+};
+
+const WizardActions = ({ children }: WizardActionsProps) => {
+ return (
+
+ {children}
+
+ );
+};
+
+export default WizardActions;
diff --git a/apps/meteor/client/components/connectionStatus/ConnectionStatusBar.stories.tsx b/apps/meteor/client/components/connectionStatus/ConnectionStatusBar.stories.tsx
index 857f42760ed35..267c2df2a801d 100644
--- a/apps/meteor/client/components/connectionStatus/ConnectionStatusBar.stories.tsx
+++ b/apps/meteor/client/components/connectionStatus/ConnectionStatusBar.stories.tsx
@@ -1,7 +1,7 @@
-import { ConnectionStatusContext } from '@rocket.chat/ui-contexts';
+import { mockAppRoot } from '@rocket.chat/mock-providers';
+import type { ServerContextValue } from '@rocket.chat/ui-contexts';
import { action } from '@storybook/addon-actions';
import type { Meta, StoryFn } from '@storybook/react';
-import type { ContextType, ReactElement } from 'react';
import ConnectionStatusBar from './ConnectionStatusBar';
@@ -13,9 +13,14 @@ export default {
},
} satisfies Meta;
-const stateDecorator = (value: ContextType) => (fn: () => ReactElement) => (
- {fn()}
-);
+const stateDecorator = (value: Partial) =>
+ mockAppRoot()
+ .withServerContext({
+ ...value,
+ reconnect: action('reconnect'),
+ disconnect: action('disconnect'),
+ })
+ .buildStoryDecorator();
const Template: StoryFn = () => ;
@@ -25,8 +30,6 @@ Connected.decorators = [
connected: true,
status: 'connected',
retryTime: undefined,
- reconnect: action('reconnect'),
- isLoggingIn: false,
}),
];
@@ -36,8 +39,6 @@ Connecting.decorators = [
connected: false,
status: 'connecting',
retryTime: undefined,
- reconnect: action('reconnect'),
- isLoggingIn: false,
}),
];
@@ -47,8 +48,6 @@ Failed.decorators = [
connected: false,
status: 'failed',
retryTime: undefined,
- reconnect: action('reconnect'),
- isLoggingIn: false,
}),
];
@@ -58,8 +57,6 @@ Waiting.decorators = [
connected: false,
status: 'waiting',
retryTime: Date.now() + 300000,
- reconnect: action('reconnect'),
- isLoggingIn: false,
}),
];
@@ -69,7 +66,5 @@ Offline.decorators = [
connected: false,
status: 'offline',
retryTime: undefined,
- reconnect: action('reconnect'),
- isLoggingIn: false,
}),
];
diff --git a/apps/meteor/client/components/message/content/attachments/QuoteAttachment.tsx b/apps/meteor/client/components/message/content/attachments/QuoteAttachment.tsx
index 4f44f1d810c6f..2a4f975c6641a 100644
--- a/apps/meteor/client/components/message/content/attachments/QuoteAttachment.tsx
+++ b/apps/meteor/client/components/message/content/attachments/QuoteAttachment.tsx
@@ -18,7 +18,7 @@ import AttachmentInner from './structure/AttachmentInner';
const quoteStyles = css`
.rcx-attachment__details {
.rcx-message-body {
- color: ${Palette.text['font-hint']};
+ color: ${Palette.text['font-default']};
}
}
&:hover,
@@ -53,14 +53,14 @@ export const QuoteAttachment = ({ attachment }: QuoteAttachmentProps): ReactElem
{displayAvatarPreference && }
{attachment.author_name}
{attachment.ts && (
{formatTime(attachment.ts)}
diff --git a/apps/meteor/client/components/message/content/attachments/file/AudioAttachment.tsx b/apps/meteor/client/components/message/content/attachments/file/AudioAttachment.tsx
index d24ec6ba2ff9e..227874cfb8eaf 100644
--- a/apps/meteor/client/components/message/content/attachments/file/AudioAttachment.tsx
+++ b/apps/meteor/client/components/message/content/attachments/file/AudioAttachment.tsx
@@ -1,7 +1,9 @@
import type { AudioAttachmentProps } from '@rocket.chat/core-typings';
import { AudioPlayer } from '@rocket.chat/fuselage';
import { useMediaUrl } from '@rocket.chat/ui-contexts';
+import { useMemo } from 'react';
+import { useReloadOnError } from './hooks/useReloadOnError';
import MarkdownText from '../../../../MarkdownText';
import MessageCollapsible from '../../../MessageCollapsible';
import MessageContentBody from '../../../MessageContentBody';
@@ -18,11 +20,14 @@ const AudioAttachment = ({
collapsed,
}: AudioAttachmentProps) => {
const getURL = useMediaUrl();
+ const src = useMemo(() => getURL(url), [getURL, url]);
+ const { mediaRef } = useReloadOnError(src, 'audio');
+
return (
<>
{descriptionMd ? : }
-
+
>
);
diff --git a/apps/meteor/client/components/message/content/attachments/file/VideoAttachment.tsx b/apps/meteor/client/components/message/content/attachments/file/VideoAttachment.tsx
index fedfa382de829..4768e01d41cda 100644
--- a/apps/meteor/client/components/message/content/attachments/file/VideoAttachment.tsx
+++ b/apps/meteor/client/components/message/content/attachments/file/VideoAttachment.tsx
@@ -1,7 +1,9 @@
import type { VideoAttachmentProps } from '@rocket.chat/core-typings';
import { Box, MessageGenericPreview } from '@rocket.chat/fuselage';
import { useMediaUrl } from '@rocket.chat/ui-contexts';
+import { useMemo } from 'react';
+import { useReloadOnError } from './hooks/useReloadOnError';
import { userAgentMIMETypeFallback } from '../../../../../lib/utils/userAgentMIMETypeFallback';
import MarkdownText from '../../../../MarkdownText';
import MessageCollapsible from '../../../MessageCollapsible';
@@ -19,13 +21,15 @@ const VideoAttachment = ({
collapsed,
}: VideoAttachmentProps) => {
const getURL = useMediaUrl();
+ const src = useMemo(() => getURL(url), [getURL, url]);
+ const { mediaRef } = useReloadOnError(src, 'video');
return (
<>
{descriptionMd ? : }
-
+
diff --git a/apps/meteor/client/components/message/content/attachments/file/hooks/useReloadOnError.spec.tsx b/apps/meteor/client/components/message/content/attachments/file/hooks/useReloadOnError.spec.tsx
new file mode 100644
index 0000000000000..2b1f7415ab452
--- /dev/null
+++ b/apps/meteor/client/components/message/content/attachments/file/hooks/useReloadOnError.spec.tsx
@@ -0,0 +1,235 @@
+import { renderHook, act } from '@testing-library/react';
+
+import { useReloadOnError } from './useReloadOnError';
+import { FakeResponse } from '../../../../../../../tests/mocks/utils/FakeResponse';
+
+interface ITestMediaElement extends HTMLAudioElement {
+ _emit: (type: string) => void;
+}
+
+function makeMediaEl(): ITestMediaElement {
+ const el = document.createElement('audio') as ITestMediaElement;
+ (el as any).play = jest.fn().mockResolvedValue(undefined);
+ Object.defineProperty(el, 'paused', { value: false, configurable: true });
+ el._emit = (type: string) => el.dispatchEvent(new Event(type));
+ return el;
+}
+
+describe('useReloadOnError', () => {
+ const OLD_FETCH = global.fetch;
+
+ beforeEach(() => {
+ jest.useFakeTimers();
+ jest.spyOn(console, 'debug').mockImplementation(() => null);
+ jest.spyOn(console, 'warn').mockImplementation(() => null);
+ jest.spyOn(console, 'error').mockImplementation(() => null);
+
+ // default mock: fresh redirect URL + ISO expiry 60s ahead
+ global.fetch = jest.fn().mockResolvedValue(
+ new FakeResponse(
+ JSON.stringify({
+ redirectUrl: '/sampleurl?token=xyz',
+ expires: new Date(Date.now() + 60_000).toISOString(),
+ }),
+ { status: 200, headers: { 'Content-Type': 'application/json' } },
+ ),
+ ) as any;
+ });
+
+ afterEach(() => {
+ jest.runOnlyPendingTimers();
+ jest.useRealTimers();
+ global.fetch = OLD_FETCH as any;
+ jest.restoreAllMocks();
+ jest.resetAllMocks();
+ });
+
+ it('refreshes media src on error and preserves playback position', async () => {
+ const original = '/sampleurl?token=abc';
+ const { result } = renderHook(() => useReloadOnError(original, 'audio'));
+
+ const media = makeMediaEl();
+ media.currentTime = 12;
+
+ act(() => {
+ result.current.mediaRef(media);
+ });
+
+ const loadSpy = jest.spyOn(media, 'load');
+
+ await act(async () => {
+ media._emit('error');
+ });
+
+ expect(global.fetch).toHaveBeenCalledTimes(1);
+
+ expect(media.src).toContain('/sampleurl?token=xyz');
+
+ await act(async () => {
+ media._emit('loadedmetadata');
+ media._emit('canplay');
+ });
+
+ expect(loadSpy).toHaveBeenCalled();
+ expect(media.currentTime).toBe(12);
+ expect((media as any).play).toHaveBeenCalled();
+ });
+
+ it('refreshes media src on stalled and preserves playback position', async () => {
+ const original = '/sampleurl?token=abc';
+ const { result } = renderHook(() => useReloadOnError(original, 'audio'));
+
+ const media = makeMediaEl();
+ media.currentTime = 12;
+
+ act(() => {
+ result.current.mediaRef(media);
+ });
+
+ const loadSpy = jest.spyOn(media, 'load');
+
+ await act(async () => {
+ media._emit('stalled');
+ });
+
+ expect(global.fetch).toHaveBeenCalledTimes(1);
+
+ expect(media.src).toContain('/sampleurl?token=xyz');
+
+ await act(async () => {
+ media._emit('loadedmetadata');
+ media._emit('canplay');
+ });
+
+ expect(loadSpy).toHaveBeenCalled();
+ expect(media.currentTime).toBe(12);
+ expect((media as any).play).toHaveBeenCalled();
+ });
+
+ it('does nothing when URL is not expired (second event before expiry)', async () => {
+ // Pin system time so Date.now() is deterministic under fake timers
+ const fixed = new Date('2030-01-01T00:00:00.000Z');
+ jest.setSystemTime(fixed);
+
+ // Backend replies with expiry 60s in the future (relative to pinned time)
+ global.fetch = jest.fn().mockResolvedValue(
+ new FakeResponse(
+ JSON.stringify({
+ redirectUrl: '/new?x=1',
+ expires: new Date(fixed.getTime() + 60_000).toISOString(),
+ }),
+ { status: 200, headers: { 'Content-Type': 'application/json' } },
+ ),
+ ) as any;
+
+ const { result } = renderHook(() => useReloadOnError('/sampleurl?token=abc', 'audio'));
+ const media = makeMediaEl();
+
+ act(() => {
+ result.current.mediaRef(media);
+ });
+
+ // First event → fetch + set expires
+ await act(async () => {
+ media._emit('stalled');
+ });
+ expect(global.fetch).toHaveBeenCalledTimes(1);
+
+ // Second event before expiry → early return, no new fetch
+ await act(async () => {
+ media._emit('stalled');
+ });
+ expect(global.fetch).toHaveBeenCalledTimes(1);
+ });
+
+ it('recovers on stalled after expiry and restores seek position', async () => {
+ // Pin time
+ const fixed = new Date('2030-01-01T00:00:00.000Z');
+ jest.setSystemTime(fixed);
+
+ // 1st fetch (first recovery) -> expires in 5s
+ const firstReply = new FakeResponse(
+ JSON.stringify({
+ redirectUrl: '/fresh?token=first',
+ expires: new Date(fixed.getTime() + 5_000).toISOString(),
+ }),
+ { status: 200, headers: { 'Content-Type': 'application/json' } },
+ );
+
+ // 2nd fetch (after expiry) -> new url, further expiry
+ const secondReply = new FakeResponse(
+ JSON.stringify({
+ redirectUrl: '/fresh?token=second',
+ expires: new Date(fixed.getTime() + 65_000).toISOString(),
+ }),
+ { status: 200, headers: { 'Content-Type': 'application/json' } },
+ );
+
+ // Mock fetch to return first, then second
+ (global.fetch as jest.Mock) = jest.fn().mockResolvedValueOnce(firstReply).mockResolvedValueOnce(secondReply);
+
+ const { result } = renderHook(() => useReloadOnError('/sampleurl?token=old', 'audio'));
+ const media = makeMediaEl();
+ act(() => {
+ result.current.mediaRef(media);
+ });
+
+ // Initial recovery to set expiresAt (simulate an error)
+ await act(async () => {
+ media._emit('error');
+ });
+ expect(global.fetch).toHaveBeenCalledTimes(1);
+ expect(media.src).toContain('/fresh?token=first');
+
+ // Complete the ready cycle
+ await act(async () => {
+ media._emit('loadedmetadata');
+ media._emit('canplay');
+ });
+
+ // Fast-forward time beyond expiry
+ jest.setSystemTime(new Date(fixed.getTime() + 6_000));
+
+ // User scrubs to a new position just before stall is detected
+ media.currentTime = 42;
+
+ const loadSpy = jest.spyOn(media, 'load');
+
+ // Now we stall after expiry -> should trigger a new fetch
+ await act(async () => {
+ media._emit('stalled');
+ });
+
+ expect(global.fetch).toHaveBeenCalledTimes(2);
+ expect(media.src).toContain('/fresh?token=second');
+
+ // Complete the ready cycle
+ await act(async () => {
+ media._emit('loadedmetadata');
+ media._emit('canplay');
+ });
+
+ // Ensure we reloaded and restored the seek position + playback
+ expect(loadSpy).toHaveBeenCalled();
+ expect(media.currentTime).toBe(42);
+ expect((media as any).play).toHaveBeenCalled();
+ });
+
+ it('ignores initial play when expiry is unknown', async () => {
+ // no fetch expected on first play because expiresAt is not known yet
+ global.fetch = jest.fn();
+
+ const { result } = renderHook(() => useReloadOnError('/foo', 'audio'));
+ const media = makeMediaEl();
+
+ act(() => {
+ result.current.mediaRef(media);
+ });
+
+ await act(async () => {
+ media._emit('play');
+ });
+
+ expect(global.fetch).not.toHaveBeenCalled();
+ });
+});
diff --git a/apps/meteor/client/components/message/content/attachments/file/hooks/useReloadOnError.tsx b/apps/meteor/client/components/message/content/attachments/file/hooks/useReloadOnError.tsx
new file mode 100644
index 0000000000000..b5b73c8e2ec13
--- /dev/null
+++ b/apps/meteor/client/components/message/content/attachments/file/hooks/useReloadOnError.tsx
@@ -0,0 +1,157 @@
+import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
+import { useSafeRefCallback } from '@rocket.chat/ui-client';
+import { useCallback, useRef, useState } from 'react';
+
+const events = ['error', 'stalled', 'play'];
+
+function toURL(urlString: string): URL {
+ try {
+ return new URL(urlString);
+ } catch {
+ return new URL(urlString, window.location.href);
+ }
+}
+
+const getRedirectURLInfo = async (url: string): Promise<{ redirectUrl: string | false; expires: number | null }> => {
+ const _url = toURL(url);
+ _url.searchParams.set('replyWithRedirectUrl', 'true');
+ const response = await fetch(_url, {
+ credentials: 'same-origin',
+ });
+
+ if (!response.ok) {
+ throw new Error(`Failed to fetch URL info: ${response.statusText}`);
+ }
+
+ const data = await response.json();
+
+ return {
+ redirectUrl: data.redirectUrl,
+ expires: data.expires ? new Date(data.expires).getTime() : null,
+ };
+};
+
+const renderBufferingUIFallback = (vidEl: HTMLVideoElement) => {
+ const computed = getComputedStyle(vidEl);
+
+ const videoTempStyles = {
+ width: vidEl.style.width,
+ height: vidEl.style.height,
+ };
+ Object.assign(vidEl.style, {
+ width: computed.width,
+ height: computed.height,
+ });
+
+ return () => {
+ Object.assign(vidEl.style, videoTempStyles);
+ };
+};
+
+export const useReloadOnError = (url: string, type: 'video' | 'audio') => {
+ const [expiresAt, setExpiresAt] = useState(null);
+ const isRecovering = useRef(false);
+ const firstRecoveryAttempted = useRef(false);
+
+ const handleMediaURLRecovery = useEffectEvent(async (event: Event) => {
+ if (isRecovering.current) {
+ console.debug(`Media URL recovery already in progress, skipping ${event.type} event`);
+ return;
+ }
+ isRecovering.current = true;
+
+ const node = event.target as HTMLMediaElement | null;
+ if (!node) {
+ isRecovering.current = false;
+ return;
+ }
+
+ if (firstRecoveryAttempted.current) {
+ if (!expiresAt) {
+ console.debug('No expiration time set, skipping recovery');
+ isRecovering.current = false;
+ return;
+ }
+ } else if (event.type === 'play') {
+ // The user has initiated a playback for the first time, probably we should wait for the stalled or error event
+ // the url may still be valid since we dont know the expiration time yet
+ isRecovering.current = false;
+ return;
+ }
+
+ firstRecoveryAttempted.current = true;
+
+ if (expiresAt && Date.now() < expiresAt) {
+ console.debug('Media URL is still valid, skipping recovery');
+ isRecovering.current = false;
+ return;
+ }
+
+ console.debug('Handling media URL recovery for event:', event.type);
+
+ let cleanup: (() => void) | undefined;
+ if (type === 'video') {
+ cleanup = renderBufferingUIFallback(node as HTMLVideoElement);
+ }
+
+ const wasPlaying = !node.paused;
+ const { currentTime } = node;
+
+ try {
+ const { redirectUrl: newUrl, expires: newExpiresAt } = await getRedirectURLInfo(url);
+ setExpiresAt(newExpiresAt);
+ node.src = newUrl || url;
+
+ const onCanPlay = async () => {
+ node.removeEventListener('canplay', onCanPlay);
+
+ node.currentTime = currentTime;
+ if (wasPlaying) {
+ try {
+ await node.play();
+ } catch (playError) {
+ console.warn('Failed to resume playback after URL recovery:', playError);
+ } finally {
+ isRecovering.current = false;
+ }
+ }
+ };
+
+ const onMetaDataLoaded = () => {
+ node.removeEventListener('loadedmetadata', onMetaDataLoaded);
+ isRecovering.current = false;
+ cleanup?.();
+ };
+
+ node.addEventListener('canplay', onCanPlay, { once: true });
+ node.addEventListener('loadedmetadata', onMetaDataLoaded, { once: true });
+ node.load();
+ } catch (err) {
+ console.error('Error during URL recovery:', err);
+ isRecovering.current = false;
+ cleanup?.();
+ }
+ });
+
+ const mediaRefCallback = useSafeRefCallback(
+ useCallback(
+ (node: HTMLAudioElement | null) => {
+ if (!node) {
+ return;
+ }
+
+ events.forEach((event) => {
+ node.addEventListener(event, handleMediaURLRecovery);
+ });
+ return () => {
+ events.forEach((event) => {
+ node.removeEventListener(event, handleMediaURLRecovery);
+ });
+ };
+ },
+ [handleMediaURLRecovery],
+ ),
+ );
+
+ return { mediaRef: mediaRefCallback };
+};
diff --git a/apps/meteor/client/components/message/content/attachments/structure/AttachmentDetails.tsx b/apps/meteor/client/components/message/content/attachments/structure/AttachmentDetails.tsx
index 522edba439bb9..be9e4a39515c1 100644
--- a/apps/meteor/client/components/message/content/attachments/structure/AttachmentDetails.tsx
+++ b/apps/meteor/client/components/message/content/attachments/structure/AttachmentDetails.tsx
@@ -4,7 +4,7 @@ import type { ComponentPropsWithoutRef } from 'react';
type AttachmentDetailsProps = ComponentPropsWithoutRef;
const AttachmentDetails = (props: AttachmentDetailsProps) => (
-
+
);
export default AttachmentDetails;
diff --git a/apps/meteor/client/components/message/header/hooks/useMessageRoles.ts b/apps/meteor/client/components/message/header/hooks/useMessageRoles.ts
index cf85a4011e2ae..018cedd2e18fb 100644
--- a/apps/meteor/client/components/message/header/hooks/useMessageRoles.ts
+++ b/apps/meteor/client/components/message/header/hooks/useMessageRoles.ts
@@ -2,11 +2,11 @@ import type { IRole, IRoom, IUser } from '@rocket.chat/core-typings';
import { useCallback } from 'react';
import { useShallow } from 'zustand/shallow';
-import { Roles } from '../../../../../app/models/client';
import type { RoomRoles } from '../../../../hooks/useRoomRolesQuery';
import { useRoomRolesQuery } from '../../../../hooks/useRoomRolesQuery';
import type { UserRoles } from '../../../../hooks/useUserRolesQuery';
import { useUserRolesQuery } from '../../../../hooks/useUserRolesQuery';
+import { Roles } from '../../../../stores';
export const useMessageRoles = (userId: IUser['_id'] | undefined, roomId: IRoom['_id'], shouldLoadRoles: boolean): Array => {
const { data: userRoles } = useUserRolesQuery({
diff --git a/apps/meteor/client/components/message/toolbar/MessageToolbarActionMenu.tsx b/apps/meteor/client/components/message/toolbar/MessageToolbarActionMenu.tsx
index ecea1dc5baa12..af0501ca8f288 100644
--- a/apps/meteor/client/components/message/toolbar/MessageToolbarActionMenu.tsx
+++ b/apps/meteor/client/components/message/toolbar/MessageToolbarActionMenu.tsx
@@ -93,7 +93,7 @@ const MessageToolbarActionMenu = ({ message, context, room, subscription, onChan
const groupOptions = [...data, ...(actionButtonApps.data ?? [])]
.map((option) => ({
- variant: option.color === 'alert' ? 'danger' : '',
+ variant: option.variant,
id: option.id,
icon: option.icon,
content: t(option.label),
@@ -140,17 +140,7 @@ const MessageToolbarActionMenu = ({ message, context, room, subscription, onChan
};
});
- return (
-
- );
+ return ;
};
export default MessageToolbarActionMenu;
diff --git a/apps/meteor/client/components/message/toolbar/MessageToolbarStarsActionMenu.tsx b/apps/meteor/client/components/message/toolbar/MessageToolbarStarsActionMenu.tsx
index 5a2d9b7e7dd8f..eca8b4e845a2f 100644
--- a/apps/meteor/client/components/message/toolbar/MessageToolbarStarsActionMenu.tsx
+++ b/apps/meteor/client/components/message/toolbar/MessageToolbarStarsActionMenu.tsx
@@ -31,7 +31,7 @@ const MessageToolbarStarsActionMenu = ({ message, context, onChangeMenuVisibilit
const groupOptions = starsAction.data.reduce((acc, option) => {
const transformedOption = {
- variant: option.color === 'alert' ? 'danger' : '',
+ variant: option.variant,
id: option.id,
icon: option.icon,
content: t(option.label),
@@ -76,8 +76,6 @@ const MessageToolbarStarsActionMenu = ({ message, context, onChangeMenuVisibilit
title={t('AI_Actions')}
sections={groupOptions}
placement='bottom-end'
- data-qa-id='menu'
- data-qa-type='message-action-stars-menu-options'
onOpenChange={onChangeMenuVisibility}
/>
);
diff --git a/apps/meteor/client/components/message/toolbar/items/actions/ReactionMessageAction.tsx b/apps/meteor/client/components/message/toolbar/items/actions/ReactionMessageAction.tsx
index f3e1d00062f05..f8873d35f55bf 100644
--- a/apps/meteor/client/components/message/toolbar/items/actions/ReactionMessageAction.tsx
+++ b/apps/meteor/client/components/message/toolbar/items/actions/ReactionMessageAction.tsx
@@ -1,6 +1,6 @@
import { isOmnichannelRoom, type IMessage, type IRoom, type ISubscription } from '@rocket.chat/core-typings';
import { useFeaturePreview } from '@rocket.chat/ui-client';
-import { useUser, useMethod } from '@rocket.chat/ui-contexts';
+import { useUser, useEndpoint } from '@rocket.chat/ui-contexts';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@@ -20,7 +20,7 @@ type ReactionMessageActionProps = {
const ReactionMessageAction = ({ message, room, subscription }: ReactionMessageActionProps) => {
const chat = useChat();
const user = useUser();
- const setReaction = useMethod('setReaction');
+ const setReaction = useEndpoint('POST', '/v1/chat.react');
const quickReactionsEnabled = useFeaturePreview('quickReactions');
const { quickReactions, addRecentEmoji } = useEmojiPickerData();
const { t } = useTranslation();
@@ -44,7 +44,10 @@ const ReactionMessageAction = ({ message, room, subscription }: ReactionMessageA
}
const toggleReaction = (emoji: string) => {
- setReaction(`:${emoji}:`, message._id);
+ setReaction({
+ emoji: `:${emoji}:`,
+ messageId: message._id,
+ });
addRecentEmoji(emoji);
};
diff --git a/apps/meteor/client/components/message/toolbar/useDeleteMessageAction.ts b/apps/meteor/client/components/message/toolbar/useDeleteMessageAction.ts
index ff8a24d12b841..fff4a8c986734 100644
--- a/apps/meteor/client/components/message/toolbar/useDeleteMessageAction.ts
+++ b/apps/meteor/client/components/message/toolbar/useDeleteMessageAction.ts
@@ -43,7 +43,7 @@ export const useDeleteMessageAction = (
icon: 'trash',
label: 'Delete',
context: ['message', 'message-mobile', 'threads', 'federated', 'videoconf', 'videoconf-threads'],
- color: 'alert',
+ variant: 'danger',
type: 'management',
async action() {
await chat?.flows.requestMessageDeletion(message);
diff --git a/apps/meteor/client/components/message/toolbar/useFollowMessageAction.ts b/apps/meteor/client/components/message/toolbar/useFollowMessageAction.ts
index 58c14756d2c02..b962b8fb75ac3 100644
--- a/apps/meteor/client/components/message/toolbar/useFollowMessageAction.ts
+++ b/apps/meteor/client/components/message/toolbar/useFollowMessageAction.ts
@@ -2,9 +2,9 @@ import type { IMessage, IRoom } from '@rocket.chat/core-typings';
import { isOmnichannelRoom } from '@rocket.chat/core-typings';
import { useSetting, useToastMessageDispatch, useUser } from '@rocket.chat/ui-contexts';
-import { Messages } from '../../../../app/models/client';
import type { MessageActionContext, MessageActionConfig } from '../../../../app/ui-utils/client/lib/MessageAction';
import { t } from '../../../../app/utils/lib/i18n';
+import { Messages } from '../../../stores';
import { useToggleFollowingThreadMutation } from '../../../views/room/contextualBar/Threads/hooks/useToggleFollowingThreadMutation';
export const useFollowMessageAction = (
diff --git a/apps/meteor/client/components/message/toolbar/useReplyInDMAction.ts b/apps/meteor/client/components/message/toolbar/useReplyInDMAction.ts
index 7bd70c230dd64..40bbfdfbc46d9 100644
--- a/apps/meteor/client/components/message/toolbar/useReplyInDMAction.ts
+++ b/apps/meteor/client/components/message/toolbar/useReplyInDMAction.ts
@@ -1,13 +1,12 @@
import { type IMessage, type ISubscription, type IRoom, isE2EEMessage } from '@rocket.chat/core-typings';
-import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts';
import { usePermission, useRouter, useUser } from '@rocket.chat/ui-contexts';
import { useCallback, useMemo } from 'react';
import { useShallow } from 'zustand/shallow';
-import { Rooms, Subscriptions } from '../../../../app/models/client';
import type { MessageActionConfig } from '../../../../app/ui-utils/client/lib/MessageAction';
import { useEmbeddedLayout } from '../../../hooks/useEmbeddedLayout';
import { roomCoordinator } from '../../../lib/rooms/roomCoordinator';
+import { Rooms, Subscriptions } from '../../../stores';
export const useReplyInDMAction = (
message: IMessage,
@@ -31,7 +30,7 @@ export const useReplyInDMAction = (
const dmRoom = Rooms.use(useShallow((state) => (shouldFindRoom ? state.find(roomPredicate) : undefined)));
const subsPredicate = useCallback(
- (record: SubscriptionWithRoom) => record.rid === dmRoom?._id || record.u._id === user?._id,
+ (record: ISubscription) => record.rid === dmRoom?._id || record.u._id === user?._id,
[dmRoom, user?._id],
);
const dmSubs = Subscriptions.use(useShallow((state) => state.find(subsPredicate)));
diff --git a/apps/meteor/client/components/message/toolbar/useReportMessageAction.tsx b/apps/meteor/client/components/message/toolbar/useReportMessageAction.tsx
index da0c4a47d4cce..ba281c5a1f5a2 100644
--- a/apps/meteor/client/components/message/toolbar/useReportMessageAction.tsx
+++ b/apps/meteor/client/components/message/toolbar/useReportMessageAction.tsx
@@ -34,7 +34,7 @@ export const useReportMessageAction = (
icon: 'report',
label: 'Report',
context: ['message', 'message-mobile', 'threads', 'federated', 'videoconf', 'videoconf-threads'],
- color: 'alert',
+ variant: 'danger',
type: 'management',
action() {
setModal(
diff --git a/apps/meteor/client/components/message/toolbar/useTranslateAction.ts b/apps/meteor/client/components/message/toolbar/useTranslateAction.ts
index f9448ced93f80..8b377471bf572 100644
--- a/apps/meteor/client/components/message/toolbar/useTranslateAction.ts
+++ b/apps/meteor/client/components/message/toolbar/useTranslateAction.ts
@@ -3,9 +3,9 @@ import { useMethod, usePermission, useSetting, useUser } from '@rocket.chat/ui-c
import { useMemo } from 'react';
import { AutoTranslate } from '../../../../app/autotranslate/client';
-import { Messages } from '../../../../app/models/client';
import type { MessageActionConfig } from '../../../../app/ui-utils/client/lib/MessageAction';
import { roomCoordinator } from '../../../lib/rooms/roomCoordinator';
+import { Messages } from '../../../stores';
import { hasTranslationLanguageInAttachments, hasTranslationLanguageInMessage } from '../../../views/room/MessageList/lib/autoTranslate';
export const useTranslateAction = (
diff --git a/apps/meteor/client/components/message/toolbar/useUnFollowMessageAction.ts b/apps/meteor/client/components/message/toolbar/useUnFollowMessageAction.ts
index 1e5c58a53c3c1..ad5df95d15149 100644
--- a/apps/meteor/client/components/message/toolbar/useUnFollowMessageAction.ts
+++ b/apps/meteor/client/components/message/toolbar/useUnFollowMessageAction.ts
@@ -2,9 +2,9 @@ import type { IMessage, IRoom } from '@rocket.chat/core-typings';
import { isOmnichannelRoom } from '@rocket.chat/core-typings';
import { useSetting, useToastMessageDispatch, useUser } from '@rocket.chat/ui-contexts';
-import { Messages } from '../../../../app/models/client';
import type { MessageActionContext, MessageActionConfig } from '../../../../app/ui-utils/client/lib/MessageAction';
import { t } from '../../../../app/utils/lib/i18n';
+import { Messages } from '../../../stores';
import { useToggleFollowingThreadMutation } from '../../../views/room/contextualBar/Threads/hooks/useToggleFollowingThreadMutation';
export const useUnFollowMessageAction = (
diff --git a/apps/meteor/client/components/message/toolbar/useViewOriginalTranslationAction.ts b/apps/meteor/client/components/message/toolbar/useViewOriginalTranslationAction.ts
index 103471e1a6f6d..1ba6e7c98310a 100644
--- a/apps/meteor/client/components/message/toolbar/useViewOriginalTranslationAction.ts
+++ b/apps/meteor/client/components/message/toolbar/useViewOriginalTranslationAction.ts
@@ -3,9 +3,9 @@ import { useMethod, usePermission, useSetting, useUser } from '@rocket.chat/ui-c
import { useMemo } from 'react';
import { AutoTranslate } from '../../../../app/autotranslate/client';
-import { Messages } from '../../../../app/models/client';
import type { MessageActionConfig } from '../../../../app/ui-utils/client/lib/MessageAction';
import { roomCoordinator } from '../../../lib/rooms/roomCoordinator';
+import { Messages } from '../../../stores';
import { hasTranslationLanguageInAttachments, hasTranslationLanguageInMessage } from '../../../views/room/MessageList/lib/autoTranslate';
export const useViewOriginalTranslationAction = (
diff --git a/apps/meteor/client/components/message/variants/SystemMessage.spec.tsx b/apps/meteor/client/components/message/variants/SystemMessage.spec.tsx
index c0482e831dbf8..9df7e7669440d 100644
--- a/apps/meteor/client/components/message/variants/SystemMessage.spec.tsx
+++ b/apps/meteor/client/components/message/variants/SystemMessage.spec.tsx
@@ -56,7 +56,7 @@ describe('SystemMessage', () => {
it('should render system message', () => {
const message = createBaseMessage('& test &');
- render(, { legacyRoot: true, wrapper: wrapper.build() });
+ render(, { wrapper: wrapper.build() });
expect(screen.getByText('changed room description to: & test &')).toBeInTheDocument();
});
@@ -64,7 +64,7 @@ describe('SystemMessage', () => {
it('should not show escaped html while rendering system message', () => {
const message = createBaseMessage('& test &');
- render(, { legacyRoot: true, wrapper: wrapper.build() });
+ render(, { wrapper: wrapper.build() });
expect(screen.getByText('changed room description to: & test &')).toBeInTheDocument();
expect(screen.queryByText('changed room description to: & test &')).not.toBeInTheDocument();
@@ -73,7 +73,7 @@ describe('SystemMessage', () => {
it('should not inject html', () => {
const message = createBaseMessage('OK');
- render(, { legacyRoot: true, wrapper: wrapper.build() });
+ render(, { wrapper: wrapper.build() });
expect(screen.queryByTitle('test-title')).not.toBeInTheDocument();
expect(screen.getByText('changed room description to: OK')).toBeInTheDocument();
diff --git a/apps/meteor/client/components/message/variants/threadPreview/ThreadMessagePreviewBody.spec.tsx b/apps/meteor/client/components/message/variants/threadPreview/ThreadMessagePreviewBody.spec.tsx
new file mode 100644
index 0000000000000..7e25162e48958
--- /dev/null
+++ b/apps/meteor/client/components/message/variants/threadPreview/ThreadMessagePreviewBody.spec.tsx
@@ -0,0 +1,87 @@
+import { mockAppRoot } from '@rocket.chat/mock-providers';
+import { composeStories } from '@storybook/react';
+import { render, screen } from '@testing-library/react';
+import { axe } from 'jest-axe';
+
+import * as stories from './ThreadMessagePreviewBody.stories';
+
+const { Default } = composeStories(stories);
+
+const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName || 'Story', Story]);
+
+jest.mock('../../../../lib/utils/fireGlobalEvent', () => ({
+ fireGlobalEvent: jest.fn(),
+}));
+
+jest.mock('../../../../views/room/hooks/useGoToRoom', () => ({
+ useGoToRoom: jest.fn(),
+}));
+
+test.each(testCases)(`renders ThreadMessagePreviewBody without crashing`, async (_storyname, Story) => {
+ const view = render(, { wrapper: mockAppRoot().build() });
+
+ expect(view.baseElement).toMatchSnapshot();
+});
+
+test.each(testCases)('ThreadMessagePreviewBody should have no a11y violations', async (_storyname, Story) => {
+ const { container } = render(, { wrapper: mockAppRoot().build() });
+
+ const results = await axe(container);
+
+ expect(results).toHaveNoViolations();
+});
+
+it('should not show an empty thread preview', async () => {
+ const { container } = render(
+ ,
+ { wrapper: mockAppRoot().build() },
+ );
+ expect(container).toMatchSnapshot();
+ const text = screen.getByText('http://localhost:3000/group/ds?msg=ZoX9pDowqNb4BiWxf');
+
+ expect(text).toBeInTheDocument;
+});
diff --git a/apps/meteor/client/components/message/variants/threadPreview/ThreadMessagePreviewBody.stories.tsx b/apps/meteor/client/components/message/variants/threadPreview/ThreadMessagePreviewBody.stories.tsx
new file mode 100644
index 0000000000000..24fb32c76d3e5
--- /dev/null
+++ b/apps/meteor/client/components/message/variants/threadPreview/ThreadMessagePreviewBody.stories.tsx
@@ -0,0 +1,29 @@
+import { mockAppRoot } from '@rocket.chat/mock-providers';
+import type { Meta, StoryFn } from '@storybook/react';
+import type { ComponentProps } from 'react';
+
+import ThreadMessagePreviewBody from './ThreadMessagePreviewBody';
+
+export default {
+ title: 'Components/ThreadMessagePreviewBody',
+ component: ThreadMessagePreviewBody,
+ parameters: {
+ layout: 'fullscreen',
+ },
+ decorators: [mockAppRoot().withSetting('UI_Use_Real_Name', true).withJohnDoe().buildStoryDecorator()],
+ args: {
+ message: {
+ _id: 'message-id',
+ ts: new Date(),
+ msg: 'This is a message',
+ u: {
+ _id: 'user-id',
+ username: 'username',
+ },
+ rid: 'room-id',
+ _updatedAt: new Date(),
+ },
+ },
+} satisfies Meta;
+
+export const Default: StoryFn> = (args) => ;
diff --git a/apps/meteor/client/components/message/variants/threadPreview/ThreadMessagePreviewBody.tsx b/apps/meteor/client/components/message/variants/threadPreview/ThreadMessagePreviewBody.tsx
index d83db7abf8e3d..72c4b8cb460dd 100644
--- a/apps/meteor/client/components/message/variants/threadPreview/ThreadMessagePreviewBody.tsx
+++ b/apps/meteor/client/components/message/variants/threadPreview/ThreadMessagePreviewBody.tsx
@@ -30,7 +30,7 @@ const ThreadMessagePreviewBody = ({ message }: ThreadMessagePreviewBodyProps): R
return <>{t('Message_with_attachment')}>;
}
if (!isEncryptedMessage || message.e2e === 'done') {
- return mdTokens ? (
+ return mdTokens?.length ? (
diff --git a/apps/meteor/client/components/message/variants/threadPreview/__snapshots__/ThreadMessagePreviewBody.spec.tsx.snap b/apps/meteor/client/components/message/variants/threadPreview/__snapshots__/ThreadMessagePreviewBody.spec.tsx.snap
new file mode 100644
index 0000000000000..a8388e5c6e4d1
--- /dev/null
+++ b/apps/meteor/client/components/message/variants/threadPreview/__snapshots__/ThreadMessagePreviewBody.spec.tsx.snap
@@ -0,0 +1,15 @@
+// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
+
+exports[`renders ThreadMessagePreviewBody without crashing 1`] = `
+
+
+ This is a message
+
+
+`;
+
+exports[`should not show an empty thread preview 1`] = `
+
+ http://localhost:3000/group/ds?msg=ZoX9pDowqNb4BiWxf
+
+`;
diff --git a/apps/meteor/client/definitions/IRocketChatDesktop.ts b/apps/meteor/client/definitions/IRocketChatDesktop.ts
deleted file mode 100644
index a0707e9ba6d75..0000000000000
--- a/apps/meteor/client/definitions/IRocketChatDesktop.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-type OutlookEventsResponse = { status: 'success' | 'canceled' };
-
-export interface IRocketChatDesktop {
- openInternalVideoChatWindow?: (url: string, options: { providerName: string | undefined }) => void;
- getOutlookEvents?: (date: Date) => Promise;
- setOutlookExchangeUrl?: (url: string, userId: string) => Promise;
- hasOutlookCredentials?: () => Promise;
- clearOutlookCredentials?: () => Promise | void;
- openDocumentViewer?: (url: string, format: string, options: any) => void;
-}
diff --git a/apps/meteor/client/definitions/global.d.ts b/apps/meteor/client/definitions/global.d.ts
index 58e383ee58d8b..b19bfe2db3b06 100644
--- a/apps/meteor/client/definitions/global.d.ts
+++ b/apps/meteor/client/definitions/global.d.ts
@@ -1,4 +1,4 @@
-import type { IRocketChatDesktop } from './IRocketChatDesktop';
+import type { IRocketChatDesktop } from '@rocket.chat/desktop-api';
declare global {
// eslint-disable-next-line @typescript-eslint/naming-convention
diff --git a/apps/meteor/client/hooks/menuActions/useToggleNotificationsAction.ts b/apps/meteor/client/hooks/menuActions/useToggleNotificationsAction.ts
new file mode 100644
index 0000000000000..c99e6720fa2fe
--- /dev/null
+++ b/apps/meteor/client/hooks/menuActions/useToggleNotificationsAction.ts
@@ -0,0 +1,30 @@
+import type { IRoom } from '@rocket.chat/core-typings';
+import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
+import { useEndpoint, useToastMessageDispatch } from '@rocket.chat/ui-contexts';
+import { useTranslation } from 'react-i18next';
+
+type useToggleNotificationActionProps = {
+ rid: IRoom['_id'];
+ isNotificationEnabled: boolean;
+ roomName: string;
+};
+
+export const useToggleNotificationAction = ({ rid, isNotificationEnabled, roomName }: useToggleNotificationActionProps) => {
+ const toggleNotification = useEndpoint('POST', '/v1/rooms.saveNotification');
+ const dispatchToastMessage = useToastMessageDispatch();
+ const { t } = useTranslation();
+
+ const handleToggleNotification = useEffectEvent(async () => {
+ try {
+ await toggleNotification({ roomId: rid, notifications: { disableNotifications: isNotificationEnabled ? '1' : '0' } });
+ dispatchToastMessage({
+ type: 'success',
+ message: t(isNotificationEnabled ? 'Room_notifications_off' : 'Room_notifications_on', { roomName }),
+ });
+ } catch (error) {
+ dispatchToastMessage({ type: 'error', message: error });
+ }
+ });
+
+ return handleToggleNotification;
+};
diff --git a/apps/meteor/client/hooks/notification/useDesktopNotification.ts b/apps/meteor/client/hooks/notification/useDesktopNotification.ts
index c58cccd59f878..6d7a24317e4d0 100644
--- a/apps/meteor/client/hooks/notification/useDesktopNotification.ts
+++ b/apps/meteor/client/hooks/notification/useDesktopNotification.ts
@@ -3,8 +3,8 @@ import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
import { useUser } from '@rocket.chat/ui-contexts';
import { useNotification } from './useNotification';
-import { e2e } from '../../../app/e2e/client';
import { RoomManager } from '../../lib/RoomManager';
+import { e2e } from '../../lib/e2ee';
import { getAvatarAsPng } from '../../lib/utils/getAvatarAsPng';
export const useDesktopNotification = () => {
diff --git a/apps/meteor/client/hooks/notification/useNewMessageNotification.ts b/apps/meteor/client/hooks/notification/useNewMessageNotification.ts
index 36b94568a27a5..8f1442a5424c6 100644
--- a/apps/meteor/client/hooks/notification/useNewMessageNotification.ts
+++ b/apps/meteor/client/hooks/notification/useNewMessageNotification.ts
@@ -2,6 +2,8 @@ import type { AtLeast, ISubscription } from '@rocket.chat/core-typings';
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
import { useCustomSound } from '@rocket.chat/ui-contexts';
+import { Subscriptions } from '../../stores';
+
export const useNewMessageNotification = () => {
const { notificationSounds } = useCustomSound();
@@ -9,14 +11,12 @@ export const useNewMessageNotification = () => {
if (!sub || sub.audioNotificationValue === 'none') {
return;
}
- // TODO: Fix this - Room Notifications Preferences > sound > desktop is not working.
- // plays the user notificationSound preference
- // if (sub.audioNotificationValue && sub.audioNotificationValue !== '0') {
- // void CustomSounds.play(sub.audioNotificationValue, {
- // volume: Number((notificationsSoundVolume / 100).toPrecision(2)),
- // });
- // }
+ const subscription = Subscriptions.state.find((record) => record.rid === sub.rid);
+
+ if (subscription?.audioNotificationValue) {
+ return notificationSounds.playNewMessageCustom(subscription.audioNotificationValue);
+ }
notificationSounds.playNewMessage();
});
diff --git a/apps/meteor/client/hooks/roomActions/useAppsRoomStarActions.tsx b/apps/meteor/client/hooks/roomActions/useAppsRoomStarActions.tsx
index 3b718bb4263ad..457d71254e9c0 100644
--- a/apps/meteor/client/hooks/roomActions/useAppsRoomStarActions.tsx
+++ b/apps/meteor/client/hooks/roomActions/useAppsRoomStarActions.tsx
@@ -24,7 +24,6 @@ export const useAppsRoomStarActions = () => {
if (!result.data) {
return undefined;
}
-
const filteredActions = result.data.filter(applyButtonFilters);
if (filteredActions.length === 0) {
@@ -37,6 +36,7 @@ export const useAppsRoomStarActions = () => {
icon: 'stars',
groups: ['group', 'channel', 'live', 'team', 'direct', 'direct_multiple'],
featured: true,
+ order: 3,
renderToolboxItem: ({ id, icon, title, disabled, className }) => (
}
diff --git a/apps/meteor/client/hooks/roomActions/useE2EERoomAction.spec.ts b/apps/meteor/client/hooks/roomActions/useE2EERoomAction.spec.ts
index c9b891d7539c4..533d9c1112bfc 100644
--- a/apps/meteor/client/hooks/roomActions/useE2EERoomAction.spec.ts
+++ b/apps/meteor/client/hooks/roomActions/useE2EERoomAction.spec.ts
@@ -2,23 +2,21 @@ import { imperativeModal } from '@rocket.chat/ui-client';
import { useSetting, usePermission, useEndpoint } from '@rocket.chat/ui-contexts';
import { act, renderHook, waitFor } from '@testing-library/react';
-import { E2EEState } from '../../../app/e2e/client/E2EEState';
-import { e2e } from '../../../app/e2e/client/rocketchat.e2e';
import { OtrRoomState } from '../../../app/otr/lib/OtrRoomState';
-import { dispatchToastMessage } from '../../lib/toast';
+import { E2EEState } from '../../lib/e2ee/E2EEState';
+import { e2e } from '../../lib/e2ee/rocketchat.e2e';
import { useRoom, useRoomSubscription } from '../../views/room/contexts/RoomContext';
import { useE2EEState } from '../../views/room/hooks/useE2EEState';
import { useOTR } from '../useOTR';
import { useE2EERoomAction } from './useE2EERoomAction';
+const dispatchToastMessage = jest.fn();
+
jest.mock('@rocket.chat/ui-contexts', () => ({
useSetting: jest.fn(),
usePermission: jest.fn(),
useEndpoint: jest.fn(),
-}));
-
-jest.mock('../../lib/toast', () => ({
- dispatchToastMessage: jest.fn(),
+ useToastMessageDispatch: jest.fn(() => dispatchToastMessage),
}));
jest.mock('@rocket.chat/ui-client', () => ({
@@ -38,7 +36,7 @@ jest.mock('../useOTR', () => ({
useOTR: jest.fn(),
}));
-jest.mock('../../../app/e2e/client/rocketchat.e2e', () => ({
+jest.mock('../../lib/e2ee/rocketchat.e2e', () => ({
e2e: {
isReady: jest.fn(),
},
diff --git a/apps/meteor/client/hooks/roomActions/useE2EERoomAction.ts b/apps/meteor/client/hooks/roomActions/useE2EERoomAction.ts
index c27ef23a5ae48..fafe63bd88c91 100644
--- a/apps/meteor/client/hooks/roomActions/useE2EERoomAction.ts
+++ b/apps/meteor/client/hooks/roomActions/useE2EERoomAction.ts
@@ -1,15 +1,14 @@
import { isRoomFederated } from '@rocket.chat/core-typings';
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
import { imperativeModal } from '@rocket.chat/ui-client';
-import { useSetting, usePermission, useEndpoint } from '@rocket.chat/ui-contexts';
+import { useSetting, usePermission, useEndpoint, useToastMessageDispatch } from '@rocket.chat/ui-contexts';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
-import { E2EEState } from '../../../app/e2e/client/E2EEState';
-import { E2ERoomState } from '../../../app/e2e/client/E2ERoomState';
import { OtrRoomState } from '../../../app/otr/lib/OtrRoomState';
+import { E2EEState } from '../../lib/e2ee/E2EEState';
+import { E2ERoomState } from '../../lib/e2ee/E2ERoomState';
import { getRoomTypeTranslation } from '../../lib/getRoomTypeTranslation';
-import { dispatchToastMessage } from '../../lib/toast';
import { useRoom, useRoomSubscription } from '../../views/room/contexts/RoomContext';
import type { RoomToolboxActionConfig } from '../../views/room/contexts/RoomToolboxContext';
import { useE2EERoomState } from '../../views/room/hooks/useE2EERoomState';
@@ -31,6 +30,7 @@ export const useE2EERoomAction = () => {
const permitted = (room.t === 'd' || (permittedToEditRoom && permittedToToggleEncryption)) && readyToEncrypt;
const federated = isRoomFederated(room);
const { t } = useTranslation();
+ const dispatchToastMessage = useToastMessageDispatch();
const { otrState } = useOTR();
const isE2EERoomNotReady = () => {
diff --git a/apps/meteor/client/hooks/roomActions/useVideoCallRoomAction.tsx b/apps/meteor/client/hooks/roomActions/useVideoCallRoomAction.tsx
index 9146a2fc651a0..c5501f527f594 100644
--- a/apps/meteor/client/hooks/roomActions/useVideoCallRoomAction.tsx
+++ b/apps/meteor/client/hooks/roomActions/useVideoCallRoomAction.tsx
@@ -77,7 +77,7 @@ export const useVideoCallRoomAction = () => {
icon: 'video',
featured: true,
action: handleOpenVideoConf,
- order: -1,
+ order: 1,
groups,
disabled,
tooltip,
diff --git a/apps/meteor/client/hooks/roomActions/useVoiceCallRoomAction.tsx b/apps/meteor/client/hooks/roomActions/useVoiceCallRoomAction.tsx
index c858a591387a7..e9dc459951814 100644
--- a/apps/meteor/client/hooks/roomActions/useVoiceCallRoomAction.tsx
+++ b/apps/meteor/client/hooks/roomActions/useVoiceCallRoomAction.tsx
@@ -65,6 +65,7 @@ export const useVoiceCallRoomAction = () => {
featured: true,
action: handleOnClick,
groups: ['direct'] as const,
+ order: 2,
disabled,
tooltip,
};
diff --git a/apps/meteor/client/hooks/useCreateChannelTypePermission.ts b/apps/meteor/client/hooks/useCreateChannelTypePermission.ts
new file mode 100644
index 0000000000000..9a5184b39146d
--- /dev/null
+++ b/apps/meteor/client/hooks/useCreateChannelTypePermission.ts
@@ -0,0 +1,42 @@
+import type { IRoom } from '@rocket.chat/core-typings';
+import { usePermission } from '@rocket.chat/ui-contexts';
+import { useMemo } from 'react';
+
+/**
+ * Determines if a user's permissions restrict them to creating only one type of channel.
+ *
+ * This hook checks a user's permissions for creating public and private channels,
+ * either globally or within a specific team. It returns a string indicating the
+ * single channel type they can create, or `false` if they can create both or neither.
+ *
+ * @param {string} [teamRoomId] The optional ID of the main team room to check for team-specific permissions.
+ * @returns {'c' | 'p' | false} A string ('c' or 'p') if the user can only create one channel type, otherwise `false`.
+ */
+export const useCreateChannelTypePermission = (teamRoomId?: IRoom['_id']) => {
+ const canCreateChannel = usePermission('create-c');
+ const canCreatePrivateChannel = usePermission('create-p');
+
+ const canCreateTeamChannel = usePermission('create-team-channel', teamRoomId);
+ const canCreateTeamGroup = usePermission('create-team-group', teamRoomId);
+
+ return useMemo(() => {
+ if (teamRoomId) {
+ if (!canCreateTeamChannel && canCreateTeamGroup) {
+ return 'p';
+ }
+
+ if (canCreateTeamChannel && !canCreateTeamGroup) {
+ return 'c';
+ }
+ }
+
+ if (!canCreateChannel && canCreatePrivateChannel) {
+ return 'p';
+ }
+
+ if (canCreateChannel && !canCreatePrivateChannel) {
+ return 'c';
+ }
+ return false;
+ }, [canCreateChannel, canCreatePrivateChannel, canCreateTeamChannel, canCreateTeamGroup, teamRoomId]);
+};
diff --git a/apps/meteor/client/hooks/useDecryptedMessage.spec.ts b/apps/meteor/client/hooks/useDecryptedMessage.spec.ts
index cbcc577311f08..5b35e8d6e3352 100644
--- a/apps/meteor/client/hooks/useDecryptedMessage.spec.ts
+++ b/apps/meteor/client/hooks/useDecryptedMessage.spec.ts
@@ -2,14 +2,14 @@ import { isE2EEMessage } from '@rocket.chat/core-typings';
import { renderHook, waitFor } from '@testing-library/react';
import { useDecryptedMessage } from './useDecryptedMessage';
-import { e2e } from '../../app/e2e/client/rocketchat.e2e';
+import { e2e } from '../lib/e2ee/rocketchat.e2e';
// Mock the dependencies
jest.mock('@rocket.chat/core-typings', () => ({
isE2EEMessage: jest.fn(),
}));
-jest.mock('../../app/e2e/client/rocketchat.e2e', () => ({
+jest.mock('../lib/e2ee/rocketchat.e2e', () => ({
e2e: {
decryptMessage: jest.fn(),
},
diff --git a/apps/meteor/client/hooks/useDecryptedMessage.ts b/apps/meteor/client/hooks/useDecryptedMessage.ts
index f98012b11f2b3..e560aacc5b111 100644
--- a/apps/meteor/client/hooks/useDecryptedMessage.ts
+++ b/apps/meteor/client/hooks/useDecryptedMessage.ts
@@ -4,7 +4,7 @@ import { useSafely } from '@rocket.chat/fuselage-hooks';
import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
-import { e2e } from '../../app/e2e/client/rocketchat.e2e';
+import { e2e } from '../lib/e2ee/rocketchat.e2e';
export const useDecryptedMessage = (message: IMessage): string => {
const { t } = useTranslation();
diff --git a/apps/meteor/client/hooks/useDeviceLogout.tsx b/apps/meteor/client/hooks/useDeviceLogout.tsx
index 3265560b6f12b..c13528d4d4c80 100644
--- a/apps/meteor/client/hooks/useDeviceLogout.tsx
+++ b/apps/meteor/client/hooks/useDeviceLogout.tsx
@@ -1,58 +1,55 @@
import { GenericModal } from '@rocket.chat/ui-client';
-import { useSetModal, useTranslation, useToastMessageDispatch, useRoute, useRouteParameter } from '@rocket.chat/ui-contexts';
+import { useSetModal, useToastMessageDispatch, useRoute, useRouteParameter } from '@rocket.chat/ui-contexts';
+import { useQueryClient } from '@tanstack/react-query';
import { useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
-import { useEndpointAction } from './useEndpointAction';
+import { useEndpointMutation } from './useEndpointMutation';
+import { deviceManagementQueryKeys } from '../lib/queryKeys';
-export const useDeviceLogout = (
- sessionId: string,
- endpoint: '/v1/sessions/logout' | '/v1/sessions/logout.me',
-): ((onReload: () => void) => void) => {
- const t = useTranslation();
+export const useDeviceLogout = (sessionId: string, endpoint: '/v1/sessions/logout' | '/v1/sessions/logout.me'): (() => void) => {
+ const { t } = useTranslation();
const setModal = useSetModal();
const dispatchToastMessage = useToastMessageDispatch();
const deviceManagementRouter = useRoute('device-management');
const routeId = useRouteParameter('id');
- const logoutDevice = useEndpointAction('POST', endpoint);
+ const queryClient = useQueryClient();
- const handleCloseContextualBar = useCallback((): void => deviceManagementRouter.push({}), [deviceManagementRouter]);
+ const { mutateAsync: logoutDevice } = useEndpointMutation('POST', endpoint, {
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: deviceManagementQueryKeys.all });
+ isContextualBarOpen && handleCloseContextualBar();
+ dispatchToastMessage({ type: 'success', message: t('Device_Logged_Out') });
+ },
+ onSettled: () => {
+ setModal(null);
+ },
+ });
- const isContextualBarOpen = routeId === sessionId;
+ const handleCloseContextualBar = useCallback(() => deviceManagementRouter.push({}), [deviceManagementRouter]);
- const handleLogoutDeviceModal = useCallback(
- (onReload: () => void) => {
- const closeModal = (): void => setModal(null);
-
- const handleLogoutDevice = async (): Promise => {
- try {
- await logoutDevice({ sessionId });
- onReload();
- isContextualBarOpen && handleCloseContextualBar();
- dispatchToastMessage({ type: 'success', message: t('Device_Logged_Out') });
- } catch (error) {
- dispatchToastMessage({ type: 'error', message: error });
- } finally {
- closeModal();
- }
- };
-
- setModal(
-
- {t('Device_Logout_Text')}
- ,
- );
- },
- [setModal, t, logoutDevice, sessionId, isContextualBarOpen, handleCloseContextualBar, dispatchToastMessage],
- );
+ const isContextualBarOpen = routeId === sessionId;
- return handleLogoutDeviceModal;
+ return useCallback(() => {
+ const closeModal = () => setModal(null);
+
+ const handleLogoutDevice = async () => {
+ await logoutDevice({ sessionId });
+ };
+
+ setModal(
+
+ {t('Device_Logout_Text')}
+ ,
+ );
+ }, [setModal, t, logoutDevice, sessionId]);
};
diff --git a/apps/meteor/client/hooks/useDialModal.tsx b/apps/meteor/client/hooks/useDialModal.tsx
index 9c7ae5ad1663c..a71d31d51efd6 100644
--- a/apps/meteor/client/hooks/useDialModal.tsx
+++ b/apps/meteor/client/hooks/useDialModal.tsx
@@ -1,9 +1,8 @@
-import { useSetModal } from '@rocket.chat/ui-contexts';
+import { useSetModal, useToastMessageDispatch } from '@rocket.chat/ui-contexts';
import { Suspense, lazy, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useIsVoipEnterprise } from '../contexts/CallContext';
-import { dispatchToastMessage } from '../lib/toast';
const DialPadModal = lazy(() => import('../voip/modal/DialPad/DialPadModal'));
@@ -19,8 +18,9 @@ type DialModalControls = {
export const useDialModal = (): DialModalControls => {
const setModal = useSetModal();
- const isEnterprise = useIsVoipEnterprise();
const { t } = useTranslation();
+ const dispatchToastMessage = useToastMessageDispatch();
+ const isEnterprise = useIsVoipEnterprise();
const closeDialModal = useCallback(() => setModal(null), [setModal]);
@@ -39,7 +39,7 @@ export const useDialModal = (): DialModalControls => {
,
);
},
- [setModal, isEnterprise, t, closeDialModal],
+ [isEnterprise, setModal, closeDialModal, dispatchToastMessage, t],
);
return useMemo(
diff --git a/apps/meteor/client/hooks/useEndpointAction.ts b/apps/meteor/client/hooks/useEndpointAction.ts
deleted file mode 100644
index 96ed190f15887..0000000000000
--- a/apps/meteor/client/hooks/useEndpointAction.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import type { Method, PathPattern, UrlParams } from '@rocket.chat/rest-typings';
-import type { EndpointFunction } from '@rocket.chat/ui-contexts';
-import { useToastMessageDispatch, useEndpoint } from '@rocket.chat/ui-contexts';
-import { useMutation } from '@tanstack/react-query';
-
-type UseEndpointActionOptions = (undefined extends UrlParams
- ? {
- keys?: UrlParams;
- }
- : {
- keys: UrlParams;
- }) & {
- successMessage?: string;
-};
-export function useEndpointAction(
- method: TMethod,
- pathPattern: TPathPattern,
- options: NoInfer> = { keys: {} as UrlParams },
-) {
- const sendData = useEndpoint(method, pathPattern, options.keys as UrlParams);
-
- const dispatchToastMessage = useToastMessageDispatch();
-
- const mutation = useMutation({
- mutationFn: sendData,
- onSuccess: () => {
- if (options.successMessage) {
- dispatchToastMessage({ type: 'success', message: options.successMessage });
- }
- },
- onError: (error) => {
- dispatchToastMessage({ type: 'error', message: error });
- },
- });
-
- return mutation.mutateAsync as EndpointFunction;
-}
diff --git a/apps/meteor/client/hooks/useEndpointMutation.ts b/apps/meteor/client/hooks/useEndpointMutation.ts
new file mode 100644
index 0000000000000..3263b10dc232a
--- /dev/null
+++ b/apps/meteor/client/hooks/useEndpointMutation.ts
@@ -0,0 +1,43 @@
+import type { Serialized } from '@rocket.chat/core-typings';
+import type { Method, OperationParams, OperationResult, PathPattern, UrlParams } from '@rocket.chat/rest-typings';
+import { useToastMessageDispatch, useEndpoint } from '@rocket.chat/ui-contexts';
+import type { UseMutationOptions, UseMutationResult } from '@tanstack/react-query';
+import { useMutation } from '@tanstack/react-query';
+
+type UseEndpointActionOptions = (undefined extends UrlParams
+ ? {
+ keys?: UrlParams;
+ }
+ : {
+ keys: UrlParams;
+ }) &
+ Omit<
+ UseMutationOptions<
+ Serialized>,
+ Error,
+ undefined extends OperationParams ? void : OperationParams
+ >,
+ 'mutationFn'
+ >;
+
+export function useEndpointMutation(
+ method: TMethod,
+ pathPattern: TPathPattern,
+ { keys, ...options }: NoInfer> = { keys: {} as UrlParams },
+) {
+ const sendData = useEndpoint(method, pathPattern, keys as UrlParams);
+
+ const dispatchToastMessage = useToastMessageDispatch();
+
+ return useMutation({
+ mutationFn: sendData,
+ onError: (error) => {
+ dispatchToastMessage({ type: 'error', message: error });
+ },
+ ...options,
+ }) as UseMutationResult<
+ Serialized>,
+ Error,
+ undefined extends OperationParams ? void : OperationParams
+ >;
+}
diff --git a/apps/meteor/client/hooks/useEndpointUpload.ts b/apps/meteor/client/hooks/useEndpointUpload.ts
deleted file mode 100644
index aafbbc1709a62..0000000000000
--- a/apps/meteor/client/hooks/useEndpointUpload.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import type { UploadResult } from '@rocket.chat/ui-contexts';
-import { useToastMessageDispatch, useUpload } from '@rocket.chat/ui-contexts';
-import { useCallback } from 'react';
-
-export const useEndpointUpload = (
- endpoint: Parameters[0],
- successMessage: string,
-): ((formData: FormData) => Promise<{ success: boolean }>) => {
- const sendData = useUpload(endpoint);
- const dispatchToastMessage = useToastMessageDispatch();
-
- return useCallback(
- async (formData: FormData) => {
- try {
- const data = sendData(formData);
-
- const promise = data instanceof Promise ? data : data.promise;
-
- const result = await (promise as unknown as Promise);
-
- if (!result.success) {
- throw new Error(String(result.status));
- }
-
- successMessage && dispatchToastMessage({ type: 'success', message: successMessage });
-
- return result as any;
- } catch (error) {
- dispatchToastMessage({ type: 'error', message: error });
- return { success: false };
- }
- },
- [dispatchToastMessage, sendData, successMessage],
- );
-};
diff --git a/apps/meteor/client/hooks/useEndpointUploadMutation.ts b/apps/meteor/client/hooks/useEndpointUploadMutation.ts
new file mode 100644
index 0000000000000..82b6a5f91a5c8
--- /dev/null
+++ b/apps/meteor/client/hooks/useEndpointUploadMutation.ts
@@ -0,0 +1,26 @@
+import type { PathFor, PathPattern } from '@rocket.chat/rest-typings';
+import { useToastMessageDispatch, useUpload } from '@rocket.chat/ui-contexts';
+import { useMutation, type UseMutationOptions } from '@tanstack/react-query';
+
+type UseEndpointUploadOptions = Omit, 'mutationFn'>;
+
+export const useEndpointUploadMutation = (endpoint: TPathPattern, options?: UseEndpointUploadOptions) => {
+ const sendData = useUpload(endpoint as PathFor<'POST'>);
+ const dispatchToastMessage = useToastMessageDispatch();
+
+ return useMutation({
+ mutationFn: async (formData: FormData) => {
+ const data = sendData(formData);
+ const promise = data instanceof Promise ? data : data.promise;
+ const result = await promise;
+
+ if (!result.success) {
+ throw new Error(String(result.status));
+ }
+ },
+ onError: (error) => {
+ dispatchToastMessage({ type: 'error', message: error });
+ },
+ ...options,
+ });
+};
diff --git a/apps/meteor/client/hooks/useIdleConnection.ts b/apps/meteor/client/hooks/useIdleConnection.ts
index 1d22fbc9f803c..d4d5c8d9a7609 100644
--- a/apps/meteor/client/hooks/useIdleConnection.ts
+++ b/apps/meteor/client/hooks/useIdleConnection.ts
@@ -1,13 +1,12 @@
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
-import { ServerContext, useConnectionStatus, useSetting } from '@rocket.chat/ui-contexts';
-import { useContext } from 'react';
+import { useConnectionStatus, useSetting } from '@rocket.chat/ui-contexts';
import { useIdleActiveEvents } from './useIdleActiveEvents';
export const useIdleConnection = (uid: string | null) => {
const { status } = useConnectionStatus();
const allowAnonymousRead = useSetting('Accounts_AllowAnonymousRead');
- const { disconnect: disconnectServer, reconnect: reconnectServer } = useContext(ServerContext);
+ const { disconnect: disconnectServer, reconnect: reconnectServer } = useConnectionStatus();
const disconnect = useEffectEvent(() => {
if (status !== 'offline') {
diff --git a/apps/meteor/client/hooks/useLivechatInquiryStore.ts b/apps/meteor/client/hooks/useLivechatInquiryStore.ts
index ba33752c3bbbf..8641e990aac99 100644
--- a/apps/meteor/client/hooks/useLivechatInquiryStore.ts
+++ b/apps/meteor/client/hooks/useLivechatInquiryStore.ts
@@ -1,10 +1,12 @@
import type { ILivechatInquiryRecord, IRoom } from '@rocket.chat/core-typings';
import { create } from 'zustand';
+export type LivechatInquiryLocalRecord = ILivechatInquiryRecord & { alert?: boolean };
+
export const useLivechatInquiryStore = create<{
- records: (ILivechatInquiryRecord & { alert?: boolean })[];
- add: (record: ILivechatInquiryRecord & { alert?: boolean }) => void;
- merge: (record: ILivechatInquiryRecord & { alert?: boolean }) => void;
+ records: LivechatInquiryLocalRecord[];
+ add: (record: LivechatInquiryLocalRecord) => void;
+ merge: (record: LivechatInquiryLocalRecord) => void;
discard: (id: ILivechatInquiryRecord['_id']) => void;
discardForRoom: (rid: IRoom['_id']) => void;
discardAll: () => void;
diff --git a/apps/meteor/client/hooks/useLoadRoomForAllowedAnonymousRead.ts b/apps/meteor/client/hooks/useLoadRoomForAllowedAnonymousRead.ts
index 3e3b7e520b9c2..444721517775a 100644
--- a/apps/meteor/client/hooks/useLoadRoomForAllowedAnonymousRead.ts
+++ b/apps/meteor/client/hooks/useLoadRoomForAllowedAnonymousRead.ts
@@ -1,7 +1,7 @@
import { useSetting, useUserId } from '@rocket.chat/ui-contexts';
import { useEffect } from 'react';
-import { CachedChatRoom, CachedChatSubscription } from '../../app/models/client';
+import { RoomsCachedStore, SubscriptionsCachedStore } from '../cachedStores';
export const useLoadRoomForAllowedAnonymousRead = () => {
const userId = useUserId();
@@ -9,11 +9,11 @@ export const useLoadRoomForAllowedAnonymousRead = () => {
useEffect(() => {
if (!userId && accountsAllowAnonymousRead === true) {
- CachedChatRoom.init();
- CachedChatSubscription.ready.set(true);
+ RoomsCachedStore.init();
+ SubscriptionsCachedStore.setReady(true);
return () => {
- CachedChatRoom.ready.set(false);
- CachedChatSubscription.ready.set(false);
+ RoomsCachedStore.setReady(false);
+ SubscriptionsCachedStore.setReady(false);
};
}
}, [accountsAllowAnonymousRead, userId]);
diff --git a/apps/meteor/client/hooks/useReactiveVar.ts b/apps/meteor/client/hooks/useReactiveVar.ts
deleted file mode 100644
index d6853b779420c..0000000000000
--- a/apps/meteor/client/hooks/useReactiveVar.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import type { ReactiveVar } from 'meteor/reactive-var';
-import { useCallback } from 'react';
-
-import { useReactiveValue } from './useReactiveValue';
-
-/** @deprecated */
-export const useReactiveVar = (variable: ReactiveVar): T => useReactiveValue(useCallback(() => variable.get(), [variable]));
diff --git a/apps/meteor/client/hooks/useRoomInfoEndpoint.ts b/apps/meteor/client/hooks/useRoomInfoEndpoint.ts
index 6857e2590a069..294dc336fb463 100644
--- a/apps/meteor/client/hooks/useRoomInfoEndpoint.ts
+++ b/apps/meteor/client/hooks/useRoomInfoEndpoint.ts
@@ -9,14 +9,14 @@ import { roomsQueryKeys } from '../lib/queryKeys';
type UseRoomInfoEndpointOptions<
TData = Serialized<{
room: IRoom | undefined;
- parent?: Pick;
+ parent?: Pick;
team?: Pick;
}>,
> = Omit<
UseQueryOptions<
Serialized<{
room: IRoom | undefined;
- parent?: Pick;
+ parent?: Pick;
team?: Pick;
}>,
{ success: boolean; error: string },
@@ -29,7 +29,7 @@ type UseRoomInfoEndpointOptions<
export const useRoomInfoEndpoint = <
TData = Serialized<{
room: IRoom | undefined;
- parent?: Pick;
+ parent?: Pick;
team?: Pick;
}>,
>(
diff --git a/apps/meteor/client/hooks/useRoomMenuActions.ts b/apps/meteor/client/hooks/useRoomMenuActions.ts
index 46308772b4295..86c2727facea5 100644
--- a/apps/meteor/client/hooks/useRoomMenuActions.ts
+++ b/apps/meteor/client/hooks/useRoomMenuActions.ts
@@ -1,7 +1,6 @@
import type { RoomType } from '@rocket.chat/core-typings';
import type { GenericMenuItemProps } from '@rocket.chat/ui-client';
import { usePermission, useSetting, useUserSubscription } from '@rocket.chat/ui-contexts';
-import type { Fields } from '@rocket.chat/ui-contexts';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -11,12 +10,6 @@ import { useToggleReadAction } from './menuActions/useToggleReadAction';
import { useHideRoomAction } from './useHideRoomAction';
import { useOmnichannelPrioritiesMenu } from '../omnichannel/hooks/useOmnichannelPrioritiesMenu';
-const fields: Fields = {
- f: true,
- t: true,
- name: true,
-};
-
type RoomMenuActionsProps = {
rid: string;
type: RoomType;
@@ -37,7 +30,7 @@ export const useRoomMenuActions = ({
hideDefaultOptions,
}: RoomMenuActionsProps): { title: string; items: GenericMenuItemProps[] }[] => {
const { t } = useTranslation();
- const subscription = useUserSubscription(rid, fields);
+ const subscription = useUserSubscription(rid);
const isFavorite = Boolean(subscription?.f);
const canLeaveChannel = usePermission('leave-c');
diff --git a/apps/meteor/client/hooks/useRoomRolesQuery.ts b/apps/meteor/client/hooks/useRoomRolesQuery.ts
index ff0980e71ae66..cdc012b1b7832 100644
--- a/apps/meteor/client/hooks/useRoomRolesQuery.ts
+++ b/apps/meteor/client/hooks/useRoomRolesQuery.ts
@@ -1,5 +1,5 @@
import type { IUser, IRole, IRoom } from '@rocket.chat/core-typings';
-import { useMethod, useStream, useUserId } from '@rocket.chat/ui-contexts';
+import { useEndpoint, useStream, useUserId } from '@rocket.chat/ui-contexts';
import { useQuery, useQueryClient, type UseQueryOptions } from '@tanstack/react-query';
import { useEffect } from 'react';
@@ -100,14 +100,16 @@ export const useRoomRolesQuery = (rid: IRoom['_id'], option
});
}, [enabled, queryClient, rid, subscribeToNotifyLogged]);
- const getRoomRoles = useMethod('getRoomRoles');
+ const getRoomRoles = useEndpoint('GET', '/v1/rooms.roles');
return useQuery({
queryKey: roomsQueryKeys.roles(rid),
queryFn: async () => {
- const results = await getRoomRoles(rid);
+ const { roles } = await getRoomRoles({
+ rid,
+ });
- return results.map(
+ return roles.map(
(record): RoomRoles => ({
rid: record.rid,
u: record.u,
diff --git a/apps/meteor/client/hooks/useSidePanelNavigation.ts b/apps/meteor/client/hooks/useSidePanelNavigation.ts
deleted file mode 100644
index f9714580cf064..0000000000000
--- a/apps/meteor/client/hooks/useSidePanelNavigation.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { useBreakpoints } from '@rocket.chat/fuselage-hooks';
-import { useFeaturePreview } from '@rocket.chat/ui-client';
-
-export const useSidePanelNavigation = () => {
- const isSidepanelFeatureEnabled = useFeaturePreview('sidepanelNavigation');
- // ["xs", "sm", "md", "lg", "xl", xxl"]
- return useSidePanelNavigationScreenSize() && isSidepanelFeatureEnabled;
-};
-
-export const useSidePanelNavigationScreenSize = () => {
- const breakpoints = useBreakpoints();
- // ["xs", "sm", "md", "lg", "xl", xxl"]
- return breakpoints.includes('lg');
-};
diff --git a/apps/meteor/client/hooks/useSortQueryOptions.spec.tsx b/apps/meteor/client/hooks/useSortQueryOptions.spec.ts
similarity index 100%
rename from apps/meteor/client/hooks/useSortQueryOptions.spec.tsx
rename to apps/meteor/client/hooks/useSortQueryOptions.spec.ts
diff --git a/apps/meteor/client/hooks/useTeamInfoQuery.ts b/apps/meteor/client/hooks/useTeamInfoQuery.ts
new file mode 100644
index 0000000000000..4044a65a60d39
--- /dev/null
+++ b/apps/meteor/client/hooks/useTeamInfoQuery.ts
@@ -0,0 +1,26 @@
+import type { ITeam, Serialized } from '@rocket.chat/core-typings';
+import { useEndpoint } from '@rocket.chat/ui-contexts';
+import type { UseQueryOptions } from '@tanstack/react-query';
+import { keepPreviousData, useQuery } from '@tanstack/react-query';
+
+import { teamsQueryKeys } from '../lib/queryKeys';
+
+type TeamInfoQueryOptions>> = Omit<
+ UseQueryOptions>, Error, TData, ReturnType>,
+ 'queryKey' | 'queryFn'
+>;
+
+export const useTeamInfoQuery = >>(teamId: string, options: TeamInfoQueryOptions = {}) => {
+ const teamsInfoEndpoint = useEndpoint('GET', '/v1/teams.info');
+
+ return useQuery({
+ queryKey: teamsQueryKeys.teamInfo(teamId),
+ queryFn: async () => {
+ const result = await teamsInfoEndpoint({ teamId });
+ return result.teamInfo;
+ },
+ placeholderData: keepPreviousData,
+ enabled: teamId !== '',
+ ...options,
+ });
+};
diff --git a/apps/meteor/client/hooks/useUpdateAvatar.ts b/apps/meteor/client/hooks/useUpdateAvatar.ts
index ac24f6d3b57d3..d8a122683215a 100644
--- a/apps/meteor/client/hooks/useUpdateAvatar.ts
+++ b/apps/meteor/client/hooks/useUpdateAvatar.ts
@@ -3,8 +3,8 @@ import { useToastMessageDispatch, useMethod } from '@rocket.chat/ui-contexts';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
-import { useEndpointAction } from './useEndpointAction';
-import { useEndpointUpload } from './useEndpointUpload';
+import { useEndpointMutation } from './useEndpointMutation';
+import { useEndpointUploadMutation } from './useEndpointUploadMutation';
const isAvatarReset = (avatarObj: AvatarObject): avatarObj is AvatarReset => avatarObj === 'reset';
const isServiceObject = (avatarObj: AvatarObject): avatarObj is AvatarServiceObject =>
@@ -12,10 +12,7 @@ const isServiceObject = (avatarObj: AvatarObject): avatarObj is AvatarServiceObj
const isAvatarUrl = (avatarObj: AvatarObject): avatarObj is AvatarUrlObj =>
!isAvatarReset(avatarObj) && typeof avatarObj === 'object' && 'service' && 'avatarUrl' in avatarObj;
-export const useUpdateAvatar = (
- avatarObj: AvatarObject,
- userId: IUser['_id'],
-): (() => Promise<{ success: boolean } | null | undefined>) => {
+export const useUpdateAvatar = (avatarObj: AvatarObject, userId: IUser['_id']) => {
const { t } = useTranslation();
const avatarUrl = isAvatarUrl(avatarObj) ? avatarObj.avatarUrl : '';
@@ -24,22 +21,38 @@ export const useUpdateAvatar = (
const dispatchToastMessage = useToastMessageDispatch();
- const saveAvatarAction = useEndpointUpload('/v1/users.setAvatar', successMessage);
- const saveAvatarUrlAction = useEndpointAction('POST', '/v1/users.setAvatar', { successMessage });
- const resetAvatarAction = useEndpointAction('POST', '/v1/users.resetAvatar', { successMessage });
+ const { mutateAsync: saveAvatarAction } = useEndpointUploadMutation('/v1/users.setAvatar', {
+ onSuccess: () => {
+ dispatchToastMessage({ type: 'success', message: successMessage });
+ },
+ });
+ const { mutateAsync: saveAvatarUrlAction } = useEndpointMutation('POST', '/v1/users.setAvatar', {
+ onSuccess: () => {
+ dispatchToastMessage({ type: 'success', message: successMessage });
+ },
+ });
+ const { mutateAsync: resetAvatarAction } = useEndpointMutation('POST', '/v1/users.resetAvatar', {
+ onSuccess: () => {
+ dispatchToastMessage({ type: 'success', message: successMessage });
+ },
+ });
const updateAvatar = useCallback(async () => {
if (isAvatarReset(avatarObj)) {
- return resetAvatarAction({
+ await resetAvatarAction({
userId,
});
+ return;
}
+
if (isAvatarUrl(avatarObj)) {
- return saveAvatarUrlAction({
+ await saveAvatarUrlAction({
userId,
...(avatarUrl && { avatarUrl }),
});
+ return;
}
+
if (isServiceObject(avatarObj)) {
const { blob, contentType, service } = avatarObj;
try {
@@ -52,7 +65,7 @@ export const useUpdateAvatar = (
}
if (avatarObj instanceof FormData) {
avatarObj.set('userId', userId);
- return saveAvatarAction(avatarObj);
+ await saveAvatarAction(avatarObj);
}
}, [
avatarObj,
diff --git a/apps/meteor/client/hooks/useUserCustomFields.spec.tsx b/apps/meteor/client/hooks/useUserCustomFields.spec.tsx
index 4c38adc2f3e8a..c27ec3d769dd9 100644
--- a/apps/meteor/client/hooks/useUserCustomFields.spec.tsx
+++ b/apps/meteor/client/hooks/useUserCustomFields.spec.tsx
@@ -10,7 +10,6 @@ it('should not break with invalid Accounts_CustomFieldsToShowInUserInfo setting'
prop: 'value',
}),
{
- legacyRoot: true,
wrapper: mockAppRoot()
.withSetting('Accounts_CustomFieldsToShowInUserInfo', '{"Invalid": "Object", "InvalidProperty": "Invalid" }')
.build(),
diff --git a/apps/meteor/client/hooks/useUserRolesQuery.ts b/apps/meteor/client/hooks/useUserRolesQuery.ts
index 854d4afbc029c..d1798459441ca 100644
--- a/apps/meteor/client/hooks/useUserRolesQuery.ts
+++ b/apps/meteor/client/hooks/useUserRolesQuery.ts
@@ -1,5 +1,5 @@
import type { IRole, IUser } from '@rocket.chat/core-typings';
-import { useMethod, useStream, useUserId } from '@rocket.chat/ui-contexts';
+import { useStream, useUserId, useEndpoint } from '@rocket.chat/ui-contexts';
import type { UseQueryOptions } from '@tanstack/react-query';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useEffect } from 'react';
@@ -71,14 +71,14 @@ export const useUserRolesQuery = (options?: UseUserRolesQue
});
}, [enabled, queryClient, subscribeToNotifyLogged, uid]);
- const getUserRoles = useMethod('getUserRoles');
+ const getUserRoles = useEndpoint('GET', '/v1/roles.getUsersInPublicRoles');
return useQuery({
queryKey: rolesQueryKeys.userRoles(),
queryFn: async () => {
- const results = await getUserRoles();
+ const { users } = await getUserRoles();
- return results.map(
+ return users.map(
(record): UserRoles => ({
uid: record._id,
roles: record.roles,
diff --git a/apps/meteor/client/importPackages.ts b/apps/meteor/client/importPackages.ts
index aede0a7479bd8..1677e7019bf6a 100644
--- a/apps/meteor/client/importPackages.ts
+++ b/apps/meteor/client/importPackages.ts
@@ -1,4 +1,3 @@
-import '../app/apple/client';
import '../app/authorization/client';
import '../app/autotranslate/client';
import '../app/emoji/client';
@@ -7,7 +6,6 @@ import '../app/gitlab/client';
import '../app/license/client';
import '../app/lib/client';
import '../app/livechat-enterprise/client';
-import '../app/nextcloud/client';
import '../app/notifications/client';
import '../app/otr/client';
import '../app/slackbridge/client';
@@ -24,10 +22,8 @@ import '../app/slashcommands-topic/client';
import '../app/slashcommands-unarchiveroom/client';
import '../app/webrtc/client';
import '../app/wordpress/client';
-import '../app/e2e/client';
import '../app/utils/client';
import '../app/settings/client';
-import '../app/models/client';
import '../app/ui-utils/client';
import '../app/reactions/client';
import '../app/livechat/client';
diff --git a/apps/meteor/client/lib/RoomManager.ts b/apps/meteor/client/lib/RoomManager.ts
index a7933f50ec595..cbdeb6f0f0843 100644
--- a/apps/meteor/client/lib/RoomManager.ts
+++ b/apps/meteor/client/lib/RoomManager.ts
@@ -56,8 +56,6 @@ export const RoomManager = new (class RoomManager extends Emitter<{
private rooms: Map = new Map();
- private parentRid?: IRoom['_id'] | undefined;
-
constructor() {
super();
debugRoomManager &&
@@ -81,13 +79,6 @@ export const RoomManager = new (class RoomManager extends Emitter<{
}
get opened(): IRoom['_id'] | undefined {
- return this.parentRid ?? this.rid;
- }
-
- get openedSecondLevel(): IRoom['_id'] | undefined {
- if (!this.parentRid) {
- return undefined;
- }
return this.rid;
}
@@ -116,7 +107,7 @@ export const RoomManager = new (class RoomManager extends Emitter<{
this.emit('changed', this.rid);
}
- private _open(rid: IRoom['_id'], parent?: IRoom['_id']): void {
+ open(rid: IRoom['_id']): void {
if (rid === this.rid) {
return;
}
@@ -125,19 +116,10 @@ export const RoomManager = new (class RoomManager extends Emitter<{
this.rooms.set(rid, new RoomStore(rid));
}
this.rid = rid;
- this.parentRid = parent;
this.emit('opened', this.rid);
this.emit('changed', this.rid);
}
- open(rid: IRoom['_id']): void {
- this._open(rid);
- }
-
- openSecondLevel(parentId: IRoom['_id'], rid: IRoom['_id']): void {
- this._open(rid, parentId);
- }
-
getStore(rid: IRoom['_id']): RoomStore | undefined {
return this.rooms.get(rid);
}
@@ -148,11 +130,6 @@ const subscribeOpenedRoom = [
(): IRoom['_id'] | undefined => RoomManager.opened,
] as const;
-const subscribeOpenedSecondLevelRoom = [
- (callback: () => void): (() => void) => RoomManager.on('changed', callback),
- (): IRoom['_id'] | undefined => RoomManager.openedSecondLevel,
-] as const;
-
export const useOpenedRoom = (): IRoom['_id'] | undefined => useSyncExternalStore(...subscribeOpenedRoom);
export const useOpenedRoomUnreadSince = (): Date | undefined => {
@@ -170,5 +147,3 @@ export const useOpenedRoomUnreadSince = (): Date | undefined => {
return useSyncExternalStore(subscribe, getSnapshotValue);
};
-
-export const useSecondLevelOpenedRoom = (): IRoom['_id'] | undefined => useSyncExternalStore(...subscribeOpenedSecondLevelRoom);
diff --git a/apps/meteor/client/lib/cachedCollections/index.ts b/apps/meteor/client/lib/cachedCollections/index.ts
deleted file mode 100644
index caafeb022b83a..0000000000000
--- a/apps/meteor/client/lib/cachedCollections/index.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export { PrivateCachedCollection, PublicCachedCollection } from './CachedCollection';
-export { CachedCollectionManager } from './CachedCollectionManager';
-export { pipe } from './pipe';
-export { applyQueryOptions, convertSort } from './utils';
diff --git a/apps/meteor/client/lib/cachedCollections/CachedCollection.ts b/apps/meteor/client/lib/cachedStores/CachedStore.ts
similarity index 87%
rename from apps/meteor/client/lib/cachedCollections/CachedCollection.ts
rename to apps/meteor/client/lib/cachedStores/CachedStore.ts
index 72a811d7aa60f..f57a14e58e2bd 100644
--- a/apps/meteor/client/lib/cachedCollections/CachedCollection.ts
+++ b/apps/meteor/client/lib/cachedStores/CachedStore.ts
@@ -3,15 +3,14 @@ import type { StreamNames } from '@rocket.chat/ddp-client';
import localforage from 'localforage';
import { Accounts } from 'meteor/accounts-base';
import { Meteor } from 'meteor/meteor';
-import { ReactiveVar } from 'meteor/reactive-var';
import { Tracker } from 'meteor/tracker';
-import type { StoreApi, UseBoundStore } from 'zustand';
+import { create, type StoreApi, type UseBoundStore } from 'zustand';
import { baseURI } from '../baseURI';
import { onLoggedIn } from '../loggedIn';
-import { CachedCollectionManager } from './CachedCollectionManager';
+import { CachedStoresManager } from './CachedStoresManager';
import type { IDocumentMapStore } from './DocumentMapStore';
-import { MinimongoCollection } from './MinimongoCollection';
+import { watch } from './watch';
import { sdk } from '../../../app/utils/client/lib/SDKClient';
import { isTruthy } from '../../../lib/isTruthy';
import { withDebouncing } from '../../../lib/utils/highOrderFunctions';
@@ -47,8 +46,6 @@ export abstract class CachedStore implements
readonly store: UseBoundStore>>;
- readonly ready = new ReactiveVar(false);
-
protected name: Name;
protected eventType: StreamNames;
@@ -61,6 +58,8 @@ export abstract class CachedStore implements
private timer: ReturnType;
+ readonly useReady = create(() => false);
+
constructor({ name, eventType, store }: { name: Name; eventType: StreamNames; store: UseBoundStore>> }) {
this.name = name;
this.eventType = eventType;
@@ -70,7 +69,7 @@ export abstract class CachedStore implements
? console.log.bind(console, `%cCachedCollection ${this.name}`, `color: navy; font-weight: bold;`)
: () => undefined;
- CachedCollectionManager.register(this);
+ CachedStoresManager.register(this);
}
protected get eventName(): `${Name}-changed` | `${string}/${Name}-changed` {
@@ -311,8 +310,6 @@ export abstract class CachedStore implements
await this.loadFromServerAndPopulate();
}
- this.ready.set(true);
-
this.reconnectionComputation?.stop();
let wentOffline = Tracker.nonreactive(() => Meteor.status().status === 'offline');
this.reconnectionComputation = Tracker.autorun(() => {
@@ -341,9 +338,12 @@ export abstract class CachedStore implements
return this.initializationPromise;
}
- this.initializationPromise = this.performInitialization().finally(() => {
- this.initializationPromise = undefined;
- });
+ this.initializationPromise = this.performInitialization()
+ .catch(console.error)
+ .finally(() => {
+ this.initializationPromise = undefined;
+ this.setReady(true);
+ });
return this.initializationPromise;
}
@@ -354,63 +354,21 @@ export abstract class CachedStore implements
}
this.listenerUnsubscriber?.();
- this.ready.set(false);
+ this.setReady(false);
}
private reconnectionComputation: Tracker.Computation | undefined;
-}
-
-export class PublicCachedStore extends CachedStore {
- protected override getToken() {
- return undefined;
- }
-
- override clearCacheOnLogout() {
- // do nothing
- }
-}
-
-export class PrivateCachedStore extends CachedStore {
- protected override getToken() {
- return Accounts._storedLoginToken();
- }
-
- override clearCacheOnLogout() {
- void this.clearCache();
- }
-
- listen() {
- if (process.env.NODE_ENV === 'test') {
- return;
- }
-
- onLoggedIn(() => {
- void this.init();
- });
- Accounts.onLogout(() => {
- this.release();
- });
+ watchReady() {
+ return watch(this.useReady, (ready) => ready);
}
-}
-
-export abstract class CachedCollection extends CachedStore {
- readonly collection;
- constructor({ name, eventType }: { name: Name; eventType: StreamNames }) {
- const collection = new MinimongoCollection();
-
- super({
- name,
- eventType,
- store: collection.use,
- });
-
- this.collection = collection;
+ setReady(ready: boolean) {
+ this.useReady.setState(ready);
}
}
-export class PublicCachedCollection extends CachedCollection {
+export class PublicCachedStore extends CachedStore {
protected override getToken() {
return undefined;
}
@@ -420,7 +378,7 @@ export class PublicCachedCollection extends
}
}
-export class PrivateCachedCollection extends CachedCollection {
+export class PrivateCachedStore extends CachedStore {
protected override getToken() {
return Accounts._storedLoginToken();
}
diff --git a/apps/meteor/client/lib/cachedCollections/CachedCollectionManager.ts b/apps/meteor/client/lib/cachedStores/CachedStoresManager.ts
similarity index 60%
rename from apps/meteor/client/lib/cachedCollections/CachedCollectionManager.ts
rename to apps/meteor/client/lib/cachedStores/CachedStoresManager.ts
index 31ce60f58f7a0..b899dde4b33a6 100644
--- a/apps/meteor/client/lib/cachedCollections/CachedCollectionManager.ts
+++ b/apps/meteor/client/lib/cachedStores/CachedStoresManager.ts
@@ -1,6 +1,6 @@
-import type { IWithManageableCache } from './CachedCollection';
+import type { IWithManageableCache } from './CachedStore';
-class CachedCollectionManager {
+class CachedStoresManager {
private items = new Set();
register(cachedCollection: IWithManageableCache) {
@@ -14,9 +14,9 @@ class CachedCollectionManager {
}
}
-const instance = new CachedCollectionManager();
+const instance = new CachedStoresManager();
export {
/** @deprecated */
- instance as CachedCollectionManager,
+ instance as CachedStoresManager,
};
diff --git a/apps/meteor/client/lib/cachedCollections/Cursor.ts b/apps/meteor/client/lib/cachedStores/Cursor.ts
similarity index 100%
rename from apps/meteor/client/lib/cachedCollections/Cursor.ts
rename to apps/meteor/client/lib/cachedStores/Cursor.ts
diff --git a/apps/meteor/client/lib/cachedCollections/DiffSequence.ts b/apps/meteor/client/lib/cachedStores/DiffSequence.ts
similarity index 98%
rename from apps/meteor/client/lib/cachedCollections/DiffSequence.ts
rename to apps/meteor/client/lib/cachedStores/DiffSequence.ts
index ec7392c13d01d..7f20a683d9296 100644
--- a/apps/meteor/client/lib/cachedCollections/DiffSequence.ts
+++ b/apps/meteor/client/lib/cachedStores/DiffSequence.ts
@@ -1,5 +1,6 @@
+import { entriesOf } from '../objectUtils';
import type { IdMap } from './IdMap';
-import { clone, entriesOf, hasOwn, equals } from './common';
+import { clone, hasOwn, equals } from './common';
import type { Observer, OrderedObserver, UnorderedObserver } from './observers';
function isObjEmpty(obj: Record): boolean {
diff --git a/apps/meteor/client/lib/cachedCollections/DocumentMapStore.ts b/apps/meteor/client/lib/cachedStores/DocumentMapStore.ts
similarity index 100%
rename from apps/meteor/client/lib/cachedCollections/DocumentMapStore.ts
rename to apps/meteor/client/lib/cachedStores/DocumentMapStore.ts
diff --git a/apps/meteor/client/lib/cachedCollections/IdMap.ts b/apps/meteor/client/lib/cachedStores/IdMap.ts
similarity index 100%
rename from apps/meteor/client/lib/cachedCollections/IdMap.ts
rename to apps/meteor/client/lib/cachedStores/IdMap.ts
diff --git a/apps/meteor/client/lib/cachedCollections/LocalCollection.spec.ts b/apps/meteor/client/lib/cachedStores/LocalCollection.spec.ts
similarity index 100%
rename from apps/meteor/client/lib/cachedCollections/LocalCollection.spec.ts
rename to apps/meteor/client/lib/cachedStores/LocalCollection.spec.ts
diff --git a/apps/meteor/client/lib/cachedCollections/LocalCollection.ts b/apps/meteor/client/lib/cachedStores/LocalCollection.ts
similarity index 100%
rename from apps/meteor/client/lib/cachedCollections/LocalCollection.ts
rename to apps/meteor/client/lib/cachedStores/LocalCollection.ts
diff --git a/apps/meteor/client/lib/cachedCollections/MinimongoCollection.ts b/apps/meteor/client/lib/cachedStores/MinimongoCollection.ts
similarity index 100%
rename from apps/meteor/client/lib/cachedCollections/MinimongoCollection.ts
rename to apps/meteor/client/lib/cachedStores/MinimongoCollection.ts
diff --git a/apps/meteor/client/lib/cachedCollections/MinimongoError.ts b/apps/meteor/client/lib/cachedStores/MinimongoError.ts
similarity index 100%
rename from apps/meteor/client/lib/cachedCollections/MinimongoError.ts
rename to apps/meteor/client/lib/cachedStores/MinimongoError.ts
diff --git a/apps/meteor/client/lib/cachedCollections/ObserveHandle.ts b/apps/meteor/client/lib/cachedStores/ObserveHandle.ts
similarity index 100%
rename from apps/meteor/client/lib/cachedCollections/ObserveHandle.ts
rename to apps/meteor/client/lib/cachedStores/ObserveHandle.ts
diff --git a/apps/meteor/client/lib/cachedCollections/OrderedDict.ts b/apps/meteor/client/lib/cachedStores/OrderedDict.ts
similarity index 100%
rename from apps/meteor/client/lib/cachedCollections/OrderedDict.ts
rename to apps/meteor/client/lib/cachedStores/OrderedDict.ts
diff --git a/apps/meteor/client/lib/cachedCollections/Query.ts b/apps/meteor/client/lib/cachedStores/Query.ts
similarity index 100%
rename from apps/meteor/client/lib/cachedCollections/Query.ts
rename to apps/meteor/client/lib/cachedStores/Query.ts
diff --git a/apps/meteor/client/lib/cachedCollections/SynchronousQueue.ts b/apps/meteor/client/lib/cachedStores/SynchronousQueue.ts
similarity index 100%
rename from apps/meteor/client/lib/cachedCollections/SynchronousQueue.ts
rename to apps/meteor/client/lib/cachedStores/SynchronousQueue.ts
diff --git a/apps/meteor/client/lib/cachedCollections/common.ts b/apps/meteor/client/lib/cachedStores/common.ts
similarity index 94%
rename from apps/meteor/client/lib/cachedCollections/common.ts
rename to apps/meteor/client/lib/cachedStores/common.ts
index d4b4602894058..ba9d634a178f3 100644
--- a/apps/meteor/client/lib/cachedCollections/common.ts
+++ b/apps/meteor/client/lib/cachedStores/common.ts
@@ -1,5 +1,7 @@
import { getBSONType } from '@rocket.chat/mongo-adapter';
+import { entriesOf } from '../objectUtils';
+
export const hasOwn = Object.prototype.hasOwnProperty;
const isBinary = (x: unknown): x is Uint8Array => typeof x === 'object' && x !== null && x instanceof Uint8Array;
@@ -111,10 +113,6 @@ export const equals = (a: T, b: T): boolean => {
export const isPlainObject = (x: any): x is Record => x && getBSONType(x) === 3;
-export function entriesOf>(obj: T): [keyof T, T[keyof T]][] {
- return Object.entries(obj) as [keyof T, T[keyof T]][];
-}
-
const invalidCharMsg = {
'$': "start with '$'",
'.': "contain '.'",
diff --git a/apps/meteor/client/lib/cachedStores/createGlobalStore.ts b/apps/meteor/client/lib/cachedStores/createGlobalStore.ts
new file mode 100644
index 0000000000000..b80aa8156f0a5
--- /dev/null
+++ b/apps/meteor/client/lib/cachedStores/createGlobalStore.ts
@@ -0,0 +1,14 @@
+import type { StoreApi, UseBoundStore } from 'zustand';
+
+import type { IDocumentMapStore } from './DocumentMapStore';
+
+export const createGlobalStore = (store: UseBoundStore>>, extension?: U) =>
+ Object.assign(
+ {
+ use: store,
+ get state(): IDocumentMapStore {
+ return this.use.getState();
+ },
+ } as const,
+ extension,
+ );
diff --git a/apps/meteor/client/lib/cachedStores/index.ts b/apps/meteor/client/lib/cachedStores/index.ts
new file mode 100644
index 0000000000000..b0320808dda86
--- /dev/null
+++ b/apps/meteor/client/lib/cachedStores/index.ts
@@ -0,0 +1,8 @@
+export { CachedStoresManager } from './CachedStoresManager';
+export { pipe } from './pipe';
+export { applyQueryOptions } from './utils';
+export { createDocumentMapStore, type IDocumentMapStore } from './DocumentMapStore';
+export { MinimongoCollection } from './MinimongoCollection';
+export { watch } from './watch';
+export { PublicCachedStore, PrivateCachedStore } from './CachedStore';
+export { createGlobalStore } from './createGlobalStore';
diff --git a/apps/meteor/client/lib/cachedCollections/observers.ts b/apps/meteor/client/lib/cachedStores/observers.ts
similarity index 100%
rename from apps/meteor/client/lib/cachedCollections/observers.ts
rename to apps/meteor/client/lib/cachedStores/observers.ts
diff --git a/apps/meteor/client/lib/cachedCollections/pipe.spec.ts b/apps/meteor/client/lib/cachedStores/pipe.spec.ts
similarity index 100%
rename from apps/meteor/client/lib/cachedCollections/pipe.spec.ts
rename to apps/meteor/client/lib/cachedStores/pipe.spec.ts
diff --git a/apps/meteor/client/lib/cachedCollections/pipe.ts b/apps/meteor/client/lib/cachedStores/pipe.ts
similarity index 100%
rename from apps/meteor/client/lib/cachedCollections/pipe.ts
rename to apps/meteor/client/lib/cachedStores/pipe.ts
diff --git a/apps/meteor/client/lib/cachedCollections/utils.ts b/apps/meteor/client/lib/cachedStores/utils.ts
similarity index 95%
rename from apps/meteor/client/lib/cachedCollections/utils.ts
rename to apps/meteor/client/lib/cachedStores/utils.ts
index a9dbfc357fcf9..52f0d72e4974a 100644
--- a/apps/meteor/client/lib/cachedCollections/utils.ts
+++ b/apps/meteor/client/lib/cachedStores/utils.ts
@@ -15,7 +15,7 @@ type SortObject = {
/**
* Converts a MongoDB-style sort structure to a sort object.
*/
-export const convertSort = (original: OriginalStructure): SortObject => {
+const convertSort = (original: OriginalStructure): SortObject => {
const convertedSort: SortObject = [];
if (!original) {
diff --git a/apps/meteor/client/lib/cachedStores/watch.ts b/apps/meteor/client/lib/cachedStores/watch.ts
new file mode 100644
index 0000000000000..0a637fb70867e
--- /dev/null
+++ b/apps/meteor/client/lib/cachedStores/watch.ts
@@ -0,0 +1,24 @@
+import { Tracker } from 'meteor/tracker';
+import type { StoreApi, UseBoundStore } from 'zustand';
+
+/** Adds Meteor Tracker reactivity to a Zustand store lookup */
+export const watch =