snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-16 01:21:02 +05:00
commit 30810eecd7
10161 changed files with 987110 additions and 0 deletions

View File

@@ -0,0 +1,95 @@
import type { IRoom } from '@rocket.chat/core-typings';
import {
Box,
Button,
Field,
FieldLabel,
Modal,
ModalClose,
ModalContent,
ModalFooter,
ModalFooterControllers,
ModalHeader,
ModalTitle,
} from '@rocket.chat/fuselage';
import { useToastMessageDispatch, useEndpoint } from '@rocket.chat/ui-contexts';
import { memo, useCallback } from 'react';
import { useForm, Controller } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import RoomsAvailableForTeamsAutoComplete from './RoomsAvailableForTeamsAutoComplete';
type AddExistingModalFormData = {
rooms: IRoom['_id'][];
};
export type AddExistingModalProps = {
teamId: string;
onClose: () => void;
reload?: () => void;
};
// TODO: Use GenericModal instead of Modal
const AddExistingModal = ({ teamId, onClose, reload }: AddExistingModalProps) => {
const { t } = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
const addRoomEndpoint = useEndpoint('POST', '/v1/teams.addRooms');
const {
control,
formState: { isDirty },
handleSubmit,
} = useForm<AddExistingModalFormData>({ defaultValues: { rooms: [] } });
const handleAddChannels = useCallback(
async ({ rooms }: AddExistingModalFormData) => {
try {
await addRoomEndpoint({
rooms,
teamId,
});
dispatchToastMessage({ type: 'success', message: t('Channels_added') });
reload?.();
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
} finally {
onClose();
}
},
[addRoomEndpoint, teamId, onClose, dispatchToastMessage, reload, t],
);
return (
<Modal
aria-label={t('Team_Add_existing_channels')}
wrapperFunction={(props) => <Box is='form' onSubmit={handleSubmit(handleAddChannels)} {...props} />}
>
<ModalHeader>
<ModalTitle>{t('Team_Add_existing_channels')}</ModalTitle>
<ModalClose onClick={onClose} />
</ModalHeader>
<ModalContent>
<Field marginBlockEnd={24}>
<FieldLabel>{t('Channels')}</FieldLabel>
<Controller
control={control}
name='rooms'
render={({ field: { value, onChange } }) => <RoomsAvailableForTeamsAutoComplete value={value} onChange={onChange} />}
/>
</Field>
</ModalContent>
<ModalFooter>
<ModalFooterControllers>
<Button onClick={onClose}>{t('Cancel')}</Button>
<Button disabled={!isDirty} type='submit' primary>
{t('Add')}
</Button>
</ModalFooterControllers>
</ModalFooter>
</Modal>
);
};
export default memo(AddExistingModal);

View File

@@ -0,0 +1,69 @@
import { AutoComplete, Box, Option, Chip } from '@rocket.chat/fuselage';
import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
import { RoomAvatar } from '@rocket.chat/ui-avatar';
import { useEndpoint } from '@rocket.chat/ui-contexts';
import { keepPreviousData, useQuery } from '@tanstack/react-query';
import type { ComponentProps } from 'react';
import { memo, useMemo, useState } from 'react';
import { roomCoordinator } from '../../../../../lib/rooms/roomCoordinator';
export type RoomsAvailableForTeamsAutoCompleteProps = Omit<ComponentProps<typeof AutoComplete>, 'filter'>;
const RoomsAvailableForTeamsAutoComplete = ({ value, onChange, ...props }: RoomsAvailableForTeamsAutoCompleteProps) => {
const [filter, setFilter] = useState('');
const debouncedFilter = useDebouncedValue(filter, 1000);
const roomsAvailableForTeamsAutoCompleteEndpoint = useEndpoint('GET', '/v1/rooms.autocomplete.availableForTeams');
const { data } = useQuery({
queryKey: ['roomsAvailableForTeamsAutoComplete', debouncedFilter],
queryFn: async () => roomsAvailableForTeamsAutoCompleteEndpoint({ name: debouncedFilter }),
placeholderData: keepPreviousData,
});
const options = useMemo(() => {
if (!data) {
return [];
}
return data.items.map((room) => ({
value: room._id,
label: {
name: roomCoordinator.getRoomName(room.t, { _id: room._id, name: room.name, fname: room.fname, prid: room.prid }),
type: room.t,
avatarETag: room.avatarETag,
},
}));
}, [data]);
return (
<AutoComplete
{...props}
multiple
value={value}
onChange={onChange}
filter={filter}
setFilter={setFilter}
renderSelected={({ selected: { value, label }, onRemove }) => (
<Chip key={value} height='x20' value={value} onClick={onRemove} marginBlock={2} marginInlineEnd={4}>
<RoomAvatar size='x20' room={{ ...label, type: label?.type || 'c', _id: value }} />
<Box is='span' margin='none' marginInlineStart={4}>
{label.name}
</Box>
</Chip>
)}
renderItem={({ value, label, ...props }) => (
<Option
key={value}
{...props}
label={label.name}
avatar={<RoomAvatar room={{ _id: value, type: label.type, avatarETag: label.avatarETag }} size='x20' />}
/>
)}
options={options}
/>
);
};
export default memo(RoomsAvailableForTeamsAutoComplete);

