From 741608e37b33412bb4ba4db0e3a6fcce2d0305dd Mon Sep 17 00:00:00 2001 From: Daniel Schreiber Date: Wed, 19 Jul 2017 14:21:31 +0300 Subject: [PATCH] Create new role of leader. Admin can set or remove a leader. Display leader at top of chat when user scrolls up, along with a "Chat Now" button which opens a new direct message with the leader. --- packages/rocketchat-api/server/v1/groups.js | 26 +++++++ .../server/startup.js | 2 + packages/rocketchat-i18n/i18n/en.i18n.json | 4 ++ .../rocketchat-lib/server/models/Messages.js | 10 +++ .../server/models/Subscriptions.js | 3 +- .../rocketchat-theme/client/imports/base.css | 71 +++++++++++++++++++ .../client/flexTabBar.js | 3 + .../client/tabs/userInfo.html | 7 ++ .../client/tabs/userInfo.js | 47 ++++++++++++ .../rocketchat-ui/client/lib/RoomManager.js | 2 +- .../rocketchat-ui/client/views/app/room.html | 20 +++++- .../rocketchat-ui/client/views/app/room.js | 26 ++++++- server/methods/addRoomLeader.js | 67 +++++++++++++++++ server/methods/removeRoomLeader.js | 67 +++++++++++++++++ 14 files changed, 351 insertions(+), 4 deletions(-) create mode 100644 server/methods/addRoomLeader.js create mode 100644 server/methods/removeRoomLeader.js diff --git a/packages/rocketchat-api/server/v1/groups.js b/packages/rocketchat-api/server/v1/groups.js index d5c11136d38..f2b38a74761 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 1cf5ef494a2..d372b117ba0 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-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index e7580d87ff9..d532cfda72a 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -1397,6 +1397,7 @@ "Remove_Admin": "Remove Admin", "Remove_as_moderator": "Remove as moderator", "Remove_as_owner": "Remove as owner", + "Remove_as_leader": "Remove as leader", "Remove_custom_oauth": "Remove custom oauth", "Remove_from_room": "Remove from room", "Remove_last_admin": "Removing last admin", @@ -1509,6 +1510,7 @@ "Service_account_key": "Service account key", "Set_as_moderator": "Set as moderator", "Set_as_owner": "Set as owner", + "Set_as_leader": "Set as leader", "Settings": "Settings", "Settings_updated": "Settings updated", "Share_Location_Title": "Share Location?", @@ -1741,8 +1743,10 @@ "Use_User_Preferences_or_Global_Settings": "Use User Preferences or Global Settings", "User__username__is_now_a_moderator_of__room_name_": "User __username__ is now a moderator of __room_name__", "User__username__is_now_a_owner_of__room_name_": "User __username__ is now a owner of __room_name__", + "User__username__is_now_a_leader_of__room_name_": "User __username__ is now a leader of __room_name__", "User__username__removed_from__room_name__moderators": "User __username__ removed from __room_name__ moderators", "User__username__removed_from__room_name__owners": "User __username__ removed from __room_name__ owners", + "User__username__removed_from__room_name__leaders": "User __username__ removed from __room_name__ leaders", "User_added": "User added", "User_added_by": "User __user_added__ added by __user_by__.", "User_added_successfully": "User added successfully", diff --git a/packages/rocketchat-lib/server/models/Messages.js b/packages/rocketchat-lib/server/models/Messages.js index dabf1893663..7b07df2219a 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-lib/server/models/Subscriptions.js b/packages/rocketchat-lib/server/models/Subscriptions.js index 775065ab648..51cbc252472 100644 --- a/packages/rocketchat-lib/server/models/Subscriptions.js +++ b/packages/rocketchat-lib/server/models/Subscriptions.js @@ -546,7 +546,8 @@ class ModelSubscriptions extends RocketChat.models._Base { t: room.t, u: { _id: user._id, - username: user.username + username: user.username, + name: user.name } }; diff --git a/packages/rocketchat-theme/client/imports/base.css b/packages/rocketchat-theme/client/imports/base.css index 63469092c38..b310a40119f 100644 --- a/packages/rocketchat-theme/client/imports/base.css +++ b/packages/rocketchat-theme/client/imports/base.css @@ -4915,3 +4915,74 @@ 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-container { + height: 54px; +} + +.room-leader { + position: fixed; + top: 60px; + left: 0; + z-index: 1; + background: #ffffff; + width: calc(100% - 40px); + color: #555555; + border-bottom: solid 1px #eaeaea; + height: 54px; +} + +.room-leader .thumb { + top: 6px; +} + +.room-leader .right { + position: absolute; + top: 10px; + left: 70px; +} + +.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; + background: grey; +} + +.leader-status .color-ball.online { + background: green; +} + +.leader-info .leader-name { + font-size: 18px; +} + diff --git a/packages/rocketchat-ui-flextab/client/flexTabBar.js b/packages/rocketchat-ui-flextab/client/flexTabBar.js index a6627f9b1c3..18ba2ce055d 100644 --- a/packages/rocketchat-ui-flextab/client/flexTabBar.js +++ b/packages/rocketchat-ui-flextab/client/flexTabBar.js @@ -41,9 +41,12 @@ Template.flexTabBar.helpers({ Template.flexTabBar.events({ 'click .tab-button'(e, instance) { e.preventDefault(); + const $flexTab = $('.flex-tab-container .flex-tab'); if (instance.tabBar.getState() === 'opened' && instance.tabBar.getTemplate() === this.template) { + $flexTab.attr('template', ''); return instance.tabBar.close(); } else { + $flexTab.attr('template', this.template); return instance.tabBar.open(this); } } diff --git a/packages/rocketchat-ui-flextab/client/tabs/userInfo.html b/packages/rocketchat-ui-flextab/client/tabs/userInfo.html index 4b1dfaffa3c..517b9a32fc7 100644 --- a/packages/rocketchat-ui-flextab/client/tabs/userInfo.html +++ b/packages/rocketchat-ui-flextab/client/tabs/userInfo.html @@ -63,6 +63,13 @@ {{/if}} {{/if}} + {{#if canSetLeader}} + {{#if isLeader}} + + {{else}} + + {{/if}} + {{/if}} {{#if canSetModerator}} {{#if isModerator}} diff --git a/packages/rocketchat-ui-flextab/client/tabs/userInfo.js b/packages/rocketchat-ui-flextab/client/tabs/userInfo.js index b14ecbcd93a..1798fff34be 100644 --- a/packages/rocketchat-ui-flextab/client/tabs/userInfo.js +++ b/packages/rocketchat-ui-flextab/client/tabs/userInfo.js @@ -101,6 +101,10 @@ Template.userInfo.helpers({ return RocketChat.authz.hasAllPermission('set-owner', Session.get('openedRoom')); }, + canSetLeader() { + return RocketChat.authz.hasAllPermission('set-leader', Session.get('openedRoom')); + }, + isOwner() { const user = Template.instance().user.get(); if (user && user._id) { @@ -108,6 +112,13 @@ Template.userInfo.helpers({ } }, + isLeader() { + const user = Template.instance().user.get(); + if (user && user._id) { + return !!RoomRoles.findOne({ rid: Session.get('openedRoom'), 'u._id': user._id, roles: 'leader' }); + } + }, + user() { return Template.instance().user.get(); }, @@ -367,6 +378,42 @@ Template.userInfo.events({ } }, + 'click .set-leader'(e, t) { + e.preventDefault(); + const user = t.user.get(); + if (user) { + const userLeader = RoomRoles.findOne({ rid: Session.get('openedRoom'), 'u._id': user._id, roles: 'leader' }, { fields: { _id: 1 } }); + if (userLeader == null) { + return Meteor.call('addRoomLeader', Session.get('openedRoom'), user._id, (err) => { + if (err) { + return handleError(err); + } + + const room = ChatRoom.findOne(Session.get('openedRoom')); + return toastr.success(TAPi18n.__('User__username__is_now_a_leader_of__room_name_', { username: this.username, room_name: room.name })); + }); + } + } + }, + + 'click .unset-leader'(e, t) { + e.preventDefault(); + const user = t.user.get(); + if (user) { + const userLeader = RoomRoles.findOne({ rid: Session.get('openedRoom'), 'u._id': user._id, roles: 'leader' }, { fields: { _id: 1 } }); + if (userLeader != null) { + return Meteor.call('removeRoomLeader', Session.get('openedRoom'), user._id, (err) => { + if (err) { + return handleError(err); + } + + const room = ChatRoom.findOne(Session.get('openedRoom')); + return toastr.success(TAPi18n.__('User__username__removed_from__room_name__leaders', { username: this.username, room_name: room.name })); + }); + } + } + }, + 'click .deactivate'(e, instance) { e.stopPropagation(); e.preventDefault(); diff --git a/packages/rocketchat-ui/client/lib/RoomManager.js b/packages/rocketchat-ui/client/lib/RoomManager.js index 25e8ef19f1a..ca5fa5670f0 100644 --- a/packages/rocketchat-ui/client/lib/RoomManager.js +++ b/packages/rocketchat-ui/client/lib/RoomManager.js @@ -50,7 +50,7 @@ const RoomManager = new function() { msg.roles = _.union.apply(_.union, roles); ChatMessage.upsert({ _id: msg._id }, msg); } - + msg.name = room.name; Meteor.defer(() => RoomManager.updateMentionsMarksOfRoom(typeName)); RocketChat.callbacks.run('streamMessage', msg); diff --git a/packages/rocketchat-ui/client/views/app/room.html b/packages/rocketchat-ui/client/views/app/room.html index b8c25bb60e0..b80bf8cca6d 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,24 @@ {{/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 e3d9fd62c23..12a059db559 100644 --- a/packages/rocketchat-ui/client/views/app/room.js +++ b/packages/rocketchat-ui/client/views/app/room.js @@ -97,6 +97,18 @@ Template.room.helpers({ return Session.get('uploading'); }, + roomLeader() { + const roles = RoomRoles.find({rid: this._id, roles: 'leader'}).fetch(); + if (roles.length > 0) { + const u = roles[0].u; + if (u._id === Meteor.user()._id) { return null; } + const currUser = RocketChat.models.Users.find({ _id: u._id}).fetch(); + u['status'] = currUser.length > 0 ? 'online' : 'offline'; + return u; + } + return null; + }, + roomName() { const roomData = Session.get(`roomData${ this._id }`); if (!roomData) { return ''; } @@ -241,6 +253,7 @@ let isSocialSharingOpen = false; let touchMoved = false; let lastTouchX = null; let lastTouchY = null; +let lastScrollTop; Template.room.events({ 'click, touchend'(e, t) { @@ -255,6 +268,16 @@ Template.room.events({ } }, + 'scroll .messages-box .wrapper'() { + const $wrapper = $('.messages-box .wrapper'); + if ($wrapper.scrollTop() < lastScrollTop) { + $('.room-leader').removeClass('hidden'); + } else if ($wrapper.scrollTop() > $('.room-leader-container').height()) { + $('.room-leader').addClass('hidden'); + } + lastScrollTop = $wrapper.scrollTop(); + }, + 'touchstart .message'(e, t) { const touches = e.originalEvent.touches; if (touches && touches.length) { @@ -418,7 +441,7 @@ Template.room.events({ }, 'click .user-card-message'(e, instance) { - if (!Meteor.userId()) { + if (!Meteor.userId() || !this._arguments) { return; } const roomData = Session.get(`roomData${ this._arguments[1].rid }`); @@ -805,6 +828,7 @@ Template.room.onRendered(function() { $('.flex-tab-bar').on('click', (/*e, t*/) => Meteor.setTimeout(() => template.sendToBottomIfNecessaryDebounced(), 50) ); + lastScrollTop = $('.messages-box .wrapper').scrollTop(); const rtl = $('html').hasClass('rtl'); diff --git a/server/methods/addRoomLeader.js b/server/methods/addRoomLeader.js new file mode 100644 index 00000000000..b814a797e0c --- /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 00000000000..011eb8c7434 --- /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-not-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; + } +});