123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417 |
- 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,
- ENDPOINT_MESSAGE_RECEIVED
- } 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 { BUTTON_TYPES } from '../base/ui/constants.any';
- import { openChat } from '../chat/actions';
- import {
- handleLobbyChatInitialized,
- removeLobbyChatParticipant
- } from '../chat/actions.any';
- import { arePollsDisabled } from '../conference/functions.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 {
- isPrejoinEnabledInConfig,
- isPrejoinPageVisible,
- 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 ENDPOINT_MESSAGE_RECEIVED: {
- const { participant, data } = action;
-
- _maybeSendLobbyNotification(participant, data, store);
-
- break;
- }
- 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 (typeof APP !== 'undefined') {
- APP.API.notifyKnockingParticipant({
- id,
- name
- });
- }
-
- if (isParticipantsPaneVisible || navigator.product === 'ReactNative') {
- return;
- }
-
- _handleLobbyNotification({
- dispatch,
- getState
- });
- });
- });
-
- 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));
- });
- });
- }
- }
- );
-
- /**
- * 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 customActionType;
- let descriptionKey;
- let icon;
-
- if (knockingParticipants.length === 1) {
- const firstParticipant = knockingParticipants[0];
- const showChat = showLobbyChatButton(firstParticipant)(getState());
-
- descriptionKey = 'notify.participantWantsToJoin';
- notificationTitle = firstParticipant.name;
- icon = NOTIFICATION_ICON.PARTICIPANT;
- customActionNameKey = [ 'participantsPane.actions.admit', 'participantsPane.actions.reject' ];
- customActionType = [ BUTTON_TYPES.PRIMARY, BUTTON_TYPES.DESTRUCTIVE ];
- customActionHandler = [ () => batch(() => {
- dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
- dispatch(approveKnockingParticipant(firstParticipant.id));
- }),
- () => batch(() => {
- dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
- dispatch(rejectKnockingParticipant(firstParticipant.id));
- }) ];
-
- // This checks if lobby chat button is available
- // and, if so, it adds it to the customActionNameKey array
- if (showChat) {
- customActionNameKey.splice(1, 0, 'lobby.chat');
- customActionType.splice(1, 0, BUTTON_TYPES.SECONDARY);
- customActionHandler.splice(1, 0, () => batch(() => {
- dispatch(handleLobbyChatInitialized(firstParticipant.id));
- dispatch(openChat({}, arePollsDisabled(getState())));
- }));
- }
- } else {
- descriptionKey = 'notify.participantsWantToJoin';
- notificationTitle = i18n.t('notify.waitingParticipants', {
- waitingParticipants: knockingParticipants.length
- });
- icon = NOTIFICATION_ICON.PARTICIPANTS;
- customActionNameKey = [ 'notify.viewLobby' ];
- customActionType = [ BUTTON_TYPES.PRIMARY ];
- customActionHandler = [ () => batch(() => {
- dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
- dispatch(openParticipantsPane());
- }) ];
- }
-
- dispatch(showNotification({
- title: notificationTitle,
- descriptionKey,
- uid: LOBBY_NOTIFICATION_ID,
- customActionNameKey,
- customActionType,
- 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);
- const { isDisplayNameRequiredError } = state['features/lobby'];
-
- if (error.name === JitsiConferenceErrors.MEMBERS_ONLY_ERROR) {
- if (typeof error.recoverable === 'undefined') {
- error.recoverable = true;
- }
-
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- const [ _lobbyJid, lobbyWaitingForHost ] = error.params;
-
- const result = next(action);
-
- dispatch(openLobbyScreen());
-
- // if there was an error about display name and pre-join is not enabled
- if (shouldAutoKnock(state)
- || (isDisplayNameRequiredError && !isPrejoinEnabledInConfig(state))
- || lobbyWaitingForHost) {
- 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) {
- dispatch(conferenceWillJoin(membersOnly));
- }
-
- dispatch(setPasswordJoinFailed(nonFirstFailure));
-
- return result;
- } else if (error.name === JitsiConferenceErrors.DISPLAY_NAME_REQUIRED) {
- const [ isLobbyEnabled ] = error.params;
-
- const result = next(action);
-
- // if the error is due to required display name because lobby is enabled for the room
- // if not showing the prejoin page then show lobby UI
- if (isLobbyEnabled && !isPrejoinPageVisible(state)) {
- dispatch(openLobbyScreen());
- }
-
- 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
- )
- );
- }
|