mirror of
https://github.com/discourse/discourse.git
synced 2025-05-28 13:51:18 +08:00

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.
189 lines
4.5 KiB
JavaScript
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));
|
|
}
|
|
);
|
|
});
|
|
}
|