[NEW] Ignore user on channels (#10517)

[NEW] Option to ignore users on channels
pull/9950/head
Guilherme Gazzo 8 years ago committed by Rodrigo Nascimento
parent 68b06419df
commit 93b16413d3
  1. 21
      packages/rocketchat-api/server/v1/chat.js
  2. 6
      packages/rocketchat-i18n/i18n/en.i18n.json
  3. 52
      packages/rocketchat-lib/client/MessageAction.js
  4. 11
      packages/rocketchat-lib/server/lib/sendNotificationsOnMessage.js
  5. 15
      packages/rocketchat-lib/server/models/Subscriptions.js
  6. 17
      packages/rocketchat-theme/client/imports/components/messages.css
  7. 5
      packages/rocketchat-theme/client/imports/general/base_old.css
  8. 2
      packages/rocketchat-ui-flextab/client/tabs/membersList.html
  9. 23
      packages/rocketchat-ui-flextab/client/tabs/membersList.js
  10. 23
      packages/rocketchat-ui-flextab/client/tabs/userActions.js
  11. 1
      packages/rocketchat-ui-master/public/icons.svg
  12. 5
      packages/rocketchat-ui-message/client/message.html
  13. 9
      packages/rocketchat-ui-message/client/message.js
  14. 47
      packages/rocketchat-ui/client/lib/RoomHistoryManager.js
  15. 32
      packages/rocketchat-ui/client/lib/RoomManager.js
  16. 7
      packages/rocketchat-ui/client/views/app/room.js
  17. 30
      server/methods/ignoreUser.js
  18. 3
      server/publications/subscription.js

@ -317,3 +317,24 @@ RocketChat.API.v1.addRoute('chat.reportMessage', { authRequired: true }, {
return RocketChat.API.v1.success();
}
});
RocketChat.API.v1.addRoute('chat.ignoreUser', { authRequired: true }, {
get() {
const { rid, userId } = this.queryParams;
let { ignore = true } = this.queryParams;
ignore = typeof ignore === 'string' ? /true|1/.test(ignore) : ignore;
if (!rid || !rid.trim()) {
throw new Meteor.Error('error-room-id-param-not-provided', 'The required "rid" param is missing.');
}
if (!userId || !userId.trim()) {
throw new Meteor.Error('error-user-id-param-not-provided', 'The required "userId" param is missing.');
}
Meteor.runAsUser(this.userId, () => Meteor.call('ignoreUser', { rid, userId, ignore }));
return RocketChat.API.v1.success();
}
});

@ -935,6 +935,8 @@
"Iframe_Integration_send_enable_Description": "Send events to parent window",
"Iframe_Integration_send_target_origin": "Send Target Origin",
"Iframe_Integration_send_target_origin_Description": "Origin with protocol prefix, which commands are sent to e.g. 'https://localhost', or * to allow sending to anywhere.",
"Ignore": "Ignore",
"Ignored": "Ignored",
"IMAP_intercepter_already_running": "IMAP intercepter already running",
"IMAP_intercepter_Not_running": "IMAP intercepter Not running",
"Impersonate_next_agent_from_queue": "Impersonate next agent from queue",
@ -1364,6 +1366,7 @@
"Message_HideType_ru": "Hide \"User Removed\" messages",
"Message_HideType_uj": "Hide \"User Join\" messages",
"Message_HideType_ul": "Hide \"User Leave\" messages",
"Message_Ignored": "This message was ignored",
"Message_info": "Message info",
"Message_KeepHistory": "Keep Per Message Editing History",
"Message_MaxAll": "Maximum Channel Size for ALL Message",
@ -2086,6 +2089,7 @@
"unarchive-room_description": "Permission to unarchive channels",
"Unblock_User": "Unblock User",
"Uninstall": "Uninstall",
"Unignore": "Unignore",
"Unmute_someone_in_room": "Unmute someone in the room",
"Unmute_user": "Unmute user",
"Unnamed": "Unnamed",
@ -2137,8 +2141,10 @@
"User_has_been_activated": "User has been activated",
"User_has_been_deactivated": "User has been deactivated",
"User_has_been_deleted": "User has been deleted",
"User_has_been_ignored": "User has been ignored",
"User_has_been_muted_in_s": "User has been muted in %s",
"User_has_been_removed_from_s": "User has been removed from %s",
"User_has_been_unignored": "User is no longer ignored",
"User_Info": "User Info",
"User_Interface": "User Interface",
"User_is_blocked": "User is blocked",

