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 }) => (