View File

@@ -0,0 +1 @@
export { default } from './AddExistingModal';

View File

@@ -0,0 +1,82 @@
import type { IRoom } from '@rocket.chat/core-typings';
import {
Box,
Icon,
IconButton,
Option,
OptionAvatar,
OptionColumn,
OptionContent,
OptionMenu,
OptionSkeleton,
Tag,
} from '@rocket.chat/fuselage';
import { usePrefersReducedMotion } from '@rocket.chat/fuselage-hooks';
import { RoomAvatar } from '@rocket.chat/ui-avatar';
import { usePermission } from '@rocket.chat/ui-contexts';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import TeamsChannelItemMenu from './TeamsChannelItemMenu';
import { usePreventPropagation } from '../../../../hooks/usePreventPropagation';
import { roomCoordinator } from '../../../../lib/rooms/roomCoordinator';
export type TeamsChannelItemProps = {
room: IRoom;
mainRoom: IRoom;
onClickView: (room: IRoom) => void;
reload: () => void;
};
const TeamsChannelItem = ({ room, mainRoom, onClickView, reload }: TeamsChannelItemProps) => {
const { t } = useTranslation();
const rid = room._id;
const type = room.t;
const [showButton, setShowButton] = useState();
const canRemoveTeamChannel = usePermission('remove-team-channel', mainRoom._id);
const canEditTeamChannel = usePermission('edit-team-channel', mainRoom._id);
const canDeleteChannel = usePermission(`delete-${type}`, rid);
const canDeleteTeamChannel = usePermission(`delete-team-${type === 'c' ? 'channel' : 'group'}`, mainRoom._id);
const canDelete = canDeleteChannel && canDeleteTeamChannel;
const isReduceMotionEnabled = usePrefersReducedMotion();
const handleMenuEvent = {
[isReduceMotionEnabled ? 'onMouseEnter' : 'onTransitionEnd']: setShowButton,
};
const onClick = usePreventPropagation();
if (!room) {
return <OptionSkeleton />;
}
return (
<Option id={room._id} data-rid={room._id} {...handleMenuEvent} onClick={() => onClickView(room)}>
<OptionAvatar>
<RoomAvatar room={room} size='x28' />
</OptionAvatar>
<OptionColumn>{room.t === 'c' ? <Icon name='hash' size='x15' /> : <Icon name='hashtag-lock' size='x15' />}</OptionColumn>
<OptionContent>
<Box display='inline-flex' alignItems='center'>
{roomCoordinator.getRoomName(room.t, room)}{' '}
{room.teamDefault ? (
<Box marginInline={4}>
<Tag>{t('Team_Auto-join')}</Tag>
</Box>
) : (
''
)}
</Box>
</OptionContent>
{(canRemoveTeamChannel || canEditTeamChannel || canDelete) && (
<OptionMenu onClick={onClick}>
{showButton ? <TeamsChannelItemMenu room={room} mainRoom={mainRoom} reload={reload} /> : <IconButton tiny icon='kebab' />}
</OptionMenu>
)}
</Option>
);
};
export default TeamsChannelItem;

View File

