RocketChat/Rocket.Chat

View on GitHub
apps/meteor/client/contexts/CallContext.ts

Summary

Maintainability
D
1 day
Test Coverage
import type { IVoipRoom, ICallerInfo, VoIpCallerInfo } from '@rocket.chat/core-typings';
import type { Device } from '@rocket.chat/ui-contexts';
import { createContext, useContext, useMemo } from 'react';
import { useSyncExternalStore } from 'use-sync-external-store/shim';

import { useHasLicenseModule } from '../hooks/useHasLicenseModule';
import type { VoIPUser } from '../lib/voip/VoIPUser';

export type CallContextValue = CallContextDisabled | CallContextReady | CallContextError | CallContextEnabled;

type CallContextDisabled = {
    enabled: false;
    ready: false;
    outBoundCallsAllowed: undefined;
    outBoundCallsEnabled: undefined;
    outBoundCallsEnabledForUser: undefined;
};

type CallContextEnabled = {
    enabled: true;
    ready: unknown;
    outBoundCallsAllowed: undefined;
    outBoundCallsEnabled: undefined;
    outBoundCallsEnabledForUser: undefined;
};

type CallContextReady = {
    outBoundCallsEnabled: boolean;
    outBoundCallsAllowed: boolean;
    outBoundCallsEnabledForUser: boolean;
    enabled: true;
    ready: true;
    voipClient: VoIPUser;
    actions: CallActionsType;
    queueName: string;
    queueCounter: number;
    networkStatus: 'online' | 'offline';
    openedRoomInfo: { v: { token?: string }; rid: string };
    openWrapUpModal: () => void;
    openRoom: (rid: IVoipRoom['_id']) => void;
    createRoom: (caller: ICallerInfo) => Promise<IVoipRoom['_id']>;
    closeRoom: (data?: { comment?: string; tags?: string[] }) => void;
    changeAudioOutputDevice: (selectedAudioDevices: Device) => void;
    changeAudioInputDevice: (selectedAudioDevices: Device) => void;
    register: () => void;
    unregister: () => void;
};

type CallContextError = {
    enabled: true;
    ready: false;
    outBoundCallsAllowed: undefined;
    outBoundCallsEnabled: undefined;
    outBoundCallsEnabledForUser: undefined;
    error: Error | unknown;
};

const isCallContextReady = (context: CallContextValue): context is CallContextReady => (context as CallContextReady).ready;

const isCallContextError = (context: CallContextValue): context is CallContextError => (context as CallContextError).error !== undefined;

export type CallActionsType = {
    mute: () => unknown;
    unmute: () => unknown;
    pause: () => unknown;
    resume: () => unknown;
    end: () => unknown;
    pickUp: () => unknown;
    reject: () => unknown;
};

const CallContextValueDefault: CallContextValue = {
    enabled: false,
    ready: false,
    outBoundCallsAllowed: undefined,
    outBoundCallsEnabled: undefined,
    outBoundCallsEnabledForUser: undefined,
};

export const CallContext = createContext<CallContextValue>(CallContextValueDefault);

export const useIsVoipEnterprise = (): boolean => useHasLicenseModule('voip-enterprise') === true;

export const useIsCallEnabled = (): boolean => {
    const { enabled } = useContext(CallContext);
    return enabled;
};

export const useIsCallReady = (): boolean => {
    const { ready } = useContext(CallContext);

    return Boolean(ready);
};
export const useIsCallError = (): boolean => {
    const context = useContext(CallContext);
    return Boolean(isCallContextError(context));
};

export const useCallActions = (): CallActionsType => {
    const context = useContext(CallContext);

    if (!isCallContextReady(context)) {
        throw new Error('useCallActions only if Calls are enabled and ready');
    }
    return context.actions;
};

