diff --git a/app/api/server/v1/misc.js b/app/api/server/v1/misc.js
index b975ab1201b26..1af2c884a860f 100644
--- a/app/api/server/v1/misc.js
+++ b/app/api/server/v1/misc.js
@@ -262,6 +262,8 @@ const methodCall = () => ({
const result = Meteor.call(method, ...params);
return API.v1.success(mountResult({ id, result }));
} catch (error) {
+ Meteor._debug(`Exception while invoking method ${ method }`, error.stack);
+
return API.v1.success(mountResult({ id, error }));
}
},
diff --git a/app/lib/server/startup/settings.js b/app/lib/server/startup/settings.js
index ffcf73606f903..090d686083a6b 100644
--- a/app/lib/server/startup/settings.js
+++ b/app/lib/server/startup/settings.js
@@ -1374,6 +1374,12 @@ settings.addGroup('Layout', function() {
type: 'boolean',
public: true,
});
+
+ this.add('Number_of_users_autocomplete_suggestions', 5, {
+ type: 'int',
+ public: true,
+ });
+
this.add('UI_Unread_Counter_Style', 'Different_Style_For_User_Mentions', {
type: 'select',
values: [
diff --git a/app/migrations/server/migrations.js b/app/migrations/server/migrations.js
index a797c1aeed29f..c7d25a94dad63 100644
--- a/app/migrations/server/migrations.js
+++ b/app/migrations/server/migrations.js
@@ -293,6 +293,7 @@ Migrations._migrateTo = function(version, rerun) {
`Branch: ${ Info.commit.branch }`,
`Tag: ${ Info.commit.tag }`,
]));
+ console.log(e.stack);
process.exit(1);
}
}
diff --git a/app/models/server/models/Subscriptions.js b/app/models/server/models/Subscriptions.js
index c98ebdd695238..2913a30370e61 100644
--- a/app/models/server/models/Subscriptions.js
+++ b/app/models/server/models/Subscriptions.js
@@ -1308,6 +1308,10 @@ export class Subscriptions extends Base {
Rooms.incUsersCountById(room._id);
+ if (!['d', 'l'].includes(room.t)) {
+ Users.addRoomByUserId(user._id, room._id);
+ }
+
return result;
}
@@ -1326,6 +1330,8 @@ export class Subscriptions extends Base {
Rooms.incUsersCountNotDMsByIds(roomIds, -1);
}
+ Users.removeAllRoomsByUserId(userId);
+
return result;
}
@@ -1340,6 +1346,8 @@ export class Subscriptions extends Base {
Rooms.incUsersCountById(roomId, - result);
}
+ Users.removeRoomByRoomId(roomId);
+
return result;
}
@@ -1355,11 +1363,17 @@ export class Subscriptions extends Base {
Rooms.incUsersCountById(roomId, - result);
}
+ Users.removeRoomByUserId(userId, roomId);
+
return result;
}
removeByRoomIds(rids) {
- return this.remove({ rid: { $in: rids } });
+ const result = this.remove({ rid: { $in: rids } });
+
+ Users.removeRoomByRoomIds(rids);
+
+ return result;
}
// //////////////////////////////////////////////////////////////////
diff --git a/app/models/server/models/Users.js b/app/models/server/models/Users.js
index e7e40ae6caab5..14d84e6968fd4 100644
--- a/app/models/server/models/Users.js
+++ b/app/models/server/models/Users.js
@@ -28,6 +28,12 @@ export class Users extends Base {
constructor(...args) {
super(...args);
+ this.defaultFields = {
+ __rooms: 0,
+ };
+
+ this.tryEnsureIndex({ __rooms: 1 }, { sparse: 1 });
+
this.tryEnsureIndex({ roles: 1 }, { sparse: 1 });
this.tryEnsureIndex({ name: 1 });
this.tryEnsureIndex({ bio: 1 }, { sparse: 1 });
@@ -388,6 +394,48 @@ export class Users extends Base {
});
}
+ addRoomByUserId(_id, rid) {
+ return this.update({
+ _id,
+ __rooms: { $ne: rid },
+ }, {
+ $addToSet: { __rooms: rid },
+ });
+ }
+
+ removeRoomByUserId(_id, rid) {
+ return this.update({
+ _id,
+ __rooms: rid,
+ }, {
+ $pull: { __rooms: rid },
+ });
+ }
+
+ removeAllRoomsByUserId(_id) {
+ return this.update({
+ _id,
+ }, {
+ $set: { __rooms: [] },
+ });
+ }
+
+ removeRoomByRoomId(rid) {
+ return this.update({
+ __rooms: rid,
+ }, {
+ $pull: { __rooms: rid },
+ }, { multi: true });
+ }
+
+ removeRoomByRoomIds(rids) {
+ return this.update({
+ __rooms: { $in: rids },
+ }, {
+ $pullAll: { __rooms: rids },
+ }, { multi: true });
+ }
+
update2FABackupCodesByUserId(userId, backupCodes) {
return this.update({
_id: userId,
@@ -509,6 +557,19 @@ export class Users extends Base {
return this.findOne(query, options);
}
+ findOneByUsernameAndRoomIgnoringCase(username, rid, options) {
+ if (typeof username === 'string') {
+ username = new RegExp(`^${ s.escapeRegExp(username) }$`, 'i');
+ }
+
+ const query = {
+ __rooms: rid,
+ username,
+ };
+
+ return this.findOne(query, options);
+ }
+
findOneByUsernameAndServiceNameIgnoringCase(username, userId, serviceName, options) {
if (typeof username === 'string') {
username = new RegExp(`^${ s.escapeRegExp(username) }$`, 'i');
@@ -661,7 +722,7 @@ export class Users extends Base {
return this.find(query, options);
}
- findByActiveUsersExcept(searchTerm, exceptions, options, forcedSearchFields, extraQuery = []) {
+ findByActiveUsersExcept(searchTerm, exceptions, options, forcedSearchFields, extraQuery = [], { startsWith = false, endsWith = false } = {}) {
if (exceptions == null) { exceptions = []; }
if (options == null) { options = {}; }
if (!_.isArray(exceptions)) {
@@ -683,7 +744,7 @@ export class Users extends Base {
return this._db.find(query, options);
}
- const termRegex = new RegExp(s.escapeRegExp(searchTerm), 'i');
+ const termRegex = new RegExp((startsWith ? '^' : '') + s.escapeRegExp(searchTerm) + (endsWith ? '$' : ''), 'i');
const searchFields = forcedSearchFields || settings.get('Accounts_SearchFields').trim().split(',');
diff --git a/app/models/server/models/_BaseDb.js b/app/models/server/models/_BaseDb.js
index f8438ad25ba4a..c88a29a90b3ac 100644
--- a/app/models/server/models/_BaseDb.js
+++ b/app/models/server/models/_BaseDb.js
@@ -124,7 +124,15 @@ export class BaseDb extends EventEmitter {
};
}
+ _ensureDefaultFields(options) {
+ if ((options?.fields == null || Object.keys(options?.fields).length === 0) && this.baseModel.defaultFields) {
+ options.fields = this.baseModel.defaultFields;
+ }
+ }
+
_doNotMixInclusionAndExclusionFields(options) {
+ this._ensureDefaultFields(options);
+
if (options && options.fields) {
const keys = Object.keys(options.fields);
const removeKeys = keys.filter((key) => options.fields[key] === 0);
@@ -134,18 +142,18 @@ export class BaseDb extends EventEmitter {
}
}
- find(...args) {
- this._doNotMixInclusionAndExclusionFields(args[1]);
- return this.model.find(...args);
+ find(query = {}, options = {}) {
+ this._doNotMixInclusionAndExclusionFields(options);
+ return this.model.find(query, options);
}
findById(_id, options) {
return this.find({ _id }, options);
}
- findOne(...args) {
- this._doNotMixInclusionAndExclusionFields(args[1]);
- return this.model.findOne(...args);
+ findOne(query = {}, options = {}) {
+ this._doNotMixInclusionAndExclusionFields(options);
+ return this.model.findOne(query, options);
}
findOneById(_id, options) {
diff --git a/app/models/server/raw/BaseRaw.js b/app/models/server/raw/BaseRaw.js
index fa6d3fc02de02..7d126d26797d0 100644
--- a/app/models/server/raw/BaseRaw.js
+++ b/app/models/server/raw/BaseRaw.js
@@ -3,20 +3,28 @@ export class BaseRaw {
this.col = col;
}
+ _ensureDefaultFields(options) {
+ if ((options?.fields == null || Object.keys(options?.fields).length === 0) && this.defaultFields) {
+ options.fields = this.defaultFields;
+ }
+ }
+
findOneById(_id, options = {}) {
return this.findOne({ _id }, options);
}
- findOne(...args) {
- return this.col.findOne(...args);
+ findOne(query = {}, options = {}) {
+ this._ensureDefaultFields(options);
+ return this.col.findOne(query, options);
}
findUsersInRoles() {
throw new Error('overwrite-function', 'You must overwrite this function in the extended classes');
}
- find(...args) {
- return this.col.find(...args);
+ find(query = {}, options = {}) {
+ this._ensureDefaultFields(options);
+ return this.col.find(query, options);
}
update(...args) {
diff --git a/app/models/server/raw/Users.js b/app/models/server/raw/Users.js
index d729302e7cf2e..5edbe960a35d3 100644
--- a/app/models/server/raw/Users.js
+++ b/app/models/server/raw/Users.js
@@ -1,6 +1,14 @@
import { BaseRaw } from './BaseRaw';
export class UsersRaw extends BaseRaw {
+ constructor(...args) {
+ super(...args);
+
+ this.defaultFields = {
+ __rooms: 0,
+ };
+ }
+
findUsersInRoles(roles, scope, options) {
roles = [].concat(roles);
diff --git a/app/theme/client/imports/general/base_old.css b/app/theme/client/imports/general/base_old.css
index a98c4a259b2fb..b90e6116e5abe 100644
--- a/app/theme/client/imports/general/base_old.css
+++ b/app/theme/client/imports/general/base_old.css
@@ -1961,6 +1961,12 @@
background-size: contain;
}
+.rc-old .popup-user-not_in_channel {
+ display: inline-block;
+
+ float: right;
+}
+
.rc-old .popup-user-status {
display: inline-block;
diff --git a/app/ui-message/client/popup/messagePopupConfig.js b/app/ui-message/client/popup/messagePopupConfig.js
index 79fee8de05aac..f23c4bf5651f8 100644
--- a/app/ui-message/client/popup/messagePopupConfig.js
+++ b/app/ui-message/client/popup/messagePopupConfig.js
@@ -7,11 +7,12 @@ import { Session } from 'meteor/session';
import { Template } from 'meteor/templating';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
-import { Messages, Subscriptions, Users } from '../../../models';
-import { hasAllPermission, hasAtLeastOnePermission } from '../../../authorization';
+import { Messages, Subscriptions } from '../../../models/client';
+import { settings } from '../../../settings/client';
+import { hasAllPermission, hasAtLeastOnePermission } from '../../../authorization/client';
import { EmojiPicker, emoji } from '../../../emoji';
-import { call } from '../../../ui-utils';
-import { t, getUserPreference, slashCommands } from '../../../utils';
+import { call } from '../../../ui-utils/client';
+import { t, getUserPreference, slashCommands } from '../../../utils/client';
import { customMessagePopups } from './customMessagePopups';
import './messagePopupConfig.html';
import './messagePopupSlashCommand.html';
@@ -66,19 +67,20 @@ const fetchUsersFromServer = _.throttle(async (filterText, records, rid, cb) =>
}
users
- .slice(0, 5)
- .forEach(({ username, nickname, name, status, avatarETag }) => {
- if (records.length < 5) {
- records.push({
- _id: username,
- username,
- nickname,
- name,
- status,
- avatarETag,
- sort: 3,
- });
- }
+ // .slice(0, 5)
+ .forEach(({ username, nickname, name, status, avatarETag, outside }) => {
+ // if (records.length < 5) {
+ records.push({
+ _id: username,
+ username,
+ nickname,
+ name,
+ status,
+ avatarETag,
+ outside,
+ sort: 3,
+ });
+ // }
});
records.sort(({ sort: sortA }, { sort: sortB }) => sortA - sortB);
@@ -192,6 +194,8 @@ Template.messagePopupConfig.helpers({
popupUserConfig() {
const template = Template.instance();
+ const suggestionsCount = settings.get('Number_of_users_autocomplete_suggestions');
+
return {
title: t('People'),
collection: template.usersFromRoomMessages,
@@ -210,7 +214,7 @@ Template.messagePopupConfig.helpers({
.find(
{
ts: { $exists: true },
- ...filterRegex && {
+ ...filterText && {
$or: [
{ username: filterRegex },
{ name: filterRegex },
@@ -218,114 +222,117 @@ Template.messagePopupConfig.helpers({
},
},
{
- limit: 5,
+ limit: filterText ? 2 : suggestionsCount,
sort: { ts: -1 },
},
)
- .fetch();
-
- // If needed, add to list the online users
- if (items.length < 5 && filterRegex) {
- const usernamesAlreadyFetched = items.map(({ username }) => username);
- if (!hasAllPermission('view-outside-room')) {
- const usernamesFromDMs = Subscriptions
- .find(
- {
- t: 'd',
- $and: [
- {
- ...filterRegex && {
- $or: [
- { name: filterRegex },
- { fname: filterRegex },
- ],
- },
- },
- {
- name: { $nin: usernamesAlreadyFetched },
- },
- ],
- },
- {
- fields: { name: 1 },
- },
- )
- .map(({ name }) => name);
- const newItems = Users
- .find(
- {
- username: {
- $in: usernamesFromDMs,
- },
- },
- {
- fields: {
- username: 1,
- nickname: 1,
- name: 1,
- status: 1,
- },
- limit: 5 - usernamesAlreadyFetched.length,
- },
- )
- .fetch()
- .map(({ username, name, status, nickname }) => ({
- _id: username,
- username,
- nickname,
- name,
- status,
- sort: 1,
- }));
-
- items.push(...newItems);
- } else {
- const user = Meteor.users.findOne(Meteor.userId(), { fields: { username: 1 } });
- const newItems = Meteor.users.find({
- $and: [
- {
- ...filterRegex && {
- $or: [
- { username: filterRegex },
- { name: filterRegex },
- ],
- },
- },
- {
- username: {
- $nin: [
- user && user.username,
- ...usernamesAlreadyFetched,
- ],
- },
- },
- ],
- },
- {
- fields: {
- username: 1,
- nickname: 1,
- name: 1,
- status: 1,
- },
- limit: 5 - usernamesAlreadyFetched.length,
- })
- .fetch()
- .map(({ username, name, status, nickname }) => ({
- _id: username,
- username,
- nickname,
- name,
- status,
- sort: 1,
- }));
-
- items.push(...newItems);
- }
- }
+ .fetch().map((u) => {
+ u.suggestion = true;
+ return u;
+ });
+
+ // // If needed, add to list the online users
+ // if (items.length < 5 && filterRegex) {
+ // const usernamesAlreadyFetched = items.map(({ username }) => username);
+ // if (!hasAllPermission('view-outside-room')) {
+ // const usernamesFromDMs = Subscriptions
+ // .find(
+ // {
+ // t: 'd',
+ // $and: [
+ // {
+ // ...filterRegex && {
+ // $or: [
+ // { name: filterRegex },
+ // { fname: filterRegex },
+ // ],
+ // },
+ // },
+ // {
+ // name: { $nin: usernamesAlreadyFetched },
+ // },
+ // ],
+ // },
+ // {
+ // fields: { name: 1 },
+ // },
+ // )
+ // .map(({ name }) => name);
+ // const newItems = Users
+ // .find(
+ // {
+ // username: {
+ // $in: usernamesFromDMs,
+ // },
+ // },
+ // {
+ // fields: {
+ // username: 1,
+ // nickname: 1,
+ // name: 1,
+ // status: 1,
+ // },
+ // limit: 5 - usernamesAlreadyFetched.length,
+ // },
+ // )
+ // .fetch()
+ // .map(({ username, name, status, nickname }) => ({
+ // _id: username,
+ // username,
+ // nickname,
+ // name,
+ // status,
+ // sort: 1,
+ // }));
+
+ // items.push(...newItems);
+ // } else {
+ // const user = Meteor.users.findOne(Meteor.userId(), { fields: { username: 1 } });
+ // const newItems = Meteor.users.find({
+ // $and: [
+ // {
+ // ...filterRegex && {
+ // $or: [
+ // { username: filterRegex },
+ // { name: filterRegex },
+ // ],
+ // },
+ // },
+ // {
+ // username: {
+ // $nin: [
+ // user && user.username,
+ // ...usernamesAlreadyFetched,
+ // ],
+ // },
+ // },
+ // ],
+ // },
+ // {
+ // fields: {
+ // username: 1,
+ // nickname: 1,
+ // name: 1,
+ // status: 1,
+ // },
+ // limit: 5 - usernamesAlreadyFetched.length,
+ // })
+ // .fetch()
+ // .map(({ username, name, status, nickname }) => ({
+ // _id: username,
+ // username,
+ // nickname,
+ // name,
+ // status,
+ // sort: 1,
+ // }));
+
+ // items.push(...newItems);
+ // }
+ // }
// Get users from Server
- if (items.length < 5 && filterText !== '') {
+ if (items.length < suggestionsCount && filterText !== '') {
fetchUsersFromServer(filterText, items, rid, cb);
}
diff --git a/app/ui-message/client/popup/messagePopupUser.html b/app/ui-message/client/popup/messagePopupUser.html
index f86a3d49d6f9e..2e8944155ef49 100644
--- a/app/ui-message/client/popup/messagePopupUser.html
+++ b/app/ui-message/client/popup/messagePopupUser.html
@@ -4,4 +4,10 @@
{{/unless}}
{{username}} {{name}}{{#if nickname}} ({{nickname}}){{/if}}
+ {{#if outside}}
+
+ {{/if}}
+ {{#if suggestion}}
+
+ {{/if}}
diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json
index 6bade089c7315..3d8df0d52a2c6 100644
--- a/packages/rocketchat-i18n/i18n/en.i18n.json
+++ b/packages/rocketchat-i18n/i18n/en.i18n.json
@@ -1691,6 +1691,7 @@
"Nickname": "Nickname",
"Nickname_Placeholder": "Enter your nickname...",
"Not_Following": "Not Following",
+ "Not_in_channel": "Not in channel",
"Follow_social_profiles": "Follow our social profiles, fork us on github and share your thoughts about the rocket.chat app on our trello board.",
"Fonts": "Fonts",
"Food_and_Drink": "Food & Drink",
@@ -2652,7 +2653,8 @@
"Number_of_federated_users": "Number of federated users",
"Number_of_federated_servers": "Number of federated servers",
"Number_of_messages": "Number of messages",
- "RetentionPolicy_DoNotExcludeDiscussion": "Do not exclude discussion messages",
+ "Number_of_users_autocomplete_suggestions": "Number of users' autocomplete suggestions",
+ "RetentionPolicy DoNotExcludeDiscussion": "Do not exclude discussion messages",
"OAuth Apps": "OAuth Apps",
"OAuth_Application": "OAuth Application",
"OAuth_Applications": "OAuth Applications",
@@ -3401,6 +3403,7 @@
"Support_Cordova_App_Description": "Allow the old mobile app, based on Cordova technology, to access the server enabling CORS for some APIs",
"Survey": "Survey",
"Survey_instructions": "Rate each question according to your satisfaction, 1 meaning you are completely unsatisfied and 5 meaning you are completely satisfied.",
+ "Suggestion_from_recent_messages": "Suggestion from recent messages",
"Symbols": "Symbols",
"Sync": "Sync",
"Sync / Import": "Sync / Import",
@@ -3957,4 +3960,4 @@
"Your_server_link": "Your server link",
"Your_temporary_password_is_password": "Your temporary password is [password].",
"Your_workspace_is_ready": "Your workspace is ready to use 🎉"
-}
\ No newline at end of file
+}
diff --git a/packages/rocketchat-i18n/i18n/pt-BR.i18n.json b/packages/rocketchat-i18n/i18n/pt-BR.i18n.json
index 2131f8a84ab10..b9978818f3b49 100644
--- a/packages/rocketchat-i18n/i18n/pt-BR.i18n.json
+++ b/packages/rocketchat-i18n/i18n/pt-BR.i18n.json
@@ -2325,6 +2325,7 @@
"No_discussions_yet": "Ainda sem discussões",
"No_Threads": "Nenhuma thread encontrada",
"No_user_with_username_%s_was_found": "Nenhum usuário com nome de usuário \"%s\" foi encontrado!",
+ "Not_in_channel": "Não está no canal",
"Nobody_available": "Ninguém disponÃvel",
"Node_version": "Versão do Node",
"None": "Nada",
@@ -2349,6 +2350,7 @@
"Notify_all_in_this_room": "Notificar todos nesta sala",
"Num_Agents": "# Agentes",
"Number_of_messages": "Número de mensagens",
+ "Number_of_users_autocomplete_suggestions": "Número de sugestões para autocomplete de usuários",
"RetentionPolicy_DoNotExcludeDiscussion": "Não exclua mensagens de discussões",
"OAuth Apps": "Apps OAuth",
"OAuth_Application": "Aplicação OAuth",
@@ -2978,6 +2980,7 @@
"Support": "Apoio",
"Survey": "Pesquisa",
"Survey_instructions": "Classifique cada questão de acordo com a sua satisfação, 1 significa que você está completamente insatisfeito e 5 significa que você está completamente satisfeito.",
+ "Suggestion_from_recent_messages": "Sugestão das mensagens recentes",
"Symbols": "SÃmbolos",
"Sync": "Sincronizar",
"Sync / Import": "Sincronizar / Importar",
@@ -3478,4 +3481,4 @@
"Your_question": "A sua pergunta",
"Your_server_link": "O link do seu servidor",
"Your_workspace_is_ready": "O seu espaço de trabalho está pronto a usar 🎉"
-}
\ No newline at end of file
+}
diff --git a/server/methods/getUsersOfRoom.js b/server/methods/getUsersOfRoom.js
index 089733f985ca4..2f570e7b8adf0 100644
--- a/server/methods/getUsersOfRoom.js
+++ b/server/methods/getUsersOfRoom.js
@@ -1,54 +1,30 @@
import { Meteor } from 'meteor/meteor';
-import s from 'underscore.string';
-import { Subscriptions } from '../../app/models';
+import { Subscriptions, Users } from '../../app/models/server';
import { hasPermission } from '../../app/authorization';
import { settings } from '../../app/settings';
function findUsers({ rid, status, skip, limit, filter = '' }) {
- const regex = new RegExp(s.trim(s.escapeRegExp(filter)), 'i');
- return Subscriptions.model.rawCollection().aggregate([
- {
- $match: { rid },
+ const options = {
+ fields: {
+ name: 1,
+ username: 1,
+ nickname: 1,
+ status: 1,
+ avatarETag: 1,
},
- {
- $lookup:
- {
- from: 'users',
- localField: 'u._id',
- foreignField: '_id',
- as: 'u',
- },
+ sort: {
+ statusConnection: -1,
+ [settings.get('UI_Use_Real_Name') ? 'name' : 'username']: 1,
},
- {
- $project: {
- 'u._id': 1,
- 'u.name': 1,
- 'u.username': 1,
- 'u.nickname': 1,
- 'u.status': 1,
- 'u.avatarETag': 1,
- },
- },
- ...status ? [{ $match: { 'u.status': status } }] : [],
- ...filter.trim() ? [{ $match: { $or: [{ 'u.name': regex }, { 'u.username': regex }, { 'u.nickname': regex }] } }] : [],
- {
- $sort: {
- [settings.get('UI_Use_Real_Name') ? 'u.name' : 'u.username']: 1,
- },
- },
- ...skip > 0 ? [{ $skip: skip }] : [],
- ...limit > 0 ? [{ $limit: limit }] : [],
- {
- $project: {
- _id: { $arrayElemAt: ['$u._id', 0] },
- name: { $arrayElemAt: ['$u.name', 0] },
- username: { $arrayElemAt: ['$u.username', 0] },
- nickname: { $arrayElemAt: ['$u.nickname', 0] },
- avatarETag: { $arrayElemAt: ['$u.avatarETag', 0] },
- },
- },
- ]).toArray();
+ ...skip > 0 && { skip },
+ ...limit > 0 && { limit },
+ };
+
+ return Users.findByActiveUsersExcept(filter, undefined, options, undefined, [{
+ __rooms: rid,
+ ...status && { status },
+ }]).fetch();
}
Meteor.methods({
@@ -68,21 +44,8 @@ Meteor.methods({
}
const total = Subscriptions.findByRoomIdWhenUsernameExists(rid).count();
- const users = await findUsers({ rid, status: { $ne: 'offline' }, limit, skip, filter });
- if (showAll && (!limit || users.length < limit)) {
- const offlineUsers = await findUsers({
- rid,
- status: { $eq: 'offline' },
- limit: limit ? limit - users.length : 0,
- skip: skip || 0,
- filter,
- });
- return {
- total,
- records: users.concat(offlineUsers),
- };
- }
+ const users = await findUsers({ rid, status: !showAll ? { $ne: 'offline' } : undefined, limit, skip, filter });
return {
total,
diff --git a/server/publications/spotlight.js b/server/publications/spotlight.js
index 9fe9ba87fe3ce..fdcb4b559c523 100644
--- a/server/publications/spotlight.js
+++ b/server/publications/spotlight.js
@@ -2,10 +2,10 @@ import { Meteor } from 'meteor/meteor';
import { DDPRateLimiter } from 'meteor/ddp-rate-limiter';
import s from 'underscore.string';
-import { hasPermission } from '../../app/authorization';
-import { Users, Subscriptions, Rooms } from '../../app/models';
-import { settings } from '../../app/settings';
-import { roomTypes } from '../../app/utils';
+import { hasAllPermission, hasPermission, canAccessRoom } from '../../app/authorization/server';
+import { Users, Subscriptions, Rooms } from '../../app/models/server';
+import { settings } from '../../app/settings/server';
+import { roomTypes } from '../../app/utils/server';
function fetchRooms(userId, rooms) {
if (!settings.get('Store_Last_Message') || hasPermission(userId, 'preview-c-room')) {
@@ -18,99 +18,213 @@ function fetchRooms(userId, rooms) {
});
}
+function searchRooms({ userId, text }) {
+ const regex = new RegExp(s.trim(s.escapeRegExp(text)), 'i');
+
+ const roomOptions = {
+ limit: 5,
+ fields: {
+ t: 1,
+ name: 1,
+ joinCodeRequired: 1,
+ lastMessage: 1,
+ },
+ sort: {
+ name: 1,
+ },
+ };
+
+ if (userId == null) {
+ if (!settings.get('Accounts_AllowAnonymousRead') === true) {
+ return [];
+ }
+
+ return fetchRooms(userId, Rooms.findByNameAndTypeNotDefault(regex, 'c', roomOptions).fetch());
+ }
+
+ if (hasAllPermission(userId, ['view-outside-room', 'view-c-room'])) {
+ const searchableRoomTypes = Object.entries(roomTypes.roomTypes)
+ .filter((roomType) => roomType[1].includeInRoomSearch())
+ .map((roomType) => roomType[0]);
+
+ const roomIds = Subscriptions.findByUserIdAndTypes(userId, searchableRoomTypes, { fields: { rid: 1 } }).fetch().map((s) => s.rid);
+ const exactRoom = Rooms.findOneByNameAndType(text, searchableRoomTypes, roomOptions);
+ if (exactRoom) {
+ roomIds.push(exactRoom.rid);
+ }
+
+ return fetchRooms(userId, Rooms.findByNameAndTypesNotInIds(regex, searchableRoomTypes, roomIds, roomOptions).fetch());
+ }
+
+ return [];
+}
+
+function mapOutsiders(u) {
+ u.outside = true;
+ return u;
+}
+
+function processLimitAndUsernames(options, usernames, users) {
+ // Reduce the results from the limit for the next query
+ options.limit -= users.length;
+
+ // If the limit was reached, return
+ if (options.limit <= 0) {
+ return users;
+ }
+
+ // Prevent the next query to get the same users
+ usernames.push(...users.map((u) => u.username).filter((u) => !usernames.includes(u)));
+}
+
+function _searchInsiderUsers({ rid, text, usernames, options, users, insiderExtraQuery, match = { startsWith: false, endsWith: false } }) {
+ // Get insiders first
+ if (rid) {
+ users.push(...Users.findByActiveUsersExcept(text, usernames, options, undefined, insiderExtraQuery, match).fetch());
+
+ // If the limit was reached, return
+ if (processLimitAndUsernames(options, usernames, users)) {
+ return users;
+ }
+ }
+}
+
+function _searchOutsiderUsers({ text, usernames, options, users, canListOutsiders, match = { startsWith: false, endsWith: false } }) {
+ // Then get the outsiders if allowed
+ if (canListOutsiders) {
+ users.push(...Users.findByActiveUsersExcept(text, usernames, options, undefined, undefined, match).fetch().map(mapOutsiders));
+
+ // If the limit was reached, return
+ if (processLimitAndUsernames(options, usernames, users)) {
+ return users;
+ }
+ }
+}
+
+function searchUsers({ userId, rid, text, usernames }) {
+ const users = [];
+
+ const options = {
+ limit: settings.get('Number_of_users_autocomplete_suggestions'),
+ fields: {
+ username: 1,
+ nickname: 1,
+ name: 1,
+ status: 1,
+ statusText: 1,
+ avatarETag: 1,
+ },
+ sort: {
+ [settings.get('UI_Use_Real_Name') ? 'name' : 'username']: 1,
+ },
+ };
+
+ const room = Rooms.findOneById(rid, { fields: { _id: 1, t: 1, uids: 1 } });
+
+ if (rid && !room) {
+ return users;
+ }
+
+ const canListOutsiders = hasAllPermission(userId, 'view-outside-room', 'view-d-room');
+ const canListInsiders = canListOutsiders || (rid && canAccessRoom(room, { _id: userId }));
+
+ // If can't list outsiders and, wither, the rid was not passed or the user has no access to the room, return
+ if (!canListOutsiders && !canListInsiders) {
+ return users;
+ }
+
+ const insiderExtraQuery = [];
+
+ if (rid) {
+ switch (room.t) {
+ case 'd':
+ insiderExtraQuery.push({
+ _id: { $in: room.uids.filter((id) => id !== userId) },
+ });
+ break;
+ case 'l':
+ insiderExtraQuery.push({
+ _id: { $in: Subscriptions.findByRoomId(room._id).fetch().map((s) => s.u?._id).filter((id) => id && id !== userId) },
+ });
+ break;
+ default:
+ insiderExtraQuery.push({
+ __rooms: rid,
+ });
+ break;
+ }
+ }
+
+ const searchParams = { rid, text, usernames, options, users, canListOutsiders, insiderExtraQuery };
+
+ // Exact match for username only
+ if (rid) {
+ const exactMatch = Users.findOneByUsernameAndRoomIgnoringCase(text, rid, { fields: options.fields });
+ if (exactMatch) {
+ users.push(exactMatch);
+ processLimitAndUsernames(options, usernames, users);
+ }
+ }
+
+ if (users.length === 0 && canListOutsiders) {
+ const exactMatch = Users.findOneByUsernameIgnoringCase(text, { fields: options.fields });
+ if (exactMatch) {
+ users.push(exactMatch);
+ processLimitAndUsernames(options, usernames, users);
+ }
+ }
+
+ // Exact match for insiders
+ // if (_searchInsiderUsers({ ...searchParams, match: { startsWith: true, endsWith: true } })) {
+ // return users;
+ // }
+
+ // Exact match for outsiders
+ // if (_searchOutsiderUsers({ ...searchParams, match: { startsWith: true, endsWith: true } })) {
+ // return users;
+ // }
+
+ // Starts with for insiders
+ // if (_searchInsiderUsers({ ...searchParams, match: { startsWith: true } })) {
+ // return users;
+ // }
+
+ // Contains for insiders
+ if (_searchInsiderUsers(searchParams)) {
+ return users;
+ }
+
+ // Starts with for outsiders
+ // if (_searchOutsiderUsers({ ...searchParams, match: { startsWith: true } })) {
+ // return users;
+ // }
+
+ // Contains for outsiders
+ if (_searchOutsiderUsers(searchParams)) {
+ return users;
+ }
+
+ return users;
+}
+
Meteor.methods({
spotlight(text, usernames = [], type = { users: true, rooms: true }, rid) {
- const searchForChannels = text[0] === '#';
- const searchForDMs = text[0] === '@';
- if (searchForChannels) {
+ if (text[0] === '#') {
type.users = false;
text = text.slice(1);
}
- if (searchForDMs) {
+
+ if (text[0] === '@') {
type.rooms = false;
text = text.slice(1);
}
- const regex = new RegExp(s.trim(s.escapeRegExp(text)), 'i');
- const result = {
- users: [],
- rooms: [],
- };
- const roomOptions = {
- limit: 5,
- fields: {
- t: 1,
- name: 1,
- joinCodeRequired: 1,
- lastMessage: 1,
- },
- sort: {
- name: 1,
- },
- };
- const { userId } = this;
- if (userId == null) {
- if (settings.get('Accounts_AllowAnonymousRead') === true) {
- result.rooms = fetchRooms(userId, Rooms.findByNameAndTypeNotDefault(regex, 'c', roomOptions).fetch());
- }
- return result;
- }
- const userOptions = {
- limit: 5,
- fields: {
- username: 1,
- nickname: 1,
- name: 1,
- status: 1,
- statusText: 1,
- avatarETag: 1,
- },
- sort: {},
- };
- if (settings.get('UI_Use_Real_Name')) {
- userOptions.sort.name = 1;
- } else {
- userOptions.sort.username = 1;
- }
- if (hasPermission(userId, 'view-outside-room')) {
- if (type.users === true && hasPermission(userId, 'view-d-room')) {
- const exactUser = Users.findOneByUsernameIgnoringCase(text, userOptions);
- if (exactUser && !usernames.includes(exactUser.username)) {
- result.users.push(exactUser);
- usernames.push(exactUser.username);
- }
- result.users = result.users.concat(Users.findByActiveUsersExcept(text, usernames, userOptions).fetch());
- }
-
- if (type.rooms === true && hasPermission(userId, 'view-c-room')) {
- const searchableRoomTypes = Object.entries(roomTypes.roomTypes)
- .filter((roomType) => roomType[1].includeInRoomSearch())
- .map((roomType) => roomType[0]);
-
- const roomIds = Subscriptions.findByUserIdAndTypes(userId, searchableRoomTypes, { fields: { rid: 1 } }).fetch().map((s) => s.rid);
- const exactRoom = Rooms.findOneByNameAndType(text, searchableRoomTypes, roomOptions);
- if (exactRoom) {
- result.exactRoom.push(exactRoom);
- roomIds.push(exactRoom.rid);
- }
-
- result.rooms = result.rooms.concat(fetchRooms(userId, Rooms.findByNameAndTypesNotInIds(regex, searchableRoomTypes, roomIds, roomOptions).fetch()));
- }
- } else if (type.users === true && rid) {
- const subscriptions = Subscriptions.find({
- rid,
- 'u.username': {
- $regex: regex,
- $nin: [...usernames, Meteor.user().username],
- },
- }, { limit: userOptions.limit }).fetch().map(({ u }) => u._id);
- result.users = Users.find({ _id: { $in: subscriptions } }, {
- fields: userOptions.fields,
- sort: userOptions.sort,
- }).fetch();
- }
+ const { userId } = this;
- return result;
+ return {
+ users: type.users === true ? searchUsers({ userId, rid, text, usernames }) : [],
+ rooms: type.rooms === true ? searchRooms({ userId, text }) : [],
+ };
},
});
diff --git a/server/startup/migrations/index.js b/server/startup/migrations/index.js
index 5e01f1de5e87f..cbd1354d420a9 100644
--- a/server/startup/migrations/index.js
+++ b/server/startup/migrations/index.js
@@ -195,4 +195,5 @@ import './v195';
import './v196';
import './v197';
import './v198';
+import './v199';
import './xrun';
diff --git a/server/startup/migrations/v183.js b/server/startup/migrations/v183.js
index df71bd061730e..2babe6168fddd 100644
--- a/server/startup/migrations/v183.js
+++ b/server/startup/migrations/v183.js
@@ -1,3 +1,4 @@
+import { Meteor } from 'meteor/meteor';
import { Random } from 'meteor/random';
import { Migrations } from '../../../app/migrations';
@@ -56,11 +57,15 @@ const fixSelfDMs = () => {
rid: correctId,
},
}, { multi: true });
- Uploads.update({ rid: room._id }, {
- $set: {
- rid: correctId,
- },
- }, { multi: true });
+
+ // Fix error of upload permission check using Meteor.userId()
+ Meteor.runAsUser(room.uids[0], () => {
+ Uploads.update({ rid: room._id }, {
+ $set: {
+ rid: correctId,
+ },
+ }, { multi: true });
+ });
});
};
diff --git a/server/startup/migrations/v199.js b/server/startup/migrations/v199.js
new file mode 100644
index 0000000000000..bd3d111216d02
--- /dev/null
+++ b/server/startup/migrations/v199.js
@@ -0,0 +1,73 @@
+import { Meteor } from 'meteor/meteor';
+import Future from 'fibers/future';
+
+import { Users, Subscriptions } from '../../../app/models/server/raw';
+import { Migrations } from '../../../app/migrations/server';
+
+const batchSize = 5000;
+
+async function migrateUserRecords(total, current) {
+ console.log(`Migrating ${ current }/${ total }`);
+
+ const items = await Users.find({ __rooms: { $exists: false } }, { projection: { _id: 1 } }).limit(batchSize).toArray();
+
+ const actions = [];
+
+ for await (const user of items) {
+ const rooms = await Subscriptions.find({
+ 'u._id': user._id,
+ t: { $nin: ['d', 'l'] },
+ }, { projection: { rid: 1 } }).toArray();
+
+ actions.push({
+ updateOne: {
+ filter: { _id: user._id },
+ update: {
+ $set: {
+ __rooms: rooms.map(({ rid }) => rid),
+ },
+ },
+ },
+ });
+ }
+
+ if (actions.length === 0) {
+ return;
+ }
+
+ const batch = await Users.col.bulkWrite(actions, { ordered: false });
+ if (actions.length === batchSize) {
+ await batch;
+ return migrateUserRecords(total, current + batchSize);
+ }
+
+ return batch;
+}
+
+Migrations.add({
+ version: 199,
+ up() {
+ const fut = new Future();
+
+ console.log('Changing schema of User records, this may take a long time ...');
+
+ Meteor.setTimeout(async () => {
+ const users = Users.find({ __rooms: { $exists: false } }, { projection: { _id: 1 } });
+ const total = await users.count();
+ await users.close();
+
+ if (total < batchSize * 10) {
+ await migrateUserRecords(total, 0);
+ return fut.return();
+ }
+
+ await migrateUserRecords(total, 0);
+
+ fut.return();
+ }, 250);
+
+ fut.wait();
+
+ console.log('Changing schema of User records finished.');
+ },
+});