mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2025-12-30 11:22:31 +00:00
* fix(visitors): Fixes going live when first moderator joins. * squash(jwt): Drop unused field. * squash(jwt): Fixes loading token_util for visitors component. * squash(jwt): Validate nbf if it exists as it is optional. * squash(visitors): Keep prefer visitor state for not live meetings. * squash(visitors): Automatically go live only when there is a moderator in the meeting. * squash(visitors): Automatically go live only when there is an occupant in the meeting. * squash(visitors): Drops a debug log. * squash(visitors): Makes sure we first disconnect before attempting a reconnect. If the reconnect happens too quickly, before being disconnected and the conference is still not live we will detect that we are still connected and will skip connecting to visitors service, and in the next moment we will disconnect. * squash(visitors): Slow down successful reconnects. If a meeting was just live but was destroyed jicofo will return it is not live, but service will return that it just got live. Slows down reconnects and at some point the service will return that the meeting is not live. The drawback is that it will take some time to connect when the meeting is created and back live. * squash(visitors): Randomize the delay up to the available value.
386 lines
14 KiB
TypeScript
386 lines
14 KiB
TypeScript
import i18n from 'i18next';
|
|
import { batch } from 'react-redux';
|
|
|
|
import { IStore } from '../app/types';
|
|
import { IStateful } from '../base/app/types';
|
|
import {
|
|
CONFERENCE_JOINED,
|
|
CONFERENCE_JOIN_IN_PROGRESS,
|
|
ENDPOINT_MESSAGE_RECEIVED,
|
|
UPDATE_CONFERENCE_METADATA
|
|
} from '../base/conference/actionTypes';
|
|
import { SET_CONFIG } from '../base/config/actionTypes';
|
|
import { CONNECTION_FAILED } from '../base/connection/actionTypes';
|
|
import { connect, setPreferVisitor } from '../base/connection/actions';
|
|
import { disconnect } from '../base/connection/actions.any';
|
|
import { JitsiConferenceEvents, JitsiConnectionErrors } from '../base/lib-jitsi-meet';
|
|
import { PARTICIPANT_UPDATED } from '../base/participants/actionTypes';
|
|
import { raiseHand } from '../base/participants/actions';
|
|
import {
|
|
getLocalParticipant,
|
|
getParticipantById,
|
|
isLocalParticipantModerator
|
|
} from '../base/participants/functions';
|
|
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
|
import { toState } from '../base/redux/functions';
|
|
import { BUTTON_TYPES } from '../base/ui/constants.any';
|
|
import { hideNotification, showNotification } from '../notifications/actions';
|
|
import {
|
|
NOTIFICATION_ICON,
|
|
NOTIFICATION_TIMEOUT_TYPE,
|
|
VISITORS_NOT_LIVE_NOTIFICATION_ID,
|
|
VISITORS_PROMOTION_NOTIFICATION_ID
|
|
} from '../notifications/constants';
|
|
import { INotificationProps } from '../notifications/types';
|
|
import { open as openParticipantsPane } from '../participants-pane/actions';
|
|
import { joinConference } from '../prejoin/actions';
|
|
|
|
import { UPDATE_VISITORS_IN_QUEUE_COUNT } from './actionTypes';
|
|
import {
|
|
approveRequest,
|
|
clearPromotionRequest,
|
|
denyRequest,
|
|
goLive,
|
|
promotionRequestReceived,
|
|
setInVisitorsQueue,
|
|
setVisitorDemoteActor,
|
|
setVisitorsSupported,
|
|
updateVisitorsCount,
|
|
updateVisitorsInQueueCount
|
|
} from './actions';
|
|
import { getPromotionRequests, getVisitorsCount, getVisitorsInQueueCount } from './functions';
|
|
import logger from './logger';
|
|
import { WebsocketClient } from './websocket-client';
|
|
|
|
MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
|
switch (action.type) {
|
|
case CONFERENCE_JOIN_IN_PROGRESS: {
|
|
const { conference } = action;
|
|
|
|
conference.on(JitsiConferenceEvents.PROPERTIES_CHANGED, (properties: { 'visitor-count': number; }) => {
|
|
const visitorCount = Number(properties?.['visitor-count']);
|
|
|
|
if (!isNaN(visitorCount) && getVisitorsCount(getState) !== visitorCount) {
|
|
dispatch(updateVisitorsCount(visitorCount));
|
|
}
|
|
});
|
|
break;
|
|
}
|
|
case CONFERENCE_JOINED: {
|
|
const { conference } = action;
|
|
|
|
if (getState()['features/visitors'].iAmVisitor) {
|
|
const { demoteActorDisplayName } = getState()['features/visitors'];
|
|
|
|
dispatch(setVisitorDemoteActor(undefined));
|
|
|
|
const notificationParams: INotificationProps = {
|
|
titleKey: 'visitors.notification.title',
|
|
descriptionKey: 'visitors.notification.description'
|
|
};
|
|
|
|
if (demoteActorDisplayName) {
|
|
notificationParams.descriptionKey = 'visitors.notification.demoteDescription';
|
|
notificationParams.descriptionArguments = {
|
|
actor: demoteActorDisplayName
|
|
};
|
|
}
|
|
|
|
// check for demote actor and update notification
|
|
dispatch(showNotification(notificationParams, NOTIFICATION_TIMEOUT_TYPE.STICKY));
|
|
} else {
|
|
dispatch(setVisitorsSupported(conference.isVisitorsSupported()));
|
|
conference.on(JitsiConferenceEvents.VISITORS_SUPPORTED_CHANGED, (value: boolean) => {
|
|
dispatch(setVisitorsSupported(value));
|
|
});
|
|
}
|
|
|
|
conference.on(JitsiConferenceEvents.VISITORS_MESSAGE, (
|
|
msg: { action: string; actor: string; from: string; id: string; nick: string; on: boolean; }) => {
|
|
|
|
if (msg.action === 'demote-request') {
|
|
// we need it before the disconnect
|
|
const participantById = getParticipantById(getState, msg.actor);
|
|
const localParticipant = getLocalParticipant(getState);
|
|
|
|
if (localParticipant && localParticipant.id === msg.id) {
|
|
// handle demote
|
|
dispatch(disconnect(true))
|
|
.then(() => dispatch(setPreferVisitor(true)))
|
|
.then(() => {
|
|
// we need to set the name, so we can use it later in the notification
|
|
if (participantById) {
|
|
dispatch(setVisitorDemoteActor(participantById.name));
|
|
}
|
|
|
|
return dispatch(connect());
|
|
});
|
|
}
|
|
} else if (msg.action === 'promotion-request') {
|
|
const request = {
|
|
from: msg.from,
|
|
nick: msg.nick
|
|
};
|
|
|
|
if (msg.on) {
|
|
dispatch(promotionRequestReceived(request));
|
|
} else {
|
|
dispatch(clearPromotionRequest(request));
|
|
}
|
|
_handlePromotionNotification({
|
|
dispatch,
|
|
getState
|
|
});
|
|
} else {
|
|
logger.error('Unknown action:', msg.action);
|
|
}
|
|
});
|
|
|
|
conference.on(JitsiConferenceEvents.VISITORS_REJECTION, () => {
|
|
dispatch(raiseHand(false));
|
|
});
|
|
|
|
break;
|
|
}
|
|
case ENDPOINT_MESSAGE_RECEIVED: {
|
|
const { data } = action;
|
|
|
|
if (data?.action === 'promotion-response' && data.approved) {
|
|
const request = getPromotionRequests(getState())
|
|
.find((r: any) => r.from === data.id);
|
|
|
|
request && dispatch(clearPromotionRequest(request));
|
|
}
|
|
break;
|
|
}
|
|
case CONNECTION_FAILED: {
|
|
const { error } = action;
|
|
|
|
if (error?.name !== JitsiConnectionErrors.NOT_LIVE_ERROR) {
|
|
break;
|
|
}
|
|
|
|
const { hosts, visitors: visitorsConfig } = getState()['features/base/config'];
|
|
const { locationURL, preferVisitor } = getState()['features/base/connection'];
|
|
|
|
if (!visitorsConfig?.queueService || !locationURL || !preferVisitor) {
|
|
break;
|
|
}
|
|
|
|
// let's subscribe for visitor waiting queue
|
|
const { room } = getState()['features/base/conference'];
|
|
const conferenceJid = `${room}@${hosts?.muc}`;
|
|
|
|
WebsocketClient.getInstance()
|
|
.connect(`wss://${visitorsConfig?.queueService}/visitor/websocket`,
|
|
`/secured/conference/visitor/topic.${conferenceJid}`,
|
|
msg => {
|
|
if ('status' in msg && msg.status === 'live') {
|
|
logger.info('The conference is now live!');
|
|
|
|
WebsocketClient.getInstance().disconnect()
|
|
.then(() => {
|
|
let delay = 0;
|
|
|
|
// now let's connect to meeting
|
|
if ('randomDelayMs' in msg) {
|
|
delay = msg.randomDelayMs;
|
|
}
|
|
|
|
if (WebsocketClient.getInstance().connectCount > 1) {
|
|
// if we keep connecting/disconnecting, let's slow it down
|
|
delay = 30 * 1000;
|
|
}
|
|
|
|
setTimeout(() => {
|
|
dispatch(joinConference());
|
|
dispatch(setInVisitorsQueue(false));
|
|
}, Math.random() * delay);
|
|
});
|
|
}
|
|
},
|
|
|
|
getState()['features/base/jwt'].jwt,
|
|
() => {
|
|
dispatch(setInVisitorsQueue(true));
|
|
});
|
|
|
|
break;
|
|
}
|
|
case PARTICIPANT_UPDATED: {
|
|
const { visitors: visitorsConfig } = toState(getState)['features/base/config'];
|
|
|
|
if (visitorsConfig?.queueService && isLocalParticipantModerator(getState)) {
|
|
const { metadata } = getState()['features/base/conference'];
|
|
|
|
if (metadata?.visitors?.live === false && !WebsocketClient.getInstance().isActive()) {
|
|
// when go live is available and false, we should subscribe
|
|
// to the service if available to listen for waiting visitors
|
|
_subscribeQueueStats(getState(), dispatch);
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
case SET_CONFIG: {
|
|
const result = next(action);
|
|
const { preferVisitor } = action.config;
|
|
|
|
if (preferVisitor !== undefined) {
|
|
setPreferVisitor(preferVisitor);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
case UPDATE_CONFERENCE_METADATA: {
|
|
const { metadata } = action;
|
|
const { visitors: visitorsConfig } = toState(getState)['features/base/config'];
|
|
|
|
if (!visitorsConfig?.queueService) {
|
|
break;
|
|
}
|
|
|
|
if (isLocalParticipantModerator(getState)) {
|
|
if (metadata?.visitors?.live === false) {
|
|
if (!WebsocketClient.getInstance().isActive()) {
|
|
// if metadata go live changes to goLive false and local is moderator
|
|
// we should subscribe to the service if available to listen for waiting visitors
|
|
_subscribeQueueStats(getState(), dispatch);
|
|
}
|
|
|
|
_showNotLiveNotification(dispatch, getVisitorsInQueueCount(getState));
|
|
} else if (metadata?.visitors?.live) {
|
|
dispatch(hideNotification(VISITORS_NOT_LIVE_NOTIFICATION_ID));
|
|
WebsocketClient.getInstance().disconnect();
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
case UPDATE_VISITORS_IN_QUEUE_COUNT: {
|
|
_showNotLiveNotification(dispatch, action.count);
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
return next(action);
|
|
});
|
|
|
|
/**
|
|
* Shows a notification that the meeting is not live.
|
|
*
|
|
* @param {Dispatch} dispatch - The Redux dispatch function.
|
|
* @param {number} count - The count of visitors waiting.
|
|
* @returns {void}
|
|
*/
|
|
function _showNotLiveNotification(dispatch: IStore['dispatch'], count: number): void {
|
|
// let's show notification
|
|
dispatch(showNotification({
|
|
titleKey: 'notify.waitingVisitorsTitle',
|
|
descriptionKey: 'notify.waitingVisitors',
|
|
descriptionArguments: {
|
|
waitingVisitors: count
|
|
},
|
|
disableClosing: true,
|
|
uid: VISITORS_NOT_LIVE_NOTIFICATION_ID,
|
|
customActionNameKey: [ 'participantsPane.actions.goLive' ],
|
|
customActionType: [ BUTTON_TYPES.PRIMARY ],
|
|
customActionHandler: [ () => batch(() => {
|
|
dispatch(hideNotification(VISITORS_NOT_LIVE_NOTIFICATION_ID));
|
|
dispatch(goLive());
|
|
}) ],
|
|
icon: NOTIFICATION_ICON.PARTICIPANTS
|
|
}, NOTIFICATION_TIMEOUT_TYPE.STICKY));
|
|
}
|
|
|
|
/**
|
|
* Subscribe for moderator stats.
|
|
*
|
|
* @param {Function|Object} stateful - The redux store or {@code getState}
|
|
* function.
|
|
* @param {Dispatch} dispatch - The Redux dispatch function.
|
|
* @returns {void}
|
|
*/
|
|
function _subscribeQueueStats(stateful: IStateful, dispatch: IStore['dispatch']) {
|
|
const { hosts } = toState(stateful)['features/base/config'];
|
|
const { room } = toState(stateful)['features/base/conference'];
|
|
const conferenceJid = `${room}@${hosts?.muc}`;
|
|
|
|
const { visitors: visitorsConfig } = toState(stateful)['features/base/config'];
|
|
|
|
WebsocketClient.getInstance()
|
|
.connect(`wss://${visitorsConfig?.queueService}/visitor/websocket`,
|
|
`/secured/conference/state/topic.${conferenceJid}`,
|
|
msg => {
|
|
if ('visitorsWaiting' in msg) {
|
|
dispatch(updateVisitorsInQueueCount(msg.visitorsWaiting));
|
|
}
|
|
},
|
|
toState(stateful)['features/base/jwt'].jwt);
|
|
}
|
|
|
|
/**
|
|
* Function to handle the promotion notification.
|
|
*
|
|
* @param {Object} store - The Redux store.
|
|
* @returns {void}
|
|
*/
|
|
function _handlePromotionNotification(
|
|
{ dispatch, getState }: { dispatch: IStore['dispatch']; getState: IStore['getState']; }) {
|
|
const requests = getPromotionRequests(getState());
|
|
|
|
if (requests.length === 0) {
|
|
dispatch(hideNotification(VISITORS_PROMOTION_NOTIFICATION_ID));
|
|
|
|
return;
|
|
}
|
|
|
|
let notificationTitle;
|
|
let customActionNameKey;
|
|
let customActionHandler;
|
|
let customActionType;
|
|
let descriptionKey;
|
|
let icon;
|
|
|
|
if (requests.length === 1) {
|
|
const firstRequest = requests[0];
|
|
|
|
descriptionKey = 'notify.participantWantsToJoin';
|
|
notificationTitle = firstRequest.nick;
|
|
icon = NOTIFICATION_ICON.PARTICIPANT;
|
|
customActionNameKey = [ 'participantsPane.actions.admit', 'participantsPane.actions.reject' ];
|
|
customActionType = [ BUTTON_TYPES.PRIMARY, BUTTON_TYPES.DESTRUCTIVE ];
|
|
customActionHandler = [ () => batch(() => {
|
|
dispatch(hideNotification(VISITORS_PROMOTION_NOTIFICATION_ID));
|
|
dispatch(approveRequest(firstRequest));
|
|
}),
|
|
() => batch(() => {
|
|
dispatch(hideNotification(VISITORS_PROMOTION_NOTIFICATION_ID));
|
|
dispatch(denyRequest(firstRequest));
|
|
}) ];
|
|
} else {
|
|
descriptionKey = 'notify.participantsWantToJoin';
|
|
notificationTitle = i18n.t('notify.waitingParticipants', {
|
|
waitingParticipants: requests.length
|
|
});
|
|
icon = NOTIFICATION_ICON.PARTICIPANTS;
|
|
customActionNameKey = [ 'notify.viewVisitors' ];
|
|
customActionType = [ BUTTON_TYPES.PRIMARY ];
|
|
customActionHandler = [ () => batch(() => {
|
|
dispatch(hideNotification(VISITORS_PROMOTION_NOTIFICATION_ID));
|
|
dispatch(openParticipantsPane());
|
|
}) ];
|
|
}
|
|
|
|
dispatch(showNotification({
|
|
title: notificationTitle,
|
|
descriptionKey,
|
|
uid: VISITORS_PROMOTION_NOTIFICATION_ID,
|
|
customActionNameKey,
|
|
customActionType,
|
|
customActionHandler,
|
|
icon
|
|
}, NOTIFICATION_TIMEOUT_TYPE.STICKY));
|
|
}
|