RocketChat/Rocket.Chat

View on GitHub
apps/meteor/app/ui/client/lib/UserAction.ts

Summary

Maintainability
A
45 mins
Test Coverage
import type { IExtras, IRoomActivity, IActionsObject, IUser } from '@rocket.chat/core-typings';
import { debounce } from 'lodash';
import { Meteor } from 'meteor/meteor';
import { ReactiveDict } from 'meteor/reactive-dict';

import { settings } from '../../../settings/client';
import { sdk } from '../../../utils/client/lib/SDKClient';

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<string, Set<string>>();
const rooms = new Map<string, (username: string, activityType: string[], extras?: object | undefined) => void>();

const performingUsers = new ReactiveDict<IActionsObject>();

const shownName = function (user: IUser | null | undefined): string | undefined {
    if (!user) {
        return;
    }
    if (settings.get('UI_Use_Real_Name')) {
        return user.name;
    }
    return user.username;
};

const emitActivities = debounce(async (rid: string, extras: IExtras): Promise<void> => {
    const activities = roomActivities.get(extras?.tmid || rid) || new Set();
    sdk.publish('notify-room', [`${rid}/${USER_ACTIVITY}`, shownName(Meteor.user() as unknown as IUser), [...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);
}
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 user = Meteor.users.findOne(Meteor.userId() || undefined, {
                fields: { name: 1, username: 1 },
            }) as IUser;
            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);
    }
})();