From 263403d864715f415623c97c74e9ad87ea02ce7c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 23:13:00 +0000 Subject: [PATCH 1/6] Initial plan From 330fcd7f58d97eda1a9b313acc82248b9e8f162f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 23:17:57 +0000 Subject: [PATCH 2/6] Add API test and fix UI to exclude existing members from autocomplete Co-authored-by: ggazzo <5263975+ggazzo@users.noreply.github.com> --- .../UserAutoCompleteMultiple.tsx | 7 +++--- apps/meteor/client/lib/queryKeys.ts | 3 ++- .../RoomMembers/AddUsers/AddUsers.tsx | 18 +++++++++++++-- apps/meteor/tests/end-to-end/api/channels.ts | 22 +++++++++++++++++++ 4 files changed, 44 insertions(+), 6 deletions(-) diff --git a/apps/meteor/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultiple.tsx b/apps/meteor/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultiple.tsx index 413cab7f6ea0c..9b1bf73c6f9a5 100644 --- a/apps/meteor/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultiple.tsx +++ b/apps/meteor/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultiple.tsx @@ -15,6 +15,7 @@ type UserAutoCompleteMultipleProps = { placeholder?: string; federated?: boolean; error?: string; + exceptions?: string[]; } & Omit, 'is' | 'onChange' | 'value'>; type UserAutoCompleteOptionType = { @@ -29,7 +30,7 @@ type UserAutoCompleteOptions = { const matrixRegex = new RegExp('@(.*:.*)'); -const UserAutoCompleteMultiple = ({ onChange, value, placeholder, federated, ...props }: UserAutoCompleteMultipleProps): ReactElement => { +const UserAutoCompleteMultiple = ({ onChange, value, placeholder, federated, exceptions = [], ...props }: UserAutoCompleteMultipleProps): ReactElement => { const [filter, setFilter] = useState(''); const [selectedCache, setSelectedCache] = useState({}); @@ -37,10 +38,10 @@ const UserAutoCompleteMultiple = ({ onChange, value, placeholder, federated, ... const getUsers = useEndpoint('GET', '/v1/users.autocomplete'); const { data } = useQuery({ - queryKey: usersQueryKeys.userAutoComplete(debouncedFilter, federated ?? false), + queryKey: usersQueryKeys.userAutoComplete(debouncedFilter, federated ?? false, exceptions), queryFn: async () => { - const users = await getUsers({ selector: JSON.stringify({ term: debouncedFilter }) }); + const users = await getUsers({ selector: JSON.stringify({ term: debouncedFilter, exceptions }) }); const options = users.items.map((item): [string, UserAutoCompleteOptionType] => [item.username, item]); // Add extra option if filter text matches `username:server` diff --git a/apps/meteor/client/lib/queryKeys.ts b/apps/meteor/client/lib/queryKeys.ts index 44529ad6d2543..8c00f09f43682 100644 --- a/apps/meteor/client/lib/queryKeys.ts +++ b/apps/meteor/client/lib/queryKeys.ts @@ -119,7 +119,8 @@ export const usersQueryKeys = { all: ['users'] as const, userInfo: ({ uid, username }: { uid?: IUser['_id']; username?: IUser['username'] }) => [...usersQueryKeys.all, 'info', { uid, username }] as const, - userAutoComplete: (filter: string, federated: boolean) => [...usersQueryKeys.all, 'autocomplete', filter, federated] as const, + userAutoComplete: (filter: string, federated: boolean, exceptions: string[] = []) => + [...usersQueryKeys.all, 'autocomplete', filter, federated, exceptions] as const, }; export const teamsQueryKeys = { diff --git a/apps/meteor/client/views/room/contextualBar/RoomMembers/AddUsers/AddUsers.tsx b/apps/meteor/client/views/room/contextualBar/RoomMembers/AddUsers/AddUsers.tsx index 7c7151d056c5f..a307f6d9ae99c 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomMembers/AddUsers/AddUsers.tsx +++ b/apps/meteor/client/views/room/contextualBar/RoomMembers/AddUsers/AddUsers.tsx @@ -11,8 +11,9 @@ import { ContextualbarFooter, ContextualbarDialog, } from '@rocket.chat/ui-client'; -import { useToastMessageDispatch, useMethod, useRoomToolbox } from '@rocket.chat/ui-contexts'; -import { useId } from 'react'; +import { useToastMessageDispatch, useMethod, useRoomToolbox, useEndpoint } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; +import { useId, useMemo } from 'react'; import type { ReactElement } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; @@ -42,6 +43,18 @@ const AddUsers = ({ rid, onClickBack, reload }: AddUsersProps): ReactElement => const { closeTab } = useRoomToolbox(); const saveAction = useMethod('addUsersToRoom'); + // Fetch existing room members to exclude them from autocomplete + const getRoomMembers = useEndpoint('GET', room.t === 'd' ? '/v1/im.members' : '/v1/rooms.membersOrderedByRole'); + const { data: membersData } = useQuery({ + queryKey: ['room-members', rid], + queryFn: () => getRoomMembers({ roomId: rid, offset: 0, count: 1000 }), + }); + + const existingMemberUsernames = useMemo( + () => membersData?.members?.map((member) => member.username).filter(Boolean) || [], + [membersData], + ); + const { handleSubmit, control, @@ -84,6 +97,7 @@ const AddUsers = ({ rid, onClickBack, reload }: AddUsersProps): ReactElement => federated={isFederated} placeholder={t('Choose_users')} aria-describedby={`${usersFieldId}-error`} + exceptions={existingMemberUsernames} {...field} /> )} diff --git a/apps/meteor/tests/end-to-end/api/channels.ts b/apps/meteor/tests/end-to-end/api/channels.ts index f2d348240f3cf..c0feecae17016 100644 --- a/apps/meteor/tests/end-to-end/api/channels.ts +++ b/apps/meteor/tests/end-to-end/api/channels.ts @@ -250,6 +250,28 @@ describe('[Channels]', () => { }); }); + it('/channels.invite should still succeed when inviting a user already in the channel', async () => { + const roomInfo = await getRoomInfo(channel._id); + + return request + .post(api('channels.invite')) + .set(credentials) + .send({ + roomId: channel._id, + userId: 'rocket.cat', + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('channel._id'); + expect(res.body).to.have.nested.property('channel.name', apiPublicChannelName); + expect(res.body).to.have.nested.property('channel.t', 'c'); + // When adding an already-existing user, no new system message is created + expect(res.body).to.have.nested.property('channel.msgs', roomInfo.channel.msgs); + }); + }); + it('/channels.addOwner', (done) => { void request .post(api('channels.addOwner')) From 030b5206e678f279cc317b55d4ea60f9175031ae Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 23:19:36 +0000 Subject: [PATCH 3/6] Address code review feedback: improve type safety and add comment about member limit Co-authored-by: ggazzo <5263975+ggazzo@users.noreply.github.com> --- .../room/contextualBar/RoomMembers/AddUsers/AddUsers.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/meteor/client/views/room/contextualBar/RoomMembers/AddUsers/AddUsers.tsx b/apps/meteor/client/views/room/contextualBar/RoomMembers/AddUsers/AddUsers.tsx index a307f6d9ae99c..463decb399caf 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomMembers/AddUsers/AddUsers.tsx +++ b/apps/meteor/client/views/room/contextualBar/RoomMembers/AddUsers/AddUsers.tsx @@ -44,14 +44,16 @@ const AddUsers = ({ rid, onClickBack, reload }: AddUsersProps): ReactElement => const saveAction = useMethod('addUsersToRoom'); // Fetch existing room members to exclude them from autocomplete + // Note: Limited to 100 members due to API_Upper_Count_Limit setting + // In very large channels (>100 members), some existing members might still appear in autocomplete const getRoomMembers = useEndpoint('GET', room.t === 'd' ? '/v1/im.members' : '/v1/rooms.membersOrderedByRole'); const { data: membersData } = useQuery({ queryKey: ['room-members', rid], - queryFn: () => getRoomMembers({ roomId: rid, offset: 0, count: 1000 }), + queryFn: () => getRoomMembers({ roomId: rid, offset: 0, count: 100 }), }); const existingMemberUsernames = useMemo( - () => membersData?.members?.map((member) => member.username).filter(Boolean) || [], + () => membersData?.members?.map((member) => member.username).filter((username): username is string => !!username) || [], [membersData], ); From a4f7ad7104ace669b5f4edc2c6ec4b2b160e249d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 23:20:40 +0000 Subject: [PATCH 4/6] Add error handling and query enablement for member fetching Co-authored-by: ggazzo <5263975+ggazzo@users.noreply.github.com> --- .../RoomMembers/AddUsers/AddUsers.tsx | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/apps/meteor/client/views/room/contextualBar/RoomMembers/AddUsers/AddUsers.tsx b/apps/meteor/client/views/room/contextualBar/RoomMembers/AddUsers/AddUsers.tsx index 463decb399caf..2ec2c262502e9 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomMembers/AddUsers/AddUsers.tsx +++ b/apps/meteor/client/views/room/contextualBar/RoomMembers/AddUsers/AddUsers.tsx @@ -46,16 +46,22 @@ const AddUsers = ({ rid, onClickBack, reload }: AddUsersProps): ReactElement => // Fetch existing room members to exclude them from autocomplete // Note: Limited to 100 members due to API_Upper_Count_Limit setting // In very large channels (>100 members), some existing members might still appear in autocomplete - const getRoomMembers = useEndpoint('GET', room.t === 'd' ? '/v1/im.members' : '/v1/rooms.membersOrderedByRole'); - const { data: membersData } = useQuery({ + const getRoomMembers = useEndpoint('GET', '/v1/rooms.membersOrderedByRole'); + const { data: membersData, isError } = useQuery({ queryKey: ['room-members', rid], queryFn: () => getRoomMembers({ roomId: rid, offset: 0, count: 100 }), + // Disable query if room type is not supported + enabled: room.t === 'c' || room.t === 'p', }); - const existingMemberUsernames = useMemo( - () => membersData?.members?.map((member) => member.username).filter((username): username is string => !!username) || [], - [membersData], - ); + const existingMemberUsernames = useMemo(() => { + // If the query is disabled or failed, return empty array (no exceptions) + // This means existing members might appear in autocomplete, but backend will handle duplicates + if (isError || !membersData?.members) { + return []; + } + return membersData.members.map((member) => member.username).filter((username): username is string => !!username); + }, [membersData, isError]); const { handleSubmit, From 83c27d5283b17ff111eced71af82bcd45b69c5c7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 23:21:30 +0000 Subject: [PATCH 5/6] Improve cache efficiency and variable naming for final review Co-authored-by: ggazzo <5263975+ggazzo@users.noreply.github.com> --- apps/meteor/client/lib/queryKeys.ts | 3 ++- .../room/contextualBar/RoomMembers/AddUsers/AddUsers.tsx | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/meteor/client/lib/queryKeys.ts b/apps/meteor/client/lib/queryKeys.ts index 8c00f09f43682..78cbee1fc109e 100644 --- a/apps/meteor/client/lib/queryKeys.ts +++ b/apps/meteor/client/lib/queryKeys.ts @@ -120,7 +120,8 @@ export const usersQueryKeys = { userInfo: ({ uid, username }: { uid?: IUser['_id']; username?: IUser['username'] }) => [...usersQueryKeys.all, 'info', { uid, username }] as const, userAutoComplete: (filter: string, federated: boolean, exceptions: string[] = []) => - [...usersQueryKeys.all, 'autocomplete', filter, federated, exceptions] as const, + // Sort exceptions to ensure consistent cache keys + [...usersQueryKeys.all, 'autocomplete', filter, federated, [...exceptions].sort()] as const, }; export const teamsQueryKeys = { diff --git a/apps/meteor/client/views/room/contextualBar/RoomMembers/AddUsers/AddUsers.tsx b/apps/meteor/client/views/room/contextualBar/RoomMembers/AddUsers/AddUsers.tsx index 2ec2c262502e9..9b1d6b07d7589 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomMembers/AddUsers/AddUsers.tsx +++ b/apps/meteor/client/views/room/contextualBar/RoomMembers/AddUsers/AddUsers.tsx @@ -47,7 +47,7 @@ const AddUsers = ({ rid, onClickBack, reload }: AddUsersProps): ReactElement => // Note: Limited to 100 members due to API_Upper_Count_Limit setting // In very large channels (>100 members), some existing members might still appear in autocomplete const getRoomMembers = useEndpoint('GET', '/v1/rooms.membersOrderedByRole'); - const { data: membersData, isError } = useQuery({ + const { data: membersData, isError: isMembersQueryError } = useQuery({ queryKey: ['room-members', rid], queryFn: () => getRoomMembers({ roomId: rid, offset: 0, count: 100 }), // Disable query if room type is not supported @@ -57,11 +57,11 @@ const AddUsers = ({ rid, onClickBack, reload }: AddUsersProps): ReactElement => const existingMemberUsernames = useMemo(() => { // If the query is disabled or failed, return empty array (no exceptions) // This means existing members might appear in autocomplete, but backend will handle duplicates - if (isError || !membersData?.members) { + if (isMembersQueryError || !membersData?.members) { return []; } return membersData.members.map((member) => member.username).filter((username): username is string => !!username); - }, [membersData, isError]); + }, [membersData, isMembersQueryError]); const { handleSubmit, From 2f47a44bff7959e95a31bcb4e8046d419cffe2aa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 23:22:13 +0000 Subject: [PATCH 6/6] Performance optimizations: reduce array iterations and memoize sorting Co-authored-by: ggazzo <5263975+ggazzo@users.noreply.github.com> --- apps/meteor/client/lib/queryKeys.ts | 4 ++-- .../contextualBar/RoomMembers/AddUsers/AddUsers.tsx | 10 +++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/apps/meteor/client/lib/queryKeys.ts b/apps/meteor/client/lib/queryKeys.ts index 78cbee1fc109e..37f5c07bb1a64 100644 --- a/apps/meteor/client/lib/queryKeys.ts +++ b/apps/meteor/client/lib/queryKeys.ts @@ -120,8 +120,8 @@ export const usersQueryKeys = { userInfo: ({ uid, username }: { uid?: IUser['_id']; username?: IUser['username'] }) => [...usersQueryKeys.all, 'info', { uid, username }] as const, userAutoComplete: (filter: string, federated: boolean, exceptions: string[] = []) => - // Sort exceptions to ensure consistent cache keys - [...usersQueryKeys.all, 'autocomplete', filter, federated, [...exceptions].sort()] as const, + // Expects exceptions to be pre-sorted in the calling component for cache efficiency + [...usersQueryKeys.all, 'autocomplete', filter, federated, exceptions] as const, }; export const teamsQueryKeys = { diff --git a/apps/meteor/client/views/room/contextualBar/RoomMembers/AddUsers/AddUsers.tsx b/apps/meteor/client/views/room/contextualBar/RoomMembers/AddUsers/AddUsers.tsx index 9b1d6b07d7589..b57e66da9520a 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomMembers/AddUsers/AddUsers.tsx +++ b/apps/meteor/client/views/room/contextualBar/RoomMembers/AddUsers/AddUsers.tsx @@ -60,7 +60,15 @@ const AddUsers = ({ rid, onClickBack, reload }: AddUsersProps): ReactElement => if (isMembersQueryError || !membersData?.members) { return []; } - return membersData.members.map((member) => member.username).filter((username): username is string => !!username); + // Use single iteration with reduce for better performance + return membersData.members + .reduce((acc, member) => { + if (member.username) { + acc.push(member.username); + } + return acc; + }, []) + .sort(); // Sort here for consistent query cache keys }, [membersData, isMembersQueryError]); const {