Files
discourse/app/assets/javascripts/discourse/lib/user-search.js.es6
Sam Saffron f780920759 FEATURE: mention in secure category to prioritize groups
This feature allows @ mentions to prioritize showing members of a group who
have explicit permission to a category.

This makes it far easier to @ mention group member when composing topics in
categories where only the group has access.

For example:

If Sam, Jane an Joan have access to bugs category.

Then `@` will auto complete to (jane,joan,sam) ordered on last seen at

This feature works on new topics and existing topics. There is an explicit
exclusion of trust level 0,1,2 groups cause they get too big.
2019-08-06 17:57:56 +10:00

189 lines
4.5 KiB
JavaScript

import debounce from "discourse/lib/debounce";
import { CANCELLED_STATUS } from "discourse/lib/autocomplete";
import { userPath } from "discourse/lib/url";
import { emailValid } from "discourse/lib/utilities";
var cache = {},
cacheKey,
cacheTime,
currentTerm,
oldSearch;
function performSearch(
term,
topicId,
categoryId,
includeGroups,
includeMentionableGroups,
includeMessageableGroups,
allowedUsers,
groupMembersOf,
resultsFn
) {
var cached = cache[term];
if (cached) {
resultsFn(cached);
return;
}
// I am not strongly against unconditionally returning
// however this allows us to return a list of probable
// users we want to mention, early on a topic
if (term === "" && !topicId && !categoryId) {
return [];
}
// need to be able to cancel this
oldSearch = $.ajax(userPath("search/users"), {
data: {
term: term,
topic_id: topicId,
category_id: categoryId,
include_groups: includeGroups,
include_mentionable_groups: includeMentionableGroups,
include_messageable_groups: includeMessageableGroups,
groups: groupMembersOf,
topic_allowed_users: allowedUsers
}
});
var returnVal = CANCELLED_STATUS;
oldSearch
.then(function(r) {
cache[term] = r;
cacheTime = new Date();
// If there is a newer search term, return null
if (term === currentTerm) {
returnVal = r;
}
})
.always(function() {
oldSearch = null;
resultsFn(returnVal);
});
}
var debouncedSearch = debounce(performSearch, 300);
function organizeResults(r, options) {
if (r === CANCELLED_STATUS) {
return r;
}
var exclude = options.exclude || [],
limit = options.limit || 5,
users = [],
emails = [],
groups = [],
results = [];
if (r.users) {
r.users.every(function(u) {
if (exclude.indexOf(u.username) === -1) {
users.push(u);
results.push(u);
}
return results.length <= limit;
});
}
if (options.allowEmails && emailValid(options.term)) {
let e = { username: options.term };
emails = [e];
results.push(e);
}
if (r.groups) {
r.groups.every(function(g) {
if (
options.term.toLowerCase() === g.name.toLowerCase() ||
results.length < limit
) {
if (exclude.indexOf(g.name) === -1) {
groups.push(g);
results.push(g);
}
}
return true;
});
}
results.users = users;
results.emails = emails;
results.groups = groups;
return results;
}
// all punctuations except for -, _ and . which are allowed in usernames
// note: these are valid in names, but will end up tripping search anyway so just skip
// this means searching for `sam saffron` is OK but if my name is `sam$ saffron` autocomplete
// will not find me, which is a reasonable compromise
//
// we also ignore if we notice a double space or a string that is only a space
const ignoreRegex = /([\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,\/:;<=>?\[\]^`{|}~])|\s\s|^\s$/;
function skipSearch(term, allowEmails) {
if (term.indexOf("@") > -1 && !allowEmails) {
return true;
}
return !!term.match(ignoreRegex);
}
export default function userSearch(options) {
if (options.term && options.term.length > 0 && options.term[0] === "@") {
options.term = options.term.substring(1);
}
var term = options.term || "",
includeGroups = options.includeGroups,
includeMentionableGroups = options.includeMentionableGroups,
includeMessageableGroups = options.includeMessageableGroups,
allowedUsers = options.allowedUsers,
topicId = options.topicId,
categoryId = options.categoryId,
groupMembersOf = options.groupMembersOf;
if (oldSearch) {
oldSearch.abort();
oldSearch = null;
}
currentTerm = term;
return new Ember.RSVP.Promise(function(resolve) {
const newCacheKey = `${topicId}-${categoryId}`;
if (new Date() - cacheTime > 30000 || cacheKey !== newCacheKey) {
cache = {};
}
cacheKey = newCacheKey;
var clearPromise = setTimeout(function() {
resolve(CANCELLED_STATUS);
}, 5000);
if (skipSearch(term, options.allowEmails)) {
resolve([]);
return;
}
debouncedSearch(
term,
topicId,
categoryId,
includeGroups,
includeMentionableGroups,
includeMessageableGroups,
allowedUsers,
groupMembersOf,
function(r) {
clearTimeout(clearPromise);
resolve(organizeResults(r, options));
}
);
});
}