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