Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NEW] WIP: custom mention groups #16311

Draft
wants to merge 1 commit into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/api/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,6 @@ import './v1/webdav';
import './v1/oauthapps';
import './v1/custom-sounds';
import './v1/custom-user-status';
import './v1/mention-groups';

export { API, APIClass, defaultRateLimiterOptions } from './api';
24 changes: 24 additions & 0 deletions app/api/server/lib/mentionGroups.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
import { MentionGroups } from '../../../models/server/raw';

export async function findMentionGroupAutocomplete({ /* uid,*/ selector }) {
// if (!await hasPermissionAsync(uid, 'view-other-user-channels')) {
// return { items: [] };
// }
const options = {
fields: {
_id: 1,
name: 1,
},
limit: 10,
sort: {
name: 1,
},
};

const groups = await MentionGroups.findByNameContaining(selector.name, options, selector.exceptions).toArray();

return {
items: groups,
};
}
24 changes: 24 additions & 0 deletions app/api/server/lib/roles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
import { Roles } from '../../../models/server/raw';

export async function findRoleAutocomplete({ /* uid,*/ selector }) {
// if (!await hasPermissionAsync(uid, 'view-other-user-channels')) {
// return { items: [] };
// }
const options = {
fields: {
_id: 1,
name: 1,
},
limit: 10,
sort: {
name: 1,
},
};

const roles = await Roles.findByNameContaining(selector.name, options, selector.exceptions).toArray();

return {
items: roles,
};
}
12 changes: 10 additions & 2 deletions app/api/server/lib/rooms.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export async function findAdminRooms({ uid, filter, types = [], pagination: { of
};
}

export async function findChannelAndPrivateAutocomplete({ uid, selector }) {
async function findChannelAndOrPrivateAutocomplete(withPrivate, { uid, selector }) {
if (!await hasPermissionAsync(uid, 'view-other-user-channels')) {
return { items: [] };
}
Expand All @@ -69,9 +69,17 @@ export async function findChannelAndPrivateAutocomplete({ uid, selector }) {
},
};

const rooms = await Rooms.findChannelAndPrivateByNameStarting(selector.name, options).toArray();
const rooms = await Rooms.findChannelAndOrPrivateByNameStarting(selector.name, options, withPrivate, selector.exceptions).toArray();

return {
items: rooms,
};
}

export function findChannelAndPrivateAutocomplete(params) {
return findChannelAndOrPrivateAutocomplete(true, params);
}

export function findChannelAutocomplete(params) {
return findChannelAndOrPrivateAutocomplete(false, params);
}
16 changes: 16 additions & 0 deletions app/api/server/v1/mention-groups.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { API } from '../api';
import { findMentionGroupAutocomplete } from '../lib/mentionGroups';

API.v1.addRoute('mentionGroups.autocomplete', { authRequired: true }, {
get() {
const { selector } = this.queryParams;
if (!selector) {
return API.v1.failure('The \'selector\' param is required');
}

return API.v1.success(Promise.await(findMentionGroupAutocomplete({
uid: this.userId,
selector: JSON.parse(selector),
})));
},
});
16 changes: 16 additions & 0 deletions app/api/server/v1/roles.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Match, check } from 'meteor/check';
import { Roles } from '../../../models';
import { API } from '../api';
import { getUsersInRole, hasPermission } from '../../../authorization/server';
import { findRoleAutocomplete } from '../lib/roles';

API.v1.addRoute('roles.list', { authRequired: true }, {
get() {
Expand Down Expand Up @@ -86,3 +87,18 @@ API.v1.addRoute('roles.getUsersInRole', { authRequired: true }, {
return API.v1.success({ users });
},
});


API.v1.addRoute('roles.autocomplete', { authRequired: true }, {
get() {
const { selector } = this.queryParams;
if (!selector) {
return API.v1.failure('The \'selector\' param is required');
}

return API.v1.success(Promise.await(findRoleAutocomplete({
uid: this.userId,
selector: JSON.parse(selector),
})));
},
});
16 changes: 15 additions & 1 deletion app/api/server/v1/rooms.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Busboy from 'busboy';
import { FileUpload } from '../../../file-upload';
import { Rooms, Messages } from '../../../models';
import { API } from '../api';
import { findAdminRooms, findChannelAndPrivateAutocomplete } from '../lib/rooms';
import { findAdminRooms, findChannelAndPrivateAutocomplete, findChannelAutocomplete } from '../lib/rooms';

function findRoomByIdOrName({ params, checkedArchived = true }) {
if ((!params.roomId || !params.roomId.trim()) && (!params.roomName || !params.roomName.trim())) {
Expand Down Expand Up @@ -308,3 +308,17 @@ API.v1.addRoute('rooms.autocomplete.channelAndPrivate', { authRequired: true },
})));
},
});

