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 uid=uid}}
{{agentStatusText}}
-
+
{{/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 @@

    {{_ "User_Info"}}

  • {{name}}

    {{#if username}}

    @{{username}}

    {{/if}} - + {{# userPresence uid=uid}}
    {{userStatusText}}
    -
    +
    {{/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 @@ + 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 uid=uid}}
    {{userStatusText}}
    -
    +
    {{/userPresence}} {{else}} {{#if roomTopic}}{{{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