Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NEW] Stream to get individual presence updates #22950

Merged
merged 32 commits into from
Oct 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
adbf171
Add new stream to get individual presence updates
sampaiodiego Aug 17, 2021
8a2b902
Merge branch 'develop' of github.com:RocketChat/Rocket.Chat into new-…
sampaiodiego Aug 17, 2021
43c89da
Merge branch 'develop' into new-stream-presence-per-user
sampaiodiego Aug 17, 2021
5967576
Send status text on user-presence stream
sampaiodiego Aug 17, 2021
35076e3
Remove useUserData hook in favor of usePresence
sampaiodiego Aug 17, 2021
5a0c1c5
Merge branch 'new-stream-presence-per-user' of github.com:RocketChat/…
sampaiodiego Aug 17, 2021
12e3043
Merge branch 'develop' into new-stream-presence-per-user
ggazzo Aug 19, 2021
6d3b322
Merge branch 'develop' of github.com:RocketChat/Rocket.Chat into new-…
ggazzo Aug 30, 2021
f5e3403
Merge branch 'new-stream-presence-per-user' of github.com:RocketChat/…
ggazzo Aug 30, 2021
b42c5af
Merge remote-tracking branch 'origin/develop' into new-stream-presenc…
ggazzo Sep 1, 2021
2cc0362
Merge remote-tracking branch 'origin/develop' into new-stream-presenc…
ggazzo Sep 1, 2021
008ccb4
WIP
ggazzo Sep 2, 2021
52de8b0
StatusMessage deprecated
ggazzo Sep 2, 2021
ba81b05
fix build
ggazzo Sep 2, 2021
a7a5212
Merge branch 'develop' into new-stream-presence-per-user
sampaiodiego Sep 2, 2021
1bb4943
typo
ggazzo Sep 3, 2021
80872cf
Merge branch 'new-stream-presence-per-user' of github.com:RocketChat/…
ggazzo Sep 3, 2021
3e322dc
Merge branch 'develop' into new-stream-presence-per-user
sampaiodiego Sep 7, 2021
b88590f
Merge branch 'develop' into new-stream-presence-per-user
sampaiodiego Sep 10, 2021
65c2fbf
Merge branch 'develop' into new-stream-presence-per-user
sampaiodiego Sep 10, 2021
0b13b64
Merge branch 'develop' into new-stream-presence-per-user
sampaiodiego Sep 14, 2021
ba20d62
Merge branch 'new-stream-presence-per-user' of github.com:RocketChat/…
ggazzo Sep 28, 2021
f0d6f7e
Merge branch 'develop' of github.com:RocketChat/Rocket.Chat into new-…
ggazzo Sep 28, 2021
f2c1f91
fix
ggazzo Sep 28, 2021
7e95ad5
Improve code to work with microservices
ggazzo Sep 29, 2021
08c648d
.
ggazzo Sep 29, 2021
2f46931
..
ggazzo Sep 29, 2021
bca9fac
Merge branch 'develop' into new-stream-presence-per-user
sampaiodiego Oct 13, 2021
94335c6
Code cleanup
sampaiodiego Oct 13, 2021
1de3ccf
Merge remote-tracking branch 'origin/develop' into new-stream-presenc…
sampaiodiego Oct 14, 2021
44ea98a
Cleaning up a bit more
sampaiodiego Oct 14, 2021
156677e
Merge remote-tracking branch 'origin/develop' into new-stream-presenc…
sampaiodiego Oct 14, 2021
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
3 changes: 3 additions & 0 deletions app/notifications/client/lib/Notifications.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';

import './Presence';

