snapshot of https://github.com/jitsi/jitsi-meet.git @ fd1749dd28f4138aa80234fc7f311b19aaffa0e7 (2026-09-15)
This commit is contained in:
350
react/features/conference/middleware.any.ts
Normal file
350
react/features/conference/middleware.any.ts
Normal file
@@ -0,0 +1,350 @@
|
||||
import i18n from 'i18next';
|
||||
import { batch } from 'react-redux';
|
||||
|
||||
// @ts-expect-error
|
||||
import { API_ID } from '../../../modules/API/constants';
|
||||
import { appNavigate } from '../app/actions';
|
||||
import { redirectToStaticPage } from '../app/actions.any';
|
||||
import { IReduxState, IStore } from '../app/types';
|
||||
import {
|
||||
CONFERENCE_FAILED,
|
||||
CONFERENCE_JOINED,
|
||||
CONFERENCE_LEFT,
|
||||
ENDPOINT_MESSAGE_RECEIVED
|
||||
} from '../base/conference/actionTypes';
|
||||
import { getCurrentConference } from '../base/conference/functions';
|
||||
import { getDisableLowerHandByModerator } from '../base/config/functions.any';
|
||||
import { hangup } from '../base/connection/actions';
|
||||
import { getURLWithoutParamsNormalized } from '../base/connection/utils';
|
||||
import { hideDialog } from '../base/dialog/actions';
|
||||
import { isDialogOpen } from '../base/dialog/functions';
|
||||
import { getLocalizedDateFormatter } from '../base/i18n/dateUtil';
|
||||
import { translateToHTML } from '../base/i18n/functions';
|
||||
import i18next from '../base/i18n/i18next';
|
||||
import { browser } from '../base/lib-jitsi-meet';
|
||||
import { pinParticipant, raiseHand, raiseHandClear } from '../base/participants/actions';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
|
||||
import { SET_REDUCED_UI } from '../base/responsive-ui/actionTypes';
|
||||
import { LOWER_HAND_MESSAGE } from '../base/tracks/constants';
|
||||
import { BUTTON_TYPES } from '../base/ui/constants.any';
|
||||
import { isEmbedded } from '../base/util/embedUtils';
|
||||
import { isCalendarEnabled } from '../calendar-sync/functions';
|
||||
import FeedbackDialog from '../feedback/components/FeedbackDialog';
|
||||
import { setFilmstripEnabled } from '../filmstrip/actions.any';
|
||||
import { isVpaasMeeting } from '../jaas/functions';
|
||||
import { hideNotification, showNotification, showWarningNotification } from '../notifications/actions';
|
||||
import {
|
||||
CALENDAR_NOTIFICATION_ID,
|
||||
NOTIFICATION_ICON,
|
||||
NOTIFICATION_TIMEOUT_TYPE
|
||||
} from '../notifications/constants';
|
||||
import { showStartRecordingNotification } from '../recording/actions';
|
||||
import { showSalesforceNotification } from '../salesforce/actions';
|
||||
import { setToolboxEnabled } from '../toolbox/actions.any';
|
||||
|
||||
import { DISMISS_CALENDAR_NOTIFICATION } from './actionTypes';
|
||||
import { dismissCalendarNotification } from './actions';
|
||||
import { IFRAME_DISABLED_TIMEOUT_MINUTES, IFRAME_EMBED_ALLOWED_LOCATIONS } from './constants';
|
||||
|
||||
|
||||
let intervalID: any;
|
||||
|
||||
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
const result = next(action);
|
||||
|
||||
switch (action.type) {
|
||||
case CONFERENCE_JOINED: {
|
||||
_conferenceJoined(store);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case SET_REDUCED_UI: {
|
||||
_setReducedUI(store);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case DISMISS_CALENDAR_NOTIFICATION:
|
||||
case CONFERENCE_LEFT:
|
||||
case CONFERENCE_FAILED: {
|
||||
clearInterval(intervalID);
|
||||
intervalID = null;
|
||||
|
||||
break;
|
||||
}
|
||||
case ENDPOINT_MESSAGE_RECEIVED: {
|
||||
const { participant, data } = action;
|
||||
const { dispatch, getState } = store;
|
||||
|
||||
if (data.name === LOWER_HAND_MESSAGE
|
||||
&& participant.isModerator()
|
||||
&& !getDisableLowerHandByModerator(getState())) {
|
||||
dispatch(raiseHand(false));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
/**
|
||||
* Set up state change listener to perform maintenance tasks when the conference
|
||||
* is left or failed, close all dialogs and unpin any pinned participants.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
state => getCurrentConference(state),
|
||||
(conference, { dispatch, getState }, prevConference) => {
|
||||
const { authRequired, membersOnly, passwordRequired }
|
||||
= getState()['features/base/conference'];
|
||||
|
||||
if (conference !== prevConference) {
|
||||
// Unpin participant, in order to avoid the local participant
|
||||
// remaining pinned, since it's not destroyed across runs.
|
||||
dispatch(pinParticipant(null));
|
||||
|
||||
// Clear raised hands.
|
||||
dispatch(raiseHandClear());
|
||||
|
||||
// XXX I wonder if there is a better way to do this. At this stage
|
||||
// we do know what dialogs we want to keep but the list of those
|
||||
// we want to hide is a lot longer. Thus we take a bit of a shortcut
|
||||
// and explicitly check.
|
||||
if (typeof authRequired === 'undefined'
|
||||
&& typeof passwordRequired === 'undefined'
|
||||
&& typeof membersOnly === 'undefined'
|
||||
&& !isDialogOpen(getState(), FeedbackDialog)) {
|
||||
// Conference changed, left or failed... and there is no
|
||||
// pending authentication, nor feedback request, so close any
|
||||
// dialog we might have open.
|
||||
dispatch(hideDialog());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Configures the UI. In reduced UI mode some components will
|
||||
* be hidden if there is no space to render them.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified {@code action}
|
||||
* is being dispatched.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _setReducedUI({ dispatch, getState }: IStore) {
|
||||
const { reducedUI } = getState()['features/base/responsive-ui'];
|
||||
|
||||
dispatch(setToolboxEnabled(!reducedUI));
|
||||
dispatch(setFilmstripEnabled(!reducedUI));
|
||||
}
|
||||
|
||||
/**
|
||||
* Does extra sync up on properties that may need to be updated after the
|
||||
* conference was joined.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified {@code action}
|
||||
* is being dispatched.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _conferenceJoined({ dispatch, getState }: IStore) {
|
||||
_setReducedUI({
|
||||
dispatch,
|
||||
getState
|
||||
});
|
||||
|
||||
if (!intervalID) {
|
||||
intervalID = setInterval(() =>
|
||||
_maybeDisplayCalendarNotification({
|
||||
dispatch,
|
||||
getState
|
||||
}), 10 * 1000);
|
||||
}
|
||||
|
||||
dispatch(showSalesforceNotification());
|
||||
dispatch(showStartRecordingNotification());
|
||||
|
||||
_checkIframe(getState(), dispatch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional checks for embedding in iframe.
|
||||
*
|
||||
* @param {IReduxState} state - The current state of the app.
|
||||
* @param {Function} dispatch - The Redux dispatch function.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _checkIframe(state: IReduxState, dispatch: IStore['dispatch']) {
|
||||
let allowIframe = false;
|
||||
|
||||
if (document.referrer === '' && browser.isElectron()) {
|
||||
// no iframe
|
||||
allowIframe = true;
|
||||
} else {
|
||||
try {
|
||||
allowIframe = IFRAME_EMBED_ALLOWED_LOCATIONS.includes(new URL(document.referrer).hostname);
|
||||
} catch (e) {
|
||||
// wrong URL in referrer
|
||||
}
|
||||
}
|
||||
|
||||
if (isEmbedded() && state['features/base/config'].disableIframeAPI && !isVpaasMeeting(state) && !allowIframe) {
|
||||
// show sticky notification and redirect in 5 minutes
|
||||
const { locationURL } = state['features/base/connection'];
|
||||
let translationKey = 'notify.disabledIframe';
|
||||
const hostname = locationURL?.hostname ?? '';
|
||||
let domain = '';
|
||||
|
||||
const mapping: Record<string, string> = {
|
||||
'8x8.vc': 'https://jaas.8x8.vc',
|
||||
'meet.jit.si': 'https://jitsi.org/jaas'
|
||||
};
|
||||
|
||||
const jaasDomain = mapping[hostname];
|
||||
|
||||
if (jaasDomain) {
|
||||
translationKey = `notify.disabledIframeSecondary${browser.isReactNative() ? 'Native' : 'Web'}`;
|
||||
domain = hostname;
|
||||
}
|
||||
|
||||
if (browser.isReactNative()) {
|
||||
dispatch(showWarningNotification({
|
||||
description: i18next.t(translationKey,
|
||||
{
|
||||
domain,
|
||||
timeout: IFRAME_DISABLED_TIMEOUT_MINUTES
|
||||
}
|
||||
)
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.STICKY));
|
||||
|
||||
setTimeout(() => {
|
||||
dispatch(hangup());
|
||||
}, IFRAME_DISABLED_TIMEOUT_MINUTES * 60 * 1000);
|
||||
} else {
|
||||
dispatch(showWarningNotification({
|
||||
description: translateToHTML(
|
||||
i18next.t.bind(i18next),
|
||||
translationKey,
|
||||
{
|
||||
domain,
|
||||
jaasDomain,
|
||||
timeout: IFRAME_DISABLED_TIMEOUT_MINUTES
|
||||
}
|
||||
)
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.STICKY));
|
||||
|
||||
setTimeout(() => {
|
||||
// redirect to the promotional page
|
||||
dispatch(redirectToStaticPage('static/close3.html', `#jitsi_meet_external_api_id=${API_ID}`));
|
||||
}, IFRAME_DISABLED_TIMEOUT_MINUTES * 60 * 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Periodically checks if there is an event in the calendar for which we
|
||||
* need to show a notification.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified {@code action}
|
||||
* is being dispatched.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _maybeDisplayCalendarNotification({ dispatch, getState }: IStore) {
|
||||
const state = getState();
|
||||
|
||||
const calendarEnabled = isCalendarEnabled(state);
|
||||
const { events: eventList } = state['features/calendar-sync'];
|
||||
const { locationURL } = state['features/base/connection'];
|
||||
const { reducedUI } = state['features/base/responsive-ui'];
|
||||
|
||||
const currentConferenceURL
|
||||
= locationURL ? getURLWithoutParamsNormalized(locationURL) : '';
|
||||
const ALERT_MILLISECONDS = 5 * 60 * 1000;
|
||||
const now = Date.now();
|
||||
|
||||
let eventToShow;
|
||||
|
||||
if (!calendarEnabled && reducedUI) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventList?.length) {
|
||||
|
||||
for (const event of eventList) {
|
||||
const eventURL
|
||||
= event?.url && getURLWithoutParamsNormalized(new URL(event.url));
|
||||
|
||||
if (eventURL && eventURL !== currentConferenceURL) {
|
||||
// @ts-ignore
|
||||
if ((!eventToShow && event.startDate > now && event.startDate < now + ALERT_MILLISECONDS)
|
||||
|
||||
// @ts-ignore
|
||||
|| (event.startDate < now && event.endDate > now)) {
|
||||
eventToShow = event;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_calendarNotification(
|
||||
{
|
||||
dispatch,
|
||||
getState
|
||||
}, eventToShow
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calendar notification.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified {@code action}
|
||||
* is being dispatched.
|
||||
* @param {eventToShow} eventToShow - Next or ongoing event.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _calendarNotification({ dispatch, getState }: IStore, eventToShow: any) {
|
||||
const state = getState();
|
||||
|
||||
const { locationURL } = state['features/base/connection'];
|
||||
|
||||
const currentConferenceURL
|
||||
= locationURL ? getURLWithoutParamsNormalized(locationURL) : '';
|
||||
const now = Date.now();
|
||||
|
||||
if (!eventToShow) {
|
||||
return;
|
||||
}
|
||||
|
||||
const customActionNameKey = [ 'notify.joinMeeting', 'notify.dontRemindMe' ];
|
||||
const customActionType = [ BUTTON_TYPES.PRIMARY, BUTTON_TYPES.DESTRUCTIVE ];
|
||||
const customActionHandler = [ () => batch(() => {
|
||||
dispatch(hideNotification(CALENDAR_NOTIFICATION_ID));
|
||||
if (eventToShow?.url && (eventToShow.url !== currentConferenceURL)) {
|
||||
dispatch(appNavigate(eventToShow.url));
|
||||
}
|
||||
}), () => dispatch(dismissCalendarNotification()) ];
|
||||
const description
|
||||
= getLocalizedDateFormatter(eventToShow.startDate).fromNow();
|
||||
const icon = NOTIFICATION_ICON.WARNING;
|
||||
const title = (eventToShow.startDate < now) && (eventToShow.endDate > now)
|
||||
? `${i18n.t('calendarSync.ongoingMeeting')}: \n${eventToShow.title}`
|
||||
: `${i18n.t('calendarSync.nextMeeting')}: \n${eventToShow.title}`;
|
||||
const uid = CALENDAR_NOTIFICATION_ID;
|
||||
|
||||
dispatch(showNotification({
|
||||
customActionHandler,
|
||||
customActionNameKey,
|
||||
customActionType,
|
||||
description,
|
||||
icon,
|
||||
maxLines: 1,
|
||||
title,
|
||||
uid
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.STICKY));
|
||||
}
|
||||
Reference in New Issue
Block a user