@ -4,6 +4,17 @@ import _ from 'underscore';
import moment from 'moment';
import toastr from 'toastr';
const success = function success(fn) {
return function(error, result) {
if (error) {
return handleError(error);
}
if (result) {
fn.call(this, result);
}
};
};
RocketChat.MessageAction = new class {
/*
config expects the following keys (only id is mandatory):
@ -279,4 +290,45 @@ Meteor.startup(function() {
order: 6,
group: 'menu'
});
RocketChat.MessageAction.addButton({
id: 'ignore-user',
icon: 'ban',
label: t('Ignore'),
context: ['message', 'message-mobile'],
action() {
const [, {rid, u: {_id}}] = this._arguments;
Meteor.call('ignoreUser', { rid, userId:_id, ignore: true}, success(() => toastr.success(t('User_has_been_ignored'))));
},
condition(message) {
const subscription = RocketChat.models.Subscriptions.findOne({rid: message.rid});
return Meteor.userId() !== message.u._id && !(subscription.ignored && subscription.ignored.indexOf(message.u._id) > -1);
},
order: 20,
group: 'menu'
});
RocketChat.MessageAction.addButton({
id: 'unignore-user',
icon: 'ban',
label: t('Unignore'),
context: ['message', 'message-mobile'],
action() {
const [, {rid, u: {_id}}] = this._arguments;
Meteor.call('ignoreUser', { rid, userId:_id, ignore: false}, success(() => toastr.success(t('User_has_been_unignored'))));
},
condition(message) {
const subscription = RocketChat.models.Subscriptions.findOne({rid: message.rid});
return Meteor.userId() !== message.u._id && subscription.ignored && subscription.ignored.indexOf(message.u._id) > -1;
},
order: 20,
group: 'menu'
});
});

@ -205,12 +205,11 @@ RocketChat.callbacks.add('afterSaveMessage', function(message, room, userId) {
// Don't fetch all users if room exceeds max members
const maxMembersForNotification = RocketChat.settings.get('Notifications_Max_Room_Members');
const disableAllMessageNotifications = room.usernames.length > maxMembersForNotification && maxMembersForNotification !== 0;
const subscriptions = RocketChat.models.Subscriptions.findNotificationPreferencesByRoom(room._id, disableAllMessageNotifications);
const subscriptions = RocketChat.models.Subscriptions.findNotificationPreferencesByRoom(room._id, disableAllMessageNotifications) || [];
const userIds = [];
subscriptions.forEach((s) => {
userIds.push(s.u._id);
});
subscriptions.forEach(s => userIds.push(s.u._id));
const users = {};
RocketChat.models.Users.findUsersByIds(userIds, { fields: { 'settings.preferences': 1 } }).forEach((user) => {
users[user._id] = user;
});
@ -223,6 +222,10 @@ RocketChat.callbacks.add('afterSaveMessage', function(message, room, userId) {
return;
}
if (Array.isArray(subscription.ignored) && subscription.ignored.find(message.u._id)) {
return;
}
const {
audioNotifications = RocketChat.getUserPreference(users[subscription.u._id], 'audioNotifications'),
desktopNotifications = RocketChat.getUserPreference(users[subscription.u._id], 'desktopNotifications'),

@ -454,6 +454,21 @@ class ModelSubscriptions extends RocketChat.models._Base {
return this.update(query, update, { multi: true });
}
ignoreUser({_id, ignoredUser : ignored, ignore = true}) {
const query = {
_id
};
const update = {
};
if (ignore) {
update.$addToSet = { ignored };
} else {
update.$pull = { ignored };
}
return this.update(query, update);
}
setAlertForRoomIdExcludingUserId(roomId, userId) {
const query = {
rid: roomId,

@ -41,6 +41,23 @@
}
.message {
& .toggle-hidden {
display: none;
}
&--ignored {
& .body {
display: none;
}
& .toggle-hidden {
display: block;
}
& + .message--ignored.sequential {
display: none;
}
}
&.active {
& .message-actions__label {
color: var(--rc-color-button-primary);

@ -5384,6 +5384,11 @@ body:not(.is-cordova) {
cursor: pointer;
}
.toggle-hidden {
cursor: pointer;
font-style: italic;
}
/* kinda hacky, needed in oembedFrageWidget.html */
.rc-old br.only-after-a {

@ -37,7 +37,7 @@
{{> avatar username=user.username}}
<div class="rc-member-list__username">
<div class="rc-member-list__status rc-member-list__status--{{status}}"></div>
{{displayName}} {{utcOffset}}
{{ignored}} {{displayName}} {{utcOffset}}
</div>
{{> icon user=. block="rc-member-list__menu js-action" icon="menu" }}
</li>

@ -1,8 +1,13 @@
/* globals WebRTC popover */
/* globals WebRTC popover isRtl */
import _ from 'underscore';
import {getActions} from './userActions';
Template.membersList.helpers({
ignored() {
const {user} = this;
const sub = RocketChat.models.Subscriptions.findOne({rid: Session.get('openedRoom')});
return sub && sub.ignored && sub.ignored.indexOf(user._id) > -1 ? `(${ t('Ignored') })` : '';
},
tAddUsers() {
return t('Add_users');
},
@ -63,7 +68,7 @@ Template.membersList.helpers({
return {
user,
status: (onlineUsers[user.username] != null ? onlineUsers[user.username].status : undefined),
status: (onlineUsers[user.username] != null ? onlineUsers[user.username].status : 'offline'),
muted: Array.from(roomMuted).includes(user.username),
utcOffset
};
@ -76,7 +81,7 @@ Template.membersList.helpers({
}
// show online users first.
// sortBy is stable, so we can do this
users = _.sortBy(users, u => u.status == null);
users = _.sortBy(users, u => u.status === 'offline');
let hasMore = undefined;
const usersLimit = Template.instance().usersLimit.get();
@ -216,11 +221,21 @@ Template.membersList.events({
e.preventDefault();
const config = {
columns,
mousePosition: () => ({
x: e.currentTarget.getBoundingClientRect().right + 10,
y: e.currentTarget.getBoundingClientRect().bottom + 100
}),
customCSSProperties: () => ({
top: `${ e.currentTarget.getBoundingClientRect().bottom + 10 }px`,
left: isRtl() ? `${ e.currentTarget.getBoundingClientRect().left - 10 }px` : undefined
}),
data: {
rid: this._id,
username: instance.data.username,
instance
},
offsetHorizontal: 15,
activeElement: e.currentTarget,
currentTarget: e.currentTarget,
onDestroyed:() => {
e.currentTarget.parentElement.classList.remove('active');
@ -256,6 +271,7 @@ Template.membersList.onCreated(function() {
this.showDetail = new ReactiveVar(false);
this.filter = new ReactiveVar('');
this.users = new ReactiveVar([]);
this.total = new ReactiveVar;
this.loading = new ReactiveVar(true);
@ -264,7 +280,6 @@ Template.membersList.onCreated(function() {
Tracker.autorun(() => {
if (this.data.rid == null) { return; }
this.loading.set(true);
return Meteor.call('getUsersOfRoom', this.data.rid, this.showAllUsers.get(), (error, users) => {
this.users.set(users.records);

@ -6,7 +6,10 @@ import toastr from 'toastr';
export const getActions = function({ user, directActions, hideAdminControls }) {
const hasPermission = RocketChat.authz.hasAllPermission;
const isIgnored = () => {
const sub = RocketChat.models.Subscriptions.findOne({rid : Session.get('openedRoom')});
return sub && sub.ignored && sub.ignored.indexOf(user._id) > -1;
};
const canSetLeader= () => {
return RocketChat.authz.hasAllPermission('set-leader', Session.get('openedRoom'));
};
@ -302,6 +305,24 @@ export const getActions = function({ user, directActions, hideAdminControls }) {
}));
})
};
}, () => {
if (!directActions || user._id === Meteor.userId()) {
return;
}
if (isIgnored()) {
return {
group: 'channel',
icon : 'ban',
name: t('Unignore'),
action: prevent(getUser, ({_id}) => Meteor.call('ignoreUser', { rid: Session.get('openedRoom'), userId:_id, ignore: false}, success(() => toastr.success(t('User_has_been_unignored')))))
};
}
return {
group: 'channel',
icon : 'ban',
name: t('Ignore'),
action: prevent(getUser, ({_id}) => Meteor.call('ignoreUser', { rid: Session.get('openedRoom'), userId:_id, ignore: true}, success(() => toastr.success(t('User_has_been_ignored')))))
};
}, () => {
if (!directActions || !canMuteUser()) {
return;

@ -105,4 +105,5 @@
<symbol id="icon-loading" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" class="lds-rolling"><circle cx="50" cy="50" fill="none" stroke="currentColor" stroke-width="5" r="26" stroke-dasharray="122.52211349000194 42.840704496667314" transform="rotate(150 50 50)"><animateTransform attributeName="transform" type="rotate" calcMode="linear" values="0 50 50;360 50 50" keyTimes="0;1" dur="1s" begin="0s" repeatCount="indefinite"></animateTransform></circle></symbol>
<symbol id="icon-sort-down" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path d="M287.968 288H32.038c-28.425 0-42.767 34.488-22.627 54.627l127.962 128c12.496 12.496 32.758 12.497 45.255 0l127.968-128C330.695 322.528 316.45 288 287.968 288zM160 448L32 320h256L160 448z"/></symbol>
<symbol id="icon-sort-up" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path d="M32.032 224h255.93c28.425 0 42.767-34.488 22.627-54.627l-127.962-128c-12.496-12.496-32.758-12.497-45.255 0l-127.968 128C-10.695 189.472 3.55 224 32.032 224zM160 64l128 128H32L160 64z"/></symbol>
<symbol id="icon-ban" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M256 8C119.033 8 8 119.033 8 256s111.033 248 248 248 248-111.033 248-248S392.967 8 256 8zM103.265 408.735c-80.622-80.622-84.149-208.957-10.9-293.743l304.644 304.643c-84.804 73.264-213.138 69.706-293.744-10.9zm316.37-11.727L114.992 92.365c84.804-73.263 213.137-69.705 293.743 10.9 80.622 80.621 84.149 208.957 10.9 293.743z"/></symbol>
</svg>

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

@ -1,5 +1,5 @@
<template name="message">
<li id="{{_id}}" data-context={{actionContext}} class="message background-transparent-dark-hover {{isSequential}} {{system}} {{t}} {{own}} {{isTemp}} {{chatops}} {{customClass}}" data-username="{{u.username}}" data-groupable="{{isGroupable}}" data-date="{{date}}" data-timestamp="{{timestamp}}">
<li id="{{_id}}" data-context={{actionContext}} class="message background-transparent-dark-hover {{ignoredClass}} {{sequentialClass}} {{system}} {{t}} {{own}} {{isTemp}} {{chatops}} {{customClass}}" data-username="{{u.username}}" data-groupable="{{isGroupable}}" data-date="{{date}}" data-timestamp="{{timestamp}}">
{{#if avatar}}
{{#if avatarFromUsername}}
<button class="thumb user-card-message" data-username="{{u.username}}" tabindex="1">{{> avatar username=avatarFromUsername}}</button>
@ -55,6 +55,9 @@
{{>icon icon=roomIcon}}{{channelName}}
</span>
{{/if}}
{{#if isIgnored}}
<span class="toggle-hidden icon-right-dir" data-message="{{_id}}"> {{_ "Message_Ignored"}} </span>
{{/if}}
<div class="body color-primary-font-color {{system true}}" dir="auto" data-unread-text="{{_ "Unread_Messages"}}">
{{#if isSnippet}}
<div class="snippet-name">{{_ "Snippet_name"}}: {{snippetName}}</div>

@ -6,6 +6,12 @@ Template.message.helpers({
encodeURI(text) {
return encodeURI(text);
},
isIgnored() {
return this.ignored;
},
ignoredClass() {
return this.ignored ? 'message--ignored' : '';
},
isBot() {
if (this.bot != null) {
return 'bot';
@ -47,6 +53,9 @@ Template.message.helpers({
}
},
isSequential() {
return this.groupable !== false;
},
sequentialClass() {
if (this.groupable !== false) {
return 'sequential';
}

@ -1,6 +1,20 @@
/* globals readMessage UserRoles RoomRoles*/
import _ from 'underscore';
export const upsertMessage = ({msg, subscription}) => {
const userId = msg.u && msg.u._id;
if (subscription && subscription.ignored && subscription.ignored.indexOf(userId) > -1) {
msg.ignored = true;
}
const roles = [
(userId && UserRoles.findOne(userId, { fields: { roles: 1 }})) || {},
(userId && RoomRoles.findOne({rid: msg.rid, 'u._id': userId})) || {}
].map(e => e.roles);
msg.roles = _.union.apply(_.union, roles);
return ChatMessage.upsert({_id: msg._id}, msg);
};
export const RoomHistoryManager = new class {
constructor() {
this.defaultLimit = 50;
@ -67,16 +81,7 @@ export const RoomHistoryManager = new class {
previousHeight = wrapper.scrollHeight;
}
messages.forEach(item => {
if (item.t !== 'command') {
const roles = [
(item.u && item.u._id && UserRoles.findOne(item.u._id, { fields: { roles: 1 }})) || {},
(item.u && item.u._id && RoomRoles.findOne({rid: item.rid, 'u._id': item.u._id})) || {}
].map(e => e.roles);
item.roles = _.union.apply(_.union, roles);
ChatMessage.upsert({_id: item._id}, item);
}
});
messages.forEach(msg => msg.t !== 'command' && upsertMessage({msg, subscription}));
if (wrapper) {
const heightDiff = wrapper.scrollHeight - previousHeight;
@ -126,14 +131,9 @@ export const RoomHistoryManager = new class {
if (ts) {
return Meteor.call('loadNextMessages', rid, ts, limit, function(err, result) {
for (const item of Array.from((result != null ? result.messages : undefined) || [])) {
if (item.t !== 'command') {
const roles = [
(item.u && item.u._id && UserRoles.findOne(item.u._id, { fields: { roles: 1 }})) || {},
(item.u && item.u._id && RoomRoles.findOne({rid: item.rid, 'u._id': item.u._id})) || {}
].map(e => e.roles);
item.roles = _.union.apply(_.union, roles);
ChatMessage.upsert({_id: item._id}, item);
for (const msg of Array.from((result != null ? result.messages : undefined) || [])) {
if (msg.t !== 'command') {
upsertMessage({msg, subscription});
}
}
@ -190,14 +190,9 @@ export const RoomHistoryManager = new class {
}
return Meteor.call('loadSurroundingMessages', message, limit, function(err, result) {
for (const item of Array.from((result != null ? result.messages : undefined) || [])) {
if (item.t !== 'command') {
const roles = [
(item.u && item.u._id && UserRoles.findOne(item.u._id, { fields: { roles: 1 }})) || {},
(item.u && item.u._id && RoomRoles.findOne({rid: item.rid, 'u._id': item.u._id})) || {}
].map(e => e.roles);
item.roles = _.union.apply(_.union, roles);
ChatMessage.upsert({_id: item._id}, item);
for (const msg of Array.from((result != null ? result.messages : undefined) || [])) {
if (msg.t !== 'command') {
upsertMessage({msg, subscription});
}
}

@ -1,5 +1,5 @@
import _ from 'underscore';
import { upsertMessage } from './RoomHistoryManager';
const RoomManager = new function() {
const openedRooms = {};
const msgStream = new Meteor.Streamer('room-messages');
@ -27,7 +27,6 @@ const RoomManager = new function() {
if (room != null) {
openedRooms[typeName].rid = room._id;
RoomHistoryManager.getMoreIfIsEmpty(room._id);
if (openedRooms[typeName].streamActive !== true) {
@ -41,12 +40,8 @@ const RoomManager = new function() {
// Do not load command messages into channel
if (msg.t !== 'command') {
const roles = [
(msg.u && msg.u._id && UserRoles.findOne(msg.u._id, { fields: { roles: 1 }})) || {},
(msg.u && msg.u._id && RoomRoles.findOne({rid: msg.rid, 'u._id': msg.u._id})) || {}
].map(e => e.roles);
msg.roles = _.union.apply(_.union, roles);
ChatMessage.upsert({ _id: msg._id }, msg);
const subscription = ChatSubscription.findOne({rid: openedRooms[typeName].rid});
upsertMessage({msg, subscription});
msg.room = {
type,
name
@ -227,18 +222,9 @@ const loadMissedMessages = function(rid) {
if (lastMessage == null) {
return;
}
const subscription = ChatSubscription.findOne({rid});
return Meteor.call('loadMissedMessages', rid, lastMessage.ts, (err, result) =>
Array.from(result).map((item) =>
RocketChat.promises.run('onClientMessageReceived', item).then(function(item) {
/* globals UserRoles RoomRoles*/
const roles = [
(item.u && item.u._id && UserRoles.findOne(item.u._id)) || {},
(item.u && item.u._id && RoomRoles.findOne({rid: item.rid, 'u._id': item.u._id})) || {}
].map(({roles}) => roles);
item.roles = _.union.apply(_, roles);
return ChatMessage.upsert({_id: item._id}, item);
}))
Array.from(result).map(item => RocketChat.promises.run('onClientMessageReceived', item).then(msg => upsertMessage({msg, subscription})))
);
};
@ -311,3 +297,11 @@ Tracker.autorun(function() {
export { RoomManager };
this.RoomManager = RoomManager;
RocketChat.callbacks.add('afterLogoutCleanUp', () => RoomManager.closeAllRooms(), RocketChat.callbacks.priority.MEDIUM, 'roommanager-after-logout-cleanup');
RocketChat.Notifications.onUser('subscriptions-changed', (action, sub) => {
ChatMessage.update({rid: sub.rid}, {$unset : {ignored : ''}}, {multi : true});
if (sub && sub.ignored) {
ChatMessage.update({rid: sub.rid, t: {$ne: 'command'}, 'u._id': { $in : sub.ignored }}, { $set: {ignored : true}}, {multi : true});
}
});

@ -111,6 +111,7 @@ const mountPopover = (e, i, outerContext) => {
}
],
instance: i,
currentTarget: e.currentTarget,
data: outerContext,
activeElement: $(e.currentTarget).parents('.message')[0],
onRendered: () => new Clipboard('.rc-popover__item')
@ -722,9 +723,11 @@ Template.room.events({
showCancelButton: true,
cancelButtonText: t('Close')
});
}
},
'click .toggle-hidden'(e) {
const id = e.currentTarget.dataset.message;
document.querySelector(`#${ id }`).classList.toggle('message--ignored');
}
});

@ -0,0 +1,30 @@
/* globals RocketChat */
Meteor.methods({
ignoreUser({rid, userId: ignoredUser, ignore = true}) {
check(ignoredUser, String);
check(rid, String);
check(ignore, Boolean);
const userId = Meteor.userId();
if (!userId) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
method: 'ignoreUser'
});
}
const subscription = RocketChat.models.Subscriptions.findOneByRoomIdAndUserId(rid, userId);
if (!subscription) {
throw new Meteor.Error('error-invalid-subscription', 'Invalid subscription', { method: 'ignoreUser' });
}
const subscriptionIgnoredUser = RocketChat.models.Subscriptions.findOneByRoomIdAndUserId(rid, ignoredUser);
if (!subscriptionIgnoredUser) {
throw new Meteor.Error('error-invalid-subscription', 'Invalid subscription', { method: 'ignoreUser' });
}
return !!RocketChat.models.Subscriptions.ignoreUser({_id: subscription._id, ignoredUser, ignore});
}
});

@ -28,7 +28,8 @@ const fields = {
autoTranslate: 1,
autoTranslateLanguage: 1,
disableNotifications: 1,
hideUnreadStatus: 1
hideUnreadStatus: 1,
ignored: 1
};
Meteor.methods({

Loading…
Cancel
Save