diff --git a/packages/rocketchat-channel-settings/client/views/channelSettings.html b/packages/rocketchat-channel-settings/client/views/channelSettings.html index 89f36607485b..4cd38d31a1ae 100644 --- a/packages/rocketchat-channel-settings/client/views/channelSettings.html +++ b/packages/rocketchat-channel-settings/client/views/channelSettings.html @@ -163,6 +163,23 @@ {{/if}} {{/with}} + {{#with settings.broadcast}} + {{#if canView}} +
+
+ +
+
+ {{/if}} + {{/with}} {{#with settings.joinCode}}
@@ -228,7 +245,11 @@
- + {{#if broadcast}} + + {{/if}} {{/with}} {{#each channelSettings}}
diff --git a/packages/rocketchat-channel-settings/client/views/channelSettings.js b/packages/rocketchat-channel-settings/client/views/channelSettings.js index 0753e1256228..0a20941fe5bc 100644 --- a/packages/rocketchat-channel-settings/client/views/channelSettings.js +++ b/packages/rocketchat-channel-settings/client/views/channelSettings.js @@ -225,7 +225,7 @@ Template.channelSettingsEditing.onCreated(function() { return RocketChat.roomTypes.roomTypes[room.t].allowRoomSettingChange(room, RoomSettingsEnum.READ_ONLY); }, canEdit() { - return RocketChat.authz.hasAllPermission('set-readonly', room._id); + return !room.broadcast && RocketChat.authz.hasAllPermission('set-readonly', room._id); }, save(value) { return call('saveRoomSettings', room._id, RoomSettingsEnum.READ_ONLY, value).then(() => toastr.success(TAPi18n.__('Read_only_changed_successfully'))); @@ -237,10 +237,10 @@ Template.channelSettingsEditing.onCreated(function() { isToggle: true, processing: new ReactiveVar(false), canView() { - return RocketChat.roomTypes.roomTypes[room.t].allowRoomSettingChange(room, RoomSettingsEnum.REACT_WHEN_READ_ONLY) && room.ro; + return RocketChat.roomTypes.roomTypes[room.t].allowRoomSettingChange(room, RoomSettingsEnum.REACT_WHEN_READ_ONLY); }, canEdit() { - return RocketChat.authz.hasAllPermission('set-react-when-readonly', room._id); + return !room.broadcast && RocketChat.authz.hasAllPermission('set-react-when-readonly', room._id); }, save(value) { return call('saveRoomSettings', room._id, 'reactWhenReadOnly', value).then(() => { @@ -289,6 +289,21 @@ Template.channelSettingsEditing.onCreated(function() { }); } }, + broadcast: { + type: 'boolean', + label: 'Broadcast_channel', + isToggle: true, + processing: new ReactiveVar(false), + canView() { + return RocketChat.roomTypes.roomTypes[room.t].allowRoomSettingChange(room, RoomSettingsEnum.BROADCAST); + }, + canEdit() { + return false; + }, + save() { + return Promise.resolve(); + } + }, joinCode: { type: 'text', label: 'Password', @@ -452,6 +467,9 @@ Template.channelSettingsInfo.helpers({ description() { return Template.instance().room.description; }, + broadcast() { + return Template.instance().room.broadcast; + }, announcement() { return Template.instance().room.announcement ? Template.instance().room.announcement.message : ''; }, diff --git a/packages/rocketchat-channel-settings/server/methods/saveRoomSettings.js b/packages/rocketchat-channel-settings/server/methods/saveRoomSettings.js index 5ccdced2aaa3..dab07dd47f52 100644 --- a/packages/rocketchat-channel-settings/server/methods/saveRoomSettings.js +++ b/packages/rocketchat-channel-settings/server/methods/saveRoomSettings.js @@ -31,8 +31,15 @@ Meteor.methods({ }); } - const room = RocketChat.models.Rooms.findOneById(rid); + + if (room.broadcast && (settings.readOnly || settings.reactWhenReadOnly)) { + throw new Meteor.Error('error-action-not-allowed', 'Editing readOnly/reactWhenReadOnly are not allowed for broadcast rooms', { + method: 'saveRoomSettings', + action: 'Editing_room' + }); + } + if (!room) { throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'saveRoomSettings' diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 6d3a1ff4ea6e..d7d4609d42db 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -267,6 +267,7 @@ "Application_added": "Application added", "Application_Name": "Application Name", "Application_updated": "Application updated", + "Apply": "Apply", "Apply_and_refresh_all_clients": "Apply and refresh all clients", "Archive": "Archive", "archive-room": "Archive Room", @@ -343,6 +344,8 @@ "BotHelpers_userFields": "User Fields", "BotHelpers_userFields_Description": "CSV of user fields that can be accessed by bots helper methods.", "Branch": "Branch", + "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", "Bugsnag_api_key": "Bugsnag API Key", "Build_Environment": "Build Environment", diff --git a/packages/rocketchat-lib/client/defaultTabBars.js b/packages/rocketchat-lib/client/defaultTabBars.js index a323397a7873..2b184de32f9c 100644 --- a/packages/rocketchat-lib/client/defaultTabBars.js +++ b/packages/rocketchat-lib/client/defaultTabBars.js @@ -22,7 +22,19 @@ RocketChat.TabBar.addButton({ i18nTitle: 'Members_List', icon: 'team', template: 'membersList', - order: 2 + order: 2, + condition() { + const rid = Session.get('openedRoom'); + const room = RocketChat.models.Rooms.findOne({ + _id: rid + }); + + if (!room || !room.broadcast) { + return true; + } + + return RocketChat.authz.hasRole(Meteor.userId(), ['admin', 'moderator', 'owner'], rid); + } }); RocketChat.TabBar.addButton({ diff --git a/packages/rocketchat-lib/lib/RoomTypeConfig.js b/packages/rocketchat-lib/lib/RoomTypeConfig.js index 43027c05dada..20bb932467b4 100644 --- a/packages/rocketchat-lib/lib/RoomTypeConfig.js +++ b/packages/rocketchat-lib/lib/RoomTypeConfig.js @@ -6,7 +6,8 @@ export const RoomSettingsEnum = { READ_ONLY: 'readOnly', REACT_WHEN_READ_ONLY: 'reactWhenReadOnly', ARCHIVE_OR_UNARCHIVE: 'archiveOrUnarchive', - JOIN_CODE: 'joinCode' + JOIN_CODE: 'joinCode', + BROADCAST: 'broadcast' }; export const UiTextContext = { diff --git a/packages/rocketchat-lib/lib/roomTypes/private.js b/packages/rocketchat-lib/lib/roomTypes/private.js index 5522d95a8110..e887fa392102 100644 --- a/packages/rocketchat-lib/lib/roomTypes/private.js +++ b/packages/rocketchat-lib/lib/roomTypes/private.js @@ -60,6 +60,12 @@ export class PrivateRoomType extends RoomTypeConfig { switch (setting) { case RoomSettingsEnum.JOIN_CODE: return false; + case RoomSettingsEnum.BROADCAST: + return room.broadcast; + case RoomSettingsEnum.READ_ONLY: + return !room.broadcast; + case RoomSettingsEnum.REACT_WHEN_READ_ONLY: + return !room.broadcast && room.ro; default: return true; } diff --git a/packages/rocketchat-lib/lib/roomTypes/public.js b/packages/rocketchat-lib/lib/roomTypes/public.js index fb9c9e277ad3..8569b4a7c508 100644 --- a/packages/rocketchat-lib/lib/roomTypes/public.js +++ b/packages/rocketchat-lib/lib/roomTypes/public.js @@ -1,5 +1,5 @@ /* globals openRoom */ -import {RoomTypeConfig, RoomTypeRouteConfig, UiTextContext} from '../RoomTypeConfig'; +import { RoomTypeConfig, RoomTypeRouteConfig, RoomSettingsEnum, UiTextContext } from '../RoomTypeConfig'; export class PublicRoomRoute extends RoomTypeRouteConfig { constructor() { @@ -63,12 +63,21 @@ export class PublicRoomType extends RoomTypeConfig { return RocketChat.authz.hasAtLeastOnePermission(['add-user-to-any-c-room', 'add-user-to-joined-room'], room._id); } - allowRoomSettingChange() { + enableMembersListProfile() { return true; } - enableMembersListProfile() { - return true; + allowRoomSettingChange(room, setting) { + switch (setting) { + case RoomSettingsEnum.BROADCAST: + return room.broadcast; + case RoomSettingsEnum.READ_ONLY: + return !room.broadcast; + case RoomSettingsEnum.REACT_WHEN_READ_ONLY: + return !room.broadcast && room.ro; + default: + return true; + } } getUiText(context) { diff --git a/packages/rocketchat-lib/server/functions/createRoom.js b/packages/rocketchat-lib/server/functions/createRoom.js index 5a3052429dfd..3c969410c930 100644 --- a/packages/rocketchat-lib/server/functions/createRoom.js +++ b/packages/rocketchat-lib/server/functions/createRoom.js @@ -20,6 +20,11 @@ RocketChat.createRoom = function(type, name, owner, members, readOnly, extraData members.push(owner.username); } + if (extraData.broadcast) { + readOnly = true; + delete extraData.reactWhenReadOnly; + } + const now = new Date(); let room = Object.assign({ name, diff --git a/packages/rocketchat-lib/server/methods/createChannel.js b/packages/rocketchat-lib/server/methods/createChannel.js index 1515ecd3f228..e40b06d4f727 100644 --- a/packages/rocketchat-lib/server/methods/createChannel.js +++ b/packages/rocketchat-lib/server/methods/createChannel.js @@ -1,5 +1,5 @@ Meteor.methods({ - createChannel(name, members, readOnly = false, customFields = {}) { + createChannel(name, members, readOnly = false, customFields = {}, extraData = {}) { check(name, String); check(members, Match.Optional([String])); @@ -10,7 +10,6 @@ Meteor.methods({ if (!RocketChat.authz.hasPermission(Meteor.userId(), 'create-c')) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'createChannel' }); } - - return RocketChat.createRoom('c', name, Meteor.user() && Meteor.user().username, members, readOnly, {customFields}); + return RocketChat.createRoom('c', name, Meteor.user() && Meteor.user().username, members, readOnly, {customFields, ...extraData}); } }); diff --git a/packages/rocketchat-theme/client/imports/components/header.css b/packages/rocketchat-theme/client/imports/components/header.css index b5d7e6cf847b..b05a45e848cd 100644 --- a/packages/rocketchat-theme/client/imports/components/header.css +++ b/packages/rocketchat-theme/client/imports/components/header.css @@ -9,6 +9,10 @@ margin: 0 -0.5rem; + + display: flex; + flex: 0 0 auto; + padding: var(--header-padding); white-space: nowrap; diff --git a/packages/rocketchat-theme/client/imports/forms/button.css b/packages/rocketchat-theme/client/imports/forms/button.css index b23df6a93aab..f87d596d3830 100644 --- a/packages/rocketchat-theme/client/imports/forms/button.css +++ b/packages/rocketchat-theme/client/imports/forms/button.css @@ -140,6 +140,15 @@ border-radius: 50%; } } + + &-broadcast { + margin: 10px 0; + padding: 0 1rem; + + &__icon { + margin: 0 5px; + } + } } @media (width < 780px) { diff --git a/packages/rocketchat-theme/client/imports/forms/input.css b/packages/rocketchat-theme/client/imports/forms/input.css index 30e6a5c1dd56..ded7b0869ba8 100644 --- a/packages/rocketchat-theme/client/imports/forms/input.css +++ b/packages/rocketchat-theme/client/imports/forms/input.css @@ -112,6 +112,9 @@ } &--error { + .rc-tags { + border-color: var(--input-error-color); + } & .rc-input { &__element { border-color: var(--input-error-color); diff --git a/packages/rocketchat-theme/client/imports/forms/popup-list.css b/packages/rocketchat-theme/client/imports/forms/popup-list.css index 0d0efef10fa3..41ec0e51bdb2 100644 --- a/packages/rocketchat-theme/client/imports/forms/popup-list.css +++ b/packages/rocketchat-theme/client/imports/forms/popup-list.css @@ -1,6 +1,8 @@ .rc-popup-list { position: absolute; + z-index: 1; + width: 100%; padding: 0 4px; diff --git a/packages/rocketchat-theme/client/imports/forms/switch.css b/packages/rocketchat-theme/client/imports/forms/switch.css index 503f7cceb0c7..aa614e9f0a96 100644 --- a/packages/rocketchat-theme/client/imports/forms/switch.css +++ b/packages/rocketchat-theme/client/imports/forms/switch.css @@ -28,6 +28,17 @@ &__input { display: none; + &:checked { + & + .rc-switch__button { + border-color: #26d198; + background-color: var(--rc-color-success); + + & .rc-switch__button-inside { + transform: translate3d(13px, 1px, 0); + } + } + } + &:disabled { & + .rc-switch__button { cursor: default; @@ -40,17 +51,6 @@ cursor: default; } } - - &:checked { - & + .rc-switch__button { - border-color: #26d198; - background-color: var(--rc-color-success); - - & .rc-switch__button-inside { - transform: translate3d(13px, 1px, 0); - } - } - } } &__button { diff --git a/packages/rocketchat-ui-master/public/icons.svg b/packages/rocketchat-ui-master/public/icons.svg index 7b2b722e07e7..db48fee615e3 100644 --- a/packages/rocketchat-ui-master/public/icons.svg +++ b/packages/rocketchat-ui-master/public/icons.svg @@ -99,6 +99,8 @@ + + diff --git a/packages/rocketchat-ui-message/client/message.html b/packages/rocketchat-ui-message/client/message.html index bc14740efc01..c5dd433820c2 100644 --- a/packages/rocketchat-ui-message/client/message.html +++ b/packages/rocketchat-ui-message/client/message.html @@ -111,6 +111,11 @@ {{/each}} + {{# if broadcast}} + {{#with u}} + + {{/with}} + {{/if}}
{{readOnlyDescription}}
+
+ + + {{_"Broadcast_channel_Description"}} + +
diff --git a/packages/rocketchat-ui/client/views/app/createChannel.js b/packages/rocketchat-ui/client/views/app/createChannel.js index aede83016b5a..79561556292b 100644 --- a/packages/rocketchat-ui/client/views/app/createChannel.js +++ b/packages/rocketchat-ui/client/views/app/createChannel.js @@ -83,6 +83,12 @@ Template.createChannel.helpers({ 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(); + }, + 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')); }, @@ -113,8 +119,8 @@ Template.createChannel.helpers({ extensionsConfig() { const instance = Template.instance(); return { - validations : Template.instance().extensions_validations, - submits: Template.instance().extensions_submits, + validations : instance.extensions_validations, + submits: instance.extensions_submits, change: instance.change }; }, @@ -154,6 +160,10 @@ Template.createChannel.events({ t.type.set(e.target.checked ? e.target.value : 'd'); t.change(); }, + 'change [name="broadcast"]'(e, t) { + t.broadcast.set(e.target.checked); + t.change(); + }, 'change [name="readOnly"]'(e, t) { t.readOnly.set(e.target.checked); }, @@ -188,6 +198,7 @@ Template.createChannel.events({ const name = e.target.name.value; const type = instance.type.get(); const readOnly = instance.readOnly.get(); + const broadcast = instance.broadcast.get(); const isPrivate = type === 'p'; if (instance.invalid.get() || instance.inUse.get()) { @@ -200,7 +211,7 @@ Template.createChannel.events({ const extraData = Object.keys(instance.extensions_submits) .reduce((result, key) => { return { ...result, ...instance.extensions_submits[key](instance) }; - }, {}); + }, {broadcast}); Meteor.call(isPrivate ? 'createPrivateGroup' : 'createChannel', name, instance.selectedUsers.get().map(user => user.username), readOnly, {}, extraData, function(err, result) { if (err) { @@ -235,13 +246,13 @@ Template.createChannel.onRendered(function() { users.set(usersArr); }); }); -/* global AutoComplete Deps */ +/* global AutoComplete */ 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() { - Deps.autorun(() => { + Tracker.autorun(() => { filter.exceptions = [Meteor.user().username].concat(this.selectedUsers.get().map(u => u.username)); }); this.extensions_validations = {}; @@ -249,6 +260,7 @@ Template.createChannel.onCreated(function() { this.name = new ReactiveVar(''); this.type = new ReactiveVar('p'); this.readOnly = new ReactiveVar(false); + this.broadcast = new ReactiveVar(false); this.inUse = new ReactiveVar(undefined); this.invalid = new ReactiveVar(false); this.extensions_invalid = new ReactiveVar(false); @@ -257,6 +269,14 @@ Template.createChannel.onCreated(function() { 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.userFilter = new ReactiveVar(''); this.tokensRequired = new ReactiveVar(false); this.checkChannel = _.debounce((name) => { diff --git a/packages/rocketchat-ui/client/views/app/room.html b/packages/rocketchat-ui/client/views/app/room.html index e97aa55b9c7c..540edf20e707 100644 --- a/packages/rocketchat-ui/client/views/app/room.html +++ b/packages/rocketchat-ui/client/views/app/room.html @@ -112,9 +112,7 @@ {{/if}} {{/if}} - {{#each messagesHistory}} - {{#nrr nrrargs 'message' .}}{{/nrr}} - {{/each}} + {{#each messagesHistory}}{{#nrr nrrargs 'message' .}}{{/nrr}}{{/each}} {{#if hasMoreNext}}
  • {{#if isLoading}} diff --git a/packages/rocketchat-ui/client/views/app/room.js b/packages/rocketchat-ui/client/views/app/room.js index 10833072b0fa..16cf3611c272 100644 --- a/packages/rocketchat-ui/client/views/app/room.js +++ b/packages/rocketchat-ui/client/views/app/room.js @@ -381,6 +381,10 @@ let lastTouchY = null; let lastScrollTop; Template.room.events({ + 'click .js-reply-broadcast'() { + const message = this._arguments[1]; + RocketChat.roomTypes.openRouteLink('d', {name: this._arguments[1].u.username}, {...FlowRouter.current().queryParams, reply: message._id}); + }, 'click, touchend'(e, t) { Meteor.setTimeout(() => t.sendToBottomIfNecessaryDebounced(), 100); }, diff --git a/packages/rocketchat-ui/client/views/app/roomSearch.html b/packages/rocketchat-ui/client/views/app/roomSearch.html index fe3610237e24..f8ac8eb37893 100644 --- a/packages/rocketchat-ui/client/views/app/roomSearch.html +++ b/packages/rocketchat-ui/client/views/app/roomSearch.html @@ -2,5 +2,5 @@ {{name}} diff --git a/server/publications/room.js b/server/publications/room.js index 9dfe5637f707..90216804a50a 100644 --- a/server/publications/room.js +++ b/server/publications/room.js @@ -33,7 +33,8 @@ const fields = { reactWhenReadOnly: 1, sentiment: 1, tokenpass: 1, - streamingOptions: 1 + streamingOptions: 1, + broadcast: 1 }; const roomMap = (record) => {