diff --git a/packages/rocketchat-2fa/client/accountSecurity.html b/packages/rocketchat-2fa/client/accountSecurity.html index 9ed51e09d061..ad8d6a1f231a 100644 --- a/packages/rocketchat-2fa/client/accountSecurity.html +++ b/packages/rocketchat-2fa/client/accountSecurity.html @@ -14,7 +14,7 @@

{{_ "Two-factor_authentication"}}

- WARNING: Once you enable this, you will not be able to login on the native mobile apps (Rocket.Chat+) using your password until they implement the 2FA. + {{_ "Two-factor_authentication_native_mobile_app_warning"}}
{{#if isEnabled}} diff --git a/packages/rocketchat-api/server/v1/groups.js b/packages/rocketchat-api/server/v1/groups.js index d5c11136d388..f2b38a74761b 100644 --- a/packages/rocketchat-api/server/v1/groups.js +++ b/packages/rocketchat-api/server/v1/groups.js @@ -64,6 +64,18 @@ RocketChat.API.v1.addRoute('groups.addOwner', { authRequired: true }, { } }); +RocketChat.API.v1.addRoute('groups.addLeader', { authRequired: true }, { + post() { + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); + const user = this.getUserFromParams(); + Meteor.runAsUser(this.userId, () => { + Meteor.call('addRoomLeader', findResult.rid, user._id); + }); + + return RocketChat.API.v1.success(); + } +}); + //Archives a private group only if it wasn't RocketChat.API.v1.addRoute('groups.archive', { authRequired: true }, { post() { @@ -373,6 +385,20 @@ RocketChat.API.v1.addRoute('groups.removeOwner', { authRequired: true }, { } }); +RocketChat.API.v1.addRoute('groups.removeLeader', { authRequired: true }, { + post() { + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); + + const user = this.getUserFromParams(); + + Meteor.runAsUser(this.userId, () => { + Meteor.call('removeRoomLeader', findResult.rid, user._id); + }); + + return RocketChat.API.v1.success(); + } +}); + RocketChat.API.v1.addRoute('groups.rename', { authRequired: true }, { post() { if (!this.bodyParams.name || !this.bodyParams.name.trim()) { diff --git a/packages/rocketchat-authorization/server/startup.js b/packages/rocketchat-authorization/server/startup.js index 1cf5ef494a2b..2a284ea17828 100644 --- a/packages/rocketchat-authorization/server/startup.js +++ b/packages/rocketchat-authorization/server/startup.js @@ -47,6 +47,7 @@ Meteor.startup(function() { { _id: 'set-moderator', roles : ['admin', 'owner'] }, { _id: 'set-owner', roles : ['admin', 'owner'] }, { _id: 'send-many-messages', roles : ['admin', 'bot'] }, + { _id: 'set-leader', roles : ['admin', 'owner'] }, { _id: 'unarchive-room', roles : ['admin'] }, { _id: 'view-c-room', roles : ['admin', 'user', 'bot', 'anonymous'] }, { _id: 'user-generate-access-token', roles : ['admin'] }, @@ -74,6 +75,7 @@ Meteor.startup(function() { const defaultRoles = [ { name: 'admin', scope: 'Users', description: 'Admin' }, { name: 'moderator', scope: 'Subscriptions', description: 'Moderator' }, + { name: 'leader', scope: 'Subscriptions', description: 'Leader' }, { name: 'owner', scope: 'Subscriptions', description: 'Owner' }, { name: 'user', scope: 'Users', description: '' }, { name: 'bot', scope: 'Users', description: '' }, diff --git a/packages/rocketchat-emoji-custom/client/lib/emojiCustom.js b/packages/rocketchat-emoji-custom/client/lib/emojiCustom.js index ca4f9f6bccd8..50bc7d342fff 100644 --- a/packages/rocketchat-emoji-custom/client/lib/emojiCustom.js +++ b/packages/rocketchat-emoji-custom/client/lib/emojiCustom.js @@ -1,6 +1,6 @@ /* globals getEmojiUrlFromName:true, updateEmojiCustom:true, deleteEmojiCustom:true, isSetNotNull */ RocketChat.emoji.packages.emojiCustom = { - emojiCategories: { rocket: TAPi18n.__('Custom') }, + emojiCategories: { rocket: 'Custom' }, toneList: {}, list: [], diff --git a/packages/rocketchat-emoji-emojione/emojiPicker.js b/packages/rocketchat-emoji-emojione/emojiPicker.js index 6a7a2dab3c32..f3f5b27df048 100644 --- a/packages/rocketchat-emoji-emojione/emojiPicker.js +++ b/packages/rocketchat-emoji-emojione/emojiPicker.js @@ -4,14 +4,14 @@ * Mapping category hashes into human readable and translated names */ emojiCategories = { - people: TAPi18n.__('Smileys_and_People'), - nature: TAPi18n.__('Animals_and_Nature'), - food: TAPi18n.__('Food_and_Drink'), - activity: TAPi18n.__('Activity'), - travel: TAPi18n.__('Travel_and_Places'), - objects: TAPi18n.__('Objects'), - symbols: TAPi18n.__('Symbols'), - flags: TAPi18n.__('Flags') + people: 'Smileys_and_People', + nature: 'Animals_and_Nature', + food: 'Food_and_Drink', + activity: 'Activity', + travel: 'Travel_and_Places', + objects: 'Objects', + symbols: 'Symbols', + flags: 'Flags' }; toneList = { diff --git a/packages/rocketchat-emoji/emojiPicker.js b/packages/rocketchat-emoji/emojiPicker.js index 5b618d3f52ea..2c344d627760 100644 --- a/packages/rocketchat-emoji/emojiPicker.js +++ b/packages/rocketchat-emoji/emojiPicker.js @@ -9,7 +9,8 @@ function categoryName(category) { for (const emojiPackage in RocketChat.emoji.packages) { if (RocketChat.emoji.packages.hasOwnProperty(emojiPackage)) { if (RocketChat.emoji.packages[emojiPackage].emojiCategories.hasOwnProperty(category)) { - return RocketChat.emoji.packages[emojiPackage].emojiCategories[category]; + const categoryTag = RocketChat.emoji.packages[emojiPackage].emojiCategories[category]; + return TAPi18n.__(categoryTag); } } } diff --git a/packages/rocketchat-emoji/rocketchat.js b/packages/rocketchat-emoji/rocketchat.js index 878ae629f053..33a4e0d2cb81 100644 --- a/packages/rocketchat-emoji/rocketchat.js +++ b/packages/rocketchat-emoji/rocketchat.js @@ -1,7 +1,7 @@ RocketChat.emoji = { packages: { base: { - emojiCategories: { recent: TAPi18n.__('Frequently_Used') }, + emojiCategories: { recent: 'Frequently_Used' }, emojisByCategory: { recent: [] }, diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 9444e3ff77d0..e7580d87ff9d 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -1678,6 +1678,7 @@ "Two-factor_authentication_disabled": "Two-factor authentication disabled", "Two-factor_authentication_enabled": "Two-factor authentication enabled", "Two-factor_authentication_is_currently_disabled": "Two-factor authentication is currently disabled", + "Two-factor_authentication_native_mobile_app_warning" : "WARNING: Once you enable this, you will not be able to login on the native mobile apps (Rocket.Chat+) using your password until they implement the 2FA.", "Thursday": "Thursday", "Time_in_seconds": "Time in seconds", "Title": "Title", diff --git a/packages/rocketchat-lib/server/models/Messages.js b/packages/rocketchat-lib/server/models/Messages.js index dabf18936633..7b07df2219a5 100644 --- a/packages/rocketchat-lib/server/models/Messages.js +++ b/packages/rocketchat-lib/server/models/Messages.js @@ -545,6 +545,16 @@ RocketChat.models.Messages = new class extends RocketChat.models._Base { return this.createWithTypeRoomIdMessageAndUser('owner-removed', roomId, message, user, extraData); } + createNewLeaderWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('new-leader', roomId, message, user, extraData); + } + + createLeaderRemovedWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('leader-removed', roomId, message, user, extraData); + } + createSubscriptionRoleAddedWithRoomIdAndUser(roomId, user, extraData) { const message = user.username; return this.createWithTypeRoomIdMessageAndUser('subscription-role-added', roomId, message, user, extraData); diff --git a/packages/rocketchat-theme/client/imports/base.css b/packages/rocketchat-theme/client/imports/base.css index bebf21c34e8f..4f13a28f9c97 100644 --- a/packages/rocketchat-theme/client/imports/base.css +++ b/packages/rocketchat-theme/client/imports/base.css @@ -4915,3 +4915,62 @@ a + br.only-after-a { max-height: 50px !important; } } + +.room-leader a.chat-now { + position: absolute; + right: 25px; + width: 80px; + top: 15px; + height: 30px; + border: 1px solid #eaeaea; + text-align: center; + border-radius: 4px; + font-family: arial; + font-size: 14px; + text-decoration: none; + color: #555555; + cursor: pointer; + padding-top: 4px; +} + +.room-leader a.chat-now:hover{ + color: #555555; +} + +.room-leader { + color: #555555; + border-bottom: solid 1px #eaeaea; + height: 54px; + background: #fff; +} + +.room-leader .right { + position: absolute; + top: 10px; +} + +.leader-status .status-text{ + text-transform: capitalize; + padding-left: 15px; + font-size: 14px; +} +.leader-status .color-ball { + width: 10px; + height: 10px; + position: absolute; + border-radius: 5px; + margin-top: 5px; +} + +.leader-status .color-ball.online { + background: green; +} + +.leader-status .color-ball.offline { + background: grey; +} + +.leader-info .leader-name { + font-size: 18px; +} + diff --git a/packages/rocketchat-ui/client/views/app/room.html b/packages/rocketchat-ui/client/views/app/room.html index b8c25bb60e0b..bc4e8692f87d 100644 --- a/packages/rocketchat-ui/client/views/app/room.html +++ b/packages/rocketchat-ui/client/views/app/room.html @@ -28,7 +28,7 @@

{{{RocketChatMarkdown roomTopic}}}

- + {{/unless}}
@@ -100,6 +100,26 @@

{{/unless}}
    + {{#if roomLeader != "" }} +
  • + +
    +
    +
    + {{roomLeader.name}} +
    +
    + + + {{roomLeader.status}} + +
    +
    +
    + Chat Now +
  • + {{/if}} {{#if canPreview}} {{#if hasMore}}
  • diff --git a/packages/rocketchat-ui/client/views/app/room.js b/packages/rocketchat-ui/client/views/app/room.js index e3d9fd62c231..48df300102d9 100644 --- a/packages/rocketchat-ui/client/views/app/room.js +++ b/packages/rocketchat-ui/client/views/app/room.js @@ -218,6 +218,10 @@ Template.room.helpers({ return userCanDrop(this._id); }, + roomLeader() { + return RocketChat.models.Subscriptions.findUsersInRoles('leader', this._id).fetch()[0]; + }, + canPreview() { const room = Session.get(`roomData${ this._id }`); if (room && room.t !== 'c') { diff --git a/server/methods/addRoomLeader.js b/server/methods/addRoomLeader.js new file mode 100644 index 000000000000..b814a797e0c3 --- /dev/null +++ b/server/methods/addRoomLeader.js @@ -0,0 +1,67 @@ +Meteor.methods({ + addRoomLeader(rid, userId) { + check(rid, String); + check(userId, String); + + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'addRoomLeader' + }); + } + + if (!RocketChat.authz.hasPermission(Meteor.userId(), 'set-leader', rid)) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { + method: 'addRoomLeader' + }); + } + + const user = RocketChat.models.Users.findOneById(userId); + + if (!user || !user.username) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'addRoomLeader' + }); + } + + const subscription = RocketChat.models.Subscriptions.findOneByRoomIdAndUserId(rid, user._id); + + if (!subscription) { + throw new Meteor.Error('error-user-not-in-room', 'User is not in this room', { + method: 'addRoomLeader' + }); + } + + if (Array.isArray(subscription.roles) === true && subscription.roles.includes('leader') === true) { + throw new Meteor.Error('error-user-already-leader', 'User is already a leader', { + method: 'addRoomLeader' + }); + } + + RocketChat.models.Subscriptions.addRoleById(subscription._id, 'leader'); + + const fromUser = RocketChat.models.Users.findOneById(Meteor.userId()); + + RocketChat.models.Messages.createSubscriptionRoleAddedWithRoomIdAndUser(rid, user, { + u: { + _id: fromUser._id, + username: fromUser.username + }, + role: 'leader' + }); + + if (RocketChat.settings.get('UI_DisplayRoles')) { + RocketChat.Notifications.notifyLogged('roles-change', { + type: 'added', + _id: 'leader', + u: { + _id: user._id, + username: user.username, + name: user.name + }, + scope: rid + }); + } + + return true; + } +}); diff --git a/server/methods/removeRoomLeader.js b/server/methods/removeRoomLeader.js new file mode 100644 index 000000000000..4f3f61d5f086 --- /dev/null +++ b/server/methods/removeRoomLeader.js @@ -0,0 +1,67 @@ +Meteor.methods({ + removeRoomLeader(rid, userId) { + check(rid, String); + check(userId, String); + + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'removeRoomLeader' + }); + } + + if (!RocketChat.authz.hasPermission(Meteor.userId(), 'set-leader', rid)) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { + method: 'removeRoomLeader' + }); + } + + const user = RocketChat.models.Users.findOneById(userId); + + if (!user || !user.username) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'removeRoomLeader' + }); + } + + const subscription = RocketChat.models.Subscriptions.findOneByRoomIdAndUserId(rid, user._id); + + if (!subscription) { + throw new Meteor.Error('error-user-not-in-room', 'User is not in this room', { + method: 'removeRoomLeader' + }); + } + + if (Array.isArray(subscription.roles) === true && subscription.roles.includes('leader') === false) { + throw new Meteor.Error('error-user-already-leader', 'User is not a leader', { + method: 'removeRoomLeader' + }); + } + + RocketChat.models.Subscriptions.removeRoleById(subscription._id, 'leader'); + + const fromUser = RocketChat.models.Users.findOneById(Meteor.userId()); + + RocketChat.models.Messages.createSubscriptionRoleRemovedWithRoomIdAndUser(rid, user, { + u: { + _id: fromUser._id, + username: fromUser.username + }, + role: 'leader' + }); + + if (RocketChat.settings.get('UI_DisplayRoles')) { + RocketChat.Notifications.notifyLogged('roles-change', { + type: 'removed', + _id: 'leader', + u: { + _id: user._id, + username: user.username, + name: user.name + }, + scope: rid + }); + } + + return true; + } +});