diff --git a/app/api/server/v1/users.js b/app/api/server/v1/users.js
index 6ec98554deb5b..d0115d51fb0c2 100644
--- a/app/api/server/v1/users.js
+++ b/app/api/server/v1/users.js
@@ -647,7 +647,7 @@ API.v1.addRoute('users.removePersonalAccessToken', { authRequired: true }, {
API.v1.addRoute('users.presence', { authRequired: true }, {
get() {
- const { from } = this.queryParams;
+ const { from, ids } = this.queryParams;
const options = {
fields: {
@@ -659,6 +659,13 @@ API.v1.addRoute('users.presence', { authRequired: true }, {
},
};
+ if (ids) {
+ return API.v1.success({
+ users: Users.findNotOfflineByIds(Array.isArray(ids) ? ids : ids.split(','), options).fetch(),
+ full: false,
+ });
+ }
+
if (from) {
const ts = new Date(from);
const diff = (Date.now() - ts) / 1000 / 60;
diff --git a/app/livechat/client/views/app/tabbar/agentInfo.html b/app/livechat/client/views/app/tabbar/agentInfo.html
index c3c1cac243845..5c33cc4341afe 100644
--- a/app/livechat/client/views/app/tabbar/agentInfo.html
+++ b/app/livechat/client/views/app/tabbar/agentInfo.html
@@ -14,10 +14,10 @@
{{name}}
{{#if username}}@{{username}}
{{/if}}
- {{/userPresence}}
diff --git a/app/livechat/client/views/app/tabbar/agentInfo.js b/app/livechat/client/views/app/tabbar/agentInfo.js
index 8d5d04a63e7fe..6894586f32dac 100644
--- a/app/livechat/client/views/app/tabbar/agentInfo.js
+++ b/app/livechat/client/views/app/tabbar/agentInfo.js
@@ -20,7 +20,9 @@ Template.agentInfo.helpers({
const hasCustomFields = customFieldsTemplate() !== null;
return (availableDepartments.length > 0 && hasPermission('add-livechat-department-agents')) || hasCustomFields;
},
-
+ uid() {
+ return Template.instance().agent.get()._id;
+ },
name() {
const agent = Template.instance().agent.get();
return agent && agent.name ? agent.name : TAPi18n.__('Unnamed');
diff --git a/app/models/server/models/Users.js b/app/models/server/models/Users.js
index 1db4188319b73..b24f2b535631c 100644
--- a/app/models/server/models/Users.js
+++ b/app/models/server/models/Users.js
@@ -504,6 +504,16 @@ export class Users extends Base {
return this.find(query, options);
}
+ findNotOfflineByIds(users, options) {
+ const query = {
+ _id: { $in: users },
+ status: {
+ $in: ['online', 'away', 'busy'],
+ },
+ };
+ return this.find(query, options);
+ }
+
findUsersNotOffline(options) {
const query = {
username: {
diff --git a/app/ui-flextab/client/tabs/membersList.html b/app/ui-flextab/client/tabs/membersList.html
index 62cbfdc3d186b..ad92bc15ff59c 100644
--- a/app/ui-flextab/client/tabs/membersList.html
+++ b/app/ui-flextab/client/tabs/membersList.html
@@ -36,7 +36,7 @@
{{> avatar username=user.username}}
-
+ {{# userPresence uid=user._id}}
{{/userPresence}}
{{ignored}} {{displayName}} {{utcOffset}}
{{> icon user=. block="rc-member-list__menu js-more" icon="menu" }}
diff --git a/app/ui-flextab/client/tabs/userInfo.html b/app/ui-flextab/client/tabs/userInfo.html
index d097c0796a0d5..c57a711be264a 100644
--- a/app/ui-flextab/client/tabs/userInfo.html
+++ b/app/ui-flextab/client/tabs/userInfo.html
@@ -31,10 +31,10 @@
{{name}}
{{#if username}}@{{username}}
{{/if}}
- {{/userPresence}}
diff --git a/app/ui-flextab/client/tabs/userInfo.js b/app/ui-flextab/client/tabs/userInfo.js
index b9afcdc27365b..c03fd2a297e97 100644
--- a/app/ui-flextab/client/tabs/userInfo.js
+++ b/app/ui-flextab/client/tabs/userInfo.js
@@ -68,7 +68,10 @@ Template.userInfo.helpers({
}
return customFields;
},
-
+ uid() {
+ const user = Template.instance().user.get();
+ return user._id;
+ },
name() {
const user = Template.instance().user.get();
return user && user.name ? user.name : TAPi18n.__('Unnamed');
diff --git a/app/ui-sidenav/client/index.js b/app/ui-sidenav/client/index.js
index 64f39bf4942ed..216d156fe5e4e 100644
--- a/app/ui-sidenav/client/index.js
+++ b/app/ui-sidenav/client/index.js
@@ -13,6 +13,7 @@ import './sideNav';
import './roomList';
import './sortlist';
import './toolbar';
+import './userPresence';
export {
toolbarSearch,
diff --git a/app/ui-sidenav/client/sidebarItem.html b/app/ui-sidenav/client/sidebarItem.html
index 9f5832cc0f00f..0363d595a2201 100644
--- a/app/ui-sidenav/client/sidebarItem.html
+++ b/app/ui-sidenav/client/sidebarItem.html
@@ -4,7 +4,7 @@
{{> icon block="rc-icon--default-size sidebar-item__icon sidebar-item__icon" icon=icon}}
{{else}}
-
+ {{# userPresence uid=uid}}{{/userPresence}}
{{/if}}
diff --git a/app/ui-sidenav/client/sidebarItem.js b/app/ui-sidenav/client/sidebarItem.js
index 5d7f0175d9511..d1eb0292ba6eb 100644
--- a/app/ui-sidenav/client/sidebarItem.js
+++ b/app/ui-sidenav/client/sidebarItem.js
@@ -201,6 +201,12 @@ Template.sidebarItem.events({
});
Template.sidebarItemIcon.helpers({
+ uid() {
+ if (!this.rid) {
+ return this._id;
+ }
+ return this.rid.replace(this.u._id, '');
+ },
isRoom() {
return this.rid || this._id;
},
diff --git a/app/ui-sidenav/client/userPresence.html b/app/ui-sidenav/client/userPresence.html
new file mode 100644
index 0000000000000..28bfe5bc76511
--- /dev/null
+++ b/app/ui-sidenav/client/userPresence.html
@@ -0,0 +1 @@
+{{> Template.contentBlock}}
diff --git a/app/ui-sidenav/client/userPresence.js b/app/ui-sidenav/client/userPresence.js
new file mode 100644
index 0000000000000..32501e8d92532
--- /dev/null
+++ b/app/ui-sidenav/client/userPresence.js
@@ -0,0 +1,87 @@
+import { Meteor } from 'meteor/meteor';
+// import { ReactiveVar } from 'meteor/reactive-var';
+import { Template } from 'meteor/templating';
+import { Tracker } from 'meteor/tracker';
+import _ from 'underscore';
+import mem from 'mem';
+
+import { APIClient } from '../../utils/client';
+import { saveUser } from '../../../imports/startup/client/listenActiveUsers';
+
+import './userPresence.html';
+
+const data = new Map();
+const promises = new Map();
+const pending = new Map();
+
+const getAll = _.debounce(async function getAll() {
+ const ids = Array.from(pending.keys());
+
+ if (ids.length === 0) {
+ return;
+ }
+
+ const params = {
+ ids,
+ };
+
+ try {
+ const {
+ users,
+ } = await APIClient.v1.get('users.presence', params);
+
+ users.forEach((user) => saveUser(user, true));
+
+ ids.forEach((id) => {
+ const { resolve } = promises.get(id);
+ resolve();
+ });
+ } catch (e) {
+ ids.forEach((id) => {
+ const { reject } = promises.get(id);
+ reject();
+ });
+ }
+}, 1000);
+
+const get = mem(function get(id) {
+ const promise = pending.get(id) || new Promise((resolve, reject) => {
+ promises.set(id, { resolve, reject });
+ });
+ pending.set(id, promise);
+ return promise;
+});
+
+const options = {
+ threshold: 0.1,
+};
+
+let lastEntries = [];
+const handleEntries = function(entries) {
+ lastEntries = entries.filter(({ isIntersecting }) => isIntersecting);
+ lastEntries.forEach(async (entry) => {
+ const { uid } = data.get(entry.target);
+ await get(uid);
+ pending.delete(uid);
+ });
+ getAll();
+};
+
+const observer = new IntersectionObserver(handleEntries, options);
+
+Tracker.autorun(() => {
+ if (!Meteor.userId() || !Meteor.status().connected) {
+ return Meteor.users.update({}, { $unset: { status: '' } }, { multi: true });
+ }
+ mem.clear(get);
+
+ for (const node of data.keys()) {
+ observer.unobserve(node);
+ observer.observe(node);
+ }
+});
+
+Template.userPresence.onRendered(function() {
+ data.set(this.firstNode, this.data);
+ observer.observe(this.firstNode);
+});
diff --git a/app/ui/client/components/header/headerRoom.html b/app/ui/client/components/header/headerRoom.html
index 7cd0b5a2e1bb1..456547049feda 100644
--- a/app/ui/client/components/header/headerRoom.html
+++ b/app/ui/client/components/header/headerRoom.html
@@ -42,10 +42,10 @@
{{/unless}}
{{#if isDirect}}
- {{/userPresence}}
{{else}}
{{#if roomTopic}}{{/if}}
{{/if}}
diff --git a/app/ui/client/components/header/headerRoom.js b/app/ui/client/components/header/headerRoom.js
index 28f69ab444a0e..52f5360d7f5cd 100644
--- a/app/ui/client/components/header/headerRoom.js
+++ b/app/ui/client/components/header/headerRoom.js
@@ -32,7 +32,9 @@ Template.headerRoom.helpers({
isToggleFavoriteButtonChecked: () => Template.instance().state.get('favorite'),
toggleFavoriteButtonIconLabel: () => (Template.instance().state.get('favorite') ? t('Unfavorite') : t('Favorite')),
toggleFavoriteButtonIcon: () => (Template.instance().state.get('favorite') ? 'star-filled' : 'star'),
-
+ uid() {
+ return this._id.replace(Meteor.userId(), '');
+ },
back() {
return Template.instance().data.back;
},
diff --git a/imports/startup/client/listenActiveUsers.js b/imports/startup/client/listenActiveUsers.js
index a559034926af2..105be406a228d 100644
--- a/imports/startup/client/listenActiveUsers.js
+++ b/imports/startup/client/listenActiveUsers.js
@@ -1,5 +1,4 @@
import { Meteor } from 'meteor/meteor';
-import { Tracker } from 'meteor/tracker';
import { debounce } from 'underscore';
import { Notifications } from '../../../app/notifications/client';
@@ -13,7 +12,7 @@ const STATUS_MAP = [
'busy',
];
-const saveUser = (user, force = false) => {
+export const saveUser = (user, force = false) => {
// do not update my own user, my user's status will come from a subscription
if (user._id === Meteor.userId()) {
return;
@@ -72,19 +71,6 @@ const getUsersPresence = debounce(async (isConnected) => {
}
}, 1000);
-let wasConnected = false;
-Tracker.autorun(() => {
- if (!Meteor.userId() || !Meteor.status().connected) {
- return;
- }
-
- lastStatusChange = null;
-
- getUsersPresence(wasConnected);
-
- wasConnected = true;
-});
-
Meteor.startup(function() {
Notifications.onLogged('user-status', ([_id, username, status, statusText]) => {
// only set after first request completed