import i18n from 'i18next'; import { batch } from 'react-redux'; import { AnyAction } from 'redux'; import { IStore } from '../app/types'; import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app/actionTypes'; import { CONFERENCE_FAILED, CONFERENCE_JOINED } from '../base/conference/actionTypes'; import { conferenceWillJoin } from '../base/conference/actions'; import { JitsiConferenceErrors, JitsiConferenceEvents } from '../base/lib-jitsi-meet'; import { getFirstLoadableAvatarUrl, getParticipantDisplayName } from '../base/participants/functions'; import MiddlewareRegistry from '../base/redux/MiddlewareRegistry'; import StateListenerRegistry from '../base/redux/StateListenerRegistry'; import { playSound, registerSound, unregisterSound } from '../base/sounds/actions'; import { isTestModeEnabled } from '../base/testing/functions'; import { handleLobbyChatInitialized, removeLobbyChatParticipant } from '../chat/actions.any'; import { hideNotification, showNotification } from '../notifications/actions'; import { LOBBY_NOTIFICATION_ID, NOTIFICATION_ICON, NOTIFICATION_TIMEOUT_TYPE, NOTIFICATION_TYPE } from '../notifications/constants'; import { INotificationProps } from '../notifications/types'; import { open as openParticipantsPane } from '../participants-pane/actions'; import { getParticipantsPaneOpen } from '../participants-pane/functions'; import { shouldAutoKnock } from '../prejoin/functions'; import { KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED, KNOCKING_PARTICIPANT_LEFT } from './actionTypes'; import { approveKnockingParticipant, hideLobbyScreen, knockingParticipantLeft, openLobbyScreen, participantIsKnockingOrUpdated, rejectKnockingParticipant, setLobbyMessageListener, setLobbyModeEnabled, setPasswordJoinFailed, startKnocking } from './actions'; import { updateLobbyParticipantOnLeave } from './actions.any'; import { KNOCKING_PARTICIPANT_SOUND_ID } from './constants'; import { getKnockingParticipants, showLobbyChatButton } from './functions'; import { KNOCKING_PARTICIPANT_FILE } from './sounds'; import { IKnockingParticipant } from './types'; MiddlewareRegistry.register(store => next => action => { switch (action.type) { case APP_WILL_MOUNT: store.dispatch(registerSound(KNOCKING_PARTICIPANT_SOUND_ID, KNOCKING_PARTICIPANT_FILE)); break; case APP_WILL_UNMOUNT: store.dispatch(unregisterSound(KNOCKING_PARTICIPANT_SOUND_ID)); break; case CONFERENCE_FAILED: return _conferenceFailed(store, next, action); case CONFERENCE_JOINED: return _conferenceJoined(store, next, action); case KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED: { // We need the full update result to be in the store already const result = next(action); _findLoadableAvatarForKnockingParticipant(store, action.participant); _handleLobbyNotification(store); return result; } case KNOCKING_PARTICIPANT_LEFT: { // We need the full update result to be in the store already const result = next(action); _handleLobbyNotification(store); return result; } } return next(action); }); /** * Registers a change handler for state['features/base/conference'].conference to * set the event listeners needed for the lobby feature to operate. */ StateListenerRegistry.register( state => state['features/base/conference'].conference, (conference, { dispatch, getState }, previousConference) => { if (conference && !previousConference) { conference.on(JitsiConferenceEvents.MEMBERS_ONLY_CHANGED, (enabled: boolean) => { dispatch(setLobbyModeEnabled(enabled)); if (enabled) { dispatch(setLobbyMessageListener()); } }); conference.on(JitsiConferenceEvents.LOBBY_USER_JOINED, (id: string, name: string) => { const { soundsParticipantKnocking } = getState()['features/base/settings']; batch(() => { dispatch( participantIsKnockingOrUpdated({ id, name }) ); if (soundsParticipantKnocking) { dispatch(playSound(KNOCKING_PARTICIPANT_SOUND_ID)); } const isParticipantsPaneVisible = getParticipantsPaneOpen(getState()); if (navigator.product === 'ReactNative' || isParticipantsPaneVisible) { return; } _handleLobbyNotification({ dispatch, getState }); let notificationTitle; let customActionNameKey; let customActionHandler; let descriptionKey; let icon; const knockingParticipants = getKnockingParticipants(getState()); const firstParticipant = knockingParticipants[0]; const showChat = showLobbyChatButton(firstParticipant)(getState()); if (knockingParticipants.length > 1) { descriptionKey = 'notify.participantsWantToJoin'; notificationTitle = i18n.t('notify.waitingParticipants', { waitingParticipants: knockingParticipants.length }); icon = NOTIFICATION_ICON.PARTICIPANTS; customActionNameKey = [ 'notify.viewLobby' ]; customActionHandler = [ () => batch(() => { dispatch(hideNotification(LOBBY_NOTIFICATION_ID)); dispatch(openParticipantsPane()); }) ]; } else { descriptionKey = 'notify.participantWantsToJoin'; notificationTitle = firstParticipant.name; icon = NOTIFICATION_ICON.PARTICIPANT; customActionNameKey = [ 'lobby.admit', 'lobby.reject' ]; customActionHandler = [ () => batch(() => { dispatch(hideNotification(LOBBY_NOTIFICATION_ID)); dispatch(approveKnockingParticipant(firstParticipant.id)); }), () => batch(() => { dispatch(hideNotification(LOBBY_NOTIFICATION_ID)); dispatch(rejectKnockingParticipant(firstParticipant.id)); }) ]; if (showChat) { customActionNameKey.splice(1, 0, 'lobby.chat'); customActionHandler.splice(1, 0, () => batch(() => { dispatch(hideNotification(LOBBY_NOTIFICATION_ID)); dispatch(handleLobbyChatInitialized(firstParticipant.id)); })); } } dispatch(showNotification({ title: notificationTitle, descriptionKey, uid: LOBBY_NOTIFICATION_ID, customActionNameKey, customActionHandler, icon }, NOTIFICATION_TIMEOUT_TYPE.STICKY)); if (typeof APP !== 'undefined') { APP.API.notifyKnockingParticipant({ id, name }); } }); }); conference.on(JitsiConferenceEvents.LOBBY_USER_UPDATED, (id: string, participant: IKnockingParticipant) => { dispatch( participantIsKnockingOrUpdated({ ...participant, id }) ); }); conference.on(JitsiConferenceEvents.LOBBY_USER_LEFT, (id: string) => { batch(() => { dispatch(knockingParticipantLeft(id)); dispatch(removeLobbyChatParticipant()); dispatch(updateLobbyParticipantOnLeave(id)); }); }); conference.on(JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, (origin: any, sender: any) => _maybeSendLobbyNotification(origin, sender, { dispatch, getState }) ); } } ); /** * Function to handle the lobby notification. * * @param {Object} store - The Redux store. * @returns {void} */ function _handleLobbyNotification(store: IStore) { const { dispatch, getState } = store; const knockingParticipants = getKnockingParticipants(getState()); if (knockingParticipants.length === 0) { dispatch(hideNotification(LOBBY_NOTIFICATION_ID)); return; } let notificationTitle; let customActionNameKey; let customActionHandler; let descriptionKey; let icon; if (knockingParticipants.length === 1) { const firstParticipant = knockingParticipants[0]; descriptionKey = 'notify.participantWantsToJoin'; notificationTitle = firstParticipant.name; icon = NOTIFICATION_ICON.PARTICIPANT; customActionNameKey = [ 'lobby.admit', 'lobby.reject' ]; customActionHandler = [ () => batch(() => { dispatch(hideNotification(LOBBY_NOTIFICATION_ID)); dispatch(approveKnockingParticipant(firstParticipant.id)); }), () => batch(() => { dispatch(hideNotification(LOBBY_NOTIFICATION_ID)); dispatch(rejectKnockingParticipant(firstParticipant.id)); }) ]; } else { descriptionKey = 'notify.participantsWantToJoin'; notificationTitle = i18n.t('notify.waitingParticipants', { waitingParticipants: knockingParticipants.length }); icon = NOTIFICATION_ICON.PARTICIPANTS; customActionNameKey = [ 'notify.viewLobby' ]; customActionHandler = [ () => batch(() => { dispatch(hideNotification(LOBBY_NOTIFICATION_ID)); dispatch(openParticipantsPane()); }) ]; } dispatch(showNotification({ title: notificationTitle, descriptionKey, uid: LOBBY_NOTIFICATION_ID, customActionNameKey, customActionHandler, icon }, NOTIFICATION_TIMEOUT_TYPE.STICKY)); } /** * Function to handle the conference failed event and navigate the user to the lobby screen * based on the failure reason. * * @param {Object} store - The Redux store. * @param {Function} next - The Redux next function. * @param {Object} action - The Redux action. * @returns {Object} */ function _conferenceFailed({ dispatch, getState }: IStore, next: Function, action: AnyAction) { const { error } = action; const state = getState(); const { membersOnly } = state['features/base/conference']; const nonFirstFailure = Boolean(membersOnly); if (error.name === JitsiConferenceErrors.MEMBERS_ONLY_ERROR) { if (typeof error.recoverable === 'undefined') { error.recoverable = true; } const result = next(action); dispatch(openLobbyScreen()); if (shouldAutoKnock(state)) { dispatch(startKnocking()); } // In case of wrong password we need to be in the right state if in the meantime someone allows us to join if (nonFirstFailure) { // @ts-ignore dispatch(conferenceWillJoin(membersOnly)); } dispatch(setPasswordJoinFailed(nonFirstFailure)); return result; } dispatch(hideLobbyScreen()); if (error.name === JitsiConferenceErrors.CONFERENCE_ACCESS_DENIED) { dispatch( showNotification({ appearance: NOTIFICATION_TYPE.ERROR, hideErrorSupportLink: true, titleKey: 'lobby.joinRejectedTitle', descriptionKey: 'lobby.joinRejectedMessage' }, NOTIFICATION_TIMEOUT_TYPE.LONG) ); } return next(action); } /** * Handles cleanup of lobby state when a conference is joined. * * @param {Object} store - The Redux store. * @param {Function} next - The Redux next function. * @param {Object} action - The Redux action. * @returns {Object} */ function _conferenceJoined({ dispatch }: IStore, next: Function, action: AnyAction) { dispatch(hideLobbyScreen()); return next(action); } /** * Finds the loadable avatar URL and updates the participant accordingly. * * @param {Object} store - The Redux store. * @param {Object} participant - The knocking participant. * @returns {void} */ function _findLoadableAvatarForKnockingParticipant(store: IStore, { id }: { id: string; }) { const { dispatch, getState } = store; const updatedParticipant = getState()['features/lobby'].knockingParticipants.find(p => p.id === id); const { disableThirdPartyRequests } = getState()['features/base/config']; if (!disableThirdPartyRequests && updatedParticipant && !updatedParticipant.loadableAvatarUrl) { getFirstLoadableAvatarUrl(updatedParticipant, store).then((result: { isUsingCORS: boolean; src: string; }) => { if (result) { const { isUsingCORS, src } = result; dispatch( participantIsKnockingOrUpdated({ loadableAvatarUrl: src, id, isUsingCORS }) ); } }); } } /** * Check the endpoint message that arrived through the conference and * sends a lobby notification, if the message belongs to the feature. * * @param {Object} origin - The origin (initiator) of the message. * @param {Object} message - The actual message. * @param {Object} store - The Redux store. * @returns {void} */ function _maybeSendLobbyNotification(origin: any, message: any, { dispatch, getState }: IStore) { if (!origin?._id || message?.type !== 'lobby-notify') { return; } const notificationProps: INotificationProps = { descriptionArguments: { originParticipantName: getParticipantDisplayName(getState, origin._id), targetParticipantName: message.name }, titleKey: 'lobby.notificationTitle' }; switch (message.event) { case 'LOBBY-ENABLED': notificationProps.descriptionKey = `lobby.notificationLobby${message.value ? 'En' : 'Dis'}abled`; break; case 'LOBBY-ACCESS-GRANTED': notificationProps.descriptionKey = 'lobby.notificationLobbyAccessGranted'; break; case 'LOBBY-ACCESS-DENIED': notificationProps.descriptionKey = 'lobby.notificationLobbyAccessDenied'; break; } dispatch( showNotification( notificationProps, isTestModeEnabled(getState()) ? NOTIFICATION_TIMEOUT_TYPE.STICKY : NOTIFICATION_TIMEOUT_TYPE.MEDIUM ) ); }