API.v1.addRoute('rooms.autocomplete.channel', { authRequired: true }, {
get() {
const { selector } = this.queryParams;
if (!selector) {
return API.v1.failure('The \'selector\' param is required');
}

return API.v1.success(Promise.await(findChannelAutocomplete({
uid: this.userId,
selector: JSON.parse(selector),
})));
},
});
1 change: 1 addition & 0 deletions app/authorization/server/startup.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ Meteor.startup(function() {
{ _id: 'view-livechat-webhooks', roles: ['livechat-manager', 'admin'] },
{ _id: 'view-livechat-facebook', roles: ['livechat-manager', 'admin'] },
{ _id: 'view-livechat-officeHours', roles: ['livechat-manager', 'admin'] },
{ _id: 'manage-mention-groups', roles: ['admin'] },
];

for (const permission of permissions) {
Expand Down
8 changes: 6 additions & 2 deletions app/lib/server/lib/notifyUsersOnMessage.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import _ from 'underscore';
import s from 'underscore.string';
import moment from 'moment';

import { Rooms, Subscriptions } from '../../../models/server';
import { Rooms, Subscriptions, MentionGroups } from '../../../models/server';
import { settings } from '../../../settings/server';
import { callbacks } from '../../../callbacks/server';

Expand All @@ -28,6 +28,7 @@ export function updateUsersSubscriptions(message, room, users) {
if (room != null) {
let toAll = false;
let toHere = false;
let toGroup = false;
const mentionIds = [];
const highlightsIds = [];

Expand All @@ -45,6 +46,9 @@ export function updateUsersSubscriptions(message, room, users) {
if (!toHere && mention._id === 'here') {
toHere = true;
}
if (MentionGroups.findOne({ name: mention._id })) {
toGroup = true;
}
if (mention._id !== message.u._id) {
mentionIds.push(mention._id);
}
Expand All @@ -62,7 +66,7 @@ export function updateUsersSubscriptions(message, room, users) {
const unreadSetting = room.t === 'd' ? 'Unread_Count_DM' : 'Unread_Count';
const unreadCount = settings.get(unreadSetting);

if (toAll || toHere) {
if (toAll || toHere || toGroup) {
const incUnreadByGroup = ['all_messages', 'group_mentions_only', 'user_and_group_mentions_only'].includes(unreadCount);
const incUnread = room.t === 'd' || incUnreadByGroup ? 1 : 0;

Expand Down
36 changes: 28 additions & 8 deletions app/lib/server/lib/sendNotificationsOnMessage.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Meteor } from 'meteor/meteor';
import moment from 'moment';
import _ from 'underscore';

import { hasPermission } from '../../../authorization';
import { settings } from '../../../settings';
import { callbacks } from '../../../callbacks';
import { Subscriptions, Users } from '../../../models/server';
import { Subscriptions, Users, MentionGroups } from '../../../models/server';
import { roomTypes } from '../../../utils';
import { callJoinRoom, messageContainsHighlight, parseMessageTextPerUser, replaceMentionedUsernamesWithFullNames } from '../functions/notifications';
import { sendEmail, shouldNotifyEmail } from '../functions/notifications/email';
Expand Down Expand Up @@ -186,16 +187,30 @@ const lookup = {
},
};

export async function sendMessageNotifications(message, room, usersInThread = []) {
export async function sendMessageNotifications(message, room, usersInThread = [], mentionGroups = []) {
const sender = roomTypes.getConfig(room.t).getMsgSender(message.u._id);
if (!sender) {
return message;
}


const mentionGroupNames = mentionGroups.map((group) => group.name);
const mentionIds = (message.mentions || []).map(({ _id }) => _id).concat(usersInThread); // add users in thread to mentions array because they follow the same rules
const mentionIdsWithoutGroups = mentionIds.filter((_id) => _id !== 'all' && _id !== 'here');
const hasMentionToAll = mentionIds.includes('all');
const hasMentionToHere = mentionIds.includes('here');
const mentionIdsWithoutGroups = mentionIds.filter((_id) => _id !== 'all' && _id !== 'here' && !mentionGroupNames.includes(_id));
const mentionIdsToGroups = mentionIds.filter((_id) => {
const group = mentionGroups.find((group) => group.name === _id);
// no channels on the list -> group works for all of them
return group && (group.channels.length === 0 || group.channels.includes(room._id));
});
const mentionedGroups = mentionGroups.filter((group) => mentionIdsToGroups.includes(group.name));
const hasMentionToAll = mentionIds.includes('all') || mentionedGroups.some((group) => group.mentionsOffline);
const hasMentionToHere = mentionIds.includes('here') || mentionedGroups.length > 0;
const usersInGroups = _.flatten( // TODO: replace with flatMap on Node >= 11.0
mentionGroups.filter((group) =>
mentionIdsToGroups.includes(group.name)
|| group.groups.some((name) => mentionIdsToGroups.includes(name)),
).map((group) => group.users),
);

let notificationMessage = callbacks.run('beforeSendMessageNotifications', message.msg);
if (mentionIds.length > 0 && settings.get('UI_Use_Real_Name')) {
Expand All @@ -214,6 +229,7 @@ export async function sendMessageNotifications(message, room, usersInThread = []
$or: [
{ 'userHighlights.0': { $exists: 1 } },
...usersInThread.length > 0 ? [{ 'u._id': { $in: usersInThread } }] : [],
...usersInGroups.length > 0 ? [{ 'u._id': { $in: usersInGroups } }] : [],
],
};

Expand Down Expand Up @@ -271,7 +287,7 @@ export async function sendMessageNotifications(message, room, usersInThread = []
message,
notificationMessage,
room,
mentionIds,
mentionIds: [...mentionIds, ...usersInGroups],
disableAllMessageNotifications,
hasReplyToThread: usersInThread && usersInThread.includes(subscription.u._id),
}));
Expand Down Expand Up @@ -304,14 +320,15 @@ export async function sendAllNotifications(message, room) {
return message;
}

const mentionGroups = MentionGroups.find({}).fetch();
const {
sender,
hasMentionToAll,
hasMentionToHere,
notificationMessage,
mentionIds,
mentionIdsWithoutGroups,
} = await sendMessageNotifications(message, room);
} = await sendMessageNotifications(message, room, undefined, mentionGroups);

// on public channels, if a mentioned user is not member of the channel yet, he will first join the channel and then be notified based on his preferences.
if (room.t === 'c') {
Expand All @@ -325,7 +342,10 @@ export async function sendAllNotifications(message, room) {
});

Promise.all(mentions
.map(async (userId) => {
.filter((mention) => {
const group = mentionGroups.find((g) => g.name === mention);
return !group || group.mentionsOutside; // if group is undefined then this is a regular mention
}).map(async (userId) => {
await callJoinRoom(userId, room._id);

return userId;
Expand Down
26 changes: 26 additions & 0 deletions app/mentions/client/admin/route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { FlowRouter } from 'meteor/kadira:flow-router';
import { BlazeLayout } from 'meteor/kadira:blaze-layout';

import { t } from '../../../utils';

FlowRouter.route('/admin/mention-groups', {
name: 'admin-mention-groups',
action() {
return BlazeLayout.render('main', {
center: 'mentionGroups',
pageTitle: t('Mentions_Groups'),
});
},
});

FlowRouter.route('/admin/mention-group/:id?', {
name: 'admin-mention-group',
action(params) {
return BlazeLayout.render('main', {
center: 'pageSettingsContainer',
pageTitle: t('Mentions_GroupConfiguration'),
pageTemplate: 'mentionGroupSettings',
params,
});
},
});
11 changes: 11 additions & 0 deletions app/mentions/client/admin/startup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { AdminBox } from '../../../ui-utils';
import { hasAllPermission } from '../../../authorization';

AdminBox.addOption({
href: 'admin-mention-groups',
i18nLabel: 'Mentions_Groups',
icon: 'discover',
permissionGranted() {
return hasAllPermission('manage-mention-groups');
},
});
23 changes: 23 additions & 0 deletions app/mentions/client/admin/views/mentionGroupSettings.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
.users-table {
margin-top: 8px;
}

.users-table thead {
border-bottom: 1px solid rgba(216, 216, 216, 0.4);
}

.users-table th {
padding-bottom: 8px;

text-align: left;

color: var(--rc-color-primary-light);

font-size: 0.75rem;
font-weight: 500;
line-height: 1rem;
}

.users-table tbody tr:first-child td {
padding-top: 8px;
}
Loading