diff --git a/.changeset/new-ways-roll.md b/.changeset/new-ways-roll.md
new file mode 100644
index 0000000000000..65d5da2a98c65
--- /dev/null
+++ b/.changeset/new-ways-roll.md
@@ -0,0 +1,5 @@
+---
+"@rocket.chat/meteor": patch
+---
+
+fixes the selection of room and users performance when using forward message feature
diff --git a/apps/meteor/client/components/UserAndRoomAutoCompleteMultiple/UserAndRoomAutoCompleteMultiple.spec.tsx b/apps/meteor/client/components/UserAndRoomAutoCompleteMultiple/UserAndRoomAutoCompleteMultiple.spec.tsx
new file mode 100644
index 0000000000000..f37e6a32c8c74
--- /dev/null
+++ b/apps/meteor/client/components/UserAndRoomAutoCompleteMultiple/UserAndRoomAutoCompleteMultiple.spec.tsx
@@ -0,0 +1,91 @@
+import { useUser, useUserSubscriptions, useRoomAvatarPath } 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 { roomCoordinator } from '../../lib/rooms/roomCoordinator';
+
+// Mock dependencies
+jest.mock('@rocket.chat/ui-contexts', () => ({
+ useUser: jest.fn(),
+ useUserSubscriptions: jest.fn(),
+ useRoomAvatarPath: jest.fn(),
+}));
+jest.mock('../../lib/rooms/roomCoordinator', () => ({
+ roomCoordinator: { readOnly: jest.fn() },
+}));
+
+const mockUser = { _id: 'user1', username: 'testuser' };
+
+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);
+ });
+
+ it('should render options based on user subscriptions', async () => {
+ render();
+
+ const input = screen.getByRole('textbox');
+ await userEvent.click(input);
+
+ await waitFor(() => {
+ expect(screen.getByText('General')).toBeInTheDocument();
+ });
+
+ await waitFor(() => {
+ expect(screen.getByText('Direct')).toBeInTheDocument();
+ });
+ });
+
+ it('should filter out read-only rooms', async () => {
+ (roomCoordinator.readOnly as jest.Mock).mockImplementation((rid) => rid === 'room1');
+ render();
+
+ const input = screen.getByRole('textbox');
+ await userEvent.click(input);
+
+ await waitFor(() => {
+ expect(screen.queryByText('General')).not.toBeInTheDocument();
+ });
+ await waitFor(() => {
+ expect(screen.getByText('Direct')).toBeInTheDocument();
+ });
+ });
+
+ it('should call onChange when selecting an option', async () => {
+ const handleChange = jest.fn();
+ render();
+
+ const input = screen.getByRole('textbox');
+ await userEvent.click(input);
+
+ 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 747cd1b7700af..509718bb1129d 100644
--- a/apps/meteor/client/components/UserAndRoomAutoCompleteMultiple/UserAndRoomAutoCompleteMultiple.tsx
+++ b/apps/meteor/client/components/UserAndRoomAutoCompleteMultiple/UserAndRoomAutoCompleteMultiple.tsx
@@ -4,59 +4,64 @@ import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
import { escapeRegExp } from '@rocket.chat/string-helpers';
import { RoomAvatar, UserAvatar } from '@rocket.chat/ui-avatar';
import { useUser, useUserSubscriptions } from '@rocket.chat/ui-contexts';
-import type { ComponentProps, ReactElement } from 'react';
-import { memo, useCallback, useMemo, useState } from 'react';
+import type { ComponentProps } from 'react';
+import { memo, useMemo, useState } from 'react';
import { Rooms } from '../../../app/models/client';
-import { useReactiveValue } from '../../hooks/useReactiveValue';
import { roomCoordinator } from '../../lib/rooms/roomCoordinator';
-type UserAndRoomAutoCompleteMultipleProps = Omit, 'filter'>;
+type UserAndRoomAutoCompleteMultipleProps = Omit, 'filter'> & { limit?: number };
-const UserAndRoomAutoCompleteMultiple = ({ value, onChange, ...props }: UserAndRoomAutoCompleteMultipleProps): ReactElement => {
+const UserAndRoomAutoCompleteMultiple = ({ value, onChange, limit, ...props }: UserAndRoomAutoCompleteMultipleProps) => {
const user = useUser();
const [filter, setFilter] = useState('');
const debouncedFilter = useDebouncedValue(filter, 1000);
- const subscriptions = useUserSubscriptions(
- useMemo(
- () => ({
- open: { $ne: false },
- $or: [
- { lowerCaseFName: new RegExp(escapeRegExp(debouncedFilter), 'i') },
- { lowerCaseName: new RegExp(escapeRegExp(debouncedFilter), 'i') },
- ],
- }),
+ const rooms = useUserSubscriptions(
+ ...useMemo>(
+ () => [
+ {
+ open: { $ne: false },
+ $or: [
+ { lowerCaseFName: new RegExp(escapeRegExp(debouncedFilter), 'i') },
+ { lowerCaseName: new RegExp(escapeRegExp(debouncedFilter), 'i') },
+ ],
+ },
+ // We are using a higher limit here to take advantage of the amount that
+ // will be filtered below into a smaller set respecting the limit prop.
+ { limit: 100 },
+ ],
[debouncedFilter],
),
);
- const rooms = useReactiveValue(
- useCallback(
- () =>
- subscriptions.filter((subscription) => {
- if (!user) {
- return;
- }
+ const options = useMemo(() => {
+ if (!user) {
+ return [];
+ }
- if (isDirectMessageRoom(subscription) && (subscription.blocked || subscription.blocker)) {
- return;
- }
+ return rooms.reduce>((acc, room) => {
+ if (acc.length === limit) return acc;
- return !roomCoordinator.readOnly(Rooms.state.get(subscription.rid), user);
- }),
- [subscriptions, user],
- ),
- );
+ if (isDirectMessageRoom(room) && (room.blocked || room.blocker)) {
+ return acc;
+ }
- const options = useMemo(
- () =>
- rooms.map(({ rid, fname, name, avatarETag, t }) => ({
- value: rid,
- label: { name: fname || name, avatarETag, type: t },
- })),
- [rooms],
- );
+ if (roomCoordinator.readOnly(Rooms.state.get(room.rid), user)) return acc;
+
+ return [
+ ...acc,
+ {
+ value: room.rid,
+ label: {
+ name: room.fname || room.name,
+ avatarETag: room.avatarETag,
+ type: room.t,
+ },
+ },
+ ];
+ }, []);
+ }, [limit, rooms, user]);
return (
(
+ renderSelected={({ selected: { value, label }, onRemove, ...props }) => (
{label.t === 'd' ? (
@@ -78,7 +83,7 @@ const UserAndRoomAutoCompleteMultiple = ({ value, onChange, ...props }: UserAndR
)}
- renderItem={({ value, label, ...props }): ReactElement => (
+ renderItem={({ value, label, ...props }) => (