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,48 @@
import { isRoomFederated } from '@rocket.chat/core-typings';
import type { IRoom } from '@rocket.chat/core-typings';
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
import { GenericModal } from '@rocket.chat/ui-client';
import { useSetModal, useToastMessageDispatch, useTranslation, useEndpoint, usePermission } from '@rocket.chat/ui-contexts';
import { useCanEditRoom } from '../useCanEditRoom';
export const useRoomConvertToTeam = (room: IRoom) => {
const t = useTranslation();
const setModal = useSetModal();
const dispatchToastMessage = useToastMessageDispatch();
const canEdit = useCanEditRoom(room);
const hasPermissionToConvertRoomToTeam = usePermission('create-team');
const canConvertRoomToTeam = isRoomFederated(room) ? false : hasPermissionToConvertRoomToTeam;
const allowConvertToTeam = !room.teamId && !room.prid && canConvertRoomToTeam && canEdit;
const convertRoomToTeam = useEndpoint('POST', room.t === 'c' ? '/v1/channels.convertToTeam' : '/v1/groups.convertToTeam');
const handleConvertToTeam = useStableCallback(async () => {
const onConfirm = async () => {
try {
await convertRoomToTeam(room.t === 'c' ? { channelId: room._id } : { roomId: room._id });
dispatchToastMessage({ type: 'success', message: t('Room_has_been_converted') });
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
} finally {
setModal(null);
}
};
setModal(
<GenericModal
title={t('Confirmation')}
variant='warning'
onClose={() => setModal(null)}
onCancel={() => setModal(null)}
onConfirm={onConfirm}
confirmText={t('Convert')}
>
{t('Converting_channel_to_a_team')}
</GenericModal>,
);
});
return allowConvertToTeam ? handleConvertToTeam : null;
};

View File

@@ -0,0 +1,39 @@
import { mockAppRoot } from '@rocket.chat/mock-providers';
import { renderHook } from '@testing-library/react';
import { useRoomLeave } from './useRoomLeave';
import { createFakeRoom, createFakeSubscription } from '../../../../../../../tests/mocks/data';
const mockRoom = createFakeRoom({ _id: 'room1', t: 'c', name: 'room1', fname: 'Room 1' });
const mockSubscription = createFakeSubscription({ name: 'room1', t: 'c', disableNotifications: false, rid: 'room1' });
jest.mock('../../../../../../../client/lib/LegacyRoomManager', () => ({
LegacyRoomManager: {
close: jest.fn(),
},
}));
jest.mock('../../../../../../../client/lib/rooms/roomCoordinator', () => ({
roomCoordinator: {
getRoomDirectives: () => ({
getUiText: () => 'leaveWarning',
}),
},
}));
it('should return leave function if user has subscription', () => {
const wrapper = mockAppRoot()
.withPermission('leave-c')
.withSubscription({ ...mockSubscription, rid: 'room1' })
.build();
const { result } = renderHook(() => useRoomLeave(mockRoom), { wrapper });
expect(typeof result.current).toBe('function');
});
it('should return null if user does not have subscription', () => {
const wrapper = mockAppRoot().withPermission('leave-c').build();
const { result } = renderHook(() => useRoomLeave(mockRoom), { wrapper });
expect(result.current).toBeNull();
});

View File

@@ -0,0 +1,59 @@
import type { IRoom } from '@rocket.chat/core-typings';
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
import type { TranslationKey } from '@rocket.chat/ui-contexts';
import { useRouter, useSetModal, useToastMessageDispatch, useEndpoint, usePermission, useUserSubscription } from '@rocket.chat/ui-contexts';
import { useTranslation } from 'react-i18next';
import { UiTextContext } from '../../../../../../../definition/IRoomTypeConfig';
import WarningModal from '../../../../../../components/WarningModal';
import { LegacyRoomManager } from '../../../../../../lib/LegacyRoomManager';
import { roomCoordinator } from '../../../../../../lib/rooms/roomCoordinator';
export const useRoomLeave = (room: IRoom) => {
const { t } = useTranslation();
const subscription = useUserSubscription(room._id);
const setModal = useSetModal();
const dispatchToastMessage = useToastMessageDispatch();
const leaveChannel = useEndpoint('POST', '/v1/channels.leave');
const leaveGroup = useEndpoint('POST', '/v1/groups.leave');
const leaveDirect = useEndpoint('POST', '/v1/im.leave');
const router = useRouter();
const canLeave = usePermission(room.t === 'c' ? 'leave-c' : 'leave-p') && room.cl !== false && Boolean(subscription);
const handleLeave = useStableCallback(() => {
const leaveAction = async () => {
try {
if (room.t === 'c') {
await leaveChannel({ roomId: room._id });
} else if (room.t === 'p') {
await leaveGroup({ roomId: room._id });
} else if (room.t === 'd') {
await leaveDirect({ roomId: room._id });
}
router.navigate('/home');
if (room.name) {
LegacyRoomManager.close(`${room.t}${room.name}`);
}
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
}
setModal(null);
};
const warnText = roomCoordinator.getRoomDirectives(room.t).getUiText(UiTextContext.LEAVE_WARNING);
setModal(
<WarningModal
text={t(warnText as TranslationKey, { roomName: room.fname || room.name })}
confirmText={t('Leave_room')}
close={() => setModal(null)}
cancelText={t('Cancel')}
confirm={leaveAction}
/>,
);
});
return canLeave ? handleLeave : null;
};

View File

