From b634a50ad6ae20d8b0b1fd9c1b25dc4b563809c8 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Thu, 5 Nov 2020 22:01:30 -0300 Subject: [PATCH 1/2] Convert client/lib/presence to TypeScript --- app/ui-sidenav/client/userPresence.js | 2 +- client/lib/presence.js | 80 ------------- client/lib/presence.ts | 124 ++++++++++++++++++++ definition/IUser.ts | 2 +- imports/startup/client/listenActiveUsers.js | 8 +- package-lock.json | 36 +++--- 6 files changed, 150 insertions(+), 102 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 b2bf03d8e1d19..4c54be3356cb9 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/lib/presence.js b/client/lib/presence.js deleted file mode 100644 index 6cd2908daa754..0000000000000 --- 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 0000000000000..fa93fcde68f0d --- /dev/null +++ b/client/lib/presence.ts @@ -0,0 +1,124 @@ +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 getPresence = ((): ((uid: User['_id']) => void) => { + const uids = new Set(); + + let timer: ReturnType; + + const fetch = (): void => { + timer && clearTimeout(timer); + timer = setTimeout(async () => { + const params = { + ids: [...uids], + }; + + const { users } = await APIClient.v1.get('users.presence', params) as UsersPresencePayload; + + users.forEach((user) => { + emitter.emit(user._id, user); + uids.delete(user._id); + }); + + [...uids].forEach((uid) => { + emitter.emit(uid, { uid }); + }); + + uids.clear(); + }, 50); + }; + + 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', () => { + emitter.once('restart', () => { + emitter.events() + .filter(isUid) + .forEach((uid) => { + get(uid); + }); + }); + }); + + return get; +})(); + +type PresenceUpdate = Partial>; + +const update: Handler = (update) => { + if (update?._id) { + statuses.set(update._id, update.status); + } +}; + +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', { status: 'offline' }); + 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 92dd97bd9dd7d..73072e24de784 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 c9e7bcd49bcbc..e64162575f1aa 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 2bf64a47894a8..be4242d99306d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18120,7 +18120,7 @@ }, "chownr": { "version": "1.1.1", - "resolved": "", + "resolved": false, "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==", "dev": true, "optional": true @@ -18155,7 +18155,7 @@ }, "debug": { "version": "4.1.1", - "resolved": "", + "resolved": false, "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "dev": true, "optional": true, @@ -18186,7 +18186,7 @@ }, "fs-minipass": { "version": "1.2.5", - "resolved": "", + "resolved": false, "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", "dev": true, "optional": true, @@ -18220,7 +18220,7 @@ }, "glob": { "version": "7.1.3", - "resolved": "", + "resolved": false, "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", "dev": true, "optional": true, @@ -18252,7 +18252,7 @@ }, "ignore-walk": { "version": "3.0.1", - "resolved": "", + "resolved": false, "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", "dev": true, "optional": true, @@ -18273,7 +18273,7 @@ }, "inherits": { "version": "2.0.3", - "resolved": "", + "resolved": false, "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "dev": true, "optional": true @@ -18321,7 +18321,7 @@ }, "minipass": { "version": "2.3.5", - "resolved": "", + "resolved": false, "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", "dev": true, "optional": true, @@ -18332,7 +18332,7 @@ }, "minizlib": { "version": "1.2.1", - "resolved": "", + "resolved": false, "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", "dev": true, "optional": true, @@ -18352,7 +18352,7 @@ }, "ms": { "version": "2.1.1", - "resolved": "", + "resolved": false, "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", "dev": true, "optional": true @@ -18366,7 +18366,7 @@ }, "needle": { "version": "2.3.0", - "resolved": "", + "resolved": false, "integrity": "sha512-QBZu7aAFR0522EyaXZM0FZ9GLpq6lvQ3uq8gteiDUp7wKdy0lSd2hPlgFwVuW1CBkfEs9PfDQsQzZghLs/psdg==", "dev": true, "optional": true, @@ -18378,7 +18378,7 @@ }, "node-pre-gyp": { "version": "0.12.0", - "resolved": "", + "resolved": false, "integrity": "sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==", "dev": true, "optional": true, @@ -18408,14 +18408,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, @@ -18495,7 +18495,7 @@ }, "process-nextick-args": { "version": "2.0.0", - "resolved": "", + "resolved": false, "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", "dev": true, "optional": true @@ -18540,7 +18540,7 @@ }, "rimraf": { "version": "2.6.3", - "resolved": "", + "resolved": false, "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", "dev": true, "optional": true, @@ -18571,7 +18571,7 @@ }, "semver": { "version": "5.7.0", - "resolved": "", + "resolved": false, "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", "dev": true, "optional": true @@ -18631,7 +18631,7 @@ }, "tar": { "version": "4.4.8", - "resolved": "", + "resolved": false, "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", "dev": true, "optional": true, @@ -18671,7 +18671,7 @@ }, "yallist": { "version": "3.0.3", - "resolved": "", + "resolved": false, "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", "dev": true, "optional": true From 4902a5b88b3e07b0c37f4e72853657bca797a2fc Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Wed, 11 Nov 2020 13:21:50 -0300 Subject: [PATCH 2/2] Fix errors --- client/components/basic/UserStatus.js | 17 ++++----- client/lib/presence.ts | 53 ++++++++++++++++----------- 2 files changed, 39 insertions(+), 31 deletions(-) diff --git a/client/components/basic/UserStatus.js b/client/components/basic/UserStatus.js index b6bbaf99d5fbd..6e2881369041f 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.ts b/client/lib/presence.ts index fa93fcde68f0d..4c821f4fbb62c 100644 --- a/client/lib/presence.ts +++ b/client/lib/presence.ts @@ -16,31 +16,40 @@ type UsersPresencePayload = { 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) => { - const uids = new Set(); - let timer: ReturnType; - const fetch = (): void => { + const fetch = (delay = 250): void => { timer && clearTimeout(timer); timer = setTimeout(async () => { - const params = { - ids: [...uids], - }; - - const { users } = await APIClient.v1.get('users.presence', params) as UsersPresencePayload; - - users.forEach((user) => { - emitter.emit(user._id, user); - uids.delete(user._id); - }); + 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); + }); - [...uids].forEach((uid) => { - emitter.emit(uid, { uid }); - }); + currentUids.forEach((uid) => { + emitter.emit(uid, { uid, status: 'offline' }); + }); - uids.clear(); - }, 50); + currentUids.clear(); + } catch { + fetch(delay + delay); + } finally { + currentUids.forEach((item) => uids.add(item)); + } + }, delay); }; const get = (uid: User['_id']): void => { @@ -57,12 +66,11 @@ const getPresence = ((): ((uid: User['_id']) => void) => { }); emitter.on('reset', () => { + statuses.clear(); emitter.once('restart', () => { emitter.events() .filter(isUid) - .forEach((uid) => { - get(uid); - }); + .forEach(get); }); }); @@ -74,6 +82,7 @@ type PresenceUpdate = Partial = (update) => { if (update?._id) { statuses.set(update._id, update.status); + uids.delete(update._id); } }; @@ -97,7 +106,7 @@ const stop = (uid: User['_id'], handler: Handler): void => { }; const reset = (): void => { - emitter.emit('reset', { status: 'offline' }); + emitter.emit('reset', {}); statuses.clear(); };