-
-
-
- |
- {{_ "Name"}} |
- {{_ "Username"}} |
- {{_ "Email"}} |
- |
-
-
-
- {{#each agents}}
-
-
-
- {{> avatar username=username}}
-
- |
- {{name}} |
- {{username}} |
- {{emailAddress}} |
- |
-
- {{/each}}
-
-
+
+ {{#table fixed='true'}}
+
+
+
+ {{_ "Name"}}
+ |
+
+ {{_ "Username"}}
+ |
+
+ {{_ "Email"}}
+ |
+ |
+
+
+
+ {{#each agents}}
+
+
+
+ {{> avatar username=username}}
+
+
+ {{name}}
+
+
+
+
+ |
+ {{username}} |
+ {{emailAddress}} |
+
+
+
+
+ |
+
+ {{/each}}
+
+ {{/table}}
{{/requiresPermission}}
diff --git a/app/livechat/client/views/app/livechatUsers.js b/app/livechat/client/views/app/livechatUsers.js
index 2843ecd6d807..e2c014e7c508 100644
--- a/app/livechat/client/views/app/livechatUsers.js
+++ b/app/livechat/client/views/app/livechatUsers.js
@@ -1,12 +1,13 @@
import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';
import { Template } from 'meteor/templating';
-import _ from 'underscore';
-import toastr from 'toastr';
+import { ReactiveVar } from 'meteor/reactive-var';
+import { ReactiveDict } from 'meteor/reactive-dict';
-import { modal } from '../../../../ui-utils';
+import { modal, call } from '../../../../ui-utils';
import { t, handleError } from '../../../../utils';
import { AgentUsers } from '../../collections/AgentUsers';
+
import './livechatUsers.html';
let ManagerUsers;
@@ -14,8 +15,41 @@ let ManagerUsers;
Meteor.startup(function() {
ManagerUsers = new Mongo.Collection('managerUsers');
});
-
+const getUsername = (user) => user.username;
Template.livechatUsers.helpers({
+ exceptionsManagers() {
+ const { selectedManagers } = Template.instance();
+ return ManagerUsers.find({}, { fields: { username: 1 } })
+ .fetch()
+ .map(getUsername)
+ .concat(selectedManagers.get().map(getUsername));
+ },
+ exceptionsAgents() {
+ const { selectedAgents } = Template.instance();
+ return AgentUsers.find({}, { fields: { username: 1 } })
+ .fetch()
+ .map(getUsername)
+ .concat(selectedAgents.get().map(getUsername));
+ },
+ deleteLastAgent() {
+ const i = Template.instance();
+ return () => {
+ const arr = i.selectedAgents.curValue;
+ arr.pop();
+ i.selectedAgents.set(arr);
+ };
+ },
+ deleteLastManager() {
+ const i = Template.instance();
+ return () => {
+ const arr = i.selectedManagers.curValue;
+ arr.pop();
+ i.selectedManagers.set(arr);
+ };
+ },
+ isLoading() {
+ return Template.instance().state.get('loading');
+ },
managers() {
return ManagerUsers.find({}, { sort: { name: 1 } });
},
@@ -27,152 +61,174 @@ Template.livechatUsers.helpers({
return this.emails[0].address;
}
},
- agentAutocompleteSettings() {
- return {
- limit: 10,
- // inputDelay: 300
- rules: [{
- // @TODO maybe change this 'collection' and/or template
- collection: 'UserAndRoom',
- subscription: 'userAutocomplete',
- field: 'username',
- template: Template.userSearch,
- noMatchTemplate: Template.userSearchEmpty,
- matchAll: true,
- filter: {
- exceptions: _.map(AgentUsers.find({}, { fields: { username: 1 } }).fetch(), (user) => user.username),
- },
- selector(match) {
- return { term: match };
- },
- sort: 'username',
- }],
+ agentModifier() {
+ return (filter, text = '') => {
+ const f = filter.get();
+ return `@${
+ f.length === 0
+ ? text
+ : text.replace(
+ new RegExp(filter.get()),
+ (part) => `
${ part }`
+ )
+ }`;
};
},
- managerAutocompleteSettings() {
- return {
- limit: 10,
- // inputDelay: 300
- rules: [{
- // @TODO maybe change this 'collection' and/or template
- collection: 'UserAndRoom',
- subscription: 'userAutocomplete',
- field: 'username',
- template: Template.userSearch,
- noMatchTemplate: Template.userSearchEmpty,
- matchAll: true,
- filter: {
- exceptions: _.map(ManagerUsers.find({}, { fields: { username: 1 } }).fetch(), (user) => user.username),
- },
- selector(match) {
- return { term: match };
- },
- sort: 'username',
- }],
- };
+ onSelectManagers() {
+ return Template.instance().onSelectManagers;
+ },
+ onSelectAgents() {
+ return Template.instance().onSelectAgents;
+ },
+ selectedAgents() {
+ return Template.instance().selectedAgents.get();
+ },
+ selectedManagers() {
+ return Template.instance().selectedManagers.get();
+ },
+ onClickTagAgents() {
+ return Template.instance().onClickTagAgents;
+ },
+ onClickTagManagers() {
+ return Template.instance().onClickTagManagers;
},
});
Template.livechatUsers.events({
- 'click .remove-manager'(e/* , instance*/) {
+ 'click .remove-manager'(e /* , instance*/) {
e.preventDefault();
- modal.open({
- title: t('Are_you_sure'),
- type: 'warning',
- showCancelButton: true,
- confirmButtonColor: '#DD6B55',
- confirmButtonText: t('Yes'),
- cancelButtonText: t('Cancel'),
- closeOnConfirm: false,
- html: false,
- }, () => {
- Meteor.call('livechat:removeManager', this.username, function(error/* , result*/) {
- if (error) {
- return handleError(error);
- }
- modal.open({
- title: t('Removed'),
- text: t('Manager_removed'),
- type: 'success',
- timer: 1000,
- showConfirmButton: false,
+ modal.open(
+ {
+ title: t('Are_you_sure'),
+ type: 'warning',
+ showCancelButton: true,
+ confirmButtonColor: '#DD6B55',
+ confirmButtonText: t('Yes'),
+ cancelButtonText: t('Cancel'),
+ closeOnConfirm: false,
+ html: false,
+ },
+ () => {
+ Meteor.call('livechat:removeManager', this.username, function(
+ error /* , result*/
+ ) {
+ if (error) {
+ return handleError(error);
+ }
+ modal.open({
+ title: t('Removed'),
+ text: t('Manager_removed'),
+ type: 'success',
+ timer: 1000,
+ showConfirmButton: false,
+ });
});
- });
- });
+ }
+ );
},
- 'click .remove-agent'(e/* , instance*/) {
+ 'click .remove-agent'(e /* , instance*/) {
e.preventDefault();
- modal.open({
- title: t('Are_you_sure'),
- type: 'warning',
- showCancelButton: true,
- confirmButtonColor: '#DD6B55',
- confirmButtonText: t('Yes'),
- cancelButtonText: t('Cancel'),
- closeOnConfirm: false,
- html: false,
- }, () => {
- Meteor.call('livechat:removeAgent', this.username, function(error/* , result*/) {
- if (error) {
- return handleError(error);
- }
- modal.open({
- title: t('Removed'),
- text: t('Agent_removed'),
- type: 'success',
- timer: 1000,
- showConfirmButton: false,
+ modal.open(
+ {
+ title: t('Are_you_sure'),
+ type: 'warning',
+ showCancelButton: true,
+ confirmButtonColor: '#DD6B55',
+ confirmButtonText: t('Yes'),
+ cancelButtonText: t('Cancel'),
+ closeOnConfirm: false,
+ html: false,
+ },
+ () => {
+ Meteor.call('livechat:removeAgent', this.username, function(
+ error /* , result*/
+ ) {
+ if (error) {
+ return handleError(error);
+ }
+ modal.open({
+ title: t('Removed'),
+ text: t('Agent_removed'),
+ type: 'success',
+ timer: 1000,
+ showConfirmButton: false,
+ });
});
- });
- });
+ }
+ );
},
- 'submit #form-manager'(e/* , instance*/) {
+
+ async 'submit #form-manager'(e, instance) {
e.preventDefault();
+ const { selectedManagers, state } = instance;
- if (e.currentTarget.elements.username.value.trim() === '') {
- return toastr.error(t('Please_fill_a_username'));
+ const users = selectedManagers.get();
+
+ if (!users.length) {
+ return;
}
- const oldBtnValue = e.currentTarget.elements.add.value;
+ state.set('loading', true);
+ try {
+ await Promise.all(
+ users.map(({ username }) => call('livechat:addManager', username))
+ );
+ selectedManagers.set([]);
+ } finally {
+ state.set('loading', false);
+ }
+ },
- e.currentTarget.elements.add.value = t('Saving');
+ async 'submit #form-agent'(e, instance) {
+ e.preventDefault();
+ const { selectedAgents, state } = instance;
- Meteor.call('livechat:addManager', e.currentTarget.elements.username.value, function(error/* , result*/) {
- e.currentTarget.elements.add.value = oldBtnValue;
- if (error) {
- return handleError(error);
- }
+ const users = selectedAgents.get();
- toastr.success(t('Manager_added'));
- e.currentTarget.reset();
- });
- },
- 'submit #form-agent'(e/* , instance*/) {
- e.preventDefault();
+ if (!users.length) {
+ return;
+ }
- if (e.currentTarget.elements.username.value.trim() === '') {
- return toastr.error(t('Please_fill_a_username'));
+ state.set('loading', true);
+ try {
+ await Promise.all(
+ users.map(({ username }) => call('livechat:addAgent', username))
+ );
+ selectedAgents.set([]);
+ } finally {
+ state.set('loading', false);
}
+ },
+});
- const oldBtnValue = e.currentTarget.elements.add.value;
+Template.livechatUsers.onCreated(function() {
+ this.state = new ReactiveDict({
+ loading: false,
+ });
- e.currentTarget.elements.add.value = t('Saving');
+ this.selectedAgents = new ReactiveVar([]);
+ this.selectedManagers = new ReactiveVar([]);
- Meteor.call('livechat:addAgent', e.currentTarget.elements.username.value, function(error/* , result*/) {
- e.currentTarget.elements.add.value = oldBtnValue;
- if (error) {
- return handleError(error);
- }
+ this.onSelectAgents = ({ item: agent }) => {
+ this.selectedAgents.set([...this.selectedAgents.curValue, agent]);
+ };
- toastr.success(t('Agent_added'));
- e.currentTarget.reset();
- });
- },
-});
+ this.onClickTagAgents = ({ username }) => {
+ this.selectedAgents.set(this.selectedAgents.curValue.filter((user) => user.username !== username));
+ };
+
+ this.onSelectManagers = ({ item: manager }) => {
+ this.selectedManagers.set([...this.selectedManagers.curValue, manager]);
+ };
+
+ this.onClickTagManagers = ({ username }) => {
+ this.selectedManagers.set(
+ this.selectedManagers.curValue.filter((user) => user.username !== username)
+ );
+ };
-Template.livechatUsers.onCreated(function() {
this.subscribe('livechat:agents');
this.subscribe('livechat:managers');
});
diff --git a/app/livechat/imports/server/rest/inquiries.js b/app/livechat/imports/server/rest/inquiries.js
new file mode 100644
index 000000000000..a341d9978e88
--- /dev/null
+++ b/app/livechat/imports/server/rest/inquiries.js
@@ -0,0 +1,69 @@
+import { Meteor } from 'meteor/meteor';
+import { Match, check } from 'meteor/check';
+
+import { API } from '../../../../api';
+import { hasPermission } from '../../../../authorization';
+import { Users, LivechatDepartment } from '../../../../models';
+import { LivechatInquiry } from '../../../lib/LivechatInquiry';
+
+API.v1.addRoute('livechat/inquiries.list', { authRequired: true }, {
+ get() {
+ if (!hasPermission(this.userId, 'view-livechat-manager')) {
+ return API.v1.unauthorized();
+ }
+ const { offset, count } = this.getPaginationItems();
+ const { sort } = this.parseJsonQuery();
+ const { department } = this.requestParams();
+ const ourQuery = Object.assign({}, { status: 'open' });
+ if (department) {
+ const departmentFromDB = LivechatDepartment.findOneByIdOrName(department);
+ if (departmentFromDB) {
+ ourQuery.department = departmentFromDB._id;
+ }
+ }
+ const cursor = LivechatInquiry.find(ourQuery, {
+ sort: sort || { ts: -1 },
+ skip: offset,
+ limit: count,
+ fields: {
+ rid: 1,
+ name: 1,
+ ts: 1,
+ status: 1,
+ department: 1,
+ },
+ });
+ const totalCount = cursor.count();
+ const inquiries = cursor.fetch();
+
+
+ return API.v1.success({
+ inquiries,
+ offset,
+ count: inquiries.length,
+ total: totalCount,
+ });
+ },
+});
+
+API.v1.addRoute('livechat/inquiries.take', { authRequired: true }, {
+ post() {
+ if (!hasPermission(this.userId, 'view-livechat-manager')) {
+ return API.v1.unauthorized();
+ }
+ try {
+ check(this.bodyParams, {
+ inquiryId: String,
+ userId: Match.Maybe(String),
+ });
+ if (this.bodyParams.userId && !Users.findOneById(this.bodyParams.userId, { fields: { _id: 1 } })) {
+ return API.v1.failure('The user is invalid');
+ }
+ return API.v1.success({
+ inquiry: Meteor.runAsUser(this.bodyParams.userId || this.userId, () => Meteor.call('livechat:takeInquiry', this.bodyParams.inquiryId)),
+ });
+ } catch (e) {
+ return API.v1.failure(e);
+ }
+ },
+});
diff --git a/app/livechat/server/api.js b/app/livechat/server/api.js
index 38c7e462da06..5b95e5a4b3c2 100644
--- a/app/livechat/server/api.js
+++ b/app/livechat/server/api.js
@@ -3,3 +3,4 @@ import '../imports/server/rest/facebook.js';
import '../imports/server/rest/sms.js';
import '../imports/server/rest/users.js';
import '../imports/server/rest/upload.js';
+import '../imports/server/rest/inquiries.js';
diff --git a/app/livechat/server/api/v1/pageVisited.js b/app/livechat/server/api/v1/pageVisited.js
index 4b35cd3adb6e..e5ef7c42ba64 100644
--- a/app/livechat/server/api/v1/pageVisited.js
+++ b/app/livechat/server/api/v1/pageVisited.js
@@ -1,9 +1,7 @@
-import { Meteor } from 'meteor/meteor';
import { Match, check } from 'meteor/check';
import _ from 'underscore';
import { API } from '../../../../api';
-import { findGuest, findRoom } from '../lib/livechat';
import { Livechat } from '../../lib/Livechat';
API.v1.addRoute('livechat/page.visited', {
@@ -11,7 +9,7 @@ API.v1.addRoute('livechat/page.visited', {
try {
check(this.bodyParams, {
token: String,
- rid: String,
+ rid: Match.Maybe(String),
pageInfo: Match.ObjectIncluding({
change: String,
title: String,
@@ -22,17 +20,6 @@ API.v1.addRoute('livechat/page.visited', {
});
const { token, rid, pageInfo } = this.bodyParams;
-
- const guest = findGuest(token);
- if (!guest) {
- throw new Meteor.Error('invalid-token');
- }
-
- const room = findRoom(token, rid);
- if (!room) {
- throw new Meteor.Error('invalid-room');
- }
-
const obj = Livechat.savePageHistory(token, rid, pageInfo);
if (obj) {
const page = _.pick(obj, 'msg', 'navigation');
diff --git a/app/livechat/server/config.js b/app/livechat/server/config.js
index 929c914950ca..a084ce643856 100644
--- a/app/livechat/server/config.js
+++ b/app/livechat/server/config.js
@@ -279,6 +279,14 @@ Meteor.startup(function() {
i18nLabel: 'Office_hours_enabled',
});
+ settings.add('Livechat_allow_online_agents_outside_office_hours', true, {
+ type: 'boolean',
+ group: 'Livechat',
+ public: true,
+ i18nLabel: 'Allow_Online_Agents_Outside_Office_Hours',
+ enableQuery: { _id: 'Livechat_enable_office_hours', value: true },
+ });
+
settings.add('Livechat_continuous_sound_notification_new_livechat_room', false, {
type: 'boolean',
group: 'Livechat',
@@ -382,6 +390,15 @@ Meteor.startup(function() {
enableQuery: { _id: 'Livechat_Routing_Method', value: 'Guest_Pool' },
});
+ settings.add('Livechat_guest_pool_max_number_incoming_livechats_displayed', 0, {
+ type: 'int',
+ group: 'Livechat',
+ section: 'Routing',
+ i18nLabel: 'Max_number_incoming_livechats_displayed',
+ i18nDescription: 'Max_number_incoming_livechats_displayed_description',
+ enableQuery: { _id: 'Livechat_Routing_Method', value: 'Guest_Pool' },
+ });
+
settings.add('Livechat_show_queue_list_link', false, {
type: 'boolean',
group: 'Livechat',
diff --git a/app/livechat/server/hooks/externalMessage.js b/app/livechat/server/hooks/externalMessage.js
index 91b9448be491..2ef76fbb88f1 100644
--- a/app/livechat/server/hooks/externalMessage.js
+++ b/app/livechat/server/hooks/externalMessage.js
@@ -1,4 +1,3 @@
-import { Meteor } from 'meteor/meteor';
import { HTTP } from 'meteor/http';
import _ from 'underscore';
@@ -39,32 +38,30 @@ callbacks.add('afterSaveMessage', function(message, room) {
return message;
}
- Meteor.defer(() => {
- try {
- const response = HTTP.post('https://api.api.ai/api/query?v=20150910', {
- data: {
- query: message.msg,
- lang: apiaiLanguage,
- sessionId: room._id,
- },
- headers: {
- 'Content-Type': 'application/json; charset=utf-8',
- Authorization: `Bearer ${ apiaiKey }`,
- },
- });
+ try {
+ const response = HTTP.post('https://api.api.ai/api/query?v=20150910', {
+ data: {
+ query: message.msg,
+ lang: apiaiLanguage,
+ sessionId: room._id,
+ },
+ headers: {
+ 'Content-Type': 'application/json; charset=utf-8',
+ Authorization: `Bearer ${ apiaiKey }`,
+ },
+ });
- if (response.data && response.data.status.code === 200 && !_.isEmpty(response.data.result.fulfillment.speech)) {
- LivechatExternalMessage.insert({
- rid: message.rid,
- msg: response.data.result.fulfillment.speech,
- orig: message._id,
- ts: new Date(),
- });
- }
- } catch (e) {
- SystemLogger.error('Error using Api.ai ->', e);
+ if (response.data && response.data.status.code === 200 && !_.isEmpty(response.data.result.fulfillment.speech)) {
+ LivechatExternalMessage.insert({
+ rid: message.rid,
+ msg: response.data.result.fulfillment.speech,
+ orig: message._id,
+ ts: new Date(),
+ });
}
- });
+ } catch (e) {
+ SystemLogger.error('Error using Api.ai ->', e);
+ }
return message;
}, callbacks.priority.LOW, 'externalWebHook');
diff --git a/app/livechat/server/hooks/markRoomResponded.js b/app/livechat/server/hooks/markRoomResponded.js
index 4f629f901aaf..ed1be72106e6 100644
--- a/app/livechat/server/hooks/markRoomResponded.js
+++ b/app/livechat/server/hooks/markRoomResponded.js
@@ -1,4 +1,3 @@
-import { Meteor } from 'meteor/meteor';
import { callbacks } from '../../../callbacks';
import { Rooms } from '../../../models';
@@ -19,13 +18,11 @@ callbacks.add('afterSaveMessage', function(message, room) {
return message;
}
- Meteor.defer(() => {
- Rooms.setResponseByRoomId(room._id, {
- user: {
- _id: message.u._id,
- username: message.u.username,
- },
- });
+ Rooms.setResponseByRoomId(room._id, {
+ user: {
+ _id: message.u._id,
+ username: message.u.username,
+ },
});
return message;
diff --git a/app/livechat/server/hooks/saveAnalyticsData.js b/app/livechat/server/hooks/saveAnalyticsData.js
index 582fa0d9cc37..58bed9f144d4 100644
--- a/app/livechat/server/hooks/saveAnalyticsData.js
+++ b/app/livechat/server/hooks/saveAnalyticsData.js
@@ -1,5 +1,3 @@
-import { Meteor } from 'meteor/meteor';
-
import { callbacks } from '../../../callbacks';
import { Rooms } from '../../../models';
@@ -14,54 +12,54 @@ callbacks.add('afterSaveMessage', function(message, room) {
return message;
}
- Meteor.defer(() => {
- const now = new Date();
- let analyticsData;
- // if the message has a token, it was sent by the visitor
- if (!message.token) {
- const visitorLastQuery = room.metrics && room.metrics.v ? room.metrics.v.lq : room.ts;
- const agentLastReply = room.metrics && room.metrics.servedBy ? room.metrics.servedBy.lr : room.ts;
- const agentJoinTime = room.servedBy && room.servedBy.ts ? room.servedBy.ts : room.ts;
+ const now = new Date();
+ let analyticsData;
+
+ // if the message has a token, it was sent by the visitor
+ if (!message.token) {
+ const visitorLastQuery = room.metrics && room.metrics.v ? room.metrics.v.lq : room.ts;
+ const agentLastReply = room.metrics && room.metrics.servedBy ? room.metrics.servedBy.lr : room.ts;
+ const agentJoinTime = room.servedBy && room.servedBy.ts ? room.servedBy.ts : room.ts;
- const isResponseTt = room.metrics && room.metrics.response && room.metrics.response.tt;
- const isResponseTotal = room.metrics && room.metrics.response && room.metrics.response.total;
+ const isResponseTt = room.metrics && room.metrics.response && room.metrics.response.tt;
+ const isResponseTotal = room.metrics && room.metrics.response && room.metrics.response.total;
- if (agentLastReply === room.ts) { // first response
- const firstResponseDate = now;
- const firstResponseTime = (now.getTime() - visitorLastQuery) / 1000;
- const responseTime = (now.getTime() - visitorLastQuery) / 1000;
- const avgResponseTime = ((isResponseTt ? room.metrics.response.tt : 0) + responseTime) / ((isResponseTotal ? room.metrics.response.total : 0) + 1);
+ if (agentLastReply === room.ts) { // first response
+ const firstResponseDate = now;
+ const firstResponseTime = (now.getTime() - visitorLastQuery) / 1000;
+ const responseTime = (now.getTime() - visitorLastQuery) / 1000;
+ const avgResponseTime = ((isResponseTt ? room.metrics.response.tt : 0) + responseTime) / ((isResponseTotal ? room.metrics.response.total : 0) + 1);
- const firstReactionDate = now;
- const firstReactionTime = (now.getTime() - agentJoinTime) / 1000;
- const reactionTime = (now.getTime() - agentJoinTime) / 1000;
+ const firstReactionDate = now;
+ const firstReactionTime = (now.getTime() - agentJoinTime) / 1000;
+ const reactionTime = (now.getTime() - agentJoinTime) / 1000;
- analyticsData = {
- firstResponseDate,
- firstResponseTime,
- responseTime,
- avgResponseTime,
- firstReactionDate,
- firstReactionTime,
- reactionTime,
- };
- } else if (visitorLastQuery > agentLastReply) { // response, not first
- const responseTime = (now.getTime() - visitorLastQuery) / 1000;
- const avgResponseTime = ((isResponseTt ? room.metrics.response.tt : 0) + responseTime) / ((isResponseTotal ? room.metrics.response.total : 0) + 1);
+ analyticsData = {
+ firstResponseDate,
+ firstResponseTime,
+ responseTime,
+ avgResponseTime,
+ firstReactionDate,
+ firstReactionTime,
+ reactionTime,
+ };
+ } else if (visitorLastQuery > agentLastReply) { // response, not first
+ const responseTime = (now.getTime() - visitorLastQuery) / 1000;
+ const avgResponseTime = ((isResponseTt ? room.metrics.response.tt : 0) + responseTime) / ((isResponseTotal ? room.metrics.response.total : 0) + 1);
- const reactionTime = (now.getTime() - visitorLastQuery) / 1000;
+ const reactionTime = (now.getTime() - visitorLastQuery) / 1000;
+
+ analyticsData = {
+ responseTime,
+ avgResponseTime,
+ reactionTime,
+ };
+ } // ignore, its continuing response
+ }
- analyticsData = {
- responseTime,
- avgResponseTime,
- reactionTime,
- };
- } // ignore, its continuing response
- }
+ Rooms.saveAnalyticsDataByRoomId(room, message, analyticsData);
- Rooms.saveAnalyticsDataByRoomId(room, message, analyticsData);
- });
return message;
}, callbacks.priority.LOW, 'saveAnalyticsData');
diff --git a/app/livechat/server/lib/Livechat.js b/app/livechat/server/lib/Livechat.js
index f2ac561f203f..10b6acbabfe0 100644
--- a/app/livechat/server/lib/Livechat.js
+++ b/app/livechat/server/lib/Livechat.js
@@ -14,7 +14,18 @@ import { QueueMethods } from './QueueMethods';
import { Analytics } from './Analytics';
import { settings } from '../../../settings';
import { callbacks } from '../../../callbacks';
-import { Users, Rooms, Messages, Subscriptions, Settings, LivechatDepartmentAgents, LivechatDepartment, LivechatCustomField, LivechatVisitors } from '../../../models';
+import {
+ Users,
+ Rooms,
+ Messages,
+ Subscriptions,
+ Settings,
+ LivechatDepartmentAgents,
+ LivechatDepartment,
+ LivechatCustomField,
+ LivechatVisitors,
+ LivechatOfficeHour,
+} from '../../../models';
import { Logger } from '../../../logger';
import { sendMessage, deleteMessage, updateMessage } from '../../../lib';
import { addUserRoles, removeUserFromRoles } from '../../../authorization';
@@ -929,6 +940,22 @@ export const Livechat = {
});
});
},
+
+ allowAgentChangeServiceStatus(statusLivechat) {
+ if (!settings.get('Livechat_enable_office_hours')) {
+ return true;
+ }
+
+ if (settings.get('Livechat_allow_online_agents_outside_office_hours')) {
+ return true;
+ }
+
+ if (statusLivechat !== 'available') {
+ return true;
+ }
+
+ return LivechatOfficeHour.isNowWithinHours();
+ },
};
Livechat.stream = new Meteor.Streamer('livechat-room');
diff --git a/app/livechat/server/methods/changeLivechatStatus.js b/app/livechat/server/methods/changeLivechatStatus.js
index e61250830360..70fd73e2ff4a 100644
--- a/app/livechat/server/methods/changeLivechatStatus.js
+++ b/app/livechat/server/methods/changeLivechatStatus.js
@@ -1,6 +1,7 @@
import { Meteor } from 'meteor/meteor';
import { Users } from '../../../models';
+import { Livechat } from '../lib/Livechat';
Meteor.methods({
'livechat:changeLivechatStatus'() {
@@ -11,6 +12,9 @@ Meteor.methods({
const user = Meteor.user();
const newStatus = user.statusLivechat === 'available' ? 'not-available' : 'available';
+ if (!Livechat.allowAgentChangeServiceStatus(newStatus)) {
+ throw new Meteor.Error('error-office-hours-are-closed', 'Not allowed', { method: 'livechat:changeLivechatStatus' });
+ }
return Users.setLivechatStatus(user._id, newStatus);
},
diff --git a/app/livechat/server/publications/livechatInquiries.js b/app/livechat/server/publications/livechatInquiries.js
index 2ca276089326..a13c92c87eec 100644
--- a/app/livechat/server/publications/livechatInquiries.js
+++ b/app/livechat/server/publications/livechatInquiries.js
@@ -2,6 +2,7 @@ import { Meteor } from 'meteor/meteor';
import { hasPermission } from '../../../authorization';
import { LivechatInquiry } from '../../lib/LivechatInquiry';
+import { settings } from '../../../settings';
Meteor.publish('livechat:inquiry', function(_id) {
if (!this.userId) {
@@ -13,12 +14,18 @@ Meteor.publish('livechat:inquiry', function(_id) {
}
const publication = this;
-
- const cursorHandle = LivechatInquiry.find({
+ const limit = settings.get('Livechat_guest_pool_max_number_incoming_livechats_displayed');
+ const filter = {
agents: this.userId,
status: 'open',
..._id && { _id },
- }).observeChanges({
+ };
+
+ const options = {
+ ...limit && { limit },
+ };
+
+ const cursorHandle = LivechatInquiry.find(filter, options).observeChanges({
added(_id, record) {
return publication.added('rocketchat_livechat_inquiry', _id, record);
},
diff --git a/app/livechat/server/publications/livechatRooms.js b/app/livechat/server/publications/livechatRooms.js
index bba937e12a01..f0f622369249 100644
--- a/app/livechat/server/publications/livechatRooms.js
+++ b/app/livechat/server/publications/livechatRooms.js
@@ -23,7 +23,7 @@ Meteor.publish('livechat:rooms', function(filter = {}, offset = 0, limit = 20) {
const query = {};
if (filter.name) {
- query.label = new RegExp(filter.name, 'i');
+ query.fname = new RegExp(filter.name, 'i');
}
if (filter.agent) {
query['servedBy._id'] = filter.agent;
diff --git a/app/logger/server/server.js b/app/logger/server/server.js
index 8e9bc460dc35..6d15ed22883a 100644
--- a/app/logger/server/server.js
+++ b/app/logger/server/server.js
@@ -87,6 +87,11 @@ const defaultTypes = {
color: 'red',
level: 0,
},
+ deprecation: {
+ name: 'warn',
+ color: 'magenta',
+ level: 0,
+ },
};
class _Logger {
diff --git a/app/markdown/lib/parser/original/markdown.js b/app/markdown/lib/parser/original/markdown.js
index 09517cd6fd4e..e0817c542f38 100644
--- a/app/markdown/lib/parser/original/markdown.js
+++ b/app/markdown/lib/parser/original/markdown.js
@@ -23,7 +23,7 @@ const parseNotEscaped = function(msg, message) {
return token;
};
- const schemes = settings.get('Markdown_SupportSchemesForLink').split(',').join('|');
+ const schemes = (settings.get('Markdown_SupportSchemesForLink') || '').split(',').join('|');
if (settings.get('Markdown_Headers')) {
// Support # Text for h1
diff --git a/app/message-attachments/client/messageAttachment.html b/app/message-attachments/client/messageAttachment.html
index d5165110ce89..03b3a606a4c8 100644
--- a/app/message-attachments/client/messageAttachment.html
+++ b/app/message-attachments/client/messageAttachment.html
@@ -57,7 +57,7 @@
{{#if title_link}}
{{title}}
{{#if title_link_download}}
-
{{> icon icon="download"}}
+
{{> icon icon="download"}}
{{/if}}
{{else}}
{{title}}
diff --git a/app/message-pin/client/actionButton.js b/app/message-pin/client/actionButton.js
index 1fcf1f19cc21..7ea7f5913f4e 100644
--- a/app/message-pin/client/actionButton.js
+++ b/app/message-pin/client/actionButton.js
@@ -64,7 +64,7 @@ Meteor.startup(function() {
id: 'jump-to-pin-message',
icon: 'jump',
label: 'Jump_to_message',
- context: ['pinned'],
+ context: ['pinned', 'message', 'message-mobile'],
action() {
const { msg: message } = messageArgs(this);
if (window.matchMedia('(max-width: 500px)').matches) {
diff --git a/app/message-star/client/actionButton.js b/app/message-star/client/actionButton.js
index fee7c64c2dba..c6898ef9d50f 100644
--- a/app/message-star/client/actionButton.js
+++ b/app/message-star/client/actionButton.js
@@ -63,7 +63,7 @@ Meteor.startup(function() {
id: 'jump-to-star-message',
icon: 'jump',
label: 'Jump_to_message',
- context: ['starred', 'threads'],
+ context: ['starred', 'threads', 'message', 'message-mobile'],
action() {
const { msg: message } = messageArgs(this);
if (window.matchMedia('(max-width: 500px)').matches) {
diff --git a/app/meteor-accounts-saml/server/saml_rocketchat.js b/app/meteor-accounts-saml/server/saml_rocketchat.js
index fdb9ad837f88..7234b5e29eb2 100644
--- a/app/meteor-accounts-saml/server/saml_rocketchat.js
+++ b/app/meteor-accounts-saml/server/saml_rocketchat.js
@@ -122,6 +122,12 @@ Meteor.methods({
section: name,
i18nLabel: 'SAML_Custom_Logout_Behaviour',
});
+ settings.add(`SAML_Custom_${ name }_custom_authn_context`, 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport', {
+ type: 'string',
+ group: 'SAML',
+ section: name,
+ i18nLabel: 'SAML_Custom_Authn_Context',
+ });
},
});
@@ -149,6 +155,7 @@ const getSamlConfigs = function(service) {
mailOverwrite: settings.get(`${ service.key }_mail_overwrite`),
issuer: settings.get(`${ service.key }_issuer`),
logoutBehaviour: settings.get(`${ service.key }_logout_behaviour`),
+ customAuthnContext: settings.get(`${ service.key }_custom_authn_context`),
secret: {
privateKey: settings.get(`${ service.key }_private_key`),
publicCert: settings.get(`${ service.key }_public_cert`),
@@ -193,6 +200,7 @@ const configureSamlService = function(samlConfigs) {
cert: samlConfigs.secret.cert,
privateCert,
privateKey,
+ customAuthnContext: samlConfigs.customAuthnContext,
};
};
diff --git a/app/meteor-accounts-saml/server/saml_server.js b/app/meteor-accounts-saml/server/saml_server.js
index bfacc8db3b34..bd12c538e96b 100644
--- a/app/meteor-accounts-saml/server/saml_server.js
+++ b/app/meteor-accounts-saml/server/saml_server.js
@@ -124,7 +124,7 @@ Accounts.registerLoginHandler(function(loginRequest) {
const emailRegex = new RegExp(emailList.map((email) => `^${ RegExp.escape(email) }$`).join('|'), 'i');
const eduPersonPrincipalName = loginResult.profile.eppn;
- const fullName = loginResult.profile.cn || loginResult.profile.username || loginResult.profile.displayName;
+ const fullName = loginResult.profile.cn || loginResult.profile.displayName || loginResult.profile.username;
let eppnMatch = false;
let user = null;
diff --git a/app/meteor-accounts-saml/server/saml_utils.js b/app/meteor-accounts-saml/server/saml_utils.js
index b910ff6c15d5..37313596a222 100644
--- a/app/meteor-accounts-saml/server/saml_utils.js
+++ b/app/meteor-accounts-saml/server/saml_utils.js
@@ -96,9 +96,10 @@ SAML.prototype.generateAuthorizeRequest = function(req) {
request += `
\n`;
}
+ const authnContext = this.options.customAuthnContext || 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport';
request
+= '
'
- + 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport\n'
+ + `
${ authnContext }\n`
+ '';
return request;
diff --git a/app/metrics/server/callbacksMetrics.js b/app/metrics/server/callbacksMetrics.js
index 8402f6fa9f8e..86221f5d55f2 100644
--- a/app/metrics/server/callbacksMetrics.js
+++ b/app/metrics/server/callbacksMetrics.js
@@ -18,12 +18,15 @@ callbacks.run = function(hook, item, constant) {
return result;
};
-callbacks.runItem = function({ callback, result, constant, hook, time }) {
+callbacks.runItem = function({ callback, result, constant, hook, time = Date.now() }) {
const rocketchatCallbacksEnd = metrics.rocketchatCallbacks.startTimer({ hook, callback: callback.id });
const newResult = originalRunItem({ callback, result, constant });
- StatsTracker.timing('callbacks.time', Date.now() - time, [`hook:${ hook }`, `callback:${ callback.id }`]);
+ StatsTracker.timing('callbacks.time', Date.now() - time, [
+ `hook:${ hook }`,
+ `callback:${ callback.id }`,
+ ]);
rocketchatCallbacksEnd();
diff --git a/app/models/client/index.js b/app/models/client/index.js
index fbcbee481f5c..0c10bf534d30 100644
--- a/app/models/client/index.js
+++ b/app/models/client/index.js
@@ -21,6 +21,7 @@ import { UserRoles } from './models/UserRoles';
import { AuthzCachedCollection, ChatPermissions } from './models/ChatPermissions';
import { WebdavAccounts } from './models/WebdavAccounts';
import CustomSounds from './models/CustomSounds';
+import CustomUserStatus from './models/CustomUserStatus';
import EmojiCustom from './models/EmojiCustom';
const Users = _.extend({}, users, Meteor.users);
@@ -51,6 +52,7 @@ export {
ChatSubscription,
Rooms,
CustomSounds,
+ CustomUserStatus,
EmojiCustom,
WebdavAccounts,
};
diff --git a/app/models/client/models/CustomUserStatus.js b/app/models/client/models/CustomUserStatus.js
new file mode 100644
index 000000000000..2cdfa29e3627
--- /dev/null
+++ b/app/models/client/models/CustomUserStatus.js
@@ -0,0 +1,10 @@
+import { Base } from './_Base';
+
+class CustomUserStatus extends Base {
+ constructor() {
+ super();
+ this._initModel('custom_user_status');
+ }
+}
+
+export default new CustomUserStatus();
diff --git a/app/models/server/index.js b/app/models/server/index.js
index 74b882789e00..9aa14dea719e 100644
--- a/app/models/server/index.js
+++ b/app/models/server/index.js
@@ -15,6 +15,7 @@ import Statistics from './models/Statistics';
import Permissions from './models/Permissions';
import Roles from './models/Roles';
import CustomSounds from './models/CustomSounds';
+import CustomUserStatus from './models/CustomUserStatus';
import Integrations from './models/Integrations';
import IntegrationHistory from './models/IntegrationHistory';
import CredentialTokens from './models/CredentialTokens';
@@ -58,6 +59,7 @@ export {
Permissions,
Roles,
CustomSounds,
+ CustomUserStatus,
Integrations,
IntegrationHistory,
CredentialTokens,
diff --git a/app/models/server/models/CustomSounds.js b/app/models/server/models/CustomSounds.js
index 40b25d5dc80a..b9971b954229 100644
--- a/app/models/server/models/CustomSounds.js
+++ b/app/models/server/models/CustomSounds.js
@@ -8,7 +8,7 @@ class CustomSounds extends Base {
}
// find one
- findOneByID(_id, options) {
+ findOneById(_id, options) {
return this.findOne(_id, options);
}
@@ -21,7 +21,7 @@ class CustomSounds extends Base {
return this.find(query, options);
}
- findByNameExceptID(name, except, options) {
+ findByNameExceptId(name, except, options) {
const query = {
_id: { $nin: [except] },
name,
@@ -48,7 +48,7 @@ class CustomSounds extends Base {
// REMOVE
- removeByID(_id) {
+ removeById(_id) {
return this.remove(_id);
}
}
diff --git a/app/models/server/models/CustomUserStatus.js b/app/models/server/models/CustomUserStatus.js
new file mode 100644
index 000000000000..9e0818dc6354
--- /dev/null
+++ b/app/models/server/models/CustomUserStatus.js
@@ -0,0 +1,66 @@
+import { Base } from './_Base';
+
+class CustomUserStatus extends Base {
+ constructor() {
+ super('custom_user_status');
+
+ this.tryEnsureIndex({ name: 1 });
+ }
+
+ // find one
+ findOneById(_id, options) {
+ return this.findOne(_id, options);
+ }
+
+ // find
+ findByName(name, options) {
+ const query = {
+ name,
+ };
+
+ return this.find(query, options);
+ }
+
+ findByNameExceptId(name, except, options) {
+ const query = {
+ _id: { $nin: [except] },
+ name,
+ };
+
+ return this.find(query, options);
+ }
+
+ // update
+ setName(_id, name) {
+ const update = {
+ $set: {
+ name,
+ },
+ };
+
+ return this.update({ _id }, update);
+ }
+
+ setStatusType(_id, statusType) {
+ const update = {
+ $set: {
+ statusType,
+ },
+ };
+
+ return this.update({ _id }, update);
+ }
+
+ // INSERT
+ create(data) {
+ return this.insert(data);
+ }
+
+
+ // REMOVE
+ removeById(_id) {
+ return this.remove(_id);
+ }
+}
+
+export default new CustomUserStatus();
diff --git a/app/models/server/models/EmojiCustom.js b/app/models/server/models/EmojiCustom.js
index 8f9f676072f5..d0cd7d7bc4cb 100644
--- a/app/models/server/models/EmojiCustom.js
+++ b/app/models/server/models/EmojiCustom.js
@@ -10,7 +10,7 @@ class EmojiCustom extends Base {
}
// find one
- findOneByID(_id, options) {
+ findOneById(_id, options) {
return this.findOne(_id, options);
}
@@ -83,7 +83,7 @@ class EmojiCustom extends Base {
// REMOVE
- removeByID(_id) {
+ removeById(_id) {
return this.remove(_id);
}
}
diff --git a/app/models/server/models/ExportOperations.js b/app/models/server/models/ExportOperations.js
index a72fdc486e20..72c4be71ca1b 100644
--- a/app/models/server/models/ExportOperations.js
+++ b/app/models/server/models/ExportOperations.js
@@ -46,6 +46,15 @@ export class ExportOperations extends Base {
return this.find(query, options);
}
+ findAllPendingBeforeMyRequest(requestDay, options) {
+ const query = {
+ status: { $nin: ['completed'] },
+ createdAt: { $lt: requestDay },
+ };
+
+ return this.find(query, options);
+ }
+
// UPDATE
updateOperation(data) {
const update = {
diff --git a/app/models/server/models/FederationEvents.js b/app/models/server/models/FederationEvents.js
index 82a09247c0e6..55dbcb366acd 100644
--- a/app/models/server/models/FederationEvents.js
+++ b/app/models/server/models/FederationEvents.js
@@ -20,6 +20,10 @@ const normalizePeers = (basePeers, options) => {
class FederationEventsModel extends Base {
constructor() {
super('federation_events');
+
+ this.tryEnsureIndex({ t: 1 });
+ this.tryEnsureIndex({ fulfilled: 1 });
+ this.tryEnsureIndex({ ts: 1 });
}
// Sometimes events errored but the error is final
@@ -256,7 +260,11 @@ class FederationEventsModel extends Base {
// Get all unfulfilled events
getUnfulfilled() {
- return this.find({ fulfilled: false }, { sort: { ts: 1 } }).fetch();
+ return this.find({ fulfilled: false }, { sort: { ts: 1 } });
+ }
+
+ findByType(t) {
+ return this.find({ t });
}
}
diff --git a/app/models/server/models/FederationPeers.js b/app/models/server/models/FederationPeers.js
index 75acdcd44e77..283d2f31e761 100644
--- a/app/models/server/models/FederationPeers.js
+++ b/app/models/server/models/FederationPeers.js
@@ -1,32 +1,27 @@
-import { Meteor } from 'meteor/meteor';
-
import { Base } from './_Base';
-
-import { Users } from '..';
+import { Users } from '../raw';
class FederationPeersModel extends Base {
constructor() {
super('federation_peers');
- }
- refreshPeers() {
- const collectionObj = this.model.rawCollection();
- const findAndModify = Meteor.wrapAsync(collectionObj.findAndModify, collectionObj);
-
- const users = Users.find({ federation: { $exists: true } }, { fields: { federation: 1 } }).fetch();
+ this.tryEnsureIndex({ active: 1, isRemote: 1 });
+ }
- const peers = [...new Set(users.map((u) => u.federation.peer))];
+ async refreshPeers(localIdentifier) {
+ const peers = await Users.getDistinctFederationPeers();
- for (const peer of peers) {
- findAndModify({ peer }, [], {
+ peers.forEach((peer) =>
+ this.update({ peer }, {
$setOnInsert: {
+ isRemote: localIdentifier !== peer,
active: false,
peer,
last_seen_at: null,
last_failure_at: null,
},
- }, { upsert: true });
- }
+ }, { upsert: true })
+ );
this.remove({ peer: { $nin: peers } });
}
@@ -48,6 +43,18 @@ class FederationPeersModel extends Base {
this.update({ peer }, { $set: updateQuery });
}
}
+
+ findActiveRemote() {
+ return this.find({ active: true, isRemote: true });
+ }
+
+ findNotActiveRemote() {
+ return this.find({ active: false, isRemote: true });
+ }
+
+ findRemote() {
+ return this.find({ isRemote: true });
+ }
}
export const FederationPeers = new FederationPeersModel();
diff --git a/app/models/server/models/Messages.js b/app/models/server/models/Messages.js
index 8957cc1cc9c4..67d6d43b92e7 100644
--- a/app/models/server/models/Messages.js
+++ b/app/models/server/models/Messages.js
@@ -31,6 +31,8 @@ export class Messages extends Base {
// threads
this.tryEnsureIndex({ tmid: 1 }, { sparse: true });
this.tryEnsureIndex({ tcount: 1, tlm: 1 }, { sparse: true });
+ // livechat
+ this.tryEnsureIndex({ 'navigation.token': 1 }, { sparse: true });
}
setReactions(messageId, reactions) {
diff --git a/app/models/server/models/Roles.js b/app/models/server/models/Roles.js
index 579d15720880..dd25962cb94c 100644
--- a/app/models/server/models/Roles.js
+++ b/app/models/server/models/Roles.js
@@ -83,6 +83,21 @@ export class Roles extends Base {
return this.findOne(query, options);
}
+
+ canAddUserToRole(uid, roleName, scope) {
+ const role = this.findOne({ _id: roleName }, { fields: { scope: 1 } });
+ if (!role) {
+ return false;
+ }
+
+ const model = Models[role.scope];
+ if (!model) {
+ return;
+ }
+
+ const user = model.isUserInRoleScope(uid, scope);
+ return !!user;
+ }
}
export default new Roles('roles');
diff --git a/app/models/server/models/Rooms.js b/app/models/server/models/Rooms.js
index 2d7f38770932..33ddcd10a45a 100644
--- a/app/models/server/models/Rooms.js
+++ b/app/models/server/models/Rooms.js
@@ -661,13 +661,13 @@ export class Rooms extends Base {
return this.find(query, options);
}
- findByTypes(types, options) {
+ findByTypes(types, discussion = false, options = {}) {
const query = {
t: {
$in: types,
},
+ prid: { $exists: discussion },
};
-
return this.find(query, options);
}
@@ -720,10 +720,11 @@ export class Rooms extends Base {
return this.find(query, options);
}
- findByNameContaining(name, options) {
+ findByNameContaining(name, discussion = false, options = {}) {
const nameRegex = new RegExp(s.trim(s.escapeRegExp(name)), 'i');
const query = {
+ prid: { $exists: discussion },
$or: [
{ name: nameRegex },
{
@@ -732,17 +733,17 @@ export class Rooms extends Base {
},
],
};
-
return this.find(query, options);
}
- findByNameContainingAndTypes(name, types, options) {
+ findByNameContainingAndTypes(name, types, discussion = false, options = {}) {
const nameRegex = new RegExp(s.trim(s.escapeRegExp(name)), 'i');
const query = {
t: {
$in: types,
},
+ prid: { $exists: discussion },
$or: [
{ name: nameRegex },
{
@@ -751,7 +752,6 @@ export class Rooms extends Base {
},
],
};
-
return this.find(query, options);
}
diff --git a/app/models/server/models/Users.js b/app/models/server/models/Users.js
index 0daea240a2ab..e558c0177906 100644
--- a/app/models/server/models/Users.js
+++ b/app/models/server/models/Users.js
@@ -15,11 +15,13 @@ export class Users extends Base {
this.tryEnsureIndex({ name: 1 });
this.tryEnsureIndex({ lastLogin: 1 });
this.tryEnsureIndex({ status: 1 });
+ this.tryEnsureIndex({ statusText: 1 });
this.tryEnsureIndex({ active: 1 }, { sparse: 1 });
this.tryEnsureIndex({ statusConnection: 1 }, { sparse: 1 });
this.tryEnsureIndex({ type: 1 });
this.tryEnsureIndex({ 'visitorEmails.address': 1 });
this.tryEnsureIndex({ federation: 1 }, { sparse: true });
+ this.tryEnsureIndex({ isRemote: 1 }, { sparse: true });
}
getLoginTokensByUserId(userId) {
@@ -374,6 +376,16 @@ export class Users extends Base {
return this.findOne(query, options);
}
+ findOneByUsernameAndServiceNameIgnoringCase(username, serviceName, options) {
+ if (typeof username === 'string') {
+ username = new RegExp(`^${ s.escapeRegExp(username) }$`, 'i');
+ }
+
+ const query = { username, [`services.${ serviceName }.id`]: serviceName };
+
+ return this.findOne(query, options);
+ }
+
findOneByUsername(username, options) {
const query = { username };
@@ -461,6 +473,10 @@ export class Users extends Base {
return this.find(query, options);
}
+ findActive(options = {}) {
+ return this.find({ active: true }, options);
+ }
+
findActiveByUsernameOrNameRegexWithExceptions(searchTerm, exceptions, options) {
if (exceptions == null) { exceptions = []; }
if (options == null) { options = {}; }
@@ -665,6 +681,14 @@ export class Users extends Base {
return this.findOne(query, options);
}
+ findRemote(options = {}) {
+ return this.find({ isRemote: true }, options);
+ }
+
+ findActiveRemote(options = {}) {
+ return this.find({ active: true, isRemote: true }, options);
+ }
+
// UPDATE
addImportIds(_id, importIds) {
importIds = [].concat(importIds);
@@ -682,6 +706,16 @@ export class Users extends Base {
return this.update(query, update);
}
+ updateStatusText(_id, statusText) {
+ const update = {
+ $set: {
+ statusText,
+ },
+ };
+
+ return this.update(_id, update);
+ }
+
updateLastLoginById(_id) {
const update = {
$set: {
@@ -1096,6 +1130,10 @@ Find users to send a message by email if:
return this.find(query, options);
}
+
+ getActiveLocalUserCount() {
+ return this.findActive().count() - this.findActiveRemote().count();
+ }
}
export default new Users(Meteor.users, true);
diff --git a/app/models/server/models/_Base.js b/app/models/server/models/_Base.js
index 87abf5aed684..5c68facab4ef 100644
--- a/app/models/server/models/_Base.js
+++ b/app/models/server/models/_Base.js
@@ -42,6 +42,20 @@ export class Base {
return !_.isUndefined(this.findOne(query, { fields: { roles: 1 } }));
}
+ isUserInRoleScope(uid, scope) {
+ const query = this.roleBaseQuery(uid, scope);
+ if (!query) {
+ return false;
+ }
+
+ const options = {
+ fields: { _id: 1 },
+ };
+
+ const found = this.findOne(query, options);
+ return !!found;
+ }
+
addRolesByUserId(userId, roles, scope) {
roles = [].concat(roles);
const query = this.roleBaseQuery(userId, scope);
diff --git a/app/models/server/models/_BaseDb.js b/app/models/server/models/_BaseDb.js
index cd774981ffb6..c819298f5a2c 100644
--- a/app/models/server/models/_BaseDb.js
+++ b/app/models/server/models/_BaseDb.js
@@ -9,13 +9,14 @@ const baseName = 'rocketchat_';
const trash = new Mongo.Collection(`${ baseName }_trash`);
try {
trash._ensureIndex({ collection: 1 });
- trash._ensureIndex({ _deletedAt: 1 }, { expireAfterSeconds: 60 * 60 * 24 * 30 });
+ trash._ensureIndex(
+ { _deletedAt: 1 },
+ { expireAfterSeconds: 60 * 60 * 24 * 30 }
+ );
} catch (e) {
console.log(e);
}
-const isOplogEnabled = MongoInternals.defaultRemoteCollectionDriver().mongo._oplogHandle && !!MongoInternals.defaultRemoteCollectionDriver().mongo._oplogHandle.onOplogEntry;
-
export class BaseDb extends EventEmitter {
constructor(model, baseModel) {
super();
@@ -34,24 +35,38 @@ export class BaseDb extends EventEmitter {
this.wrapModel();
- let alreadyListeningToOplog = false;
// When someone start listening for changes we start oplog if available
- this.on('newListener', (event/* , listener*/) => {
- if (event === 'change' && alreadyListeningToOplog === false) {
- alreadyListeningToOplog = true;
- if (isOplogEnabled) {
- const query = {
- collection: this.collectionName,
- };
-
- MongoInternals.defaultRemoteCollectionDriver().mongo._oplogHandle.onOplogEntry(query, this.processOplogRecord.bind(this));
- // Meteor will handle if we have a value https://github.com/meteor/meteor/blob/5dcd0b2eb9c8bf881ffbee98bc4cb7631772c4da/packages/mongo/oplog_tailing.js#L5
- if (process.env.METEOR_OPLOG_TOO_FAR_BEHIND == null) {
- MongoInternals.defaultRemoteCollectionDriver().mongo._oplogHandle._defineTooFarBehind(Number.MAX_SAFE_INTEGER);
- }
- }
+ const handleListener = (event /* , listener*/) => {
+ if (event !== 'change') {
+ return;
}
- });
+
+ this.removeListener('newListener', handleListener);
+
+ const query = {
+ collection: this.collectionName,
+ };
+
+ if (!MongoInternals.defaultRemoteCollectionDriver().mongo._oplogHandle) {
+ throw new Error(`Error: Unable to find Mongodb Oplog. You must run the server with oplog enabled. Try the following:\n
+ 1. Start your mongodb in a replicaset mode: mongod --smallfiles --oplogSize 128 --replSet rs0\n
+ 2. Start the replicaset via mongodb shell: mongo mongo/meteor --eval "rs.initiate({ _id: ''rs0'', members: [ { _id: 0, host: ''localhost:27017'' } ]})"\n
+ 3. Start your instance with OPLOG configuration: export MONGO_OPLOG_URL=mongodb://localhost:27017/local MONGO_URL=mongodb://localhost:27017/meteor node main.js
+ `);
+ }
+
+ MongoInternals.defaultRemoteCollectionDriver().mongo._oplogHandle.onOplogEntry(
+ query,
+ this.processOplogRecord.bind(this)
+ );
+ // Meteor will handle if we have a value https://github.com/meteor/meteor/blob/5dcd0b2eb9c8bf881ffbee98bc4cb7631772c4da/packages/mongo/oplog_tailing.js#L5
+ if (process.env.METEOR_OPLOG_TOO_FAR_BEHIND == null) {
+ MongoInternals.defaultRemoteCollectionDriver().mongo._oplogHandle._defineTooFarBehind(
+ Number.MAX_SAFE_INTEGER
+ );
+ }
+ };
+ this.on('newListener', handleListener);
this.tryEnsureIndex({ _updatedAt: 1 });
}
@@ -129,7 +144,12 @@ export class BaseDb extends EventEmitter {
}
updateHasPositionalOperator(update) {
- return Object.keys(update).some((key) => key.includes('.$') || (Match.test(update[key], Object) && this.updateHasPositionalOperator(update[key])));
+ return Object.keys(update).some(
+ (key) =>
+ key.includes('.$')
+ || (Match.test(update[key], Object)
+ && this.updateHasPositionalOperator(update[key]))
+ );
}
processOplogRecord(action) {
diff --git a/app/models/server/raw/BaseRaw.js b/app/models/server/raw/BaseRaw.js
new file mode 100644
index 000000000000..a2668cca3779
--- /dev/null
+++ b/app/models/server/raw/BaseRaw.js
@@ -0,0 +1,17 @@
+export class BaseRaw {
+ constructor(col) {
+ this.col = col;
+ }
+
+ findOneById(_id, options) {
+ return this.findOne({ _id }, options);
+ }
+
+ findOne(...args) {
+ return this.col.findOne(...args);
+ }
+
+ findUsersInRoles() {
+ throw new Error('overwrite-function', 'You must overwrite this function in the extended classes');
+ }
+}
diff --git a/app/models/server/raw/Permissions.js b/app/models/server/raw/Permissions.js
new file mode 100644
index 000000000000..3c5c1aaf6e67
--- /dev/null
+++ b/app/models/server/raw/Permissions.js
@@ -0,0 +1,4 @@
+import { BaseRaw } from './BaseRaw';
+
+export class PermissionsRaw extends BaseRaw {
+}
diff --git a/app/models/server/raw/Roles.js b/app/models/server/raw/Roles.js
new file mode 100644
index 000000000000..bda23eea6b55
--- /dev/null
+++ b/app/models/server/raw/Roles.js
@@ -0,0 +1,25 @@
+import { BaseRaw } from './BaseRaw';
+
+import * as Models from './index';
+
+export class RolesRaw extends BaseRaw {
+ async isUserInRoles(userId, roles, scope) {
+ roles = [].concat(roles);
+
+ for (let i = 0, total = roles.length; i < total; i++) {
+ const roleName = roles[i];
+
+ // eslint-disable-next-line no-await-in-loop
+ const role = await this.findOne({ _id: roleName });
+ const roleScope = (role && role.scope) || 'Users';
+ const model = Models[roleScope];
+
+ // eslint-disable-next-line no-await-in-loop
+ const permitted = await (model && model.isUserInRole && model.isUserInRole(userId, roleName, scope));
+ if (permitted) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/app/models/server/raw/Rooms.js b/app/models/server/raw/Rooms.js
new file mode 100644
index 000000000000..d04e5aadee3a
--- /dev/null
+++ b/app/models/server/raw/Rooms.js
@@ -0,0 +1,26 @@
+import { BaseRaw } from './BaseRaw';
+
+export class RoomsRaw extends BaseRaw {
+ findOneByRoomIdAndUserId(rid, uid, options) {
+ const query = {
+ rid,
+ 'u._id': uid,
+ };
+
+ return this.col.findOne(query, options);
+ }
+
+ isUserInRole(uid, roleName, rid) {
+ if (rid == null) {
+ return;
+ }
+
+ const query = {
+ 'u._id': uid,
+ rid,
+ roles: roleName,
+ };
+
+ return this.findOne(query, { fields: { roles: 1 } });
+ }
+}
diff --git a/app/models/server/raw/Settings.js b/app/models/server/raw/Settings.js
new file mode 100644
index 000000000000..fb2a1c3e3195
--- /dev/null
+++ b/app/models/server/raw/Settings.js
@@ -0,0 +1,9 @@
+import { BaseRaw } from './BaseRaw';
+
+export class SettingsRaw extends BaseRaw {
+ async getValueById(_id) {
+ const setting = await this.col.findOne({ _id }, { projection: { value: 1 } });
+
+ return setting.value;
+ }
+}
diff --git a/app/models/server/raw/Subscriptions.js b/app/models/server/raw/Subscriptions.js
new file mode 100644
index 000000000000..ac7115393b41
--- /dev/null
+++ b/app/models/server/raw/Subscriptions.js
@@ -0,0 +1,26 @@
+import { BaseRaw } from './BaseRaw';
+
+export class SubscriptionsRaw extends BaseRaw {
+ findOneByRoomIdAndUserId(rid, uid, options) {
+ const query = {
+ rid,
+ 'u._id': uid,
+ };
+
+ return this.col.findOne(query, options);
+ }
+
+ isUserInRole(uid, roleName, rid) {
+ if (rid == null) {
+ return;
+ }
+
+ const query = {
+ 'u._id': uid,
+ rid,
+ roles: roleName,
+ };
+
+ return this.findOne(query, { fields: { roles: 1 } });
+ }
+}
diff --git a/app/models/server/raw/Users.js b/app/models/server/raw/Users.js
new file mode 100644
index 000000000000..95baf14b3d3c
--- /dev/null
+++ b/app/models/server/raw/Users.js
@@ -0,0 +1,26 @@
+import { BaseRaw } from './BaseRaw';
+
+export class UsersRaw extends BaseRaw {
+ findUsersInRoles(roles, scope, options) {
+ roles = [].concat(roles);
+
+ const query = {
+ roles: { $in: roles },
+ };
+
+ return this.find(query, options);
+ }
+
+ isUserInRole(userId, roleName) {
+ const query = {
+ _id: userId,
+ roles: roleName,
+ };
+
+ return this.findOne(query, { fields: { roles: 1 } });
+ }
+
+ getDistinctFederationPeers() {
+ return this.col.distinct('federation.peer', { federation: { $exists: true } });
+ }
+}
diff --git a/app/models/server/raw/index.js b/app/models/server/raw/index.js
new file mode 100644
index 000000000000..9a56a1f09e21
--- /dev/null
+++ b/app/models/server/raw/index.js
@@ -0,0 +1,19 @@
+import PermissionsModel from '../models/Permissions';
+import { PermissionsRaw } from './Permissions';
+import RolesModel from '../models/Roles';
+import { RolesRaw } from './Roles';
+import SubscriptionsModel from '../models/Subscriptions';
+import { SubscriptionsRaw } from './Subscriptions';
+import SettingsModel from '../models/Settings';
+import { SettingsRaw } from './Settings';
+import UsersModel from '../models/Users';
+import { UsersRaw } from './Users';
+import RoomsModel from '../models/Rooms';
+import { RoomsRaw } from './Rooms';
+
+export const Permissions = new PermissionsRaw(PermissionsModel.model.rawCollection());
+export const Roles = new RolesRaw(RolesModel.model.rawCollection());
+export const Subscriptions = new SubscriptionsRaw(SubscriptionsModel.model.rawCollection());
+export const Settings = new SettingsRaw(SettingsModel.model.rawCollection());
+export const Users = new UsersRaw(UsersModel.model.rawCollection());
+export const Rooms = new RoomsRaw(RoomsModel.model.rawCollection());
diff --git a/app/oauth2-server-config/client/admin/views/oauthApp.html b/app/oauth2-server-config/client/admin/views/oauthApp.html
index 1696d4c3dd4e..5bcab86d702b 100644
--- a/app/oauth2-server-config/client/admin/views/oauthApp.html
+++ b/app/oauth2-server-config/client/admin/views/oauthApp.html
@@ -22,7 +22,7 @@
diff --git a/app/oauth2-server-config/client/admin/views/oauthApp.js b/app/oauth2-server-config/client/admin/views/oauthApp.js
index 1ef9f76d3287..e0f4cc0f71c6 100644
--- a/app/oauth2-server-config/client/admin/views/oauthApp.js
+++ b/app/oauth2-server-config/client/admin/views/oauthApp.js
@@ -31,6 +31,10 @@ Template.oauthApp.helpers({
if (data) {
data.authorization_url = Meteor.absoluteUrl('oauth/authorize');
data.access_token_url = Meteor.absoluteUrl('oauth/token');
+ if (Array.isArray(data.redirectUri)) {
+ data.redirectUri = data.redirectUri.join('\n');
+ }
+
Template.instance().record.set(data);
return data;
}
diff --git a/app/oauth2-server-config/server/admin/functions/parseUriList.js b/app/oauth2-server-config/server/admin/functions/parseUriList.js
new file mode 100644
index 000000000000..f00fa792f80a
--- /dev/null
+++ b/app/oauth2-server-config/server/admin/functions/parseUriList.js
@@ -0,0 +1,17 @@
+export const parseUriList = (userUri) => {
+ if (userUri.indexOf('\n') < 0 && userUri.indexOf(',') < 0) {
+ return userUri;
+ }
+
+ const uriList = [];
+ userUri.split(/[,\n]/).forEach((item) => {
+ const uri = item.trim();
+ if (uri === '') {
+ return;
+ }
+
+ uriList.push(uri);
+ });
+
+ return uriList;
+};
diff --git a/app/oauth2-server-config/server/admin/methods/addOAuthApp.js b/app/oauth2-server-config/server/admin/methods/addOAuthApp.js
index a5ef900f34bd..cb4d73e19f27 100644
--- a/app/oauth2-server-config/server/admin/methods/addOAuthApp.js
+++ b/app/oauth2-server-config/server/admin/methods/addOAuthApp.js
@@ -4,6 +4,7 @@ import _ from 'underscore';
import { hasPermission } from '../../../../authorization';
import { Users, OAuthApps } from '../../../../models';
+import { parseUriList } from '../functions/parseUriList';
Meteor.methods({
addOAuthApp(application) {
@@ -19,6 +20,13 @@ Meteor.methods({
if (!_.isBoolean(application.active)) {
throw new Meteor.Error('error-invalid-arguments', 'Invalid arguments', { method: 'addOAuthApp' });
}
+
+ application.redirectUri = parseUriList(application.redirectUri);
+
+ if (application.redirectUri.length === 0) {
+ throw new Meteor.Error('error-invalid-redirectUri', 'Invalid redirectUri', { method: 'addOAuthApp' });
+ }
+
application.clientId = Random.id();
application.clientSecret = Random.secret();
application._createdAt = new Date();
diff --git a/app/oauth2-server-config/server/admin/methods/updateOAuthApp.js b/app/oauth2-server-config/server/admin/methods/updateOAuthApp.js
index 6043a34a23f0..007f5be2e95c 100644
--- a/app/oauth2-server-config/server/admin/methods/updateOAuthApp.js
+++ b/app/oauth2-server-config/server/admin/methods/updateOAuthApp.js
@@ -3,6 +3,7 @@ import _ from 'underscore';
import { hasPermission } from '../../../../authorization';
import { OAuthApps, Users } from '../../../../models';
+import { parseUriList } from '../functions/parseUriList';
Meteor.methods({
updateOAuthApp(applicationId, application) {
@@ -22,11 +23,18 @@ Meteor.methods({
if (currentApplication == null) {
throw new Meteor.Error('error-application-not-found', 'Application not found', { method: 'updateOAuthApp' });
}
+
+ const redirectUri = parseUriList(application.redirectUri);
+
+ if (redirectUri.length === 0) {
+ throw new Meteor.Error('error-invalid-redirectUri', 'Invalid redirectUri', { method: 'updateOAuthApp' });
+ }
+
OAuthApps.update(applicationId, {
$set: {
name: application.name,
active: application.active,
- redirectUri: application.redirectUri,
+ redirectUri,
_updatedAt: new Date(),
_updatedBy: Users.findOne(this.userId, {
fields: {
diff --git a/app/search/client/provider/result.js b/app/search/client/provider/result.js
index aa64d32da6ba..1d62162bd202 100644
--- a/app/search/client/provider/result.js
+++ b/app/search/client/provider/result.js
@@ -1,4 +1,5 @@
import { Meteor } from 'meteor/meteor';
+import { Tracker } from 'meteor/tracker';
import { ReactiveVar } from 'meteor/reactive-var';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Session } from 'meteor/session';
@@ -39,9 +40,22 @@ Meteor.startup(function() {
});
});
-Template.DefaultSearchResultTemplate.onCreated(function() {
- const self = this;
+Template.DefaultSearchResultTemplate.onRendered(function() {
+ const list = this.firstNode.parentNode.querySelector('.rocket-default-search-results');
+ this.autorun(() => {
+ const result = this.data.result.get();
+ if (result && this.hasMore.get()) {
+ Tracker.afterFlush(() => {
+ if (list.scrollHeight < list.offsetHeight) {
+ this.data.payload.limit = (this.data.payload.limit || this.pageSize) + this.pageSize;
+ this.data.search();
+ }
+ });
+ }
+ });
+});
+Template.DefaultSearchResultTemplate.onCreated(function() {
// paging
this.pageSize = this.data.settings.PageSize;
@@ -53,7 +67,7 @@ Template.DefaultSearchResultTemplate.onCreated(function() {
this.autorun(() => {
const result = this.data.result.get();
- self.hasMore.set(!(result && result.message.docs.length < (self.data.payload.limit || self.pageSize)));
+ this.hasMore.set(!(result && result.message.docs.length < (this.data.payload.limit || this.pageSize)));
});
});
@@ -86,7 +100,7 @@ Template.DefaultSearchResultTemplate.helpers({
return Template.instance().hasMore.get();
},
message(msg) {
- return { customClass: 'search', actionContext: 'search', ...msg };
+ return { customClass: 'search', actionContext: 'search', ...msg, groupable: false };
},
messageContext,
});
diff --git a/app/search/client/style/style.css b/app/search/client/style/style.css
index 2b611bb6c241..cd7509548840 100644
--- a/app/search/client/style/style.css
+++ b/app/search/client/style/style.css
@@ -23,8 +23,9 @@
.rocket-search-result {
display: flex;
+ overflow: hidden;
flex-direction: column;
- flex: 1;
+ flex: 1 1 0;
}
.rocket-default-search-settings {
diff --git a/app/search/server/events/events.js b/app/search/server/events/events.js
index 82fac3527e87..79e3c8aa8142 100644
--- a/app/search/server/events/events.js
+++ b/app/search/server/events/events.js
@@ -24,11 +24,11 @@ const eventService = new EventService();
*/
callbacks.add('afterSaveMessage', function(m) {
eventService.promoteEvent('message.save', m._id, m);
-});
+}, callbacks.priority.MEDIUM, 'search-events');
callbacks.add('afterDeleteMessage', function(m) {
eventService.promoteEvent('message.delete', m._id);
-});
+}, callbacks.priority.MEDIUM, 'search-events-delete');
/**
* Listen to user and room changes via cursor
diff --git a/app/settings/server/index.js b/app/settings/server/index.js
index 3ddef2f62407..edcce3a33231 100644
--- a/app/settings/server/index.js
+++ b/app/settings/server/index.js
@@ -1,4 +1,5 @@
import { settings } from './functions/settings';
+import './observer';
export {
settings,
diff --git a/app/settings/server/observer.js b/app/settings/server/observer.js
new file mode 100644
index 000000000000..7c376078aaa4
--- /dev/null
+++ b/app/settings/server/observer.js
@@ -0,0 +1,19 @@
+import { Meteor } from 'meteor/meteor';
+
+import { Settings } from '../../models/server';
+import { setValue } from './raw';
+
+const updateValue = (id, fields) => {
+ if (typeof fields.value === 'undefined') {
+ return;
+ }
+ setValue(id, fields.value);
+};
+
+Meteor.startup(() => Settings.find({}, { fields: { value: 1 } }).observeChanges({
+ added: updateValue,
+ changed: updateValue,
+ removed(id) {
+ setValue(id, undefined);
+ },
+}));
diff --git a/app/settings/server/raw.js b/app/settings/server/raw.js
new file mode 100644
index 000000000000..9bfd51fbc029
--- /dev/null
+++ b/app/settings/server/raw.js
@@ -0,0 +1,21 @@
+import { Settings } from '../../models/server/raw';
+
+const cache = new Map();
+
+export const setValue = (_id, value) => cache.set(_id, value);
+
+const setFromDB = async (_id) => {
+ const value = await Settings.getValueById(_id);
+
+ setValue(_id, value);
+
+ return value;
+};
+
+export const getValue = async (_id) => {
+ if (!cache.has(_id)) {
+ return setFromDB(_id);
+ }
+
+ return cache.get(_id);
+};
diff --git a/app/setup-wizard/client/setupWizard.js b/app/setup-wizard/client/setupWizard.js
index 9cebce0ebed9..f07f58b0ca84 100644
--- a/app/setup-wizard/client/setupWizard.js
+++ b/app/setup-wizard/client/setupWizard.js
@@ -12,6 +12,7 @@ import { callbacks } from '../../callbacks';
import { hasRole } from '../../authorization';
import { Users } from '../../models';
import { t, handleError } from '../../utils';
+import { call } from '../../ui-utils';
const cannotSetup = () => {
const showSetupWizard = settings.get('Show_Setup_Wizard');
@@ -100,26 +101,43 @@ const persistSettings = (state, callback) => {
});
};
-Template.setupWizard.onCreated(function() {
- this.state = new ReactiveDict();
- this.state.set('currentStep', 1);
- this.state.set('registerServer', true);
- this.state.set('optIn', true);
+Template.setupWizard.onCreated(async function() {
+ const statusDefault = {
+ currentStep: 1,
+ registerServer: true,
+ optIn: true,
+ };
+ this.state = new ReactiveDict(statusDefault);
this.wizardSettings = new ReactiveVar([]);
this.allowStandaloneServer = new ReactiveVar(false);
if (localStorage.getItem('wizardFinal')) {
- FlowRouter.go('setup-wizard-final');
- return;
+ return FlowRouter.go('setup-wizard-final');
}
const jsonString = localStorage.getItem('wizard');
- const state = (jsonString && JSON.parse(jsonString)) || {};
- Object.entries(state).forEach((entry) => this.state.set(...entry));
+ const state = (jsonString && JSON.parse(jsonString)) || statusDefault;
+ this.state.set(state);
+
+ this.autorun(async () => {
+ if (!Meteor.userId()) {
+ return;
+ }
+ const { settings, allowStandaloneServer } = await call('getSetupWizardParameters') || {};
+ this.wizardSettings.set(settings);
+ this.allowStandaloneServer.set(allowStandaloneServer);
+ });
- this.autorun((c) => {
+ this.autorun(() => {
+ const state = this.state.all();
+ state['registration-pass'] = '';
+ localStorage.setItem('wizard', JSON.stringify(state));
+ });
+
+ this.autorun(async (c) => {
const cantSetup = cannotSetup();
+
if (typeof cantSetup === 'undefined') {
return;
}
@@ -130,27 +148,14 @@ Template.setupWizard.onCreated(function() {
return;
}
- const state = this.state.all();
- state['registration-pass'] = '';
- localStorage.setItem('wizard', JSON.stringify(state));
-
- if (Meteor.userId()) {
- Meteor.call('getSetupWizardParameters', (error, { settings, allowStandaloneServer }) => {
- if (error) {
- return handleError(error);
- }
-
- this.wizardSettings.set(settings);
- this.allowStandaloneServer.set(allowStandaloneServer);
- });
+ if (!Meteor.userId()) {
+ return this.state.set('currentStep', 1);
+ }
- if (this.state.get('currentStep') === 1) {
- this.state.set('currentStep', 2);
- } else {
- this.state.set('registration-pass', '');
- }
- } else if (this.state.get('currentStep') !== 1) {
- this.state.set('currentStep', 1);
+ if (this.state.get('currentStep') === 1) {
+ this.state.set('currentStep', 2);
+ } else {
+ this.state.set('registration-pass', '');
}
});
});
diff --git a/app/slashcommands-join/server/server.js b/app/slashcommands-join/server/server.js
index ef6bb96377cd..5d8cc8ac2e04 100644
--- a/app/slashcommands-join/server/server.js
+++ b/app/slashcommands-join/server/server.js
@@ -1,8 +1,3 @@
-
-/*
-* Join is a named function that will replace /join commands
-* @param {Object} message - The message object
-*/
import { Meteor } from 'meteor/meteor';
import { Match } from 'meteor/check';
import { Random } from 'meteor/random';
@@ -12,7 +7,7 @@ import { Rooms, Subscriptions } from '../../models';
import { Notifications } from '../../notifications';
import { slashCommands } from '../../utils';
-slashCommands.add('join', function Join(command, params, item) {
+function Join(command, params, item) {
if (command !== 'join' || !Match.test(params, String)) {
return;
}
@@ -42,7 +37,9 @@ slashCommands.add('join', function Join(command, params, item) {
});
}
Meteor.call('joinRoom', room._id);
-}, {
+}
+
+slashCommands.add('join', Join, {
description: 'Join_the_given_channel',
params: '#channel',
});
diff --git a/app/slashcommands-status/client/index.js b/app/slashcommands-status/client/index.js
new file mode 100644
index 000000000000..11e5ad1b8640
--- /dev/null
+++ b/app/slashcommands-status/client/index.js
@@ -0,0 +1 @@
+import '../lib/status';
diff --git a/app/slashcommands-status/index.js b/app/slashcommands-status/index.js
new file mode 100644
index 000000000000..a67eca871efb
--- /dev/null
+++ b/app/slashcommands-status/index.js
@@ -0,0 +1,8 @@
+import { Meteor } from 'meteor/meteor';
+
+if (Meteor.isClient) {
+ module.exports = require('./client/index.js');
+}
+if (Meteor.isServer) {
+ module.exports = require('./server/index.js');
+}
diff --git a/app/slashcommands-status/lib/status.js b/app/slashcommands-status/lib/status.js
new file mode 100644
index 000000000000..a03a9e516117
--- /dev/null
+++ b/app/slashcommands-status/lib/status.js
@@ -0,0 +1,43 @@
+import { Meteor } from 'meteor/meteor';
+import { TAPi18n } from 'meteor/tap:i18n';
+import { Random } from 'meteor/random';
+
+import { handleError, slashCommands } from '../../utils';
+import { Notifications } from '../../notifications';
+
+function Status(command, params, item) {
+ if (command === 'status') {
+ const user = Meteor.users.findOne(Meteor.userId());
+
+ Meteor.call('setUserStatus', null, params, (err) => {
+ if (err) {
+ if (Meteor.isClient) {
+ return handleError(err);
+ }
+
+ if (err.error === 'error-not-allowed') {
+ Notifications.notifyUser(Meteor.userId(), 'message', {
+ _id: Random.id(),
+ rid: item.rid,
+ ts: new Date(),
+ msg: TAPi18n.__('StatusMessage_Change_Disabled', null, user.language),
+ });
+ }
+
+ throw err;
+ } else {
+ Notifications.notifyUser(Meteor.userId(), 'message', {
+ _id: Random.id(),
+ rid: item.rid,
+ ts: new Date(),
+ msg: TAPi18n.__('StatusMessage_Changed_Successfully', null, user.language),
+ });
+ }
+ });
+ }
+}
+
+slashCommands.add('status', Status, {
+ description: 'Slash_Status_Description',
+ params: 'Slash_Status_Params',
+});
diff --git a/app/slashcommands-status/server/index.js b/app/slashcommands-status/server/index.js
new file mode 100644
index 000000000000..11e5ad1b8640
--- /dev/null
+++ b/app/slashcommands-status/server/index.js
@@ -0,0 +1 @@
+import '../lib/status';
diff --git a/app/slashcommands-topic/lib/topic.js b/app/slashcommands-topic/lib/topic.js
index 1bc495e8f277..1e490046336a 100644
--- a/app/slashcommands-topic/lib/topic.js
+++ b/app/slashcommands-topic/lib/topic.js
@@ -4,10 +4,6 @@ import { handleError, slashCommands } from '../../utils';
import { ChatRoom } from '../../models';
import { callbacks } from '../../callbacks';
import { hasPermission } from '../../authorization';
-/*
- * Join is a named function that will replace /topic commands
- * @param {Object} message - The message object
- */
function Topic(command, params, item) {
if (command === 'topic') {
diff --git a/app/statistics/server/functions/get.js b/app/statistics/server/functions/get.js
index 05623782291e..52aa2a4b9278 100644
--- a/app/statistics/server/functions/get.js
+++ b/app/statistics/server/functions/get.js
@@ -15,23 +15,22 @@ import {
Uploads,
Messages,
LivechatVisitors,
+ Integrations,
} from '../../../models/server';
import { settings } from '../../../settings/server';
import { Info, getMongoInfo } from '../../../utils/server';
import { Migrations } from '../../../migrations/server';
import { statistics } from '../statisticsNamespace';
+import { Apps } from '../../../apps/server';
+import { getStatistics as federationGetStatistics } from '../../../federation/server/methods/dashboard';
const wizardFields = [
'Organization_Type',
- 'Organization_Name',
'Industry',
'Size',
'Country',
- 'Website',
- 'Site_Name',
'Language',
'Server_Type',
- 'Allow_Marketing_Emails',
'Register_Server',
];
@@ -48,14 +47,6 @@ statistics.get = function _getStatistics() {
}
});
- const firstUser = Users.getOldest({ name: 1, emails: 1 });
- statistics.wizard.contactName = firstUser && firstUser.name;
- statistics.wizard.contactEmail = firstUser && firstUser.emails && firstUser.emails[0].address;
-
- if (settings.get('Organization_Email')) {
- statistics.wizard.contactEmail = settings.get('Organization_Email');
- }
-
// Version
statistics.uniqueId = settings.get('uniqueID');
if (Settings.findOne('uniqueID')) {
@@ -102,6 +93,13 @@ statistics.get = function _getStatistics() {
statistics.totalDirectMessages = _.reduce(Rooms.findByType('d', { fields: { msgs: 1 } }).fetch(), function _countDirectMessages(num, room) { return num + room.msgs; }, 0);
statistics.totalLivechatMessages = _.reduce(Rooms.findByType('l', { fields: { msgs: 1 } }).fetch(), function _countLivechatMessages(num, room) { return num + room.msgs; }, 0);
+ // Federation statistics
+ const federationOverviewData = federationGetStatistics();
+
+ statistics.federatedServers = federationOverviewData.numberOfActivePeers + federationOverviewData.numberOfInactivePeers;
+ statistics.federatedServersActive = federationOverviewData.numberOfActivePeers;
+ statistics.federatedUsers = federationOverviewData.numberOfFederatedUsers;
+
statistics.lastLogin = Users.getLastLogin();
statistics.lastMessageSentAt = Messages.getLastTimestamp();
statistics.lastSeenSubscription = Subscriptions.getLastSeen();
@@ -151,5 +149,23 @@ statistics.get = function _getStatistics() {
statistics.usages = getUsages();
}
+ statistics.apps = {
+ engineVersion: Info.marketplaceApiVersion,
+ enabled: Apps && Apps.isEnabled(),
+ totalInstalled: Apps && Apps.getManager().get().length,
+ totalActive: Apps && Apps.getManager().get({ enabled: true }).length,
+ };
+
+ const integrations = Integrations.find().fetch();
+
+ statistics.integrations = {
+ totalIntegrations: integrations.length,
+ totalIncoming: integrations.filter((integration) => integration.type === 'webhook-incoming').length,
+ totalIncomingActive: integrations.filter((integration) => integration.enabled === true && integration.type === 'webhook-incoming').length,
+ totalOutgoing: integrations.filter((integration) => integration.type === 'webhook-outgoing').length,
+ totalOutgoingActive: integrations.filter((integration) => integration.enabled === true && integration.type === 'webhook-outgoing').length,
+ totalWithScriptEnabled: integrations.filter((integration) => integration.scriptEnabled === true).length,
+ };
+
return statistics;
};
diff --git a/app/theme/client/imports/components/contextual-bar.css b/app/theme/client/imports/components/contextual-bar.css
index 81659afb9a24..9c53099942d4 100644
--- a/app/theme/client/imports/components/contextual-bar.css
+++ b/app/theme/client/imports/components/contextual-bar.css
@@ -163,123 +163,125 @@
}
}
-.attachments {
- &__item {
- position: relative;
+.flex-tab__result {
+ .attachments {
+ &__item {
+ position: relative;
- margin-bottom: 10px;
+ margin-bottom: 10px;
- transition: background-color 300ms linear;
+ transition: background-color 300ms linear;
- &.active,
- &:hover {
- cursor: pointer;
+ &.active,
+ &:hover {
+ cursor: pointer;
- background-color: #f7f8fa;
+ background-color: #f7f8fa;
- .attachments-menu {
- display: inline-block;
+ .attachments-menu {
+ display: inline-block;
+ }
}
- }
- &-link {
- display: flex;
- flex-direction: row;
+ &-link {
+ display: flex;
+ flex-direction: row;
- padding: 8px 0;
- align-items: center;
+ padding: 8px 0;
+ align-items: center;
+ }
}
- }
- &__file,
- &__thumb {
- display: inline-block;
- display: flex;
- flex-direction: column;
- flex: 0 0 auto;
+ &__file,
+ &__thumb {
+ display: inline-block;
+ display: flex;
+ flex-direction: column;
+ flex: 0 0 auto;
- width: 50px;
+ width: 50px;
- height: 50px;
- margin: 0 8px;
+ height: 50px;
+ margin: 0 8px;
- border-radius: 2px;
+ border-radius: 2px;
- background: radial-gradient(ellipse at center, rgba(155, 169, 186, 1) 0%, rgba(131, 143, 158, 1) 100%);
- background-size: cover;
+ background: radial-gradient(ellipse at center, rgba(155, 169, 186, 1) 0%, rgba(131, 143, 158, 1) 100%);
+ background-size: cover;
- font-size: 24px;
+ font-size: 24px;
- align-items: center;
- justify-content: center;
- }
-
- &__file {
- &--pdf {
- background: radial-gradient(ellipse at center, rgba(250, 97, 97, 1) 0%, rgba(251, 19, 19, 1) 100%);
+ align-items: center;
+ justify-content: center;
}
- &--sheets {
- background: radial-gradient(ellipse at center, rgba(0, 163, 82, 1) 0%, rgba(2, 114, 59, 1) 100%);
- }
+ &__file {
+ &--pdf {
+ background: radial-gradient(ellipse at center, rgba(250, 97, 97, 1) 0%, rgba(251, 19, 19, 1) 100%);
+ }
- &--ppt {
- background: radial-gradient(ellipse at center, rgba(250, 109, 77, 1) 0%, rgba(208, 71, 40, 1) 100%);
+ &--sheets {
+ background: radial-gradient(ellipse at center, rgba(0, 163, 82, 1) 0%, rgba(2, 114, 59, 1) 100%);
+ }
+
+ &--ppt {
+ background: radial-gradient(ellipse at center, rgba(250, 109, 77, 1) 0%, rgba(208, 71, 40, 1) 100%);
+ }
}
- }
- &__type {
- overflow: hidden;
- flex: 0 0 auto;
+ &__type {
+ overflow: hidden;
+ flex: 0 0 auto;
- max-width: 100%;
- padding: 0 10px;
+ max-width: 100%;
+ padding: 0 10px;
- text-overflow: ellipsis;
+ text-overflow: ellipsis;
- color: white;
+ color: white;
- font-size: 10px;
- }
+ font-size: 10px;
+ }
- &__name {
+ &__name {
- overflow: hidden;
+ overflow: hidden;
- min-width: 0;
- margin: 0 8px;
+ min-width: 0;
+ margin: 0 8px;
- white-space: nowrap;
- text-overflow: ellipsis;
+ white-space: nowrap;
+ text-overflow: ellipsis;
- color: #2f343d;
+ color: #2f343d;
- font-size: 14px;
- line-height: 1.5;
- }
+ font-size: 14px;
+ line-height: 1.5;
+ }
- &__details {
- margin: 0 8px 2px;
+ &__details {
+ margin: 0 8px 2px;
- white-space: nowrap;
- text-overflow: ellipsis;
+ white-space: nowrap;
+ text-overflow: ellipsis;
- font-size: 12px;
- }
+ font-size: 12px;
+ }
- &__bold {
- font-weight: 600;
- }
+ &__bold {
+ font-weight: 600;
+ }
- &__content {
- display: flex;
+ &__content {
+ display: flex;
- overflow: hidden;
- flex-direction: column;
+ overflow: hidden;
+ flex-direction: column;
- flex: 1 1 100%;
+ flex: 1 1 100%;
- color: #9ea2a8;
+ color: #9ea2a8;
+ }
}
}
diff --git a/app/theme/client/imports/components/header.css b/app/theme/client/imports/components/header.css
index 6f4cd1e72e8a..d4eadbf5217a 100644
--- a/app/theme/client/imports/components/header.css
+++ b/app/theme/client/imports/components/header.css
@@ -206,8 +206,13 @@
text-overflow: ellipsis;
}
- &-visual-status {
- text-transform: capitalize;
+ &__visual-status {
+ overflow: hidden;
+
+ width: 100%;
+ max-width: fit-content;
+
+ text-overflow: ellipsis;
}
&__status {
diff --git a/app/theme/client/imports/components/popover.css b/app/theme/client/imports/components/popover.css
index ef22fd3a1efd..15c2b539286d 100644
--- a/app/theme/client/imports/components/popover.css
+++ b/app/theme/client/imports/components/popover.css
@@ -118,6 +118,30 @@
&--star-filled .rc-icon {
fill: currentColor;
}
+
+ &--online {
+ & .rc-popover__icon {
+ color: var(--status-online);
+ }
+ }
+
+ &--away {
+ & .rc-popover__icon {
+ color: var(--status-away);
+ }
+ }
+
+ &--busy {
+ & .rc-popover__icon {
+ color: var(--status-busy);
+ }
+ }
+
+ &--offline {
+ & .rc-popover__icon {
+ color: var(--status-invisible);
+ }
+ }
}
&__label {
diff --git a/app/theme/client/imports/components/table.css b/app/theme/client/imports/components/table.css
index 19d96082a231..40d06314fee0 100644
--- a/app/theme/client/imports/components/table.css
+++ b/app/theme/client/imports/components/table.css
@@ -7,6 +7,16 @@
font-weight: 500;
+ &-content {
+ display: flex;
+ flex-direction: column;
+ flex: 1 1 auto;
+
+ height: 100%;
+ min-height: 200px;
+ justify-content: stretch;
+ }
+
&--fixed {
table-layout: fixed;
}
diff --git a/app/theme/client/imports/components/tabs.css b/app/theme/client/imports/components/tabs.css
index cf7a87448cd2..187394482509 100644
--- a/app/theme/client/imports/components/tabs.css
+++ b/app/theme/client/imports/components/tabs.css
@@ -13,8 +13,9 @@
}
.tab {
+ display: flex;
+
margin: 0 1rem;
- padding: 1rem 0;
cursor: pointer;
@@ -24,13 +25,25 @@
border-bottom: 2px solid transparent;
+ font-family: inherit;
font-size: 1rem;
-
font-weight: 500;
line-height: 1.25rem;
+ align-items: stretch;
+ flex-flow: row nowrap;
&.active {
color: var(--rc-color-button-primary);
border-bottom-color: var(--rc-color-button-primary);
}
+
+ &:focus {
+ text-decoration: underline;
+ }
+
+ & > span {
+ flex: 1;
+
+ padding: 1rem 0;
+ }
}
diff --git a/app/theme/client/imports/forms/input.css b/app/theme/client/imports/forms/input.css
index 0be7e3f66a48..ad82c59e6263 100644
--- a/app/theme/client/imports/forms/input.css
+++ b/app/theme/client/imports/forms/input.css
@@ -57,6 +57,8 @@ textarea.rc-input__element {
height: 2.5rem;
padding: 0 1rem;
+ text-align: start;
+
text-overflow: ellipsis;
color: var(--input-text-color);
@@ -74,6 +76,8 @@ textarea.rc-input__element {
}
&::placeholder {
+
+ text-align: start;
text-overflow: ellipsis;
color: var(--input-placeholder-color);
diff --git a/app/theme/client/imports/forms/tags.css b/app/theme/client/imports/forms/tags.css
index 15705e275887..5d1dce60c5a8 100644
--- a/app/theme/client/imports/forms/tags.css
+++ b/app/theme/client/imports/forms/tags.css
@@ -2,7 +2,7 @@
display: flex;
width: 100%;
- min-height: 43px;
+ min-height: 40px;
padding: 0 1rem 0 2.25rem;
@@ -20,7 +20,7 @@
&__tag {
display: flex;
- margin: 0.25rem;
+ margin: 0.15rem;
padding: 0.35rem 0.5rem;
color: var(--tags-text-color);
diff --git a/app/theme/client/imports/general/base.css b/app/theme/client/imports/general/base.css
index eaea675afe08..f7b8eb0bc8f4 100644
--- a/app/theme/client/imports/general/base.css
+++ b/app/theme/client/imports/general/base.css
@@ -219,52 +219,3 @@ button {
.hidden {
display: none;
}
-
-.loading-animation {
- position: absolute;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
-
- display: flex;
-
- text-align: center;
- align-items: center;
- justify-content: center;
-}
-
-.loading-animation > .bounce {
- display: inline-block;
-
- width: 10px;
- height: 10px;
- margin: 2px;
-
- animation: loading-bouncedelay 1.4s infinite ease-in-out both;
-
- border-radius: 100%;
- background-color: rgba(255, 255, 255, 0.6);
-}
-
-.loading-animation .bounce1 {
- -webkit-animation-delay: -0.32s;
- animation-delay: -0.32s;
-}
-
-.loading-animation .bounce2 {
- -webkit-animation-delay: -0.16s;
- animation-delay: -0.16s;
-}
-
-@keyframes loading-bouncedelay {
- 0%,
- 80%,
- 100% {
- transform: scale(0);
- }
-
- 40% {
- transform: scale(1);
- }
-}
diff --git a/app/theme/client/imports/general/base_old.css b/app/theme/client/imports/general/base_old.css
index 3edd8420674b..5c4c10743925 100644
--- a/app/theme/client/imports/general/base_old.css
+++ b/app/theme/client/imports/general/base_old.css
@@ -145,7 +145,17 @@
}
.rc-old .text-center {
+ display: flex;
+
text-align: center;
+ justify-content: center;
+}
+
+.rc-old .text-left {
+ display: flex;
+
+ text-align: left;
+ justify-content: start;
}
.connection-status > .alert {
@@ -3732,58 +3742,58 @@ rc-old select,
}
}
- & .edit-form {
- padding: 20px 20px 0;
+ & .room-info-content > div {
+ margin: 0 0 20px;
+ }
+}
- white-space: normal;
+.rc-old .edit-form {
+ padding: 20px 20px 0;
- & h3 {
- margin-bottom: 8px;
+ white-space: normal;
- font-size: 24px;
- line-height: 22px;
- }
+ & h3 {
+ margin-bottom: 8px;
- & p {
- font-size: 12px;
- font-weight: 300;
- line-height: 18px;
- }
+ font-size: 24px;
+ line-height: 22px;
+ }
- & > .input-line {
- margin-top: 20px;
+ & p {
+ font-size: 12px;
+ font-weight: 300;
+ line-height: 18px;
+ }
- & #password {
- width: 70%;
- }
+ & > .input-line {
+ margin-top: 20px;
- & #roleSelect {
- width: 70%;
- }
+ & #password {
+ width: 70%;
}
- & nav {
- padding: 0;
-
- &.buttons {
- margin-top: 2em;
- }
+ & #roleSelect {
+ width: 70%;
}
+ }
- & .form-divisor {
- height: 9px;
- margin: 2em 0;
-
- text-align: center;
+ & nav {
+ padding: 0;
- & > span {
- padding: 0 1em;
- }
+ &.buttons {
+ margin-top: 2em;
}
}
- & .room-info-content > div {
- margin: 0 0 20px;
+ & .form-divisor {
+ height: 9px;
+ margin: 2em 0;
+
+ text-align: center;
+
+ & > span {
+ padding: 0 1em;
+ }
}
}
@@ -4944,8 +4954,7 @@ rc-old select,
.rc-old .load-more {
position: relative;
- height: 10px;
- padding: 1rem 0;
+ height: 2rem;
}
.rc-old .flex-tab {
@@ -5181,6 +5190,8 @@ rc-old select,
& .form-group {
display: inline-block;
+
+ vertical-align: middle;
}
}
@@ -5365,9 +5376,8 @@ rc-old select,
position: absolute;
right: 25px;
- width: 80px;
height: 30px;
- padding-top: 4px;
+ padding: 4px;
cursor: pointer;
text-align: center;
diff --git a/app/theme/client/imports/general/forms.css b/app/theme/client/imports/general/forms.css
index 323905dd2e6a..45a0d9ad2474 100644
--- a/app/theme/client/imports/general/forms.css
+++ b/app/theme/client/imports/general/forms.css
@@ -253,7 +253,7 @@
height: 100%;
&--new {
- padding: 0 1.5rem;
+ padding: 1.5rem;
}
&__content {
diff --git a/app/theme/client/imports/general/variables.css b/app/theme/client/imports/general/variables.css
index 8404b70812c7..90c8edd03ff2 100644
--- a/app/theme/client/imports/general/variables.css
+++ b/app/theme/client/imports/general/variables.css
@@ -218,14 +218,14 @@
--sidebar-item-thumb-size: 18px;
--sidebar-item-thumb-size-medium: 27px;
--sidebar-item-thumb-size-extended: 36px;
- --sidebar-item-text-color: var(--color-gray);
+ --sidebar-item-text-color: var(--rc-color-primary-light);
--sidebar-item-background: inherit;
--sidebar-item-hover-background: var(--rc-color-primary-darkest);
--sidebar-item-active-background: var(--rc-color-primary-dark);
--sidebar-item-active-color: var(--sidebar-item-text-color);
- --sidebar-item-unread-color: var(--color-white);
+ --sidebar-item-unread-color: var(--rc-color-content);
--sidebar-item-unread-font-weight: 600;
- --sidebar-item-popup-background: var(--color-dark-medium);
+ --sidebar-item-popup-background: var(--rc-color-primary-dark);
--sidebar-item-user-status-size: 6px;
--sidebar-item-user-status-radius: 50%;
--sidebar-item-text-size: 0.875rem;
diff --git a/app/ui-account/client/accountPreferences.js b/app/ui-account/client/accountPreferences.js
index 247fc0785179..cd4058b3664d 100644
--- a/app/ui-account/client/accountPreferences.js
+++ b/app/ui-account/client/accountPreferences.js
@@ -235,8 +235,9 @@ Template.accountPreferences.onCreated(function() {
if (results.requested) {
modal.open({
title: t('UserDataDownload_Requested'),
- text: t('UserDataDownload_Requested_Text'),
+ text: t('UserDataDownload_Requested_Text', { pending_operations: results.pendingOperationsBeforeMyRequest }),
type: 'success',
+ html: true,
});
return true;
@@ -244,10 +245,15 @@ Template.accountPreferences.onCreated(function() {
if (results.exportOperation) {
if (results.exportOperation.status === 'completed') {
+ const text = results.url
+ ? TAPi18n.__('UserDataDownload_CompletedRequestExistedWithLink_Text', { download_link: results.url })
+ : t('UserDataDownload_CompletedRequestExisted_Text');
+
modal.open({
title: t('UserDataDownload_Requested'),
- text: t('UserDataDownload_CompletedRequestExisted_Text'),
+ text,
type: 'success',
+ html: true,
});
return true;
@@ -255,8 +261,9 @@ Template.accountPreferences.onCreated(function() {
modal.open({
title: t('UserDataDownload_Requested'),
- text: t('UserDataDownload_RequestExisted_Text'),
+ text: t('UserDataDownload_RequestExisted_Text', { pending_operations: results.pendingOperationsBeforeMyRequest }),
type: 'success',
+ html: true,
});
return true;
}
diff --git a/app/ui-account/client/accountProfile.html b/app/ui-account/client/accountProfile.html
index c3e3e5a40a57..59435c30f2ff 100644
--- a/app/ui-account/client/accountProfile.html
+++ b/app/ui-account/client/accountProfile.html
@@ -70,6 +70,26 @@
{{/if}}