@@ -0,0 +1,39 @@
import { isRoomFederated } from '@rocket.chat/core-typings';
import type { IRoom } from '@rocket.chat/core-typings';
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
import { useSetModal, useToastMessageDispatch, useTranslation, useEndpoint } from '@rocket.chat/ui-contexts';
import ChannelToTeamModal from '../../ChannelToTeamModal';
import { useCanEditRoom } from '../useCanEditRoom';
export const useRoomMoveToTeam = (room: IRoom) => {
const t = useTranslation();
const setModal = useSetModal();
const dispatchToastMessage = useToastMessageDispatch();
const canEdit = useCanEditRoom(room);
const canMoveToTeam = !isRoomFederated(room) && !room.teamId && !room.prid && canEdit;
const moveChannelToTeam = useEndpoint('POST', '/v1/teams.addRooms');
const handleMoveToTeam = useStableCallback(async () => {
const onConfirm = async (teamId: IRoom['teamId']) => {
if (!teamId) {
throw new Error('teamId not provided');
}
try {
await moveChannelToTeam({ rooms: [room._id], teamId });
dispatchToastMessage({ type: 'success', message: t('Rooms_added_successfully') });
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
} finally {
setModal(null);
}
};
setModal(<ChannelToTeamModal onCancel={() => setModal(null)} onConfirm={onConfirm} />);
});
return canMoveToTeam ? handleMoveToTeam : null;
};

View File

@@ -0,0 +1,18 @@
import type { IRoom } from '@rocket.chat/core-typings';
import { isRoomFederated } from '@rocket.chat/core-typings';
import { usePermission, useUser } from '@rocket.chat/ui-contexts';
import * as Federation from '../../../../../lib/federation/Federation';
import { useRoomSubscription } from '../../../contexts/RoomContext';
export const useCanEditRoom = (room: IRoom) => {
const user = useUser();
const subscription = useRoomSubscription();
const hasPermissionToEdit = usePermission('edit-room', room._id);
const isFederated = isRoomFederated(room);
const canEdit =
user && isFederated ? Federation.isEditableByTheUser(user, room, subscription) && hasPermissionToEdit : hasPermissionToEdit;
return canEdit;
};

View File

@@ -0,0 +1,104 @@
import type { IRoom } from '@rocket.chat/core-typings';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useRoomConvertToTeam } from './actions/useRoomConvertToTeam';
import { useRoomLeave } from './actions/useRoomLeave';
import { useRoomMoveToTeam } from './actions/useRoomMoveToTeam';
import { useHideRoomAction } from '../../../../../hooks/useHideRoomAction';
import { useDeleteRoom } from '../../../../hooks/roomActions/useDeleteRoom';
type UseRoomActionsOptions = {
onClickEnterRoom?: () => void;
onClickEdit?: () => void;
resetState?: () => void;
};
export const useRoomActions = (room: IRoom, options: UseRoomActionsOptions) => {
const { onClickEnterRoom, onClickEdit, resetState } = options;
const { t } = useTranslation();
const handleLeave = useRoomLeave(room);
const { handleDelete, canDeleteRoom } = useDeleteRoom(room, { reload: resetState });
const handleMoveToTeam = useRoomMoveToTeam(room);
const handleConvertToTeam = useRoomConvertToTeam(room);
const handleHide = useHideRoomAction({ rid: room._id, type: room.t, name: room.name ?? '' });
return useMemo(() => {
const memoizedActions = {
items: [
{
id: 'hide',
content: t('Hide'),
icon: 'eye-off' as const,
onClick: handleHide,
},
...(onClickEnterRoom
? [
{
id: 'enter',
content: t('Enter'),
icon: 'login' as const,
onClick: onClickEnterRoom,
},
]
: []),
...(onClickEdit
? [
{
id: 'edit',
content: t('Edit'),
icon: 'edit' as const,
onClick: onClickEdit,
},
]
: []),
...(handleLeave
? [
{
id: 'leave',
content: t('Leave'),
icon: 'sign-out' as const,
onClick: handleLeave,
},
]
: []),
...(handleMoveToTeam
? [
{
id: 'move_channel_team',
content: t('Teams_move_channel_to_team'),
icon: 'team-arrow-right' as const,
onClick: handleMoveToTeam,
},
]
: []),
...(handleConvertToTeam
? [
{
id: 'convert_channel_team',
content: t('Teams_convert_channel_to_team'),
icon: 'team' as const,
onClick: handleConvertToTeam,
},
]
: []),
...(canDeleteRoom
? [
{
id: 'delete',
content: t('Delete'),
icon: 'trash' as const,
onClick: handleDelete,
variant: 'danger',
},
]
: []),
],
};
return memoizedActions;
}, [canDeleteRoom, handleConvertToTeam, handleDelete, handleHide, handleLeave, handleMoveToTeam, onClickEdit, onClickEnterRoom, t]);
};

View File

@@ -0,0 +1,28 @@
import type { RoomInfoActionsProps } from '../RoomInfo/RoomInfoActions';
type UseSplitRoomActionsOptions = {
size?: number;
};
/**
*
* @param room
* @param options
* @returns If more than two room actions are enabled `menu.regular` will be a non-empty array
*/
export const useSplitRoomActions = (actions: RoomInfoActionsProps['actions'], options?: UseSplitRoomActionsOptions) => {
const size = options?.size || 2;
if (actions.items.length <= size) {
return { buttons: actions };
}
const buttons = { items: actions.items.slice(0, size) };
const regular = actions.items.slice(size);
const firstDanger = regular.findIndex((item) => item.variant);
const danger = regular.splice(firstDanger);
const menu = [{ items: regular }, { items: danger }];
return { buttons, menu };
};