diff --git a/.meteor/packages b/.meteor/packages
index b56ad45f922..a97cca862ec 100644
--- a/.meteor/packages
+++ b/.meteor/packages
@@ -62,6 +62,7 @@ rocketchat:slashcommands-invite
rocketchat:slashcommands-join
rocketchat:slashcommands-leave
rocketchat:slashcommands-kick
+rocketchat:slashcommands-mute
rocketchat:spotify
rocketchat:statistics
rocketchat:theme
diff --git a/.meteor/versions b/.meteor/versions
index 5ec2cf20d5f..65d76ada12a 100644
--- a/.meteor/versions
+++ b/.meteor/versions
@@ -152,6 +152,7 @@ rocketchat:slashcommands-invite@0.0.1
rocketchat:slashcommands-join@0.0.1
rocketchat:slashcommands-kick@0.0.1
rocketchat:slashcommands-leave@0.0.1
+rocketchat:slashcommands-mute@0.0.1
rocketchat:spotify@0.0.1
rocketchat:statistics@0.0.1
rocketchat:theme@0.0.1
diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json
index e761fe82684..92ba3902f8e 100644
--- a/i18n/en.i18n.json
+++ b/i18n/en.i18n.json
@@ -292,6 +292,8 @@
"More_unreads" : "More unreads",
"Msgs" : "Msgs",
"multi" : "multi",
+ "Mute_user" : "Mute user",
+ "Unmute_user" : "Unmute user",
"My_Account" : "My Account",
"n_messages" : "%s messages",
"Name" : "Name",
@@ -496,9 +498,13 @@
"User_left_female" : "Has left the channel.",
"User_left_male" : "Has left the channel.",
"User_logged_out" : "User is logged out",
+ "User_muted_in_room" : "User muted in room",
+ "User_unmuted_in_room" : "User unmuted in room",
"User_not_found_or_incorrect_password" : "User not found or incorrect password",
"User_or_channel_name" : "User or channel name",
"User_removed_by" : "User __user_removed__ removed by __user_by__.",
+ "User_muted_by" : "User __user_muted__ muted by __user_by__.",
+ "User_unmuted_by" : "User __user_unmuted__ unmuted by __user_by__.",
"User_removed_from_room" : "The user has been removed from the room",
"User_Settings" : "User Settings",
"User_updated_successfully" : "User updated successfully",
@@ -526,6 +532,7 @@
"you_are_in_preview_mode_of" : "You are in preview mode of channel #__room_name__",
"You_can_change_a_different_avatar_too" : "You can change a different avatar too",
"You_can_use_an_emoji_as_avatar" : "You can use an emoji as avatar",
+ "You_have_been_muted" : "You have been muted and cannot speak in this room",
"You_need_confirm_email" : "You need to confirm your email to login!",
"You_need_install_an_extension_to_allow_screen_sharing" : "You need install an extension to allow screen sharing",
"You_should_name_it_to_easily_manage_your_integrations" : "You should name it to easily manage your integrations.",
diff --git a/packages/rocketchat-authorization/server/startup.coffee b/packages/rocketchat-authorization/server/startup.coffee
index d7043df9ba7..1fc3643b4d2 100644
--- a/packages/rocketchat-authorization/server/startup.coffee
+++ b/packages/rocketchat-authorization/server/startup.coffee
@@ -63,6 +63,9 @@ Meteor.startup ->
{ _id: 'remove-user',
roles : ['admin', 'site-moderator', 'moderator']}
+ { _id: 'mute-user',
+ roles : ['admin', 'site-moderator', 'moderator']}
+
{ _id: 'ban-user',
roles : ['admin', 'site-moderator', 'moderator']}
diff --git a/packages/rocketchat-lib/client/MessageTypes.coffee b/packages/rocketchat-lib/client/MessageTypes.coffee
index 3e374f4d7ca..3c6f3cea63a 100644
--- a/packages/rocketchat-lib/client/MessageTypes.coffee
+++ b/packages/rocketchat-lib/client/MessageTypes.coffee
@@ -68,3 +68,17 @@ Meteor.startup ->
id: 'rtc'
render: (message) ->
RocketChat.callbacks.run 'renderRtcMessage', message
+
+ RocketChat.MessageTypes.registerType
+ id: 'user-muted'
+ system: true
+ message: 'User_muted_by'
+ data: (message) ->
+ return { user_muted: message.msg, user_by: message.u.username }
+
+ RocketChat.MessageTypes.registerType
+ id: 'user-unmuted'
+ system: true
+ message: 'User_unmuted_by'
+ data: (message) ->
+ return { user_unmuted: message.msg, user_by: message.u.username }
diff --git a/packages/rocketchat-lib/server/methods/sendMessage.coffee b/packages/rocketchat-lib/server/methods/sendMessage.coffee
index 527470eeb9e..6e55243fb45 100644
--- a/packages/rocketchat-lib/server/methods/sendMessage.coffee
+++ b/packages/rocketchat-lib/server/methods/sendMessage.coffee
@@ -15,6 +15,17 @@ Meteor.methods
if not room
return false
+ subscription = RocketChat.models.Subscriptions.findOneByRoomIdAndUserId room._id, user._id
+ console.log subscription
+ if not subscription or subscription.mute is true
+ RocketChat.Notifications.notifyUser Meteor.userId(), 'message', {
+ _id: Random.id()
+ rid: room._id
+ ts: new Date
+ msg: TAPi18n.__('You_have_been_muted', {}, user.language);
+ }
+ return false
+
RocketChat.sendMessage user, message, room, options
# Limit a user to sending 5 msgs/second
diff --git a/packages/rocketchat-lib/server/models/Messages.coffee b/packages/rocketchat-lib/server/models/Messages.coffee
index b7449cddf47..c1ef1ba654c 100644
--- a/packages/rocketchat-lib/server/models/Messages.coffee
+++ b/packages/rocketchat-lib/server/models/Messages.coffee
@@ -272,6 +272,14 @@ RocketChat.models.Messages = new class extends RocketChat.models._Base
createCommandWithRoomIdAndUser: (command, roomId, user, extraData) ->
return @createWithTypeRoomIdMessageAndUser 'command', roomId, command, user, extraData
+ createUserMutedWithRoomIdAndUser: (roomId, user, extraData) ->
+ message = user.username
+ return @createWithTypeRoomIdMessageAndUser 'user-muted', roomId, message, user, extraData
+
+ createUserUnmutedWithRoomIdAndUser: (roomId, user, extraData) ->
+ message = user.username
+ return @createWithTypeRoomIdMessageAndUser 'user-unmuted', roomId, message, user, extraData
+
# REMOVE
removeById: (_id) ->
query =
diff --git a/packages/rocketchat-lib/server/models/Subscriptions.coffee b/packages/rocketchat-lib/server/models/Subscriptions.coffee
index 20910f4e03d..5fb46c37e1f 100644
--- a/packages/rocketchat-lib/server/models/Subscriptions.coffee
+++ b/packages/rocketchat-lib/server/models/Subscriptions.coffee
@@ -210,6 +210,27 @@ RocketChat.models.Subscriptions = new class extends RocketChat.models._Base
return @update query, update, { multi: true }
+ muteUserByRoomIdAndUserId: (roomId, userId) ->
+ query =
+ rid: roomId
+ "u._id": userId
+
+ update =
+ $set:
+ mute: true
+
+ return @update query, update
+
+ unmuteUserByRoomIdAndUserId: (roomId, userId) ->
+ query =
+ rid: roomId
+ "u._id": userId
+
+ update =
+ $unset:
+ mute: true
+
+ return @update query, update
# INSERT
createWithRoomAndUser: (room, user, extraData) ->
diff --git a/packages/rocketchat-slashcommands-mute/client/mute.coffee b/packages/rocketchat-slashcommands-mute/client/mute.coffee
new file mode 100644
index 00000000000..d812e0cc179
--- /dev/null
+++ b/packages/rocketchat-slashcommands-mute/client/mute.coffee
@@ -0,0 +1,3 @@
+RocketChat.slashCommands.add 'mute', null,
+ description: TAPi18n.__ 'Mute_someone_in_room'
+ params: '@username'
diff --git a/packages/rocketchat-slashcommands-mute/client/unmute.coffee b/packages/rocketchat-slashcommands-mute/client/unmute.coffee
new file mode 100644
index 00000000000..4d59ccb7de4
--- /dev/null
+++ b/packages/rocketchat-slashcommands-mute/client/unmute.coffee
@@ -0,0 +1,3 @@
+RocketChat.slashCommands.add 'unmute', null,
+ description: TAPi18n.__ 'Unmute_someone_in_room'
+ params: '@username'
diff --git a/packages/rocketchat-slashcommands-mute/i18n/en.i18n.json b/packages/rocketchat-slashcommands-mute/i18n/en.i18n.json
new file mode 100644
index 00000000000..f20cd657298
--- /dev/null
+++ b/packages/rocketchat-slashcommands-mute/i18n/en.i18n.json
@@ -0,0 +1,6 @@
+{
+ "Username_doesnt_exist" : "The username `#%s` doesn't exist.",
+ "Username_is_not_in_this_room" : "The user `#%s` is not in this room.",
+ "Mute_someone_in_room" : "Mute someone in the room",
+ "Unmute_someone_in_room" : "Unmute someone in the room"
+}
diff --git a/packages/rocketchat-slashcommands-mute/package.js b/packages/rocketchat-slashcommands-mute/package.js
new file mode 100644
index 00000000000..adc64c1d6dd
--- /dev/null
+++ b/packages/rocketchat-slashcommands-mute/package.js
@@ -0,0 +1,39 @@
+Package.describe({
+ name: 'rocketchat:slashcommands-mute',
+ version: '0.0.1',
+ summary: 'Command handler for the /mute command',
+ git: ''
+});
+
+Package.onUse(function(api) {
+
+ api.versionsFrom('1.0');
+
+ api.use([
+ 'coffeescript',
+ 'check',
+ 'rocketchat:lib@0.0.1'
+ ]);
+
+ api.addFiles('client/mute.coffee', 'client');
+ api.addFiles('client/unmute.coffee', 'client');
+ api.addFiles('server/mute.coffee', 'server');
+ api.addFiles('server/unmute.coffee', 'server');
+
+ // TAPi18n
+ api.use('templating', 'client');
+ var _ = Npm.require('underscore');
+ var fs = Npm.require('fs');
+ tapi18nFiles = _.compact(_.map(fs.readdirSync('packages/rocketchat-slashcommands-mute/i18n'), function(filename) {
+ if (fs.statSync('packages/rocketchat-slashcommands-mute/i18n/' + filename).size > 16) {
+ return 'i18n/' + filename;
+ }
+ }));
+ api.use('tap:i18n@1.6.1', ['client', 'server']);
+ api.imply('tap:i18n');
+ api.addFiles(tapi18nFiles, ['client', 'server']);
+});
+
+Package.onTest(function(api) {
+
+});
diff --git a/packages/rocketchat-slashcommands-mute/server/mute.coffee b/packages/rocketchat-slashcommands-mute/server/mute.coffee
new file mode 100644
index 00000000000..d8bc3d3bc51
--- /dev/null
+++ b/packages/rocketchat-slashcommands-mute/server/mute.coffee
@@ -0,0 +1,40 @@
+###
+# Mute is a named function that will replace /mute commands
+###
+
+class Mute
+ constructor: (command, params, item) ->
+ if command isnt 'mute' or not Match.test params, String
+ return
+
+ username = params.trim()
+ if username is ''
+ return
+
+ username = username.replace('@', '')
+
+ user = Meteor.users.findOne Meteor.userId()
+ mutedUser = RocketChat.models.Users.findOneByUsername username
+ room = RocketChat.models.Rooms.findOneById item.rid
+
+ if not mutedUser?
+ RocketChat.Notifications.notifyUser Meteor.userId(), 'message', {
+ _id: Random.id()
+ rid: item.rid
+ ts: new Date
+ msg: TAPi18n.__('Username_doesnt_exist', { postProcess: 'sprintf', sprintf: [ username ] }, user.language);
+ }
+ return
+
+ if username not in (room.usernames or [])
+ RocketChat.Notifications.notifyUser Meteor.userId(), 'message', {
+ _id: Random.id()
+ rid: item.rid
+ ts: new Date
+ msg: TAPi18n.__('Username_is_not_in_this_room', { postProcess: 'sprintf', sprintf: [ username ] }, user.language);
+ }
+ return
+
+ Meteor.call 'muteUserInRoom', { rid: item.rid, username: username }
+
+RocketChat.slashCommands.add 'mute', Mute
diff --git a/packages/rocketchat-slashcommands-mute/server/unmute.coffee b/packages/rocketchat-slashcommands-mute/server/unmute.coffee
new file mode 100644
index 00000000000..e3b6ee25d82
--- /dev/null
+++ b/packages/rocketchat-slashcommands-mute/server/unmute.coffee
@@ -0,0 +1,40 @@
+###
+# Unmute is a named function that will replace /unmute commands
+###
+
+class Unmute
+ constructor: (command, params, item) ->
+ if command isnt 'unmute' or not Match.test params, String
+ return
+
+ username = params.trim()
+ if username is ''
+ return
+
+ username = username.replace('@', '')
+
+ user = Meteor.users.findOne Meteor.userId()
+ unmutedUser = RocketChat.models.Users.findOneByUsername username
+ room = RocketChat.models.Rooms.findOneById item.rid
+
+ if not unmutedUser?
+ RocketChat.Notifications.notifyUser Meteor.userId(), 'message', {
+ _id: Random.id()
+ rid: item.rid
+ ts: new Date
+ msg: TAPi18n.__('Username_doesnt_exist', { postProcess: 'sprintf', sprintf: [ username ] }, user.language);
+ }
+ return
+
+ if username not in (room.usernames or [])
+ RocketChat.Notifications.notifyUser Meteor.userId(), 'message', {
+ _id: Random.id()
+ rid: item.rid
+ ts: new Date
+ msg: TAPi18n.__('Username_is_not_in_this_room', { postProcess: 'sprintf', sprintf: [ username ] }, user.language);
+ }
+ return
+
+ Meteor.call 'unmuteUserInRoom', { rid: item.rid, username: username }
+
+RocketChat.slashCommands.add 'unmute', Unmute
diff --git a/packages/rocketchat-ui-flextab/flex-tab/tabs/userInfo.coffee b/packages/rocketchat-ui-flextab/flex-tab/tabs/userInfo.coffee
index 00d323ef43c..caeb439bdce 100644
--- a/packages/rocketchat-ui-flextab/flex-tab/tabs/userInfo.coffee
+++ b/packages/rocketchat-ui-flextab/flex-tab/tabs/userInfo.coffee
@@ -36,6 +36,12 @@ Template.userInfo.helpers
canRemoveUser: ->
return RocketChat.authz.hasAllPermission('remove-user', Session.get('openedRoom'))
+ canMuteUser: ->
+ return RocketChat.authz.hasAllPermission('mute-user', Session.get('openedRoom'))
+
+ userMuted: ->
+ return ChatSubscription.findOne({rid: Session.get('openedRoom')})?.mute is true
+
Template.userInfo.events
'click .pvt-msg': (e) ->
Meteor.call 'createDirectMessage', Session.get('showUserInfo'), (error, result) ->
@@ -88,6 +94,29 @@ Template.userInfo.events
else
toastr.error(TAPi18n.__ 'Not_allowed')
+ 'click .mute-user': (e, t) ->
+ e.preventDefault()
+ rid = Session.get('openedRoom')
+ room = ChatRoom.findOne rid
+ if RocketChat.authz.hasAllPermission('mute-user', rid)
+ Meteor.call 'muteUserInRoom', { rid: rid, username: @user.username }, (err, result) ->
+ if err
+ return toastr.error(err.reason or err.message)
+ toastr.success TAPi18n.__ 'User_muted_in_room'
+ else
+ toastr.error(TAPi18n.__ 'Not_allowed')
+
+ 'click .unmute-user': (e, t) ->
+ e.preventDefault()
+ rid = Session.get('openedRoom')
+ room = ChatRoom.findOne rid
+ if RocketChat.authz.hasAllPermission('mute-user', rid)
+ Meteor.call 'unmuteUserInRoom', { rid: rid, username: @user.username }, (err, result) ->
+ if err
+ return toastr.error(err.reason or err.message)
+ toastr.success TAPi18n.__ 'User_unmuted_in_room'
+ else
+ toastr.error(TAPi18n.__ 'Not_allowed')
Template.userInfo.onCreated ->
@now = new ReactiveVar moment()
diff --git a/packages/rocketchat-ui-flextab/flex-tab/tabs/userInfo.html b/packages/rocketchat-ui-flextab/flex-tab/tabs/userInfo.html
index 85fa40e7a79..bb18b652ccd 100644
--- a/packages/rocketchat-ui-flextab/flex-tab/tabs/userInfo.html
+++ b/packages/rocketchat-ui-flextab/flex-tab/tabs/userInfo.html
@@ -37,6 +37,13 @@
{{#if canRemoveUser}}
{{/if}}
+ {{#if canMuteUser}}
+ {{#if userMuted}}
+
+ {{else}}
+
+ {{/if}}
+ {{/if}}
{{/if}}
{{/if}}
diff --git a/packages/rocketchat-ui-message/message/popup/messagePopupConfig.coffee b/packages/rocketchat-ui-message/message/popup/messagePopupConfig.coffee
index e8a085eebea..022ff911f78 100644
--- a/packages/rocketchat-ui-message/message/popup/messagePopupConfig.coffee
+++ b/packages/rocketchat-ui-message/message/popup/messagePopupConfig.coffee
@@ -88,7 +88,7 @@ Template.messagePopupConfig.helpers
commands.push
_id: command
params: item.params
- description: item.description
+ description: TAPi18n.__ item.description
if commands.length > 10
break
diff --git a/packages/rocketchat-ui/views/app/room.coffee b/packages/rocketchat-ui/views/app/room.coffee
index e382d70faa3..8dafd2e346c 100644
--- a/packages/rocketchat-ui/views/app/room.coffee
+++ b/packages/rocketchat-ui/views/app/room.coffee
@@ -540,7 +540,7 @@ Template.room.onRendered ->
firstMessage = ChatMessage.findOne firstMessageOnScreen.id
if firstMessage?
subscription = ChatSubscription.findOne rid: template.data._id
- template.unreadCount.set ChatMessage.find({rid: template.data._id, ts: {$lt: firstMessage.ts, $gt: subscription.ls}}).count()
+ template.unreadCount.set ChatMessage.find({rid: template.data._id, ts: {$lt: firstMessage.ts, $gt: subscription?.ls}}).count()
else
template.unreadCount.set 0
, 300
diff --git a/server/methods/muteUserInRoom.coffee b/server/methods/muteUserInRoom.coffee
new file mode 100644
index 00000000000..7179738b3e9
--- /dev/null
+++ b/server/methods/muteUserInRoom.coffee
@@ -0,0 +1,26 @@
+Meteor.methods
+ muteUserInRoom: (data) ->
+ fromId = Meteor.userId()
+ console.log '[methods] muteUserInRoom -> '.green, 'fromId:', fromId, 'data:', data
+
+ check(data, Match.ObjectIncluding({ rid: String, username: String }))
+
+ unless RocketChat.authz.hasPermission(fromId, 'mute-user', data.rid)
+ throw new Meteor.Error 'not-allowed', 'Not allowed'
+
+ room = RocketChat.models.Rooms.findOneById data.rid
+
+ if data.username not in (room?.usernames or [])
+ throw new Meteor.Error 'not-in-room', 'User is not in this room'
+
+ mutedUser = RocketChat.models.Users.findOneByUsername data.username
+
+ RocketChat.models.Subscriptions.muteUserByRoomIdAndUserId data.rid, mutedUser._id
+
+ fromUser = RocketChat.models.Users.findOneById fromId
+ RocketChat.models.Messages.createUserMutedWithRoomIdAndUser data.rid, mutedUser,
+ u:
+ _id: fromUser._id
+ username: fromUser.username
+
+ return true
diff --git a/server/methods/unmuteUserInRoom.coffee b/server/methods/unmuteUserInRoom.coffee
new file mode 100644
index 00000000000..adfbaec666e
--- /dev/null
+++ b/server/methods/unmuteUserInRoom.coffee
@@ -0,0 +1,26 @@
+Meteor.methods
+ unmuteUserInRoom: (data) ->
+ fromId = Meteor.userId()
+ console.log '[methods] unmuteUserInRoom -> '.green, 'fromId:', fromId, 'data:', data
+
+ check(data, Match.ObjectIncluding({ rid: String, username: String }))
+
+ unless RocketChat.authz.hasPermission(fromId, 'mute-user', data.rid)
+ throw new Meteor.Error 'not-allowed', 'Not allowed'
+
+ room = RocketChat.models.Rooms.findOneById data.rid
+
+ if data.username not in (room?.usernames or [])
+ throw new Meteor.Error 'not-in-room', 'User is not in this room'
+
+ unmutedUser = RocketChat.models.Users.findOneByUsername data.username
+
+ RocketChat.models.Subscriptions.unmuteUserByRoomIdAndUserId data.rid, unmutedUser._id
+
+ fromUser = RocketChat.models.Users.findOneById fromId
+ RocketChat.models.Messages.createUserUnmutedWithRoomIdAndUser data.rid, unmutedUser,
+ u:
+ _id: fromUser._id
+ username: fromUser.username
+
+ return true
diff --git a/server/publications/subscription.coffee b/server/publications/subscription.coffee
index f188e99522e..4cc8244a9e3 100644
--- a/server/publications/subscription.coffee
+++ b/server/publications/subscription.coffee
@@ -15,3 +15,4 @@ Meteor.publish 'subscription', ->
open: 1
alert: 1
unread: 1
+ mute: 1