export const useCallerInfo = (): VoIpCallerInfo => {
    const context = useContext(CallContext);

    if (!isCallContextReady(context)) {
        throw new Error('useCallerInfo only if Calls are enabled and ready');
    }

    const { voipClient } = context;

    const [subscribe, getSnapshot] = useMemo(() => {
        let caller: VoIpCallerInfo = voipClient.callerInfo;

        const callback = (cb: () => void): (() => void) =>
            voipClient.on('stateChanged', () => {
                caller = voipClient.callerInfo;
                cb();
            });

        const getSnapshot = (): VoIpCallerInfo => caller;
        return [callback, getSnapshot];
    }, [voipClient]);

    return useSyncExternalStore(subscribe, getSnapshot);
};

export const useCallCreateRoom = (): CallContextReady['createRoom'] => {
    const context = useContext(CallContext);

    if (!isCallContextReady(context)) {
        throw new Error('useCallCreateRoom only if Calls are enabled and ready');
    }

    return context.createRoom;
};

export const useCallOpenRoom = (): CallContextReady['openRoom'] => {
    const context = useContext(CallContext);

    if (!isCallContextReady(context)) {
        throw new Error('useCallOpenRoom only if Calls are enabled and ready');
    }

    return context.openRoom;
};

export const useCallClient = (): VoIPUser => {
    const context = useContext(CallContext);

    if (!isCallContextReady(context)) {
        throw new Error('useCallClient only if Calls are enabled and ready');
    }

    return context.voipClient;
};

export const useQueueName = (): CallContextReady['queueName'] => {
    const context = useContext(CallContext);

    if (!isCallContextReady(context)) {
        throw new Error('useQueueName only if Calls are enabled and ready');
    }

    return context.queueName;
};

export const useQueueCounter = (): CallContextReady['queueCounter'] => {
    const context = useContext(CallContext);

    if (!isCallContextReady(context)) {
        throw new Error('useQueueCounter only if Calls are enabled and ready');
    }

    return context.queueCounter;
};

export const useOpenedRoomInfo = (): CallContextReady['openedRoomInfo'] => {
    const context = useContext(CallContext);

    if (!isCallContextReady(context)) {
        throw new Error('useOpenedRoomInfo only if Calls are enabled and ready');
    }

    return context.openedRoomInfo;
};

export const useChangeAudioOutputDevice = (): CallContextReady['changeAudioOutputDevice'] => {
    const context = useContext(CallContext);

    if (!isCallContextReady(context)) {
        throw new Error('useChangeAudioOutputDevice only if Calls are enabled and ready');
    }

    return context.changeAudioOutputDevice;
};

export const useChangeAudioInputDevice = (): CallContextReady['changeAudioOutputDevice'] => {
    const context = useContext(CallContext);

    if (!isCallContextReady(context)) {
        throw new Error('useChangeAudioInputDevice only if Calls are enabled and ready');
    }

    return context.changeAudioInputDevice;
};

export const useCallRegisterClient = (): (() => void) => {
    const context = useContext(CallContext);

    if (!isCallContextReady(context)) {
        throw new Error('useCallRegisterClient only if Calls are enabled and ready');
    }

    return context.register;
};

export const useCallUnregisterClient = (): (() => void) => {
    const context = useContext(CallContext);

    if (!isCallContextReady(context)) {
        throw new Error('useCallUnregisterClient only if Calls are enabled and ready');
    }

    return context.unregister;
};

export const useVoipOutboundStates = (): {
    outBoundCallsAllowed: boolean;
    outBoundCallsEnabled: boolean;
    outBoundCallsEnabledForUser: boolean;
} => {
    const isEnterprise = useIsVoipEnterprise();
    const callerInfo = useCallerInfo();

    return {
        outBoundCallsAllowed: isEnterprise,
        outBoundCallsEnabled: isEnterprise,
        outBoundCallsEnabledForUser: isEnterprise && !['IN_CALL', 'ON_HOLD', 'UNREGISTERED', 'INITIAL'].includes(callerInfo.state),
    };
};

export const useVoipNetworkStatus = (): 'online' | 'offline' => {
    const context = useContext(CallContext);

    if (!isCallContextReady(context)) {
        throw new Error('useVoipNetworkStatus only if Calls are enabled and ready');
    }

    return context.networkStatus;
};