Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite: CreateChannel modal component #20617

Merged
merged 16 commits into from
Feb 9, 2021
Merged
Show file tree
Hide file tree
Changes from 14 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
2 changes: 1 addition & 1 deletion app/api/server/v1/channels.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ function createChannelValidator(params) {

function createChannel(userId, params) {
const readOnly = typeof params.readOnly !== 'undefined' ? params.readOnly : false;
const id = Meteor.runAsUser(userId, () => Meteor.call('createChannel', params.name, params.members ? params.members : [], readOnly, params.customFields));
const id = Meteor.runAsUser(userId, () => Meteor.call('createChannel', params.name, params.members ? params.members : [], readOnly, params.customFields, params.extraData));

return {
channel: findChannelByIdOrName({ params: { roomId: id.rid }, userId: this.userId }),
Expand Down
6 changes: 5 additions & 1 deletion app/api/server/v1/groups.js
Original file line number Diff line number Diff line change
Expand Up @@ -223,12 +223,16 @@ API.v1.addRoute('groups.create', { authRequired: true }, {
if (this.bodyParams.customFields && !(typeof this.bodyParams.customFields === 'object')) {
return API.v1.failure('Body param "customFields" must be an object if provided');
}
if (this.bodyParams.extraData && !(typeof this.bodyParams.extraData === 'object')) {
return API.v1.failure('Body param "extraData" must be an object if provided');
}

const readOnly = typeof this.bodyParams.readOnly !== 'undefined' ? this.bodyParams.readOnly : false;

let id;

Meteor.runAsUser(this.userId, () => {
id = Meteor.call('createPrivateGroup', this.bodyParams.name, this.bodyParams.members ? this.bodyParams.members : [], readOnly, this.bodyParams.customFields);
id = Meteor.call('createPrivateGroup', this.bodyParams.name, this.bodyParams.members ? this.bodyParams.members : [], readOnly, this.bodyParams.customFields, this.bodyParams.extraData);
});

return API.v1.success({
Expand Down
1 change: 0 additions & 1 deletion app/lib/server/functions/createRoom.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,6 @@ export const createRoom = function(type, name, owner, members = [], readOnly, ex
if (type === 'c') {
callbacks.run('beforeCreateChannel', owner, room);
}

room = Rooms.createWithFullRoomData(room);

for (const username of members) {
Expand Down
262 changes: 262 additions & 0 deletions client/sidebar/header/CreateChannel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
import { FlowRouter } from 'meteor/kadira:flow-router';
import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { useMutableCallback, useDebouncedCallback } from '@rocket.chat/fuselage-hooks';
import { Box, Modal, ButtonGroup, Button, TextInput, Icon, Field, ToggleSwitch } from '@rocket.chat/fuselage';

import { useTranslation } from '../../contexts/TranslationContext';
import { useForm } from '../../hooks/useForm';
import { useEndpointActionExperimental } from '../../hooks/useEndpointAction';
import UserAutoCompleteMultiple from '../../../ee/client/audit/UserAutoCompleteMultiple';
import { useSetting } from '../../contexts/SettingsContext';
import { usePermission } from '../../contexts/AuthorizationContext';
import { useMethod } from '../../contexts/ServerContext';

export const CreateChannel = ({
tiagoevanp marked this conversation as resolved.
Show resolved Hide resolved
values,
handlers,
hasUnsavedChanges,
onChangeUsers,
onChangeType,
onChangeBroadcast,
canOnlyCreateOneType,
e2eEnabledForPrivateByDefault,
onCreate,
onClose,
}) => {
const t = useTranslation();
const e2eEnabled = useSetting('E2E_Enable');
const namesValidation = useSetting('UTF8_Names_Validation');
const allowSpecialNames = useSetting('UI_Allow_room_names_with_special_chars');
const channelNameExists = useMethod('roomNameExists');
const channelNameRegex = useMemo(() => {
if (allowSpecialNames) {
return '';
}
const regex = new RegExp(`^${ namesValidation }$`);

return regex;
}, [allowSpecialNames, namesValidation]);

const [nameError, setNameError] = useState();

const checkName = useDebouncedCallback(async (name) => {
setNameError(false);
if (hasUnsavedChanges) { return; }
if (!name || name.length === 0) { return setNameError(t('Field_required')); }
if (!channelNameRegex.test(name)) { return setNameError(t('error-invalid-name')); }
const isNotAvailable = await channelNameExists(name);
if (isNotAvailable) { return setNameError(t('Channel_already_exist', name)); }
}, 100, [name]);

useEffect(() => {
checkName(values.name);
}, [checkName, values.name]);

const e2edisabled = useMemo(() => !values.type || values.broadcast || !e2eEnabled || e2eEnabledForPrivateByDefault, [e2eEnabled, e2eEnabledForPrivateByDefault, values.broadcast, values.type]);

const canSave = useMemo(() => hasUnsavedChanges && !nameError, [hasUnsavedChanges, nameError]);

return <Modal>
<Modal.Header>
<Modal.Title>{t('Create_channel')}</Modal.Title>
<Modal.Close onClick={onClose}/>
</Modal.Header>
<Modal.Content>
<Field mbe='x24'>
<Field.Label>{t('Name')}</Field.Label>
<Field.Row>
<TextInput error={hasUnsavedChanges && nameError} addon={<Icon name={values.type ? 'lock' : 'hash'} size='x20' />} placeholder={t('Channel_name')} onChange={handlers.handleName}/>
</Field.Row>
{hasUnsavedChanges && nameError && <Field.Error>
{nameError}
</Field.Error>}
</Field>
<Field mbe='x24'>
<Field.Label>{t('Topic')} <Box is='span' color='neutral-600'>({t('optional')})</Box></Field.Label>
<Field.Row>
<TextInput placeholder={t('Channel_what_is_this_channel_about')} onChange={handlers.handleDescription}/>
</Field.Row>
</Field>
<Field mbe='x24'>
<Box display='flex' justifyContent='space-between' alignItems='start'>
<Box display='flex' flexDirection='column'>
<Field.Label>{t('Private')}</Field.Label>
<Field.Description>{values.type ? t('Only_invited_users_can_acess_this_channel') : t('Everyone_can_access_this_channel')}</Field.Description>
</Box>
<ToggleSwitch checked={values.type} disabled={!!canOnlyCreateOneType} onChange={onChangeType}/>
</Box>
</Field>
<Field mbe='x24' disabled={values.broadcast}>
<Box display='flex' justifyContent='space-between' alignItems='start'>
<Box display='flex' flexDirection='column'>
<Field.Label>{t('Read_only')}</Field.Label>
<Field.Description>{t('All_users_in_the_channel_can_write_new_messages')}</Field.Description>
</Box>
<ToggleSwitch checked={values.readOnly} disabled={values.broadcast} onChange={handlers.handleReadOnly}/>
</Box>
</Field>
<Field disabled={e2edisabled} mbe='x24'>
<Box display='flex' justifyContent='space-between' alignItems='start'>
<Box display='flex' flexDirection='column'>
<Field.Label>{t('Encrypted')}</Field.Label>
<Field.Description>{values.type ? t('Encrypted_channel_Description') : t('Encrypted_not_available')}</Field.Description>
</Box>
<ToggleSwitch checked={values.encrypted} disabled={e2edisabled} onChange={handlers.handleEncrypted} />
</Box>
</Field>
<Field mbe='x24'>
<Box display='flex' justifyContent='space-between' alignItems='start'>
<Box display='flex' flexDirection='column'>
<Field.Label>{t('Broadcast')}</Field.Label>
<Field.Description>{t('Broadcast_channel_Description')}</Field.Description>
</Box>
<ToggleSwitch onChange={onChangeBroadcast} />
</Box>
</Field>
<Field mbe='x24'>
<Field.Label>{`${ t('Add_members') } (${ t('optional') })`}</Field.Label>
<UserAutoCompleteMultiple value={values.users} onChange={onChangeUsers}/>
</Field>
</Modal.Content>
<Modal.Footer>
<ButtonGroup align='end'>
<Button onClick={onClose}>{t('Cancel')}</Button>
<Button disabled={!canSave} onClick={onCreate} primary>{t('Create')}</Button>
</ButtonGroup>
</Modal.Footer>
</Modal>;
};

export default memo(({
onClose,
}) => {
const createChannel = useEndpointActionExperimental('POST', 'channels.create');
const createPrivateChannel = useEndpointActionExperimental('POST', 'groups.create');
const setChannelDescription = useEndpointActionExperimental('POST', 'channels.setDescription');
const setPrivateChannelDescription = useEndpointActionExperimental('POST', 'groups.setDescription');
const canCreateChannel = usePermission('create-c');
const canCreatePrivateChannel = usePermission('create-p');
const e2eEnabledForPrivateByDefault = useSetting('E2E_Enabled_Default_PrivateRooms');
const canOnlyCreateOneType = useMemo(() => {
if (!canCreateChannel && canCreatePrivateChannel) {
return 'p';
}
if (canCreateChannel && !canCreatePrivateChannel) {
return 'c';
}
return false;
}, [canCreateChannel, canCreatePrivateChannel]);


const initialValues = {
users: [],
name: '',
description: '',
type: canOnlyCreateOneType ? canOnlyCreateOneType === 'p' : true,
readOnly: false,
encrypted: e2eEnabledForPrivateByDefault,
broadcast: false,
};
const { values, handlers, hasUnsavedChanges } = useForm(initialValues);

const {
users,
name,
description,
type,
readOnly,
broadcast,
encrypted,
} = values;
const {
handleUsers,
handleEncrypted,
handleType,
handleBroadcast,
handleReadOnly,
} = handlers;

const onChangeUsers = useMutableCallback((value, action) => {
if (!action) {
if (users.includes(value)) {
return;
}
return handleUsers([...users, value]);
}
handleUsers(users.filter((current) => current !== value));
});

const onChangeType = useMutableCallback((value) => {
if (value) {
handleEncrypted(false);
}
return handleType(value);
});

const onChangeBroadcast = useMutableCallback((value) => {
if (value) {
handleEncrypted(false);
handleReadOnly(true);
}
handleReadOnly(value);
return handleBroadcast(value);
});

const onCreate = useCallback(async () => {
const goToRoom = (rid) => {
FlowRouter.goToRoomById(rid);
};

const params = {
name,
members: users,
readOnly,
extraData: {
broadcast,
encrypted,
},
};
let roomData;

if (type) {
roomData = await createPrivateChannel(params);
goToRoom(roomData.group._id);
} else {
roomData = await createChannel(params);
goToRoom(roomData.channel._id);
}

if (roomData.success && roomData.group && description) {
setPrivateChannelDescription({ description, roomName: roomData.group.name });
} else if (roomData.success && roomData.channel && description) {
setChannelDescription({ description, roomName: roomData.channel.name });
}

onClose();
}, [broadcast,
createChannel,
createPrivateChannel,
description,
encrypted,
name,
onClose,
readOnly,
setChannelDescription,
setPrivateChannelDescription,
type,
users,
]);

return <CreateChannel
values={values}
handlers={handlers}
hasUnsavedChanges={hasUnsavedChanges}
onChangeUsers={onChangeUsers}
onChangeType={onChangeType}
onChangeBroadcast={onChangeBroadcast}
canOnlyCreateOneType={canOnlyCreateOneType}
e2eEnabledForPrivateByDefault={e2eEnabledForPrivateByDefault}
onClose={onClose}
onCreate={onCreate}
/>;
});
18 changes: 17 additions & 1 deletion client/sidebar/header/actions/CreateRoom.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { popover, modal } from '../../../../app/ui-utils';
import { useAtLeastOnePermission, usePermission } from '../../../contexts/AuthorizationContext';
import { useSetting } from '../../../contexts/SettingsContext';
import { useTranslation } from '../../../contexts/TranslationContext';
import { useSetModal } from '../../../contexts/ModalContext';
import CreateChannel from '../CreateChannel';

const CREATE_ROOM_PERMISSIONS = ['create-c', 'create-p', 'create-d', 'start-discussion', 'start-discussion-other-user'];

Expand All @@ -27,6 +29,18 @@ const openPopover = (e, items) => popover.open({
offsetVertical: e.currentTarget.clientHeight + 10,
});

const useReactModal = (setModal, Component) => useMutableCallback((e) => {
e.preventDefault();

const handleClose = () => {
setModal(null);
};

setModal(() => <Component
onClose={handleClose}
/>);
});

const useAction = (title, content) => useMutableCallback((e) => {
e.preventDefault();
modal.open({
Expand All @@ -46,13 +60,15 @@ const useAction = (title, content) => useMutableCallback((e) => {

const CreateRoom = (props) => {
const t = useTranslation();
const setModal = useSetModal();

const showCreate = useAtLeastOnePermission(CREATE_ROOM_PERMISSIONS);

const canCreateChannel = useAtLeastOnePermission(CREATE_CHANNEL_PERMISSIONS);
const canCreateDirectMessages = usePermission('create-d');
const canCreateDiscussion = useAtLeastOnePermission(CREATE_DISCUSSION_PERMISSIONS);

const createChannel = useAction(t('Create_A_New_Channel'), 'createChannel');
const createChannel = useReactModal(setModal, CreateChannel);
const createDirectMessage = useAction(t('Direct_Messages'), 'CreateDirectMessage');
const createDiscussion = useAction(t('Discussion_title'), 'CreateDiscussion');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ export default ({
const params = useMemo(() => [rid, type === 'all', { limit: 50 }, debouncedText], [rid, type, debouncedText]);

const { value, phase, more, error } = useGetUsersOfRoom(params);
console.log(value);

const canAddUsers = useAtLeastOnePermission(useMemo(() => [room.t === 'p' ? 'add-user-to-any-p-room' : 'add-user-to-any-c-room', 'add-user-to-joined-room'], [room.t]), rid);

const handleTextChange = useCallback((event) => {
Expand Down
6 changes: 6 additions & 0 deletions packages/rocketchat-i18n/i18n/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@
"Add_user": "Add user",
"Add_User": "Add User",
"Add_users": "Add users",
"Add_members": "Add Members",
"add-livechat-department-agents": "Add Omnichannel Agents to Departments",
"add-oauth-service": "Add Oauth Service",
"add-oauth-service_description": "Permission to add a new Oauth service",
Expand Down Expand Up @@ -618,6 +619,7 @@
"BotHelpers_userFields_Description": "CSV of user fields that can be accessed by bots helper methods.",
"Bots": "Bots",
"Branch": "Branch",
"Broadcast": "Broadcast",
"Broadcast_channel": "Broadcast Channel",
"Broadcast_channel_Description": "Only authorized users can write new messages, but the other users will be able to reply",
"Broadcast_Connected_Instances": "Broadcast Connected Instances",
Expand Down Expand Up @@ -706,6 +708,7 @@
"Channels": "Channels",
"Channels_are_where_your_team_communicate": "Channels are where your team communicate",
"Channels_list": "List of public channels",
"Channel_what_is_this_channel_about": "What is this channel about?",
"Chart": "Chart",
"Chat_button": "Chat button",
"Chat_close": "Chat Close",
Expand Down Expand Up @@ -1159,6 +1162,7 @@
"Country_Zimbabwe": "Zimbabwe",
"Cozy": "Cozy",
"Create": "Create",
"Create_channel": "Create Channel",
"Create_A_New_Channel": "Create a New Channel",
"Create_new": "Create new",
"Create_unique_rules_for_this_channel": "Create unique rules for this channel",
Expand Down Expand Up @@ -1486,6 +1490,7 @@
"Encrypted_channel_Description": "End to end encrypted channel. Search will not work with encrypted channels and notifications may not show the messages content.",
"Encrypted_message": "Encrypted message",
"Encrypted_setting_changed_successfully": "Encrypted setting changed successfully",
"Encrypted_not_available": "Not available for Public Channels",
"Encryption_key_saved_successfully": "Your encryption key was saved successfully.",
"EncryptionKey_Change_Disabled": "You can't set a password for your encryption key because your private key is not present on this client. In order to set a new password you need load your private key using your existing password or use a client where the key is already loaded.",
"End": "End",
Expand Down Expand Up @@ -2924,6 +2929,7 @@
"Only_On_Desktop": "Desktop mode (only sends with enter on desktop)",
"Only_works_with_chrome_version_greater_50": "Only works with Chrome browser versions > 50",
"Only_you_can_see_this_message": "Only you can see this message",
"Only_invited_users_can_acess_this_channel": "Only invited users can access this Channel",
"Oops_page_not_found": "Oops, page not found",
"Oops!": "Oops",
"Open": "Open",
Expand Down