@@ -0,0 +1,58 @@
import type { IRoom } from '@rocket.chat/core-typings';
import { CheckBox } from '@rocket.chat/fuselage';
import { GenericMenu } from '@rocket.chat/ui-client';
import type { GenericMenuItemProps } from '@rocket.chat/ui-client';
import { useTranslation } from 'react-i18next';
import { useRemoveRoomFromTeam } from './hooks/useRemoveRoomFromTeam';
import { useToggleAutoJoin } from './hooks/useToggleAutoJoin';
import { useDeleteRoom } from '../../../hooks/roomActions/useDeleteRoom';
const TeamsChannelItemMenu = ({ room, mainRoom, reload }: { room: IRoom; mainRoom: IRoom; reload?: () => void }) => {
const { t } = useTranslation();
const { handleRemoveRoom, canRemoveTeamChannel } = useRemoveRoomFromTeam(room, { reload });
const { handleDelete, canDeleteRoom } = useDeleteRoom(room, { reload });
const { handleToggleAutoJoin, canEditTeamChannel } = useToggleAutoJoin(room, { reload, mainRoom });
const toggleAutoJoin = {
id: 'toggleAutoJoin',
icon: room.t === 'c' ? 'hash' : 'hashtag-lock',
content: t('Team_Auto-join'),
onClick: handleToggleAutoJoin,
addon: <CheckBox checked={room.teamDefault} />,
};
const removeRoom = {
id: 'removeRoom',
icon: 'cross',
content: t('Team_Remove_from_team'),
onClick: handleRemoveRoom,
variant: 'danger',
};
const deleteRoom = {
id: 'deleteRoom',
icon: 'trash',
content: t('Delete'),
onClick: handleDelete,
variant: 'danger',
};
return (
<GenericMenu
title={t('More')}
placement='bottom-end'
sections={[
{
title: '',
items: [canEditTeamChannel && toggleAutoJoin, canRemoveTeamChannel && removeRoom, canDeleteRoom && deleteRoom].filter(
Boolean,
) as GenericMenuItemProps[],
},
]}
/>
);
};
export default TeamsChannelItemMenu;

View File

@@ -0,0 +1,58 @@
/* eslint-disable testing-library/no-container */
/* eslint-disable testing-library/no-node-access */
import { mockAppRoot } from '@rocket.chat/mock-providers';
import { render, waitFor } from '@testing-library/react';
import TeamsChannels from './TeamsChannels';
import { createFakeRoom } from '../../../../../tests/mocks/data';
jest.mock('../../../../lib/rooms/roomCoordinator', () => ({
roomCoordinator: {
getRouteLink: () => undefined,
},
}));
const mainRoom = createFakeRoom({ name: 'Main Room' });
const fakeRooms = Array.from({ length: 10 }, (_, index) => createFakeRoom({ t: 'c', name: `Fake Room ${index}` }));
beforeEach(() => {
Object.defineProperty(window, 'getComputedStyle', {
value: () => {
return {
getPropertyPriority: () => undefined,
getPropertyValue: () => undefined,
};
},
});
});
// TODO: Replace this with the storybook & snapshot
it('should render scrollbars', async () => {
const { container } = render(
<TeamsChannels
text=''
type='all'
reload={() => undefined}
loadMoreItems={() => undefined}
setText={() => undefined}
setType={() => undefined}
onClickClose={() => undefined}
onClickAddExisting={() => undefined}
onClickView={() => undefined}
onClickCreateNew={() => undefined}
total={fakeRooms.length}
loading={false}
mainRoom={mainRoom}
channels={fakeRooms}
/>,
{
wrapper: mockAppRoot().build(),
},
);
await waitFor(() => {
expect(container.querySelector('[data-overlayscrollbars]')).toBeInTheDocument();
});
expect(container.querySelector('[data-overlayscrollbars]')).toBeInTheDocument();
});

View File