class Notifications {
constructor(...args) {
this.logged = Meteor.userId() !== null;
Expand All @@ -17,6 +19,7 @@ class Notifications {
this.streamRoom = new Meteor.Streamer('notify-room');
this.streamRoomUsers = new Meteor.Streamer('notify-room-users');
this.streamUser = new Meteor.Streamer('notify-user');

if (this.debug === true) {
this.onAll(function() {
return console.log('RocketChat.Notifications: onAll', args);
Expand Down
13 changes: 13 additions & 0 deletions app/notifications/client/lib/Presence.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Meteor } from 'meteor/meteor';

import { Presence, STATUS_MAP } from '../../../../client/lib/presence';

// TODO implement API on Streamer to be able to listen to all streamed data
// this is a hacky way to listen to all streamed data from user-presense Streamer
(Meteor as any).StreamerCentral.on('stream-user-presence', (uid: string, args: unknown) => {
if (!Array.isArray(args)) {
throw new Error('Presence event must be an array');
}
const [username, status, statusText] = args as [string, number, string | undefined];
Presence.notify({ _id: uid, username, status: STATUS_MAP[status], statusText });
});
1 change: 1 addition & 0 deletions app/notifications/server/lib/Notifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
Users as UsersRaw,
Settings as SettingsRaw,
} from '../../../models/server/raw';
import './Presence';

// TODO: Replace this in favor of the api.broadcast
// StreamerCentral.on('broadcast', (name, eventName, args) => {
Expand Down
103 changes: 103 additions & 0 deletions app/notifications/server/lib/Presence.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { Emitter } from '@rocket.chat/emitter';

import { IUser } from '../../../../definition/IUser';
import { IPublication, IStreamerConstructor, Connection, IStreamer } from '../../../../server/modules/streamer/streamer.module';

export type UserPresenseStreamProps = {
added: IUser['_id'][];
removed: IUser['_id'][];
}

export type UserPresenseStreamArgs = {
'uid': string;
args: unknown;
}

const e = new Emitter<{
[key: string]: UserPresenseStreamArgs;
}>();


const clients = new WeakMap<Connection, UserPresence>();


export class UserPresence {
private readonly streamer: IStreamer;

private readonly publication: IPublication;

private readonly listeners: Set<string>;

constructor(publication: IPublication, streamer: IStreamer) {
this.listeners = new Set();
this.publication = publication;
this.streamer = streamer;
}

listen(uid: string): void {
if (this.listeners.has(uid)) {
return;
}
e.on(uid, this.run);
this.listeners.add(uid);
}

off = (uid: string): void => {
e.off(uid, this.run);
this.listeners.delete(uid);
}

run = (args: UserPresenseStreamArgs): void => {
const payload = this.streamer.changedPayload(this.streamer.subscriptionName, args.uid, { ...args, eventName: args.uid }); // there is no good explanation to keep eventName, I just want to save one 'DDPCommon.parseDDP' on the client side, so I'm trying to fit the Meteor Streamer's payload
(this.publication as any)._session.socket.send(payload);
}

stop(): void {
this.listeners.forEach(this.off);
clients.delete(this.publication.connection);
}

static getClient(publication: IPublication, streamer: IStreamer): [UserPresence, boolean] {
const { connection } = publication;
const stored = clients.get(connection);

const client = stored || new UserPresence(publication, streamer);

const main = Boolean(!stored);

clients.set(connection, client);

return [client, main];
}
}

export class StreamPresence {
static getInstance(Streamer: IStreamerConstructor, name = 'user-presence'): IStreamer {
return new class StreamPresence extends Streamer {
async _publish(publication: IPublication, _eventName: string, options: boolean | {useCollection?: boolean; args?: any} = false): Promise<void> {
const { added, removed } = (typeof options !== 'boolean' ? options : {}) as unknown as UserPresenseStreamProps;


const [client, main] = UserPresence.getClient(publication, this);

added?.forEach((uid) => client.listen(uid));
removed?.forEach((uid) => client.off(uid));


if (!main) {
publication.stop();
return;
}

publication.ready();

publication.onStop(() => client.stop());
}
}(name);
}
}


export const emit = (uid: string, args: UserPresenseStreamArgs): void => {
e.emit(uid, { uid, args });
};
1 change: 0 additions & 1 deletion app/ui-message/client/message.html
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@
<button type="button" class="user user-card-message color-primary-font-color" data-username="{{msg.u.username}}" tabindex="1">
{{getName}}{{#if showUsername}} <span class="message-alias border-component-color color-info-font-color">@{{msg.u.username}}</span>{{/if}}
</button>
{{> StatusMessage uid=msg.u._id}}
<span class="info border-component-color color-info-font-color"></span>
{{# if showRoles }}
{{#each role in roleTags}}
Expand Down
54 changes: 1 addition & 53 deletions app/ui-sidenav/client/userPresence.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,58 +2,12 @@ import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
import { Template } from 'meteor/templating';
import { Tracker } from 'meteor/tracker';
import _ from 'underscore';
import mem from 'mem';

import { APIClient } from '../../utils/client';
import { saveUser, interestedUserIds } from '../../../client/startup/listenActiveUsers';
import { Presence } from '../../../client/lib/presence';

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();
});
}
}, 100);

export const get = mem(function get(id) {
interestedUserIds.add(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,
};
Expand All @@ -63,10 +17,8 @@ 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);
Presence.get(uid);
});
getAll();
};

const featureExists = !!window.IntersectionObserver;
Expand All @@ -80,7 +32,6 @@ Tracker.autorun(() => {
Presence.reset();
return Meteor.users.update({ status: { $exists: true } }, { $unset: { status: true } }, { multi: true });
}
mem.clear(get);

Presence.restart();

Expand All @@ -93,11 +44,8 @@ Tracker.autorun(() => {
}


getAll();

Accounts.onLogout(() => {
Presence.reset();
interestedUserIds.clear();
});
});

Expand Down
10 changes: 7 additions & 3 deletions client/components/Message/StatusMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { Box, Icon } from '@rocket.chat/fuselage';
import React, { ReactElement, memo } from 'react';
import React, { ReactElement, memo, useEffect } from 'react';

import { useUserData } from '../../hooks/useUserData';
import { usePresence } from '../../hooks/usePresence';

// TODO: deprecate this component
const StatusMessage = ({ uid }: { uid: string }): ReactElement | null => {
const data = useUserData(uid);
const data = usePresence(uid);
useEffect(() => {
process.env.NODE_ENV === 'development' && console.warn('StatusMessage component is deprecated');
}, [data]);

if (!data || !data.statusText) {
return null;
Expand Down
2 changes: 1 addition & 1 deletion client/components/UserStatus/ReactiveUserStatus.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { usePresence } from '../../hooks/usePresence';
import UserStatus from './UserStatus';

const ReactiveUserStatus = ({ uid, ...props }) => {
const status = usePresence(uid);
const status = usePresence(uid)?.status;
return <UserStatus status={status} {...props} />;
};

Expand Down
24 changes: 20 additions & 4 deletions client/hooks/usePresence.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@
import { useUserData } from './useUserData';
import { useMemo } from 'react';
import { useSubscription } from 'use-subscription';

import { Presence, UserPresence } from '../lib/presence';

type Presence = 'online' | 'offline' | 'busy' | 'away' | 'loading';

/**
* Hook to fetch and subscribe users presence
*
* @param uid - User Id
* @returns User Presence - 'online' | 'offline' | 'busy' | 'away' | 'loading'
* @returns UserPresence
* @public
*/
export const usePresence = (uid: string): UserPresence | undefined => {
const subscription = useMemo(
() => ({
getCurrentValue: (): UserPresence | undefined => (uid ? Presence.store.get(uid) : undefined),
subscribe: (callback: any): any => {
uid && Presence.listen(uid, callback);
return (): void => {
uid && Presence.stop(uid, callback);
};
},
}),
[uid],
);

export const usePresence = (uid: string, presence: Presence): Presence =>
useUserData(uid)?.status || presence;
return useSubscription(subscription);
};
28 changes: 0 additions & 28 deletions client/hooks/useUserData.ts

This file was deleted.

Loading