Rewrite: CreateChannel modal component (#20617)

Co-authored-by: Martin <martin.schoeler@rocket.chat>
pull/20661/head
Tiago Evangelista Pinto 5 years ago committed by GitHub
parent 374281c83a
commit 2ad2d0bd7e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      app/api/server/v1/channels.js
  2. 6
      app/api/server/v1/groups.js
  3. 1
      app/lib/server/functions/createRoom.js
  4. 2
      app/ui/client/index.js
  5. 217
      app/ui/client/views/app/createChannel.html
  6. 437
      app/ui/client/views/app/createChannel.js
  7. 262
      client/sidebar/header/CreateChannel.js
  8. 18
      client/sidebar/header/actions/CreateRoom.js
  9. 2
      client/views/room/contextualBar/RoomMembers/List/RoomMembers.js
  10. 6
      packages/rocketchat-i18n/i18n/en.i18n.json

@ -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 }),

@ -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({

@ -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) {

@ -12,7 +12,6 @@ import './views/404/roomNotFound.html';
import './views/404/invalidSecretURL.html';
import './views/404/invalidInvite.html';
import './views/app/burger.html';
import './views/app/createChannel.html';
import './views/app/editStatus.html';
import './views/app/editStatus.css';
import './views/app/home.html';
@ -28,7 +27,6 @@ import './views/app/photoswipe.html';
import './views/cmsPage';
import './views/404/roomNotFound';
import './views/app/burger';
import './views/app/createChannel';
import './views/app/CreateDirectMessage';
import './views/app/editStatus';
import './views/app/home';

@ -1,217 +0,0 @@
<template name="createChannel">
<section class="create-channel">
{{#each roomType in roomTypesBeforeStandard}}
<div class="room-type-creation">
{{> Template.dynamic template=roomType.creationTemplate }}
</div>
{{/each}}
<div class="create-channel__wrapper">
<p class="create-channel__description">{{_ "Channels_are_where_your_team_communicate"}}</p>
<form id="create-channel" name="create-channel" class="create-channel__content">
<div class="create-channel__switches">
<div class="rc-switch">
<label class="rc-switch__label" tabindex="-1">
<input type="checkbox" class="rc-switch__input" name="type" value="p" checked={{roomTypeIsP}} disabled="{{cantCreateBothTypes}}">
<span class="rc-switch__button">
<span class="rc-switch__button-inside"></span>
</span>
<span class="rc-switch__text">
{{typeLabel}}
</span>
</label>
<span class="rc-switch__description">{{typeDescription}}</span>
</div>
<div class="rc-switch">
<label class="rc-switch__label">
<input type="checkbox" class="rc-switch__input" name="readOnly" checked={{readOnly}} disabled="{{broadcast}}">
<span class="rc-switch__button">
<span class="rc-switch__button-inside"></span>
</span>
<span class="rc-switch__text">
{{_ "Read_only_channel"}}
</span>
</label>
<span class="rc-switch__description">{{readOnlyDescription}}</span>
</div>
{{#if e2eEnabled}}
<div class="rc-switch">
<label class="rc-switch__label">
<input type="checkbox" class="rc-switch__input" name="encrypted" checked={{encrypted}} disabled="{{encryptedDisabled}}">
<span class="rc-switch__button">
<span class="rc-switch__button-inside"></span>
</span>
<span class="rc-switch__text">
{{_"Encrypted"}}
</span>
</label>
<span class="rc-switch__description">
{{_"Encrypted_channel_Description"}}
</span>
</div>
{{/if}}
<div class="rc-switch">
<label class="rc-switch__label">
<input type="checkbox" class="rc-switch__input" name="broadcast">
<span class="rc-switch__button">
<span class="rc-switch__button-inside"></span>
</span>
<span class="rc-switch__text">
{{_"Broadcast_channel"}}
</span>
</label>
<span class="rc-switch__description">
{{_"Broadcast_channel_Description"}}
</span>
</div>
</div>
<div class="create-channel__inputs">
<div class="rc-input {{#if invalidChannel}}rc-input--error{{/if}}">
<label class="rc-input__label">
<div class="rc-input__title">{{_ "Channel_name"}}</div>
<div class="rc-input__wrapper">
<div class="rc-input__icon">
{{> icon block="rc-input__icon-svg" icon=iconType }}
</div>
<input name="name" type="text" class="rc-input__element"
placeholder="{{_ "Channel_Name_Placeholder"}}" autofocus>
</div>
</label>
{{#if inUse}}
<div class="rc-input__error">
<div class="rc-input__error-icon">
{{> icon block="rc-input__error-icon" icon="warning" classes="rc-input__error-icon-svg"}}
</div>
<div class="rc-input__error-message">{{_ "Channel_already_exist_static"}}</div>
</div>
{{/if}}
</div>
<div class="rc-input">
<label class="rc-input__label">
<div class="rc-input__title">{{_ "Invite_Users"}}</div>
<div class="rc-input__wrapper">
<div class="rc-input__icon">
{{> icon block="rc-input__icon-svg" icon="at"}}
</div>
<div class="rc-tags">
{{#each user in selectedUsers}}
{{> tag user}}
{{/each}}
<input type="text" class="rc-tags__input" placeholder="{{_ "Username_Placeholder"}}" name="users" autocomplete="off"/>
</div>
</div>
{{#with config}}
{{#if autocomplete 'isShowing'}}
{{> popupList data=config items=items ready=(autocomplete 'isLoaded')}}
{{/if}}
{{/with}}
</label>
</div>
</div>
{{#if tokenAccessEnabled}}
<div class="create-channel__switches">
<div class="rc-switch">
<label class="rc-switch__label" tabindex="-1">
<input type="checkbox" class="rc-switch__input" name="setTokensRequired" {{tokenIsDisabled}}>
<span class="rc-switch__button">
<span class="rc-switch__button-inside"></span>
</span>
<span class="rc-switch__text">
{{_ "Token_Controlled_Access"}}
</span>
</label>
</div>
</div>
{{#if tokensRequired}}
{{> tokenpass extensionsConfig}}
{{/if}}
{{/if}}
<div class="create-channel__inputs">
<input form='create-channel' class="rc-button rc-button--primary" type='submit' data-button="create" {{createIsDisabled}} value="{{_ "Create"}}" />
</div>
</form>
</div>
{{#each roomType in roomTypesAfterStandard}}
<div class="room-type-creation">
{{> Template.dynamic template=roomType.creationTemplate }}
</div>
{{/each}}
</section>
</template>
<template name="tokenpass">
<fieldset class="rc-form-fieldset {{#if invalid}}rc-form-fieldset--error{{/if}}">
<legend class="rc-form-legend">Tokenpass</legend>
<div class="create-channel__switches">
<div class="rc-switch">
<label class="rc-switch__label" tabindex="-1">
<input type="checkbox" class="rc-switch__input" name="tokenRequireAll" value="all" checked>
<span class="rc-switch__button">
<span class="rc-switch__button-inside"></span>
</span>
<span class="rc-switch__text">
{{tokenRequiment}}
</span>
</label>
<span class="rc-switch__description">{{tokenRequimentDescription}}</span>
</div>
</div>
<div class="rc-input {{#if invalidTokens}}rc-input--error{{/if}}">
<label class="rc-input__label">
<div class="rc-input__title">{{_ "Tokens_Required"}}</div>
<div class="rc-input__wrapper">
<input name="tokensRequired" type="text" class="rc-input__element" placeholder="{{_ "Tokens_Required_Input_Placeholder"}}">
</div>
</label>
<div class="rc-input__description">{{_ "Tokens_Required_Input_Description"}}</div>
</div>
<div class="rc-input">
<label class="rc-input__label">
<div class="rc-input__title">{{_ "Tokens_Minimum_Needed_Balance"}}</div>
<div class="rc-input__wrapper">
<input name="tokenMinimumNeededBalance" type="number" class="rc-input__element" placeholder="{{_ "Tokens_Minimum_Needed_Balance_Placeholder"}}" min="0">
</div>
</label>
<div class="rc-input__description">{{_ "Tokens_Minimum_Needed_Balance_Description"}}</div>
</div>
{{#if selectedTokens}}
<div class="rc-input">
<label class="rc-input__label">
<div class="rc-input__title">Tokens</div>
<div class="rc-input__wrapper">
<div class="rc-tags rc-tags--no-icon">
{{#each token in selectedTokens}}
{{> tagToken token}}
{{/each}}
</div>
</div>
</label>
</div>
{{/if}}
<input class="rc-button rc-button--primary rc-button--full" type="submit" data-button="add" {{addIsDisabled}} value="{{_ "Add"}}">
</fieldset>
</template>
<template name='tag'>
<span class="rc-tags__tag">
{{#if username}}
<span class="rc-tags__tag-image">
{{> avatar username=username}}
</span>
<span class="rc-tags__tag-text">{{username}}</span>
{{else}}
<span class="rc-tags__tag-text">{{text}}</span>
{{/if}}
{{> icon block="rc-tags__tag-icon" icon="cross"}}
</span>
</template>
<template name='tagToken'>
<span class="rc-tags__tag">
<span class="rc-tags__tag-text" title="balance: {{balance}}">{{token}}</span>
{{> icon block="rc-tags__tag-icon" icon="cross"}}
</span>
</template>

@ -1,437 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { ReactiveVar } from 'meteor/reactive-var';
import { Tracker } from 'meteor/tracker';
import { Blaze } from 'meteor/blaze';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Template } from 'meteor/templating';
import toastr from 'toastr';
import _ from 'underscore';
import { settings } from '../../../../settings';
import { callbacks } from '../../../../callbacks';
import { t, roomTypes } from '../../../../utils';
import { hasAllPermission } from '../../../../authorization';
import { AutoComplete } from '../../../../meteor-autocomplete/client';
const acEvents = {
'click .rc-popup-list__item'(e, t) {
t.ac.onItemClick(this, e);
},
'keydown [name="users"]'(e, t) {
if ([8, 46].includes(e.keyCode) && e.target.value === '') {
const users = t.selectedUsers;
const usersArr = users.get();
usersArr.pop();
return users.set(usersArr);
}
t.ac.onKeyDown(e);
},
'keyup [name="users"]'(e, t) {
t.ac.onKeyUp(e);
},
'focus [name="users"]'(e, t) {
t.ac.onFocus(e);
},
'blur [name="users"]'(e, t) {
t.ac.onBlur(e);
},
};
const validateChannelName = (name) => {
if (settings.get('UI_Allow_room_names_with_special_chars')) {
return true;
}
const reg = new RegExp(`^${ settings.get('UTF8_Names_Validation') }$`);
return name.length === 0 || reg.test(name);
};
const filterNames = (old) => {
if (settings.get('UI_Allow_room_names_with_special_chars')) {
return old;
}
const reg = new RegExp(`^${ settings.get('UTF8_Names_Validation') }$`);
return [...old.replace(' ', '').toLocaleLowerCase()].filter((f) => reg.test(f)).join('');
};
Template.createChannel.helpers({
autocomplete(key) {
const instance = Template.instance();
const param = instance.ac[key];
return typeof param === 'function' ? param.apply(instance.ac) : param;
},
items() {
return Template.instance().ac.filteredList();
},
config() {
const filter = Template.instance().userFilter;
return {
filter: filter.get(),
noMatchTemplate: 'userSearchEmpty',
modifier(text) {
const f = filter.get();
return `@${ f.length === 0 ? text : text.replace(new RegExp(filter.get()), function(part) {
return `<strong>${ part }</strong>`;
}) }`;
},
};
},
selectedUsers() {
return Template.instance().selectedUsers.get();
},
inUse() {
return Template.instance().inUse.get();
},
invalidChannel() {
const instance = Template.instance();
const invalid = instance.invalid.get();
const inUse = instance.inUse.get();
return invalid || inUse;
},
typeLabel() {
return t(Template.instance().type.get() === 'p' ? t('Private_Channel') : t('Public_Channel'));
},
typeDescription() {
return t(Template.instance().type.get() === 'p' ? t('Just_invited_people_can_access_this_channel') : t('Everyone_can_access_this_channel'));
},
broadcast() {
return Template.instance().broadcast.get();
},
encrypted() {
return Template.instance().encrypted.get();
},
encryptedDisabled() {
return Template.instance().type.get() !== 'p' || Template.instance().broadcast.get();
},
e2eEnabled() {
return settings.get('E2E_Enable');
},
readOnly() {
return Template.instance().readOnly.get();
},
readOnlyDescription() {
return t(Template.instance().readOnly.get() ? t('Only_authorized_users_can_write_new_messages') : t('All_users_in_the_channel_can_write_new_messages'));
},
cantCreateBothTypes() {
return !hasAllPermission(['create-c', 'create-p']);
},
roomTypeIsP() {
return Template.instance().type.get() === 'p';
},
createIsDisabled() {
const instance = Template.instance();
const invalid = instance.invalid.get();
const extensions_invalid = instance.extensions_invalid.get();
const inUse = instance.inUse.get();
const name = instance.name.get();
if (name.length === 0 || invalid || inUse === true || inUse === undefined || extensions_invalid) {
return 'disabled';
}
return '';
},
iconType() {
return Template.instance().type.get() === 'p' ? 'lock' : 'hashtag';
},
tokenAccessEnabled() {
return settings.get('API_Tokenpass_URL') !== '';
},
tokenIsDisabled() {
return Template.instance().type.get() !== 'p' ? 'disabled' : null;
},
tokensRequired() {
return Template.instance().tokensRequired.get() && Template.instance().type.get() === 'p';
},
extensionsConfig() {
const instance = Template.instance();
return {
validations: instance.extensions_validations,
submits: instance.extensions_submits,
change: instance.change,
};
},
roomTypesBeforeStandard() {
const orderLow = roomTypes.roomTypesOrder.filter((roomTypeOrder) => roomTypeOrder.identifier === 'c')[0].order;
return roomTypes.roomTypesOrder.filter(
(roomTypeOrder) => roomTypeOrder.order < orderLow,
).map(
(roomTypeOrder) => roomTypes.getConfig(roomTypeOrder.identifier),
).filter((roomType) => roomType.creationTemplate);
},
roomTypesAfterStandard() {
const orderHigh = roomTypes.roomTypesOrder.filter((roomTypeOrder) => roomTypeOrder.identifier === 'd')[0].order;
return roomTypes.roomTypesOrder.filter(
(roomTypeOrder) => roomTypeOrder.order > orderHigh,
).map(
(roomTypeOrder) => roomTypes.getConfig(roomTypeOrder.identifier),
).filter((roomType) => roomType.creationTemplate);
},
});
Template.createChannel.events({
...acEvents,
'click .rc-tags__tag'({ target }, t) {
const { username } = Blaze.getData(target);
t.selectedUsers.set(t.selectedUsers.get().filter((user) => user.username !== username));
},
'change [name=setTokensRequired]'(e, t) {
t.tokensRequired.set(e.currentTarget.checked);
t.change();
},
'change [name="type"]'(e, t) {
t.type.set(e.target.checked ? e.target.value : 'c');
t.change();
},
'change [name="broadcast"]'(e, t) {
t.broadcast.set(e.target.checked);
t.change();
},
'change [name="encrypted"]'(e, t) {
t.encrypted.set(e.target.checked);
t.change();
},
'change [name="readOnly"]'(e, t) {
t.readOnly.set(e.target.checked);
},
'input [name="users"]'(e, t) {
const input = e.target;
const position = input.selectionEnd || input.selectionStart;
const { length } = input.value;
const modified = filterNames(input.value);
input.value = modified;
document.activeElement === input && e && /input/i.test(e.type) && (input.selectionEnd = position + input.value.length - length);
t.userFilter.set(modified);
},
'input [name="name"]'(e, t) {
const input = e.target;
const position = input.selectionEnd || input.selectionStart;
const { length } = input.value;
const modified = filterNames(input.value);
input.value = modified;
document.activeElement === input && e && /input/i.test(e.type) && (input.selectionEnd = position + input.value.length - length);
t.invalid.set(!validateChannelName(input.value));
if (input.value !== t.name.get()) {
t.inUse.set(undefined);
t.checkChannel(input.value);
t.name.set(modified);
}
},
'submit .create-channel__content'(e, instance) {
e.preventDefault();
e.stopPropagation();
const name = e.target.name.value;
const type = instance.type.get();
const readOnly = instance.readOnly.get();
const broadcast = instance.broadcast.get();
const encrypted = instance.encrypted.get();
const isPrivate = type === 'p';
if (instance.invalid.get() || instance.inUse.get()) {
return e.target.name.focus();
}
if (!Object.keys(instance.extensions_validations).map((key) => instance.extensions_validations[key]).reduce((valid, fn) => fn(instance) && valid, true)) {
return instance.extensions_invalid.set(true);
}
const extraData = Object.keys(instance.extensions_submits)
.reduce((result, key) => ({ ...result, ...instance.extensions_submits[key](instance) }), { broadcast, encrypted });
Meteor.call(isPrivate ? 'createPrivateGroup' : 'createChannel', name, instance.selectedUsers.get().map((user) => user.username), readOnly, {}, extraData, function(err, result) {
if (err) {
if (err.error === 'error-invalid-name') {
instance.invalid.set(true);
return;
}
if (err.error === 'error-duplicate-channel-name') {
instance.inUse.set(true);
return;
}
if (err.error === 'error-invalid-room-name') {
toastr.error(t('error-invalid-room-name', { room_name: name }));
return;
}
toastr.error(err.message);
return;
}
if (!isPrivate) {
callbacks.run('aftercreateCombined', { _id: result.rid, name: result.name });
}
if (instance.data.onCreate) {
instance.data.onCreate(result);
}
return FlowRouter.go(isPrivate ? 'group' : 'channel', { ...result }, FlowRouter.current().queryParams);
});
return false;
},
});
Template.createChannel.onRendered(function() {
const users = this.selectedUsers;
this.firstNode.querySelector('[name="name"]').focus();
this.ac.element = this.firstNode.querySelector('[name="users"]');
this.ac.$element = $(this.ac.element);
this.ac.$element.on('autocompleteselect', function(e, { item }) {
const usersArr = users.get();
usersArr.push(item);
users.set(usersArr);
});
});
Template.createChannel.onCreated(function() {
this.selectedUsers = new ReactiveVar([]);
const filter = { exceptions: [Meteor.user().username].concat(this.selectedUsers.get().map((u) => u.username)) };
// this.onViewRead:??y(function() {
Tracker.autorun(() => {
filter.exceptions = [Meteor.user().username].concat(this.selectedUsers.get().map((u) => u.username));
});
this.extensions_validations = {};
this.extensions_submits = {};
this.name = new ReactiveVar('');
this.type = new ReactiveVar(hasAllPermission(['create-p']) ? 'p' : 'c');
this.readOnly = new ReactiveVar(false);
this.broadcast = new ReactiveVar(false);
this.encrypted = new ReactiveVar(settings.get('E2E_Enabled_Default_PrivateRooms'));
this.inUse = new ReactiveVar(undefined);
this.invalid = new ReactiveVar(false);
this.extensions_invalid = new ReactiveVar(false);
this.change = _.debounce(() => {
let valid = true;
Object.keys(this.extensions_validations).map((key) => this.extensions_validations[key]).forEach((f) => { valid = f(this) && valid; });
this.extensions_invalid.set(!valid);
}, 300);
Tracker.autorun(() => {
const broadcast = this.broadcast.get();
if (broadcast) {
this.readOnly.set(true);
this.encrypted.set(false);
}
const type = this.type.get();
if (type !== 'p') {
this.encrypted.set(false);
}
});
this.userFilter = new ReactiveVar('');
this.tokensRequired = new ReactiveVar(false);
this.checkChannel = _.debounce((name) => {
if (validateChannelName(name)) {
return Meteor.call('roomNameExists', name, (error, result) => {
if (error) {
return;
}
this.inUse.set(result);
});
}
this.inUse.set(undefined);
}, 1000);
this.ac = new AutoComplete(
{
selector: {
anchor: '.rc-input__label',
item: '.rc-popup-list__item',
container: '.rc-popup-list__list',
},
position: 'fixed',
limit: 10,
inputDelay: 300,
rules: [
{
// @TODO maybe change this 'collection' and/or template
collection: 'UserAndRoom',
endpoint: 'users.autocomplete',
field: 'username',
matchAll: true,
filter,
doNotChangeWidth: false,
selector(match) {
return { term: match };
},
sort: 'username',
},
],
});
// this.firstNode.querySelector('[name=name]').focus();
// this.ac.element = this.firstNode.querySelector('[name=users]');
// this.ac.$element = $(this.ac.element);
this.ac.tmplInst = this;
});
Template.tokenpass.onCreated(function() {
this.data.validations.tokenpass = (instance) => {
const result = (settings.get('API_Tokenpass_URL') !== '' && instance.tokensRequired.get() && instance.type.get() === 'p') && this.selectedTokens.get().length === 0;
this.invalid.set(result);
return !result;
};
this.data.submits.tokenpass = () => ({
tokenpass: {
require: this.requireAll.get() ? 'all' : 'any',
tokens: this.selectedTokens.get(),
},
});
this.balance = new ReactiveVar('');
this.token = new ReactiveVar('');
this.selectedTokens = new ReactiveVar([]);
this.invalid = new ReactiveVar(false);
this.requireAll = new ReactiveVar(true);
});
Template.tokenpass.helpers({
selectedTokens() {
return Template.instance().selectedTokens.get();
},
invalid() {
return Template.instance().invalid.get();
},
addIsDisabled() {
const { balance, token } = Template.instance();
return balance.get().length && token.get().length ? '' : 'disabled';
},
tokenRequiment() {
return Template.instance().requireAll.get() ? t('Require_all_tokens') : t('Require_any_token');
},
tokenRequimentDescription() {
return Template.instance().requireAll.get() ? t('All_added_tokens_will_be_required_by_the_user') : t('At_least_one_added_token_is_required_by_the_user');
},
});
Template.tokenpass.events({
'click [data-button=add]'(e, instance) {
const { balance, token, selectedTokens } = instance;
const text = token.get();
const arr = selectedTokens.get();
selectedTokens.set([...arr.filter((token) => token.token !== text), { token: text, balance: balance.get() }]);
balance.set('');
token.set('');
[...instance.findAll('input[type=text],input[type=number]')].forEach((el) => { el.value = ''; });
instance.data.change();
return false;
},
'click .rc-tags__tag'({ target }, t) {
const { token } = Blaze.getData(target);
t.selectedTokens.set(t.selectedTokens.get().filter((t) => t.token !== token));
t.data.change();
},
'input [name=tokenMinimumNeededBalance]'(e, i) {
i.balance.set(e.target.value);
},
'input [name=tokensRequired]'(e, i) {
i.token.set(e.target.value);
},
'change [name=tokenRequireAll]'(e, i) {
i.requireAll.set(e.currentTarget.checked);
},
});

@ -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 = ({
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}
/>;
});

@ -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'];
@ -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({
@ -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');

@ -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) => {

@ -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",
@ -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",
@ -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",
@ -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",
@ -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",
@ -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",

Loading…
Cancel
Save