@@ -0,0 +1,156 @@
import type { IRoom } from '@rocket.chat/core-typings';
import type { SelectOption } from '@rocket.chat/fuselage';
import { Box, Icon, TextInput, Select, Throbber, ButtonGroup, Button } from '@rocket.chat/fuselage';
import { useStableCallback, useAutoFocus, useDebouncedCallback } from '@rocket.chat/fuselage-hooks';
import {
VirtualizedScrollbars,
ContextualbarHeader,
ContextualbarIcon,
ContextualbarTitle,
ContextualbarClose,
ContextualbarContent,
ContextualbarFooter,
ContextualbarEmptyContent,
ContextualbarSection,
ContextualbarDialog,
} from '@rocket.chat/ui-client';
import type { ChangeEvent, Dispatch, SetStateAction, SyntheticEvent } from 'react';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Virtuoso } from 'react-virtuoso';
import TeamsChannelItem from './TeamsChannelItem';
import InfiniteListAnchor from '../../../../components/InfiniteListAnchor';
export type TeamsChannelsProps = {
loading: boolean;
channels: IRoom[];
mainRoom: IRoom;
text: string;
type: 'all' | 'autoJoin';
setText: (e: ChangeEvent<HTMLInputElement>) => void;
setType: Dispatch<SetStateAction<'all' | 'autoJoin'>>;
onClickClose: () => void;
onClickAddExisting: false | ((e: SyntheticEvent) => void);
onClickCreateNew: false | ((e: SyntheticEvent) => void);
total: number;
loadMoreItems: () => void;
onClickView: (room: IRoom) => void;
reload: () => void;
};
const TeamsChannels = ({
loading,
channels = [],
mainRoom,
text,
type,
setText,
setType,
onClickClose,
onClickAddExisting,
onClickCreateNew,
total,
loadMoreItems,
onClickView,
reload,
}: TeamsChannelsProps) => {
const { t } = useTranslation();
const inputRef = useAutoFocus<HTMLInputElement>(true);
const options: SelectOption[] = useMemo(
() => [
['all', t('All')],
['autoJoin', t('Team_Auto-join')],
],
[t],
);
const lm = useStableCallback(() => !loading && loadMoreItems());
const loadMoreChannels = useDebouncedCallback(
() => {
if (channels.length >= total) {
return;
}
lm();
},
300,
[lm, channels],
);
return (
<ContextualbarDialog>
<ContextualbarHeader>
<ContextualbarIcon name='hash' />
<ContextualbarTitle>{t('Team_Channels')}</ContextualbarTitle>
{onClickClose && <ContextualbarClose onClick={onClickClose} />}
</ContextualbarHeader>
<ContextualbarSection>
<TextInput
placeholder={t('Search')}
value={text}
ref={inputRef}
onChange={setText}
endAddon={<Icon name='magnifier' size='x20' />}
/>
<Box width='x144' marginInlineStart={8}>
<Select onChange={(val) => setType(val as 'all' | 'autoJoin')} value={type} options={options} />
</Box>
</ContextualbarSection>
<ContextualbarContent padding={12}>
{loading && (
<Box paddingInline={24} paddingBlock={12}>
<Throbber size='x12' />
</Box>
)}
{!loading && channels.length === 0 && <ContextualbarEmptyContent title={t('No_channels_in_team')} />}
{!loading && channels.length > 0 && (
<>
<Box paddingInline={18} paddingBlock={12}>
<Box is='span' color='hint' fontScale='p2'>
{t('Showing')}: {channels.length}
</Box>
<Box is='span' color='hint' fontScale='p2' marginInlineStart={8}>
{t('Total')}: {total}
</Box>
</Box>
<Box width='full' height='full' role='list' overflow='hidden' flexShrink={1}>
<VirtualizedScrollbars>
<Virtuoso
totalCount={total}
data={channels}
// eslint-disable-next-line react/no-multi-comp
components={{ Footer: () => <InfiniteListAnchor loadMore={loadMoreChannels} /> }}
itemContent={(index, data) => (
<TeamsChannelItem onClickView={onClickView} room={data} mainRoom={mainRoom} reload={reload} key={index} />
)}
/>
</VirtualizedScrollbars>
</Box>
</>
)}
</ContextualbarContent>
{(onClickAddExisting || onClickCreateNew) && (
<ContextualbarFooter>
<ButtonGroup stretch>
{onClickAddExisting && (
<Button onClick={onClickAddExisting} width='50%'>
{t('Team_Add_existing')}
</Button>
)}
{onClickCreateNew && (
<Button onClick={onClickCreateNew} width='50%'>
{t('Create_new')}
</Button>
)}
</ButtonGroup>
</ContextualbarFooter>
)}
</ContextualbarDialog>
);
};
export default TeamsChannels;

