jiti-meet/react/features/reactions/middleware.ts

272 lines
9.0 KiB
TypeScript

import { batch } from 'react-redux';
import { createReactionSoundsDisabledEvent } from '../analytics/AnalyticsEvents';
import { sendAnalytics } from '../analytics/functions';
import { IStore } from '../app/types';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app/actionTypes';
import { CONFERENCE_JOIN_IN_PROGRESS, SET_START_REACTIONS_MUTED } from '../base/conference/actionTypes';
import { setStartReactionsMuted } from '../base/conference/actions';
import {
getParticipantById,
getParticipantCount,
isLocalParticipantModerator
} from '../base/participants/functions';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import { SETTINGS_UPDATED } from '../base/settings/actionTypes';
import { updateSettings } from '../base/settings/actions';
import { playSound, registerSound, unregisterSound } from '../base/sounds/actions';
import { getDisabledSounds } from '../base/sounds/functions.any';
import { showNotification } from '../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants';
import {
ADD_REACTION_BUFFER,
FLUSH_REACTION_BUFFER,
PUSH_REACTIONS,
SEND_REACTIONS,
SHOW_SOUNDS_NOTIFICATION
} from './actionTypes';
import {
addReactionsToChat,
displayReactionSoundsNotification,
flushReactionBuffer,
pushReactions,
sendReactions,
setReactionQueue
} from './actions';
import {
ENDPOINT_REACTION_NAME,
IMuteCommandAttributes,
MUTE_REACTIONS_COMMAND,
RAISE_HAND_SOUND_ID,
REACTIONS,
REACTION_SOUND,
SOUNDS_THRESHOLDS
} from './constants';
import {
getReactionMessageFromBuffer,
getReactionsSoundsThresholds,
getReactionsWithId,
sendReactionsWebhook
} from './functions.any';
import logger from './logger';
import { RAISE_HAND_SOUND_FILE } from './sounds';
/**
* Middleware which intercepts Reactions actions to handle changes to the
* visibility timeout of the Reactions.
*
* @param {IStore} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: any) => {
const { dispatch, getState } = store;
switch (action.type) {
case APP_WILL_MOUNT:
batch(() => {
Object.keys(REACTIONS).forEach(key => {
for (let i = 0; i < SOUNDS_THRESHOLDS.length; i++) {
dispatch(registerSound(
`${REACTIONS[key].soundId}${SOUNDS_THRESHOLDS[i]}`,
REACTIONS[key].soundFiles[i]
)
);
}
}
);
dispatch(registerSound(RAISE_HAND_SOUND_ID, RAISE_HAND_SOUND_FILE));
});
break;
case APP_WILL_UNMOUNT:
batch(() => {
Object.keys(REACTIONS).forEach(key => {
for (let i = 0; i < SOUNDS_THRESHOLDS.length; i++) {
dispatch(unregisterSound(`${REACTIONS[key].soundId}${SOUNDS_THRESHOLDS[i]}`));
}
});
dispatch(unregisterSound(RAISE_HAND_SOUND_ID));
});
break;
case ADD_REACTION_BUFFER: {
const { timeoutID, buffer } = getState()['features/reactions'];
const { reaction } = action;
clearTimeout(timeoutID ?? 0);
buffer.push(reaction);
action.buffer = buffer;
action.timeoutID = setTimeout(() => {
dispatch(flushReactionBuffer());
}, 500);
break;
}
case CONFERENCE_JOIN_IN_PROGRESS: {
const { conference } = action;
conference.addCommandListener(
MUTE_REACTIONS_COMMAND, ({ attributes }: { attributes: IMuteCommandAttributes; }, id: any) => {
_onMuteReactionsCommand(attributes, id, store);
});
break;
}
case FLUSH_REACTION_BUFFER: {
const state = getState();
const { buffer } = state['features/reactions'];
const participantCount = getParticipantCount(state);
batch(() => {
if (participantCount > 1) {
dispatch(sendReactions());
}
dispatch(addReactionsToChat(getReactionMessageFromBuffer(buffer)));
dispatch(pushReactions(buffer));
});
sendReactionsWebhook(state, buffer);
break;
}
case PUSH_REACTIONS: {
const state = getState();
const { queue, notificationDisplayed } = state['features/reactions'];
const { soundsReactions } = state['features/base/settings'];
const disabledSounds = getDisabledSounds(state);
const reactions = action.reactions;
batch(() => {
if (!notificationDisplayed && soundsReactions && !disabledSounds.includes(REACTION_SOUND)
&& displayReactionSoundsNotification) {
dispatch(displayReactionSoundsNotification());
}
if (soundsReactions) {
const reactionSoundsThresholds = getReactionsSoundsThresholds(reactions);
reactionSoundsThresholds.forEach(reaction =>
dispatch(playSound(`${REACTIONS[reaction.reaction].soundId}${reaction.threshold}`))
);
}
dispatch(setReactionQueue([ ...queue, ...getReactionsWithId(reactions) ]));
});
break;
}
case SEND_REACTIONS: {
const state = getState();
const { buffer } = state['features/reactions'];
const { conference } = state['features/base/conference'];
if (conference) {
conference.sendEndpointMessage('', {
name: ENDPOINT_REACTION_NAME,
reactions: buffer,
timestamp: Date.now()
});
}
break;
}
// Settings changed for mute reactions in the meeting
case SET_START_REACTIONS_MUTED: {
const state = getState();
const { conference } = state['features/base/conference'];
const { muted, updateBackend } = action;
if (conference && isLocalParticipantModerator(state) && updateBackend) {
conference.sendCommand(MUTE_REACTIONS_COMMAND, { attributes: { startReactionsMuted: Boolean(muted) } });
}
break;
}
case SETTINGS_UPDATED: {
const { soundsReactions } = getState()['features/base/settings'];
if (action.settings.soundsReactions === false && soundsReactions === true) {
sendAnalytics(createReactionSoundsDisabledEvent());
}
break;
}
case SHOW_SOUNDS_NOTIFICATION: {
const state = getState();
const isModerator = isLocalParticipantModerator(state);
const { disableReactionsModeration } = state['features/base/config'];
const customActions = [ 'notify.reactionSounds' ];
const customFunctions: Function[] = [ () => dispatch(updateSettings({
soundsReactions: false
})) ];
if (isModerator && !disableReactionsModeration) {
customActions.push('notify.reactionSoundsForAll');
customFunctions.push(() => batch(() => {
dispatch(setStartReactionsMuted(true));
dispatch(updateSettings({ soundsReactions: false }));
}));
}
dispatch(showNotification({
titleKey: 'toolbar.disableReactionSounds',
customActionNameKey: customActions,
customActionHandler: customFunctions
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
break;
}
}
return next(action);
});
/**
* Notifies this instance about a "Mute Reaction Sounds" command received by the Jitsi
* conference.
*
* @param {Object} attributes - The attributes carried by the command.
* @param {string} id - The identifier of the participant who issuing the
* command. A notable idiosyncrasy to be mindful of here is that the command
* may be issued by the local participant.
* @param {Object} store - The redux store. Used to calculate and dispatch
* updates.
* @private
* @returns {void}
*/
function _onMuteReactionsCommand(attributes: IMuteCommandAttributes = {}, id: string, store: IStore) {
const state = store.getState();
// We require to know who issued the command because (1) only a
// moderator is allowed to send commands and (2) a command MUST be
// issued by a defined commander.
if (typeof id === 'undefined') {
return;
}
const participantSendingCommand = getParticipantById(state, id);
// The Command(s) API will send us our own commands and we don't want
// to act upon them.
if (participantSendingCommand?.local) {
return;
}
if (participantSendingCommand?.role !== 'moderator') {
logger.warn('Received mute-reactions command not from moderator');
return;
}
const oldState = Boolean(state['features/base/conference'].startReactionsMuted);
// @ts-ignore
const newState = attributes.startReactionsMuted === 'true';
if (oldState !== newState) {
batch(() => {
store.dispatch(setStartReactionsMuted(newState));
store.dispatch(updateSettings({ soundsReactions: !newState }));
});
}
}