Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/ui-sidenav/client/userPresence.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ Tracker.autorun(() => {
mem.clear(get);
wasConnected = isConnected;

Presence.emit('restart');
Presence.restart();

if (featureExists) {
for (const node of data.keys()) {
Expand Down
17 changes: 8 additions & 9 deletions client/components/basic/UserStatus.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <StatusBullet size={size} title={t('Online')} {...props}/>;
return <StatusBullet size={size} status={status} title={t('Online')} {...props}/>;
case 'busy':
return <StatusBullet size={size} title={t('Busy')} {...props}/>;
return <StatusBullet size={size} status={status} title={t('Busy')} {...props}/>;
case 'away':
return <StatusBullet size={size} title={t('Away')} {...props}/>;
case 'Offline':
return <StatusBullet size={size} title={t('Offline')} {...props}/>;
return <StatusBullet size={size} status={status} title={t('Away')} {...props}/>;
case 'offline':
return <StatusBullet size={size} status={status} title={t('Offline')} {...props}/>;
default:
return <StatusBullet size={size} title={t('Loading')} {...props}/>;
}
Expand All @@ -26,7 +26,6 @@ export const Away = (props) => <UserStatus status='away' {...props}/>;
export const Online = (props) => <UserStatus status='online' {...props}/>;
export const Offline = (props) => <UserStatus status='offline' {...props}/>;
export const Loading = (props) => <UserStatus {...props}/>;

export const colors = {
busy: 'danger-500',
away: 'warning-600',
Expand All @@ -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);
Expand Down
80 changes: 0 additions & 80 deletions client/lib/presence.js

This file was deleted.

133 changes: 133 additions & 0 deletions client/lib/presence.ts
Original file line number Diff line number Diff line change
@@ -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<IUser, '_id' | 'username' | 'name' | 'status' | 'utcOffset' | 'statusText' | 'avatarETag'>;

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<User['_id']>();
const getPresence = ((): ((uid: User['_id']) => void) => {
let timer: ReturnType<typeof setTimeout>;

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<Pick<User, '_id' | 'username' | 'status' | 'statusText'>>;

const update: Handler<PresenceUpdate> = (update) => {
if (update?._id) {
statuses.set(update._id, update.status);
uids.delete(update._id);
}
};

const listen = (uid: User['_id'], handler: Handler<PresenceUpdate>): 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<PresenceUpdate>): 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,
};
2 changes: 1 addition & 1 deletion definition/IUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export interface IUser {
name?: string;
services?: IUserServices;
emails?: IUserEmail[];
status?: string;
status?: USER_STATUS;
statusConnection?: string;
lastLogin?: Date;
avatarOrigin?: string;
Expand Down
8 changes: 6 additions & 2 deletions imports/startup/client/listenActiveUsers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Loading