mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2025-12-30 03:12:29 +00:00
On web we detect if we run on an iframe, and on mobile we detect if the app is one of ours.
337 lines
11 KiB
TypeScript
337 lines
11 KiB
TypeScript
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 { 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
|
|
}
|
|
}
|
|
|
|
// TODO: enable for mobile too?
|
|
if (isEmbedded() && state['features/base/config'].disableIframeAPI && !browser.isElectron()
|
|
&& !browser.isReactNative() && !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';
|
|
domain = hostname;
|
|
}
|
|
|
|
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));
|
|
}
|