[NEW] Stream to get individual presence updates (#22950)
Co-authored-by: Guilherme Gazzo <guilhermegazzo@gmail.com>pull/23461/head
parent
d9043a6c0d
commit
d03c2b7e7c
@ -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 }); |
||||
}); |
||||
@ -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,28 +0,0 @@ |
||||
import { useMemo } from 'react'; |
||||
import { useSubscription } from 'use-subscription'; |
||||
|
||||
import { Presence, UserPresence } from '../lib/presence'; |
||||
|
||||
/** |
||||
* Hook to fetch and subscribe users data |
||||
* |
||||
* @param uid - User Id |
||||
* @returns Users data: status, statusText, username, name |
||||
* @public |
||||
*/ |
||||
export const useUserData = (uid: string): UserPresence | undefined => { |
||||
const subscription = useMemo( |
||||
() => ({ |
||||
getCurrentValue: (): UserPresence | undefined => Presence.store.get(uid), |
||||
subscribe: (callback: any): any => { |
||||
Presence.listen(uid, callback); |
||||
return (): void => { |
||||
Presence.stop(uid, callback); |
||||
}; |
||||
}, |
||||
}), |
||||
[uid], |
||||
); |
||||
|
||||
return useSubscription(subscription); |
||||
}; |
||||
@ -1,69 +0,0 @@ |
||||
import { Accounts } from 'meteor/accounts-base'; |
||||
import { Meteor } from 'meteor/meteor'; |
||||
|
||||
import { Notifications } from '../../app/notifications/client'; |
||||
import { IUser } from '../../definition/IUser'; |
||||
import { UserStatus } from '../../definition/UserStatus'; |
||||
import { Presence } from '../lib/presence'; |
||||
|
||||
const STATUS_MAP = [UserStatus.OFFLINE, UserStatus.ONLINE, UserStatus.AWAY, UserStatus.BUSY]; |
||||
|
||||
export const interestedUserIds = new Set<IUser['_id']>(); |
||||
|
||||
export const saveUser = ( |
||||
user: Pick<IUser, '_id' | 'username' | 'status' | 'statusText' | 'avatarETag'>, |
||||
force = false, |
||||
): void => { |
||||
// do not update my own user, my user's status will come from a subscription
|
||||
if (user._id === (Accounts as any).connection?._userId) { |
||||
return; |
||||
} |
||||
|
||||
const found = (Meteor.users as any)._collection._docs._map[user._id]; |
||||
|
||||
if (found && force) { |
||||
Meteor.users.update( |
||||
{ _id: user._id }, |
||||
{ |
||||
$set: { |
||||
...(user.username && { username: user.username }), |
||||
// name: user.name,
|
||||
// utcOffset: user.utcOffset,
|
||||
status: user.status, |
||||
statusText: user.statusText, |
||||
...(user.avatarETag && { avatarETag: user.avatarETag }), |
||||
}, |
||||
}, |
||||
); |
||||
|
||||
return; |
||||
} |
||||
|
||||
if (!found) { |
||||
Meteor.users.insert(user); |
||||
} |
||||
}; |
||||
|
||||
Meteor.startup(() => { |
||||
Notifications.onLogged( |
||||
'user-status', |
||||
([_id, username, status, statusText]: [ |
||||
IUser['_id'], |
||||
IUser['username'], |
||||
number, |
||||
IUser['statusText'], |
||||
]) => { |
||||
Presence.notify({ |
||||
_id, |
||||
username, |
||||
status: STATUS_MAP[status], |
||||
statusText, |
||||
}); |
||||
if (!interestedUserIds.has(_id)) { |
||||
return; |
||||
} |
||||
|
||||
saveUser({ _id, username, status: STATUS_MAP[status], statusText }, true); |
||||
}, |
||||
); |
||||
}); |
||||
Loading…
Reference in new issue