View File

@@ -0,0 +1,69 @@
import type { IRoom } from '@rocket.chat/core-typings';
import { useLocalStorage, useDebouncedValue, useStableCallback } from '@rocket.chat/fuselage-hooks';
import { useSetModal, usePermission, useAtLeastOnePermission, useRoomToolbox } from '@rocket.chat/ui-contexts';
import type { ChangeEvent } from 'react';
import { useCallback, useState } from 'react';
import AddExistingModal from './AddExistingModal';
import TeamsChannels from './TeamsChannels';
import { useTeamsChannelList } from './hooks/useTeamsChannelList';
import { roomCoordinator } from '../../../../lib/rooms/roomCoordinator';
import CreateChannelModal from '../../../../navbar/NavBarPagesGroup/actions/CreateChannelModal';
import { useRoom } from '../../../room/contexts/RoomContext';
const TeamsChannelsWithData = () => {
const room = useRoom();
const setModal = useSetModal();
const { closeTab } = useRoomToolbox();
const canAddExistingRoomToTeam = usePermission('move-room-to-team', room._id);
const canCreateRoomInTeam = useAtLeastOnePermission(['create-team-channel', 'create-team-group'], room._id);
const { teamId } = room;
if (!teamId) {
throw new Error('Invalid teamId');
}
const [type, setType] = useLocalStorage<'all' | 'autoJoin'>('channels-list-type', 'all');
const [text, setText] = useState('');
const debouncedText = useDebouncedValue(text, 800);
const { isPending, data, fetchNextPage, refetch } = useTeamsChannelList({ teamId, text: debouncedText, type });
const handleTextChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
setText(event.currentTarget.value);
}, []);
const handleAddExisting = useStableCallback(() => {
setModal(<AddExistingModal teamId={teamId} onClose={() => setModal(null)} reload={refetch} />);
});
const handleCreateNew = useStableCallback(() => {
setModal(<CreateChannelModal teamId={teamId} mainRoom={room} onClose={() => setModal(null)} reload={refetch} />);
});
const goToRoom = useStableCallback((room: IRoom) => {
roomCoordinator.openRouteLink(room.t, room);
});
return (
<TeamsChannels
loading={isPending}
mainRoom={room}
type={type}
text={text}
setType={setType}
setText={handleTextChange}
channels={data?.channels ?? []}
total={data?.total ?? 0}
onClickClose={closeTab}
onClickAddExisting={canAddExistingRoomToTeam && handleAddExisting}
onClickCreateNew={canCreateRoomInTeam && handleCreateNew}
onClickView={goToRoom}
loadMoreItems={fetchNextPage}
reload={refetch}
/>
);
};
export default TeamsChannelsWithData;

View File

@@ -0,0 +1,43 @@
import type { IRoom } from '@rocket.chat/core-typings';
import { GenericModal } from '@rocket.chat/ui-client';
import { useEndpoint, usePermission, useSetModal, useToastMessageDispatch } from '@rocket.chat/ui-contexts';
import { useTranslation } from 'react-i18next';
import { roomCoordinator } from '../../../../../lib/rooms/roomCoordinator';
export const useRemoveRoomFromTeam = (room: IRoom, { reload }: { reload?: () => void }) => {
const { t } = useTranslation();
const setModal = useSetModal();
const dispatchToastMessage = useToastMessageDispatch();
const canRemoveTeamChannel = usePermission('remove-team-channel', room._id);
const removeRoomEndpoint = useEndpoint('POST', '/v1/teams.removeRoom');
const handleRemoveRoom = () => {
const onConfirmAction = async () => {
if (!room.teamId) {
return;
}
try {
await removeRoomEndpoint({ teamId: room.teamId, roomId: room._id });
dispatchToastMessage({ type: 'error', message: t('Room_has_been_removed') });
reload?.();
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
} finally {
setModal(null);
}
};
return setModal(
<GenericModal variant='danger' onCancel={() => setModal(null)} onConfirm={onConfirmAction} confirmText={t('Remove')}>
{t('Team_Remove_from_team_modal_content', {
teamName: roomCoordinator.getRoomName(room.t, room),
})}
</GenericModal>,
);
};
return { handleRemoveRoom, canRemoveTeamChannel };
};

