From 4b68fb9cbea239a19fef202f1ffe954a98300e0f Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Wed, 11 Nov 2020 14:57:47 -0300 Subject: [PATCH] Regression: Fix presence status (#19474) Co-authored-by: Guilherme Gazzo --- app/ui-sidenav/client/userPresence.js | 2 +- client/components/basic/UserStatus.js | 17 ++- client/lib/presence.js | 80 ------------ client/lib/presence.ts | 133 ++++++++++++++++++++ definition/IUser.ts | 2 +- imports/startup/client/listenActiveUsers.js | 8 +- package-lock.json | 36 +++--- 7 files changed, 167 insertions(+), 111 deletions(-) delete mode 100644 client/lib/presence.js create mode 100644 client/lib/presence.ts diff --git a/app/ui-sidenav/client/userPresence.js b/app/ui-sidenav/client/userPresence.js index b2bf03d8e1d..4c54be3356c 100644 --- a/app/ui-sidenav/client/userPresence.js +++ b/app/ui-sidenav/client/userPresence.js @@ -86,7 +86,7 @@ Tracker.autorun(() => { mem.clear(get); wasConnected = isConnected; - Presence.emit('restart'); + Presence.restart(); if (featureExists) { for (const node of data.keys()) { diff --git a/client/components/basic/UserStatus.js b/client/components/basic/UserStatus.js index b6bbaf99d5f..6e288136904 100644 --- a/client/components/basic/UserStatus.js +++ b/client/components/basic/UserStatus.js @@ -4,18 +4,18 @@ import { StatusBullet } from '@rocket.chat/fuselage'; import { useTranslation } from '../../contexts/TranslationContext'; import { Presence } from '../../lib/presence'; -export const UserStatus = React.memo(({ small, ...props }) => { +export const UserStatus = React.memo(({ small, status, ...props }) => { const size = small ? 'small' : 'large'; const t = useTranslation(); - switch (props.status) { + switch (status) { case 'online': - return ; + return ; case 'busy': - return ; + return ; case 'away': - return ; - case 'Offline': - return ; + return ; + case 'offline': + return ; default: return ; } @@ -26,7 +26,6 @@ export const Away = (props) => ; export const Online = (props) => ; export const Offline = (props) => ; export const Loading = (props) => ; - export const colors = { busy: 'danger-500', away: 'warning-600', @@ -37,7 +36,7 @@ export const colors = { export const usePresence = (uid, presence) => { const [status, setStatus] = useState(presence); useEffect(() => { - const handle = ({ status = 'offline' }) => { + const handle = ({ status = 'loading' }) => { setStatus(status); }; Presence.listen(uid, handle); diff --git a/client/lib/presence.js b/client/lib/presence.js deleted file mode 100644 index 6cd2908daa7..00000000000 --- a/client/lib/presence.js +++ /dev/null @@ -1,80 +0,0 @@ -import { Emitter } from '@rocket.chat/emitter'; - -import { APIClient } from '../../app/utils/client'; - -export const Presence = new Emitter(); - -const Statuses = new Map(); - -const getPresence = (() => { - const uids = new Set(); - - let timer; - const fetch = () => { - timer && clearTimeout(timer); - timer = setTimeout(async () => { - const params = { - ids: [...uids], - }; - - const { - users, - } = await APIClient.v1.get('users.presence', params); - - users.forEach((user) => { - Presence.emit(user._id, user); - uids.delete(user._id); - }); - - [...uids].forEach((uid) => { - Presence.emit(uid, { uid }); - }); - - uids.clear(); - }, 50); - }; - - const get = async (uid) => { - uids.add(uid); - fetch(); - }; - - Presence.on('remove', (uid) => { - if (Presence.has(uid)) { - return; - } - Statuses.delete(uid); - }); - - Presence.on('reset', () => { - Presence.once('restart', () => Presence.events().filter((e) => Boolean(e) && !['reset', 'restart', 'remove'].includes(e) && typeof e === 'string').forEach(get)); - }); - - return get; -})(); - - -const update = ({ _id: uid, status }) => { - Statuses.set(uid, status); -}; - -Presence.listen = async (uid, handle) => { - Presence.on(uid, handle); - Presence.on(uid, update); - Presence.on('reset', handle); - if (Statuses.has(uid)) { - return handle({ status: Statuses.get(uid) }); - } - getPresence(uid); -}; - -Presence.stop = (uid, handle) => { - Presence.off(uid, handle); - Presence.off('reset', handle); - Presence.emit('remove', uid); -}; - -Presence.reset = () => { - Presence.emit('reset', { status: 'offline' }); - Statuses.clear(); -}; diff --git a/client/lib/presence.ts b/client/lib/presence.ts new file mode 100644 index 00000000000..4c821f4fbb6 --- /dev/null +++ b/client/lib/presence.ts @@ -0,0 +1,133 @@ +import { Emitter, EventType, Handler } from '@rocket.chat/emitter'; + +import { APIClient } from '../../app/utils/client'; +import { IUser } from '../../definition/IUser'; + +const emitter = new Emitter(); +const statuses = new Map(); + +type User = Pick; + +type UsersPresencePayload = { + users: User[]; + full: boolean; +}; + +const isUid = (eventType: EventType): eventType is User['_id'] => + Boolean(eventType) && typeof eventType === 'string' && !['reset', 'restart', 'remove'].includes(eventType); + +const uids = new Set(); +const getPresence = ((): ((uid: User['_id']) => void) => { + let timer: ReturnType; + + const fetch = (delay = 250): void => { + timer && clearTimeout(timer); + timer = setTimeout(async () => { + const currentUids = new Set(uids); + uids.clear(); + try { + const params = { + ids: [...currentUids], + }; + + const { users } = await APIClient.v1.get('users.presence', params) as UsersPresencePayload; + + users.forEach((user) => { + if (!statuses.has(user._id)) { + emitter.emit(user._id, user); + } + currentUids.delete(user._id); + }); + + currentUids.forEach((uid) => { + emitter.emit(uid, { uid, status: 'offline' }); + }); + + currentUids.clear(); + } catch { + fetch(delay + delay); + } finally { + currentUids.forEach((item) => uids.add(item)); + } + }, delay); + }; + + const get = (uid: User['_id']): void => { + uids.add(uid); + fetch(); + }; + + emitter.on('remove', (uid) => { + if (emitter.has(uid)) { + return; + } + + statuses.delete(uid); + }); + + emitter.on('reset', () => { + statuses.clear(); + emitter.once('restart', () => { + emitter.events() + .filter(isUid) + .forEach(get); + }); + }); + + return get; +})(); + +type PresenceUpdate = Partial>; + +const update: Handler = (update) => { + if (update?._id) { + statuses.set(update._id, update.status); + uids.delete(update._id); + } +}; + +const listen = (uid: User['_id'], handler: Handler): void => { + emitter.on(uid, handler); + emitter.on(uid, update); + emitter.on('reset', handler); + + if (statuses.has(uid)) { + return handler({ status: statuses.get(uid) }); + } + + getPresence(uid); +}; + +const stop = (uid: User['_id'], handler: Handler): void => { + emitter.off(uid, handler); + emitter.off(uid, update); + emitter.off('reset', handler); + emitter.emit('remove', uid); +}; + +const reset = (): void => { + emitter.emit('reset', {}); + statuses.clear(); +}; + +const restart = (): void => { + emitter.emit('restart'); +}; + +const notify = (update: PresenceUpdate): void => { + if (update._id) { + emitter.emit(update._id, update); + } + + if (update.username) { + emitter.emit(update.username, update); + } +}; + +export const Presence = { + listen, + stop, + reset, + restart, + notify, +}; diff --git a/definition/IUser.ts b/definition/IUser.ts index 92dd97bd9dd..73072e24de7 100644 --- a/definition/IUser.ts +++ b/definition/IUser.ts @@ -95,7 +95,7 @@ export interface IUser { name?: string; services?: IUserServices; emails?: IUserEmail[]; - status?: string; + status?: USER_STATUS; statusConnection?: string; lastLogin?: Date; avatarOrigin?: string; diff --git a/imports/startup/client/listenActiveUsers.js b/imports/startup/client/listenActiveUsers.js index c9e7bcd49bc..e64162575f1 100644 --- a/imports/startup/client/listenActiveUsers.js +++ b/imports/startup/client/listenActiveUsers.js @@ -42,8 +42,12 @@ export const saveUser = (user, force = false) => { Meteor.startup(function() { Notifications.onLogged('user-status', ([_id, username, status, statusText]) => { - Presence.emit(_id, { _id, status: STATUS_MAP[status], statusText, username }); - Presence.emit(username, { _id, status: STATUS_MAP[status], statusText, username }); + Presence.notify({ + _id, + username, + status: STATUS_MAP[status], + statusText, + }); if (!interestedUserIds.has(_id)) { return; } diff --git a/package-lock.json b/package-lock.json index 2616cbc34cf..efe2742ef58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18113,7 +18113,7 @@ }, "chownr": { "version": "1.1.1", - "resolved": "", + "resolved": false, "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==", "dev": true, "optional": true @@ -18148,7 +18148,7 @@ }, "debug": { "version": "4.1.1", - "resolved": "", + "resolved": false, "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "dev": true, "optional": true, @@ -18179,7 +18179,7 @@ }, "fs-minipass": { "version": "1.2.5", - "resolved": "", + "resolved": false, "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", "dev": true, "optional": true, @@ -18213,7 +18213,7 @@ }, "glob": { "version": "7.1.3", - "resolved": "", + "resolved": false, "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", "dev": true, "optional": true, @@ -18245,7 +18245,7 @@ }, "ignore-walk": { "version": "3.0.1", - "resolved": "", + "resolved": false, "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", "dev": true, "optional": true, @@ -18266,7 +18266,7 @@ }, "inherits": { "version": "2.0.3", - "resolved": "", + "resolved": false, "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "dev": true, "optional": true @@ -18314,7 +18314,7 @@ }, "minipass": { "version": "2.3.5", - "resolved": "", + "resolved": false, "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", "dev": true, "optional": true, @@ -18325,7 +18325,7 @@ }, "minizlib": { "version": "1.2.1", - "resolved": "", + "resolved": false, "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", "dev": true, "optional": true, @@ -18345,7 +18345,7 @@ }, "ms": { "version": "2.1.1", - "resolved": "", + "resolved": false, "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", "dev": true, "optional": true @@ -18359,7 +18359,7 @@ }, "needle": { "version": "2.3.0", - "resolved": "", + "resolved": false, "integrity": "sha512-QBZu7aAFR0522EyaXZM0FZ9GLpq6lvQ3uq8gteiDUp7wKdy0lSd2hPlgFwVuW1CBkfEs9PfDQsQzZghLs/psdg==", "dev": true, "optional": true, @@ -18371,7 +18371,7 @@ }, "node-pre-gyp": { "version": "0.12.0", - "resolved": "", + "resolved": false, "integrity": "sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==", "dev": true, "optional": true, @@ -18401,14 +18401,14 @@ }, "npm-bundled": { "version": "1.0.6", - "resolved": "", + "resolved": false, "integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==", "dev": true, "optional": true }, "npm-packlist": { "version": "1.4.1", - "resolved": "", + "resolved": false, "integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==", "dev": true, "optional": true, @@ -18488,7 +18488,7 @@ }, "process-nextick-args": { "version": "2.0.0", - "resolved": "", + "resolved": false, "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", "dev": true, "optional": true @@ -18533,7 +18533,7 @@ }, "rimraf": { "version": "2.6.3", - "resolved": "", + "resolved": false, "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", "dev": true, "optional": true, @@ -18564,7 +18564,7 @@ }, "semver": { "version": "5.7.0", - "resolved": "", + "resolved": false, "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", "dev": true, "optional": true @@ -18624,7 +18624,7 @@ }, "tar": { "version": "4.4.8", - "resolved": "", + "resolved": false, "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", "dev": true, "optional": true, @@ -18664,7 +18664,7 @@ }, "yallist": { "version": "3.0.3", - "resolved": "", + "resolved": false, "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", "dev": true, "optional": true