The communications platform that puts data protection first.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
Rocket.Chat/client/views/room/contextualBar/Info/EditRoomInfo/EditChannel.js

582 lines
16 KiB

import {
Field,
TextInput,
PasswordInput,
ToggleSwitch,
MultiSelect,
Accordion,
Callout,
NumberInput,
FieldGroup,
Button,
ButtonGroup,
Box,
Icon,
TextAreaInput,
} from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import React, { useCallback, useMemo, useRef } from 'react';
import { e2e } from '../../../../../../app/e2e/client/rocketchat.e2e';
import { MessageTypesValues } from '../../../../../../app/lib/lib/MessageTypes';
import { roomTypes, RoomSettingsEnum } from '../../../../../../app/utils/client';
import DeleteChannelWarning from '../../../../../components/DeleteChannelWarning';
import RawText from '../../../../../components/RawText';
import VerticalBar from '../../../../../components/VerticalBar';
import RoomAvatarEditor from '../../../../../components/avatar/RoomAvatarEditor';
import {
usePermission,
useAtLeastOnePermission,
useRole,
} from '../../../../../contexts/AuthorizationContext';
import { useSetModal } from '../../../../../contexts/ModalContext';
import { useMethod } from '../../../../../contexts/ServerContext';
import { useSetting } from '../../../../../contexts/SettingsContext';
import { useTranslation } from '../../../../../contexts/TranslationContext';
import { useEndpointActionExperimental } from '../../../../../hooks/useEndpointAction';
import { useForm } from '../../../../../hooks/useForm';
const typeMap = {
c: 'Channels',
p: 'Groups',
d: 'DMs',
};
const useInitialValues = (room, settings) => {
const {
t,
ro,
archived,
topic,
description,
announcement,
joinCodeRequired,
sysMes,
encrypted,
retention = {},
} = room;
const { retentionPolicyEnabled, maxAgeDefault } = settings;
const retentionEnabledDefault = useSetting(`RetentionPolicy_AppliesTo${typeMap[room.t]}`);
const excludePinnedDefault = useSetting('RetentionPolicy_DoNotPrunePinned');
const filesOnlyDefault = useSetting('RetentionPolicy_FilesOnly');
return useMemo(
() => ({
roomName:
t === 'd' ? room.usernames.join(' x ') : roomTypes.getRoomName(t, { type: t, ...room }),
roomType: t,
readOnly: !!ro,
reactWhenReadOnly: false,
archived: !!archived,
roomTopic: topic ?? '',
roomDescription: description ?? '',
roomAnnouncement: announcement ?? '',
roomAvatar: undefined,
joinCode: '',
joinCodeRequired: !!joinCodeRequired,
systemMessages: Array.isArray(sysMes) ? sysMes : [],
hideSysMes: !!sysMes?.length,
encrypted,
...(retentionPolicyEnabled && {
retentionEnabled: retention.enabled ?? retentionEnabledDefault,
retentionOverrideGlobal: !!retention.overrideGlobal,
retentionMaxAge: Math.min(retention.maxAge, maxAgeDefault) || maxAgeDefault,
retentionExcludePinned: retention.excludePinned ?? excludePinnedDefault,
retentionFilesOnly: retention.filesOnly ?? filesOnlyDefault,
}),
}),
[
announcement,
archived,
description,
excludePinnedDefault,
filesOnlyDefault,
joinCodeRequired,
maxAgeDefault,
retention.enabled,
retention.excludePinned,
retention.filesOnly,
retention.maxAge,
retention.overrideGlobal,
retentionEnabledDefault,
retentionPolicyEnabled,
ro,
room,
sysMes,
t,
topic,
encrypted,
],
);
};
const getCanChangeType = (room, canCreateChannel, canCreateGroup, isAdmin) =>
(!room.default || isAdmin) &&
((room.t === 'p' && canCreateChannel) || (room.t === 'c' && canCreateGroup));
function EditChannel({ room, onClickClose, onClickBack }) {
const t = useTranslation();
const setModal = useSetModal();
const retentionPolicyEnabled = useSetting('RetentionPolicy_Enabled');
const maxAgeDefault = useSetting(`RetentionPolicy_MaxAge_${typeMap[room.t]}`) || 30;
const saveData = useRef({});
const onChange = useCallback(({ initialValue, value, key }) => {
const { current } = saveData;
if (JSON.stringify(initialValue) !== JSON.stringify(value)) {
if (key === 'systemMessages' && value?.length > 0) {
current.hideSysMes = true;
}
current[key] = value;
} else {
delete current[key];
}
}, []);
const { values, handlers, hasUnsavedChanges, reset, commit } = useForm(
useInitialValues(room, { retentionPolicyEnabled, maxAgeDefault }),
onChange,
);
const sysMesOptions = useMemo(
() => MessageTypesValues.map(({ key, i18nLabel }) => [key, t(i18nLabel)]),
[t],
);
const {
roomName,
roomType,
readOnly,
encrypted,
roomAvatar,
archived,
roomTopic,
roomDescription,
roomAnnouncement,
reactWhenReadOnly,
joinCode,
joinCodeRequired,
systemMessages,
hideSysMes,
retentionEnabled,
retentionOverrideGlobal,
retentionMaxAge,
retentionExcludePinned,
retentionFilesOnly,
} = values;
const {
handleJoinCode,
handleJoinCodeRequired,
handleSystemMessages,
handleEncrypted,
handleHideSysMes,
handleRoomName,
handleReadOnly,
handleArchived,
handleRoomAvatar,
handleReactWhenReadOnly,
handleRoomType,
handleRoomTopic,
handleRoomDescription,
handleRoomAnnouncement,
handleRetentionEnabled,
handleRetentionOverrideGlobal,
handleRetentionMaxAge,
handleRetentionExcludePinned,
handleRetentionFilesOnly,
} = handlers;
const [
canViewName,
canViewTopic,
canViewAnnouncement,
canViewArchived,
canViewDescription,
canViewType,
canViewReadOnly,
canViewHideSysMes,
canViewJoinCode,
canViewReactWhenReadOnly,
canViewEncrypted,
] = useMemo(() => {
const isAllowed = roomTypes.getConfig(room.t)?.allowRoomSettingChange || (() => {});
return [
isAllowed(room, RoomSettingsEnum.NAME),
isAllowed(room, RoomSettingsEnum.TOPIC),
isAllowed(room, RoomSettingsEnum.ANNOUNCEMENT),
isAllowed(room, RoomSettingsEnum.ARCHIVE_OR_UNARCHIVE),
isAllowed(room, RoomSettingsEnum.DESCRIPTION),
isAllowed(room, RoomSettingsEnum.TYPE),
isAllowed(room, RoomSettingsEnum.READ_ONLY),
isAllowed(room, RoomSettingsEnum.SYSTEM_MESSAGES),
isAllowed(room, RoomSettingsEnum.JOIN_CODE),
isAllowed(room, RoomSettingsEnum.REACT_WHEN_READ_ONLY),
isAllowed(room, RoomSettingsEnum.E2E),
];
}, [room]);
const isAdmin = useRole('admin');
const canCreateChannel = usePermission('create-c');
const canCreateGroup = usePermission('create-p');
const canChangeType = getCanChangeType(room, canCreateChannel, canCreateGroup, isAdmin);
const canSetRo = usePermission('set-readonly', room._id);
const canSetReactWhenRo = usePermission('set-react-when-readonly', room._id);
const canEditRoomRetentionPolicy = usePermission('edit-room-retention-policy', room._id);
const canArchiveOrUnarchive = useAtLeastOnePermission(
useMemo(() => ['archive-room', 'unarchive-room'], []),
room._id,
);
const canDelete = usePermission(`delete-${room.t}`);
const canToggleEncryption =
usePermission('toggle-room-e2e-encryption', room._id) && (room.encrypted || e2e.isReady());
const changeArchivation = archived !== !!room.archived;
const archiveSelector = room.archived ? 'unarchive' : 'archive';
const archiveMessage = room.archived ? 'Room_has_been_unarchived' : 'Room_has_been_archived';
const saveAction = useEndpointActionExperimental(
'POST',
'rooms.saveRoomSettings',
t('Room_updated_successfully'),
);
const archiveAction = useEndpointActionExperimental(
'POST',
'rooms.changeArchivationState',
t(archiveMessage),
);
const handleSave = useMutableCallback(async () => {
const { joinCodeRequired, hideSysMes, ...data } = saveData.current;
delete data.archived;
const save = () =>
saveAction({
rid: room._id,
...data,
...(joinCode && { joinCode: joinCodeRequired ? joinCode : '' }),
...((data.systemMessages || !hideSysMes) && {
systemMessages: hideSysMes ? systemMessages : [],
}),
});
const archive = () => archiveAction({ rid: room._id, action: archiveSelector });
await Promise.all(
[hasUnsavedChanges && save(), changeArchivation && archive()].filter(Boolean),
);
saveData.current = {};
commit();
});
const deleteRoom = useMethod('eraseRoom');
const handleDelete = useMutableCallback(() => {
const onCancel = () => setModal(undefined);
const onConfirm = async () => {
await deleteRoom(room._id);
onCancel();
};
setModal(<DeleteChannelWarning onConfirm={onConfirm} onCancel={onCancel} />);
});
const changeRoomType = useMutableCallback(() => {
handleRoomType(roomType === 'p' ? 'c' : 'p');
});
const onChangeMaxAge = useMutableCallback((e) => {
handleRetentionMaxAge(Math.max(1, Number(e.currentTarget.value)));
});
return (
<>
<VerticalBar.Header>
{onClickBack && <VerticalBar.Back onClick={onClickBack} />}
<VerticalBar.Text>{t('edit-room')}</VerticalBar.Text>
{onClickClose && <VerticalBar.Close onClick={onClickClose} />}
</VerticalBar.Header>
<VerticalBar.ScrollableContent
p='x24'
is='form'
onSubmit={useMutableCallback((e) => e.preventDefault())}
>
<Box display='flex' justifyContent='center'>
<RoomAvatarEditor room={room} roomAvatar={roomAvatar} onChangeAvatar={handleRoomAvatar} />
</Box>
<Field>
<Field.Label>{t('Name')}</Field.Label>
<Field.Row>
<TextInput
disabled={!canViewName}
value={roomName}
onChange={handleRoomName}
flexGrow={1}
/>
</Field.Row>
</Field>
{canViewDescription && (
<Field>
<Field.Label>{t('Description')}</Field.Label>
<Field.Row>
<TextAreaInput
rows={4}
value={roomDescription}
onChange={handleRoomDescription}
flexGrow={1}
/>
</Field.Row>
</Field>
)}
{canViewAnnouncement && (
<Field>
<Field.Label>{t('Announcement')}</Field.Label>
<Field.Row>
<TextAreaInput
rows={4}
value={roomAnnouncement}
onChange={handleRoomAnnouncement}
flexGrow={1}
/>
</Field.Row>
</Field>
)}
{canViewTopic && (
<Field>
<Field.Label>{t('Topic')}</Field.Label>
<Field.Row>
<TextAreaInput rows={4} value={roomTopic} onChange={handleRoomTopic} flexGrow={1} />
</Field.Row>
</Field>
)}
{canViewType && (
<Field>
<Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}>
<Field.Label>{t('Private')}</Field.Label>
<Field.Row>
<ToggleSwitch
disabled={!canChangeType}
checked={roomType === 'p'}
onChange={changeRoomType}
/>
</Field.Row>
</Box>
<Field.Hint>{t('Teams_New_Private_Description_Enabled')}</Field.Hint>
</Field>
)}
{canViewReadOnly && (
<Field>
<Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}>
<Field.Label>{t('Read_only')}</Field.Label>
<Field.Row>
<ToggleSwitch disabled={!canSetRo} checked={readOnly} onChange={handleReadOnly} />
</Field.Row>
</Box>
<Field.Hint>{t('Only_authorized_users_can_write_new_messages')}</Field.Hint>
</Field>
)}
{canViewReactWhenReadOnly && (
<Field>
<Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}>
<Field.Label>{t('React_when_read_only')}</Field.Label>
<Field.Row>
<ToggleSwitch
disabled={!canSetReactWhenRo}
checked={reactWhenReadOnly}
onChange={handleReactWhenReadOnly}
/>
</Field.Row>
</Box>
<Field.Hint>{t('Only_authorized_users_can_write_new_messages')}</Field.Hint>
</Field>
)}
{canViewArchived && (
<Field>
<Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}>
<Field.Label>{t('Room_archivation_state_true')}</Field.Label>
<Field.Row>
<ToggleSwitch
disabled={!canArchiveOrUnarchive}
checked={archived}
onChange={handleArchived}
/>
</Field.Row>
</Box>
</Field>
)}
{canViewJoinCode && (
<Field>
<Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}>
<Field.Label>{t('Password_to_access')}</Field.Label>
<Field.Row>
<ToggleSwitch checked={joinCodeRequired} onChange={handleJoinCodeRequired} />
</Field.Row>
</Box>
<Field.Row>
<PasswordInput
disabled={!joinCodeRequired}
value={joinCode}
onChange={handleJoinCode}
placeholder={t('Reset_password')}
flexGrow={1}
/>
</Field.Row>
</Field>
)}
{canViewHideSysMes && (
<Field>
<Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}>
<Field.Label>{t('Hide_System_Messages')}</Field.Label>
<Field.Row>
<ToggleSwitch checked={hideSysMes} onChange={handleHideSysMes} />
</Field.Row>
</Box>
<Field.Row>
<MultiSelect
maxWidth='100%'
options={sysMesOptions}
disabled={!hideSysMes}
value={systemMessages}
onChange={handleSystemMessages}
placeholder={t('Select_an_option')}
flexGrow={1}
/>
</Field.Row>
</Field>
)}
{canViewEncrypted && (
<Field>
<Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}>
<Field.Label>{t('Encrypted')}</Field.Label>
<Field.Row>
<ToggleSwitch
disabled={!canToggleEncryption}
checked={encrypted}
onChange={handleEncrypted}
/>
</Field.Row>
</Box>
</Field>
)}
{retentionPolicyEnabled && (
<Accordion>
<Accordion.Item title={t('Prune')}>
<FieldGroup>
<Field>
<Box
display='flex'
flexDirection='row'
justifyContent='space-between'
flexGrow={1}
>
<Field.Label>{t('RetentionPolicyRoom_Enabled')}</Field.Label>
<Field.Row>
<ToggleSwitch checked={retentionEnabled} onChange={handleRetentionEnabled} />
</Field.Row>
</Box>
</Field>
<Field>
<Box
display='flex'
flexDirection='row'
justifyContent='space-between'
flexGrow={1}
>
<Field.Label>{t('RetentionPolicyRoom_OverrideGlobal')}</Field.Label>
<Field.Row>
<ToggleSwitch
disabled={!retentionEnabled || !canEditRoomRetentionPolicy}
checked={retentionOverrideGlobal}
onChange={handleRetentionOverrideGlobal}
/>
</Field.Row>
</Box>
</Field>
{retentionOverrideGlobal && (
<>
<Callout type='danger'>
<RawText>{t('RetentionPolicyRoom_ReadTheDocs')}</RawText>
</Callout>
<Field>
<Field.Label>
{t('RetentionPolicyRoom_MaxAge', { max: maxAgeDefault })}
</Field.Label>
<Field.Row>
<NumberInput
value={retentionMaxAge}
onChange={onChangeMaxAge}
flexGrow={1}
/>
</Field.Row>
</Field>
<Field>
<Box
display='flex'
flexDirection='row'
justifyContent='space-between'
flexGrow={1}
>
<Field.Label>{t('RetentionPolicyRoom_ExcludePinned')}</Field.Label>
<Field.Row>
<ToggleSwitch
checked={retentionExcludePinned}
onChange={handleRetentionExcludePinned}
/>
</Field.Row>
</Box>
</Field>
<Field>
<Box
display='flex'
flexDirection='row'
justifyContent='space-between'
flexGrow={1}
>
<Field.Label>{t('RetentionPolicyRoom_FilesOnly')}</Field.Label>
<Field.Row>
<ToggleSwitch
checked={retentionFilesOnly}
onChange={handleRetentionFilesOnly}
/>
</Field.Row>
</Box>
</Field>
</>
)}
</FieldGroup>
</Accordion.Item>
</Accordion>
)}
<Field>
<Field.Row>
<Box display='flex' flexDirection='row' justifyContent='space-between' w='full'>
<ButtonGroup stretch flexGrow={1}>
<Button type='reset' disabled={!hasUnsavedChanges} onClick={reset}>
{t('Reset')}
</Button>
<Button flexGrow={1} disabled={!hasUnsavedChanges} onClick={handleSave}>
{t('Save')}
</Button>
</ButtonGroup>
</Box>
</Field.Row>
</Field>
<Field>
<Field.Row>
<Button flexGrow={1} primary danger disabled={!canDelete} onClick={handleDelete}>
<Icon name='trash' size='x16' />
{t('Delete')}
</Button>
</Field.Row>
</Field>
</VerticalBar.ScrollableContent>
</>
);
}
export default EditChannel;