View File

@@ -0,0 +1,57 @@
import type { IRoom } from '@rocket.chat/core-typings';
import { useEndpoint } from '@rocket.chat/ui-contexts';
import { useInfiniteQuery } from '@tanstack/react-query';
import { teamsQueryKeys } from '../../../../../lib/queryKeys';
import { getConfig } from '../../../../../lib/utils/getConfig';
import { mapMessageFromApi } from '../../../../../lib/utils/mapMessageFromApi';
type TeamsChannelListOptions = {
teamId: string;
type: 'all' | 'autoJoin';
text: string;
};
export const useTeamsChannelList = ({ teamId, type, text }: TeamsChannelListOptions) => {
const listTeamRooms = useEndpoint('GET', '/v1/teams.listRooms');
const count = parseInt(`${getConfig('teamsChannelListSize', 10)}`, 10);
return useInfiniteQuery({
queryKey: teamsQueryKeys.listChannels(teamId, { type, text }),
queryFn: async ({ pageParam: offset }) => {
const { rooms, total } = await listTeamRooms({
teamId,
offset,
count,
filter: text,
type,
});
return {
items: rooms.map(({ _updatedAt, lastMessage, lm, ts, webRtcCallStartTime, usersWaitingForE2EKeys, ...room }): IRoom => ({
...(lm && { lm: new Date(lm) }),
...(ts && { ts: new Date(ts) }),
_updatedAt: new Date(_updatedAt),
...(lastMessage && { lastMessage: mapMessageFromApi(lastMessage) }),
...(webRtcCallStartTime && { webRtcCallStartTime: new Date(webRtcCallStartTime) }),
...(usersWaitingForE2EKeys && {
usersWaitingForE2EKeys: usersWaitingForE2EKeys?.map(({ userId, ts }) => ({ userId, ts: new Date(ts) })),
}),
...room,
})),
itemCount: total,
};
},
initialPageParam: 0,
getNextPageParam: (lastPage, _, lastOffset) => {
const nextOffset = lastOffset + count;
if (nextOffset >= lastPage.itemCount) return undefined;
return nextOffset;
},
select: ({ pages }) => ({
channels: pages.flatMap((page) => page.items),
total: pages.at(-1)?.itemCount,
}),
});
};

View File

@@ -0,0 +1,46 @@
import type { IRoom } from '@rocket.chat/core-typings';
import { useEndpoint, usePermission, useSetting, useToastMessageDispatch } from '@rocket.chat/ui-contexts';
import { t } from 'i18next';
import { roomCoordinator } from '../../../../../lib/rooms/roomCoordinator';
export const useToggleAutoJoin = (room: IRoom, { reload, mainRoom }: { reload?: () => void; mainRoom: IRoom }) => {
const dispatchToastMessage = useToastMessageDispatch();
const updateRoomEndpoint = useEndpoint('POST', '/v1/teams.updateRoom');
const canEditTeamChannel = usePermission('edit-team-channel', room._id);
const maxNumberOfAutoJoinMembers = useSetting('API_User_Limit', 500);
const handleToggleAutoJoin = async () => {
// Sanity check, the setting has a default value, therefore it should always be defined
if (!maxNumberOfAutoJoinMembers) {
return;
}
try {
const { room: updatedRoom } = await updateRoomEndpoint({
roomId: room._id,
isDefault: !room.teamDefault,
});
if (updatedRoom.teamDefault) {
// If the number of members in the mainRoom (the team) is greater than the limit, show an info message
// informing that not all members will be auto-joined to the channel
const messageType = mainRoom.usersCount > maxNumberOfAutoJoinMembers ? 'info' : 'success';
const message = mainRoom.usersCount > maxNumberOfAutoJoinMembers ? 'Team_Auto-join_exceeded_user_limit' : 'Team_Auto-join_updated';
dispatchToastMessage({
type: messageType,
message: t(message, {
channelName: roomCoordinator.getRoomName(room.t, room),
numberOfMembers: updatedRoom.usersCount,
limit: maxNumberOfAutoJoinMembers,
}),
});
}
reload?.();
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
}
};
return { handleToggleAutoJoin, canEditTeamChannel };
};

View File

@@ -0,0 +1 @@
export { default } from './TeamsChannelsWithData';