Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
placeholder?: string;
federated?: boolean;
error?: string;
exceptions?: string[];
} & Omit<AllHTMLAttributes<HTMLInputElement>, 'is' | 'onChange' | 'value'>;

type UserAutoCompleteOptionType = {
Expand All @@ -29,18 +30,18 @@

const matrixRegex = new RegExp('@(.*:.*)');

const UserAutoCompleteMultiple = ({ onChange, value, placeholder, federated, ...props }: UserAutoCompleteMultipleProps): ReactElement => {
const UserAutoCompleteMultiple = ({ onChange, value, placeholder, federated, exceptions = [], ...props }: UserAutoCompleteMultipleProps): ReactElement => {

Check failure on line 33 in apps/meteor/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultiple.tsx

View workflow job for this annotation

GitHub Actions / 🔎 Code Check / Code Lint

Replace `·onChange,·value,·placeholder,·federated,·exceptions·=·[],·...props·` with `⏎↹onChange,⏎↹value,⏎↹placeholder,⏎↹federated,⏎↹exceptions·=·[],⏎↹...props⏎`
const [filter, setFilter] = useState('');
const [selectedCache, setSelectedCache] = useState<UserAutoCompleteOptions>({});

const debouncedFilter = useDebouncedValue(filter, 500);
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`
Expand Down
4 changes: 3 additions & 1 deletion apps/meteor/client/lib/queryKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,9 @@ 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[] = []) =>
// Expects exceptions to be pre-sorted in the calling component for cache efficiency
[...usersQueryKeys.all, 'autocomplete', filter, federated, exceptions] as const,
};

export const teamsQueryKeys = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -42,6 +43,34 @@ const AddUsers = ({ rid, onClickBack, reload }: AddUsersProps): ReactElement =>
const { closeTab } = useRoomToolbox();
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', '/v1/rooms.membersOrderedByRole');
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
enabled: room.t === 'c' || room.t === 'p',
});

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 (isMembersQueryError || !membersData?.members) {
return [];
}
// Use single iteration with reduce for better performance
return membersData.members
.reduce<string[]>((acc, member) => {
if (member.username) {
acc.push(member.username);
}
return acc;
}, [])
.sort(); // Sort here for consistent query cache keys
}, [membersData, isMembersQueryError]);

const {
handleSubmit,
control,
Expand Down Expand Up @@ -84,6 +113,7 @@ const AddUsers = ({ rid, onClickBack, reload }: AddUsersProps): ReactElement =>
federated={isFederated}
placeholder={t('Choose_users')}
aria-describedby={`${usersFieldId}-error`}
exceptions={existingMemberUsernames}
{...field}
/>
)}
Expand Down
22 changes: 22 additions & 0 deletions apps/meteor/tests/end-to-end/api/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'))
Expand Down
Loading