snapshot of https://github.com/jitsi/jitsi-meet.git @ fd1749dd28f4138aa80234fc7f311b19aaffa0e7 (2026-09-15)
This commit is contained in:
447
react/features/lobby/middleware.ts
Normal file
447
react/features/lobby/middleware.ts
Normal file
@@ -0,0 +1,447 @@
|
||||
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 { PREJOIN_JOINING_IN_PROGRESS } from '../prejoin/actionTypes';
|
||||
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 logger from './logger';
|
||||
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;
|
||||
}
|
||||
case PREJOIN_JOINING_IN_PROGRESS: {
|
||||
if (action.value) {
|
||||
// let's hide the notification (the case with denied access and retrying) when prejoin is enabled
|
||||
store.dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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 { lobbyError, membersOnly } = state['features/base/conference'];
|
||||
const nonFirstFailure = Boolean(membersOnly);
|
||||
|
||||
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)
|
||||
|| (lobbyError && !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;
|
||||
}
|
||||
|
||||
// if both are available pre-join is with priority (the case when pre-join is enabled)
|
||||
// when pre-join is disabled, and we are in lobby with error, we want to end up in lobby UI
|
||||
// instead of hiding it and showing conference UI. Still in lobby the user can retry
|
||||
// after we show the error notification
|
||||
if (isPrejoinPageVisible(state)) {
|
||||
dispatch(hideLobbyScreen());
|
||||
}
|
||||
|
||||
// we want to finish this action before showing the notification
|
||||
// as the conference will be cleared which will clear all notifications, including this one
|
||||
const result = next(action);
|
||||
|
||||
if (error.name === JitsiConferenceErrors.CONFERENCE_ACCESS_DENIED) {
|
||||
dispatch(hideLobbyScreen());
|
||||
dispatch(
|
||||
showNotification({
|
||||
appearance: NOTIFICATION_TYPE.ERROR,
|
||||
hideErrorSupportLink: true,
|
||||
titleKey: 'lobby.joinRejectedTitle',
|
||||
uid: LOBBY_NOTIFICATION_ID,
|
||||
descriptionKey: 'lobby.joinRejectedMessage'
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.STICKY)
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
// Prosody sends this notification with the moderator which performed the action as the sender, and only a
|
||||
// moderator can enable or disable the lobby, or admit or reject a participant. Check the role, so that the
|
||||
// client shows a notification only for an action which the sender is able to perform.
|
||||
if (!origin.isModerator?.()) {
|
||||
logger.warn(`Ignoring a lobby notification from a non-moderator: ${origin._id}`);
|
||||
|
||||
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
|
||||
)
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user