import type { IExtras, IRoomActivity, IUser } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; import { debounce } from 'lodash'; import { sdk } from './SDKClient'; import { settings } from './settings'; import { getUser, getUserId } from './user'; import { Users } from '../stores'; const TIMEOUT = 15000; const RENEW = TIMEOUT / 3; const USER_ACTIVITY = 'user-activity'; export const USER_ACTIVITIES = { USER_RECORDING: 'user-recording', USER_TYPING: 'user-typing', USER_UPLOADING: 'user-uploading', USER_PLAYING: 'user-playing', }; const activityTimeouts = new Map(); const activityRenews = new Map(); const continuingIntervals = new Map(); const roomActivities = new Map>(); const rooms = new Map void>(); const performingUsers = new Map(); const performingUsersEmitter = new Emitter<{ changed: void }>(); const shownName = function (user: IUser | null | undefined): string | undefined { if (!user) { return; } if (settings.peek('UI_Use_Real_Name')) { return user.name; } return user.username; }; const emitActivities = debounce(async (rid: string, extras: IExtras): Promise => { const activities = roomActivities.get(extras?.tmid || rid) || new Set(); sdk.publish('notify-room', [`${rid}/${USER_ACTIVITY}`, shownName(getUser()), [...activities], extras]); }, 500); function handleStreamAction(rid: string, username: string, activityTypes: string[], extras?: IExtras): void { rid = extras?.tmid || rid; const roomActivities = { ...performingUsers.get(rid) }; for (const [, activity] of Object.entries(USER_ACTIVITIES)) { roomActivities[activity] = roomActivities[activity] || new Map(); const users = roomActivities[activity]; const timeout = users[username]; if (timeout) { clearTimeout(timeout); } if (activityTypes.includes(activity)) { activityTypes.splice(activityTypes.indexOf(activity), 1); users[username] = setTimeout(() => handleStreamAction(rid, username, activityTypes, extras), TIMEOUT); } else { delete users[username]; } } performingUsers.set(rid, roomActivities); performingUsersEmitter.emit('changed'); } export const UserAction = new (class { addStream(rid: string): () => void { if (rooms.get(rid)) { throw new Error('UserAction - addStream should only be called once per room'); } const handler = function (username: string, activityType: string[], extras?: object): void { const uid = getUserId(); const user = uid ? Users.state.get(uid) : undefined; if (username === shownName(user)) { return; } handleStreamAction(rid, username, activityType, extras); }; rooms.set(rid, handler); const { stop } = sdk.stream('notify-room', [`${rid}/${USER_ACTIVITY}`], handler); return () => { if (!rooms.get(rid)) { return; } stop(); rooms.delete(rid); }; } performContinuously(rid: string, activityType: string, extras: IExtras = {}): void { const trid = extras?.tmid || rid; const key = `${activityType}-${trid}`; if (continuingIntervals.get(key)) { return; } this.start(rid, activityType, extras); continuingIntervals.set( key, setInterval(() => { this.start(rid, activityType, extras); }, RENEW), ); } start(rid: string, activityType: string, extras: IExtras = {}): void { const trid = extras?.tmid || rid; const key = `${activityType}-${trid}`; if (activityRenews.get(key)) { return; } activityRenews.set( key, setTimeout(() => { clearTimeout(activityRenews.get(key)); activityRenews.delete(key); }, RENEW), ); const activities = roomActivities.get(trid) || new Set(); activities.add(activityType); roomActivities.set(trid, activities); void emitActivities(rid, extras); if (activityTimeouts.get(key)) { clearTimeout(activityTimeouts.get(key)); activityTimeouts.delete(key); } activityTimeouts.set( key, setTimeout(() => this.stop(trid, activityType, extras), TIMEOUT), ); activityTimeouts.get(key); } stop(rid: string, activityType: string, extras: IExtras): void { const trid = extras?.tmid || rid; const key = `${activityType}-${trid}`; if (activityTimeouts.get(key)) { clearTimeout(activityTimeouts.get(key)); activityTimeouts.delete(key); } if (activityRenews.get(key)) { clearTimeout(activityRenews.get(key)); activityRenews.delete(key); } if (continuingIntervals.get(key)) { clearInterval(continuingIntervals.get(key)); continuingIntervals.delete(key); } const activities = roomActivities.get(trid) || new Set(); activities.delete(activityType); roomActivities.set(trid, activities); void emitActivities(rid, extras); } get(roomId: string): IRoomActivity | undefined { return performingUsers.get(roomId); } subscribe(onChanged: () => void): () => void { return performingUsersEmitter.on('changed', onChanged); } })();