208 lines
5.1 KiB
JavaScript
208 lines
5.1 KiB
JavaScript
import discourseDebounce from "discourse/lib/debounce";
|
|
import { CANCELLED_STATUS } from "discourse/lib/autocomplete";
|
|
import { userPath } from "discourse/lib/url";
|
|
import { emailValid } from "discourse/lib/utilities";
|
|
import { Promise } from "rsvp";
|
|
|
|
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;
|
|
}
|
|
|
|
const eagerComplete = eagerCompleteSearch(term, topicId || categoryId);
|
|
|
|
if (term === "" && !eagerComplete) {
|
|
// The server returns no results in this case, so no point checking
|
|
// do not return empty list, because autocomplete will get terminated
|
|
resultsFn(CANCELLED_STATUS);
|
|
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) {
|
|
const hasResults = !!(
|
|
(r.users && r.users.length) ||
|
|
(r.groups && r.groups.length) ||
|
|
(r.emails && r.emails.length)
|
|
);
|
|
|
|
if (eagerComplete && !hasResults) {
|
|
// we are trying to eager load, but received no results
|
|
// do not return empty list, because autocomplete will get terminated
|
|
r = CANCELLED_STATUS;
|
|
}
|
|
|
|
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 = discourseDebounce(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$|^[^+]*\+[^@]*$/;
|
|
|
|
export function skipSearch(term, allowEmails) {
|
|
if (term.indexOf("@") > -1 && !allowEmails) {
|
|
return true;
|
|
}
|
|
|
|
return !!term.match(ignoreRegex);
|
|
}
|
|
|
|
export function eagerCompleteSearch(term, scopedId) {
|
|
return term === "" && !!scopedId;
|
|
}
|
|
|
|
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 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));
|
|
}
|
|
);
|
|
});
|
|
}
|