mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2025-12-30 03:12:29 +00:00
feat(visitors): Handles live conference and queue service. (#14869)
* feat(visitors): Handling of live conference and queue service. * squash: Small refactor mobile code. * squash: Drop debug log. * chore(deps) lib-jitsi-meet@latest https://github.com/jitsi/lib-jitsi-meet/compare/v1836.0.0+d05325f3...v1839.0.0+ea523fc6 * squash: Adds a count function. * squash: Drop debug print. * squash: Skip if queueService is not enabled. * squash: Avoids double subscribing for visitorsWaiting. * squash: Fixes lint error. * squash: Fixes showing dialog.
This commit is contained in:
@@ -822,6 +822,8 @@
|
||||
"viewLobby": "View lobby",
|
||||
"viewVisitors": "View visitors",
|
||||
"waitingParticipants": "{{waitingParticipants}} people",
|
||||
"waitingVisitors": "Visitors waiting in queue: {{waitingVisitors}}",
|
||||
"waitingVisitorsTitle": "The meeting is not live yet!",
|
||||
"whiteboardLimitDescription": "Please save your progress, as the user limit will soon be reached and the whiteboard will close.",
|
||||
"whiteboardLimitTitle": "Whiteboard usage"
|
||||
},
|
||||
@@ -835,6 +837,7 @@
|
||||
"audioModeration": "Unmute themselves",
|
||||
"blockEveryoneMicCamera": "Block everyone's mic and camera",
|
||||
"breakoutRooms": "Breakout rooms",
|
||||
"goLive": "Go live",
|
||||
"invite": "Invite Someone",
|
||||
"moreModerationActions": "More moderation options",
|
||||
"moreModerationControls": "More moderation controls",
|
||||
@@ -852,6 +855,7 @@
|
||||
"headings": {
|
||||
"lobby": "Lobby ({{count}})",
|
||||
"participantsList": "Meeting participants ({{count}})",
|
||||
"visitorInQueue": " (waiting {{count}})",
|
||||
"visitorRequests": " (requests {{count}})",
|
||||
"visitors": "Visitors {{count}}",
|
||||
"waitingLobby": "Waiting in lobby ({{count}})"
|
||||
@@ -1495,7 +1499,8 @@
|
||||
"noVisitorLobby": "You cannot join while there is a lobby enabled for the meeting.",
|
||||
"notAllowedPromotion": "A participant needs to allow your request first.",
|
||||
"title": "You are a visitor in the meeting"
|
||||
}
|
||||
},
|
||||
"waitingMessage": "You'll join the meeting as soon as it is live!"
|
||||
},
|
||||
"volumeSlider": "Volume slider",
|
||||
"welcomepage": {
|
||||
|
||||
21
package-lock.json
generated
21
package-lock.json
generated
@@ -35,6 +35,7 @@
|
||||
"@react-navigation/material-top-tabs": "6.6.3",
|
||||
"@react-navigation/native": "6.1.7",
|
||||
"@react-navigation/stack": "6.3.17",
|
||||
"@stomp/stompjs": "7.0.0",
|
||||
"@svgr/webpack": "6.3.1",
|
||||
"@tensorflow/tfjs-backend-wasm": "3.13.0",
|
||||
"@tensorflow/tfjs-core": "3.13.0",
|
||||
@@ -60,7 +61,7 @@
|
||||
"js-md5": "0.6.1",
|
||||
"js-sha512": "0.8.0",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1838.0.0+1993a030/lib-jitsi-meet.tgz",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1839.0.0+ea523fc6/lib-jitsi-meet.tgz",
|
||||
"lodash": "4.17.21",
|
||||
"moment": "2.29.4",
|
||||
"moment-duration-format": "2.2.2",
|
||||
@@ -5309,6 +5310,11 @@
|
||||
"@sinonjs/commons": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@stomp/stompjs": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@stomp/stompjs/-/stompjs-7.0.0.tgz",
|
||||
"integrity": "sha512-fGdq4wPDnSV/KyOsjq4P+zLc8MFWC3lMmP5FBgLWKPJTYcuCbAIrnRGjB7q2jHZdYCOD5vxLuFoKIYLy5/u8Pw=="
|
||||
},
|
||||
"node_modules/@svgr/babel-plugin-add-jsx-attribute": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-6.3.1.tgz",
|
||||
@@ -12304,8 +12310,8 @@
|
||||
},
|
||||
"node_modules/lib-jitsi-meet": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1838.0.0+1993a030/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-IRZnKBO5o2OjmDDI8ocmq5nyi6RZN0DhLIBgNm5u2sWQBgUa7Na1MqSKd/kDI9pGrw5BfygAz4cPOGCnRmkqww==",
|
||||
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1839.0.0+ea523fc6/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-pRIOBZFtIT4Y0WrrfxNKJnMbDx17ZYYbU3ygUd/hYvvyOkKMMteS4cZWqONAxn+esKhpkWksPSuZ8L4y4V5wnQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -22865,6 +22871,11 @@
|
||||
"@sinonjs/commons": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"@stomp/stompjs": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@stomp/stompjs/-/stompjs-7.0.0.tgz",
|
||||
"integrity": "sha512-fGdq4wPDnSV/KyOsjq4P+zLc8MFWC3lMmP5FBgLWKPJTYcuCbAIrnRGjB7q2jHZdYCOD5vxLuFoKIYLy5/u8Pw=="
|
||||
},
|
||||
"@svgr/babel-plugin-add-jsx-attribute": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-6.3.1.tgz",
|
||||
@@ -28085,8 +28096,8 @@
|
||||
}
|
||||
},
|
||||
"lib-jitsi-meet": {
|
||||
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1838.0.0+1993a030/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-IRZnKBO5o2OjmDDI8ocmq5nyi6RZN0DhLIBgNm5u2sWQBgUa7Na1MqSKd/kDI9pGrw5BfygAz4cPOGCnRmkqww==",
|
||||
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1839.0.0+ea523fc6/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-pRIOBZFtIT4Y0WrrfxNKJnMbDx17ZYYbU3ygUd/hYvvyOkKMMteS4cZWqONAxn+esKhpkWksPSuZ8L4y4V5wnQ==",
|
||||
"requires": {
|
||||
"@jitsi/js-utils": "2.2.1",
|
||||
"@jitsi/logger": "2.0.2",
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
"@react-navigation/material-top-tabs": "6.6.3",
|
||||
"@react-navigation/native": "6.1.7",
|
||||
"@react-navigation/stack": "6.3.17",
|
||||
"@stomp/stompjs": "7.0.0",
|
||||
"@svgr/webpack": "6.3.1",
|
||||
"@tensorflow/tfjs-backend-wasm": "3.13.0",
|
||||
"@tensorflow/tfjs-core": "3.13.0",
|
||||
@@ -66,7 +67,7 @@
|
||||
"js-md5": "0.6.1",
|
||||
"js-sha512": "0.8.0",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1838.0.0+1993a030/lib-jitsi-meet.tgz",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1839.0.0+ea523fc6/lib-jitsi-meet.tgz",
|
||||
"lodash": "4.17.21",
|
||||
"moment": "2.29.4",
|
||||
"moment-duration-format": "2.2.2",
|
||||
|
||||
@@ -54,6 +54,9 @@ export interface IConferenceMetadata {
|
||||
recording?: {
|
||||
isTranscribingEnabled: boolean;
|
||||
};
|
||||
visitors?: {
|
||||
live: boolean;
|
||||
};
|
||||
whiteboard?: {
|
||||
collabDetails: {
|
||||
roomId: string;
|
||||
|
||||
@@ -478,6 +478,7 @@ export interface IConfig {
|
||||
peopleSearchQueryTypes?: string[];
|
||||
peopleSearchUrl?: string;
|
||||
preferBosh?: boolean;
|
||||
preferVisitor?: boolean;
|
||||
preferredTranscribeLanguage?: string;
|
||||
prejoinConfig?: {
|
||||
enabled?: boolean;
|
||||
|
||||
@@ -197,6 +197,7 @@ export default [
|
||||
'participantsPane',
|
||||
'pcStatsInterval',
|
||||
'preferBosh',
|
||||
'preferVisitor',
|
||||
'prejoinConfig',
|
||||
'prejoinPageEnabled',
|
||||
'recordingService',
|
||||
|
||||
@@ -80,6 +80,7 @@ export interface IConfigState extends IConfig {
|
||||
audio?: boolean;
|
||||
video?: boolean;
|
||||
};
|
||||
queueService: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { appNavigate } from '../../app/actions.native';
|
||||
import { IStore } from '../../app/types';
|
||||
import { navigateRoot } from '../../mobile/navigation/rootNavigationContainerRef';
|
||||
import { screen } from '../../mobile/navigation/routes';
|
||||
import { JitsiConnectionErrors } from '../lib-jitsi-meet';
|
||||
|
||||
import { _connectInternal } from './actions.any';
|
||||
|
||||
@@ -13,7 +16,12 @@ export * from './actions.any';
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function connect(id?: string, password?: string) {
|
||||
return (dispatch: IStore['dispatch']) => dispatch(_connectInternal(id, password));
|
||||
return (dispatch: IStore['dispatch']) => dispatch(_connectInternal(id, password))
|
||||
.catch(error => {
|
||||
if (error === JitsiConnectionErrors.NOT_LIVE_ERROR) {
|
||||
navigateRoot(screen.visitorsQueue);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -30,6 +30,8 @@ import JitsiPortal from '../../../toolbox/components/web/JitsiPortal';
|
||||
import Toolbox from '../../../toolbox/components/web/Toolbox';
|
||||
import { LAYOUT_CLASSNAMES } from '../../../video-layout/constants';
|
||||
import { getCurrentLayout } from '../../../video-layout/functions.any';
|
||||
import VisitorsQueue from '../../../visitors/components/web/VisitorsQueue';
|
||||
import { showVisitorsQueue } from '../../../visitors/functions';
|
||||
import { init } from '../../actions.web';
|
||||
import { maybeShowSuboptimalExperienceNotification } from '../../functions.web';
|
||||
import {
|
||||
@@ -100,6 +102,11 @@ interface IProps extends AbstractProps, WithTranslation {
|
||||
*/
|
||||
_showPrejoin: boolean;
|
||||
|
||||
/**
|
||||
* If visitors queue page is visible or not.
|
||||
*/
|
||||
_showVisitorsQueue: boolean;
|
||||
|
||||
dispatch: IStore['dispatch'];
|
||||
}
|
||||
|
||||
@@ -206,6 +213,7 @@ class Conference extends AbstractConference<IProps, any> {
|
||||
_overflowDrawer,
|
||||
_showLobby,
|
||||
_showPrejoin,
|
||||
_showVisitorsQueue,
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
@@ -257,8 +265,9 @@ class Conference extends AbstractConference<IProps, any> {
|
||||
|
||||
<CalleeInfoContainer />
|
||||
|
||||
{ _showPrejoin && <Prejoin />}
|
||||
{ _showLobby && <LobbyScreen />}
|
||||
{ (_showPrejoin && !_showVisitorsQueue) && <Prejoin />}
|
||||
{ (_showLobby && !_showVisitorsQueue) && <LobbyScreen />}
|
||||
{ _showVisitorsQueue && <VisitorsQueue />}
|
||||
</div>
|
||||
<ParticipantsPane />
|
||||
<ReactionAnimations />
|
||||
@@ -402,7 +411,8 @@ function _mapStateToProps(state: IReduxState) {
|
||||
_overflowDrawer: overflowDrawer,
|
||||
_roomName: getConferenceNameForTitle(state),
|
||||
_showLobby: getIsLobbyVisible(state),
|
||||
_showPrejoin: isPrejoinPageVisible(state)
|
||||
_showPrejoin: isPrejoinPageVisible(state),
|
||||
_showVisitorsQueue: showVisitorsQueue(state)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import DialInSummary from '../../../invite/components/dial-in-summary/native/Dia
|
||||
import Prejoin from '../../../prejoin/components/native/Prejoin';
|
||||
import UnsafeRoomWarning from '../../../prejoin/components/native/UnsafeRoomWarning';
|
||||
import { isUnsafeRoomWarningEnabled } from '../../../prejoin/functions';
|
||||
import VisitorsQueue from '../../../visitors/components/native/VisitorsQueue';
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
import WelcomePage from '../../../welcome/components/WelcomePage';
|
||||
@@ -23,6 +24,7 @@ import {
|
||||
navigationContainerTheme,
|
||||
preJoinScreenOptions,
|
||||
unsafeMeetingScreenOptions,
|
||||
visitorsScreenOptions,
|
||||
welcomeScreenOptions
|
||||
} from '../screenOptions';
|
||||
|
||||
@@ -105,6 +107,10 @@ const RootNavigationContainer = ({ dispatch, isUnsafeRoomWarningAvailable, isWel
|
||||
name = { screen.unsafeRoomWarning }
|
||||
options = { unsafeMeetingScreenOptions } />
|
||||
}
|
||||
<RootStack.Screen
|
||||
component = { VisitorsQueue }
|
||||
name = { screen.visitorsQueue }
|
||||
options = { visitorsScreenOptions } />
|
||||
<RootStack.Screen
|
||||
component = { ConferenceNavigationContainer }
|
||||
name = { screen.conference.root }
|
||||
|
||||
@@ -44,6 +44,7 @@ export const screen = {
|
||||
profile: 'Profile'
|
||||
},
|
||||
unsafeRoomWarning: 'Unsafe Room Warning',
|
||||
visitorsQueue: 'Visitors Queue',
|
||||
welcome: {
|
||||
main: 'Welcome',
|
||||
tabs: {
|
||||
|
||||
@@ -55,6 +55,11 @@ export const welcomeScreenOptions = {
|
||||
*/
|
||||
export const conferenceScreenOptions = fullScreenOptions;
|
||||
|
||||
/**
|
||||
* Screen options for visitors queue.
|
||||
*/
|
||||
export const visitorsScreenOptions = fullScreenOptions;
|
||||
|
||||
/**
|
||||
* Tab bar options for chat screen.
|
||||
*/
|
||||
|
||||
@@ -178,6 +178,7 @@ const Notification = ({
|
||||
description,
|
||||
descriptionArguments,
|
||||
descriptionKey,
|
||||
disableClosing,
|
||||
hideErrorSupportLink,
|
||||
icon,
|
||||
onDismissed,
|
||||
@@ -336,14 +337,16 @@ const Notification = ({
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Icon
|
||||
className = { classes.closeIcon }
|
||||
color = { theme.palette.icon04 }
|
||||
id = 'close-notification'
|
||||
onClick = { onDismiss }
|
||||
size = { 20 }
|
||||
src = { IconCloseLarge }
|
||||
testId = { `${titleKey || descriptionKey}-dismiss` } />
|
||||
{ !disableClosing && (
|
||||
<Icon
|
||||
className = { classes.closeIcon }
|
||||
color = { theme.palette.icon04 }
|
||||
id = 'close-notification'
|
||||
onClick = { onDismiss }
|
||||
size = { 20 }
|
||||
src = { IconCloseLarge }
|
||||
testId = { `${titleKey || descriptionKey}-dismiss` } />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -104,12 +104,19 @@ export const RAISE_HAND_NOTIFICATION_ID = 'RAISE_HAND_NOTIFICATION';
|
||||
export const SALESFORCE_LINK_NOTIFICATION_ID = 'SALESFORCE_LINK_NOTIFICATION';
|
||||
|
||||
/**
|
||||
* The identifier of the lobby notification.
|
||||
* The identifier of the visitors promotion notification.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const VISITORS_PROMOTION_NOTIFICATION_ID = 'VISITORS_PROMOTION_NOTIFICATION';
|
||||
|
||||
/**
|
||||
* The identifier of the visitors notification indicating the meeting is not live.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const VISITORS_NOT_LIVE_NOTIFICATION_ID = 'VISITORS_NOT_LIVE_NOTIFICATION_ID';
|
||||
|
||||
/**
|
||||
* Amount of participants beyond which no join notification will be emitted.
|
||||
*/
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface INotificationProps {
|
||||
description?: string | React.ReactNode;
|
||||
descriptionArguments?: Object;
|
||||
descriptionKey?: string;
|
||||
disableClosing?: boolean;
|
||||
hideErrorSupportLink?: boolean;
|
||||
icon?: string;
|
||||
maxLines?: number;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IStore } from '../app/types';
|
||||
import { JitsiConferenceErrors } from '../base/lib-jitsi-meet';
|
||||
import { JitsiConferenceErrors, JitsiConnectionErrors } from '../base/lib-jitsi-meet';
|
||||
import {
|
||||
isFatalJitsiConferenceError,
|
||||
isFatalJitsiConnectionError
|
||||
@@ -8,7 +8,6 @@ import StateListenerRegistry from '../base/redux/StateListenerRegistry';
|
||||
|
||||
import { openPageReloadDialog } from './actions';
|
||||
|
||||
|
||||
/**
|
||||
* Error type. Basically like Error, but augmented with a recoverable property.
|
||||
*/
|
||||
@@ -34,6 +33,7 @@ type ErrorType = {
|
||||
* List of errors that are not fatal (or handled differently) so then the page reload dialog won't kick in.
|
||||
*/
|
||||
const RN_NO_RELOAD_DIALOG_ERRORS = [
|
||||
JitsiConnectionErrors.NOT_LIVE_ERROR,
|
||||
JitsiConferenceErrors.CONFERENCE_ACCESS_DENIED,
|
||||
JitsiConferenceErrors.CONFERENCE_DESTROYED,
|
||||
JitsiConferenceErrors.CONNECTION_ERROR,
|
||||
|
||||
@@ -3,17 +3,21 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Text, View, ViewStyle } from 'react-native';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import Button from '../../../base/ui/components/native/Button';
|
||||
import { BUTTON_MODES, BUTTON_TYPES } from '../../../base/ui/constants.native';
|
||||
import { admitMultiple } from '../../../visitors/actions';
|
||||
import { getPromotionRequests } from '../../../visitors/functions';
|
||||
import { admitMultiple, goLive } from '../../../visitors/actions';
|
||||
import {
|
||||
getPromotionRequests,
|
||||
getVisitorsCount,
|
||||
getVisitorsInQueueCount,
|
||||
isVisitorsLive
|
||||
} from '../../../visitors/functions';
|
||||
|
||||
import { VisitorsItem } from './VisitorsItem';
|
||||
import styles from './styles';
|
||||
|
||||
const VisitorsList = () => {
|
||||
const visitorsCount = useSelector((state: IReduxState) => state['features/visitors'].count || 0);
|
||||
const visitorsCount = useSelector(getVisitorsCount);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
@@ -22,9 +26,16 @@ const VisitorsList = () => {
|
||||
const admitAll = useCallback(() => {
|
||||
dispatch(admitMultiple(requests));
|
||||
}, [ dispatch, requests ]);
|
||||
const goLiveCb = useCallback(() => {
|
||||
dispatch(goLive());
|
||||
}, [ dispatch ]);
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (visitorsCount <= 0) {
|
||||
const visitorsInQueueCount = useSelector(getVisitorsInQueueCount);
|
||||
const isLive = useSelector(isVisitorsLive);
|
||||
const showVisitorsInQueue = visitorsInQueueCount > 0 && isLive === false;
|
||||
|
||||
if (visitorsCount <= 0 && !showVisitorsInQueue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -34,6 +45,10 @@ const VisitorsList = () => {
|
||||
title += t('participantsPane.headings.visitorRequests', { count: requests.length });
|
||||
}
|
||||
|
||||
if (showVisitorsInQueue) {
|
||||
title += t('participantsPane.headings.visitorInQueue', { count: visitorsInQueueCount });
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style = { styles.listDetails as ViewStyle } >
|
||||
@@ -41,7 +56,7 @@ const VisitorsList = () => {
|
||||
{ title }
|
||||
</Text>
|
||||
{
|
||||
requests.length > 1 && (
|
||||
requests.length > 1 && !showVisitorsInQueue && (
|
||||
<Button
|
||||
accessibilityLabel = 'participantsPane.actions.admitAll'
|
||||
labelKey = 'participantsPane.actions.admitAll'
|
||||
@@ -50,6 +65,16 @@ const VisitorsList = () => {
|
||||
type = { BUTTON_TYPES.PRIMARY } />
|
||||
)
|
||||
}
|
||||
{
|
||||
showVisitorsInQueue && (
|
||||
<Button
|
||||
accessibilityLabel = 'participantsPane.actions.goLive'
|
||||
labelKey = 'participantsPane.actions.goLive'
|
||||
mode = { BUTTON_MODES.TEXT }
|
||||
onClick = { goLiveCb }
|
||||
type = { BUTTON_TYPES.PRIMARY } />
|
||||
)
|
||||
}
|
||||
</View>
|
||||
{
|
||||
requests.map(r => (
|
||||
|
||||
@@ -3,10 +3,14 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { withPixelLineHeight } from '../../../base/styles/functions.web';
|
||||
import { admitMultiple } from '../../../visitors/actions';
|
||||
import { getPromotionRequests } from '../../../visitors/functions';
|
||||
import { admitMultiple, goLive } from '../../../visitors/actions';
|
||||
import {
|
||||
getPromotionRequests,
|
||||
getVisitorsCount,
|
||||
getVisitorsInQueueCount,
|
||||
isVisitorsLive
|
||||
} from '../../../visitors/functions';
|
||||
|
||||
import { VisitorsItem } from './VisitorsItem';
|
||||
|
||||
@@ -66,7 +70,10 @@ const useStyles = makeStyles()(theme => {
|
||||
*/
|
||||
export default function VisitorsList() {
|
||||
const requests = useSelector(getPromotionRequests);
|
||||
const visitorsCount = useSelector((state: IReduxState) => state['features/visitors'].count || 0);
|
||||
const visitorsCount = useSelector(getVisitorsCount);
|
||||
const visitorsInQueueCount = useSelector(getVisitorsInQueueCount);
|
||||
const isLive = useSelector(isVisitorsLive);
|
||||
const showVisitorsInQueue = visitorsInQueueCount > 0 && isLive === false;
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { classes, cx } = useStyles();
|
||||
@@ -76,7 +83,11 @@ export default function VisitorsList() {
|
||||
dispatch(admitMultiple(requests));
|
||||
}, [ dispatch, requests ]);
|
||||
|
||||
if (visitorsCount <= 0) {
|
||||
const goLiveCb = useCallback(() => {
|
||||
dispatch(goLive());
|
||||
}, [ dispatch ]);
|
||||
|
||||
if (visitorsCount <= 0 && !showVisitorsInQueue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -87,12 +98,20 @@ export default function VisitorsList() {
|
||||
{ t('participantsPane.headings.visitors', { count: visitorsCount })}
|
||||
{ requests.length > 0
|
||||
&& t('participantsPane.headings.visitorRequests', { count: requests.length }) }
|
||||
{ showVisitorsInQueue
|
||||
&& t('participantsPane.headings.visitorInQueue', { count: visitorsInQueueCount }) }
|
||||
</div>
|
||||
{
|
||||
requests.length > 1
|
||||
requests.length > 1 && !showVisitorsInQueue // Go live button is with higher priority
|
||||
&& <div
|
||||
className = { classes.link }
|
||||
onClick = { admitAll }>{t('participantsPane.actions.admitAll')}</div>
|
||||
onClick = { admitAll }>{ t('participantsPane.actions.admitAll') }</div>
|
||||
}
|
||||
{
|
||||
showVisitorsInQueue
|
||||
&& <div
|
||||
className = { classes.link }
|
||||
onClick = { goLiveCb }>{ t('participantsPane.actions.goLive') }</div>
|
||||
}
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { IStore } from '../app/types';
|
||||
import { connect } from '../base/connection/actions.native';
|
||||
import { navigateRoot } from '../mobile/navigation/rootNavigationContainerRef';
|
||||
import { screen } from '../mobile/navigation/routes';
|
||||
import { showVisitorsQueue } from '../visitors/functions';
|
||||
|
||||
/**
|
||||
* Action used to start the conference.
|
||||
@@ -8,7 +12,12 @@ import { IStore } from '../app/types';
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function joinConference(options?: Object, _ignoreJoiningInProgress = false) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
return async function(_dispatch: IStore['dispatch'], _getState: IStore['getState']) {
|
||||
return async function(dispatch: IStore['dispatch'], getState: IStore['getState']) {
|
||||
const _showVisitorsQueue = showVisitorsQueue(getState);
|
||||
|
||||
if (_showVisitorsQueue) {
|
||||
dispatch(connect());
|
||||
navigateRoot(screen.conference.root);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,6 +8,16 @@
|
||||
*/
|
||||
export const UPDATE_VISITORS_COUNT = 'UPDATE_VISITORS_COUNT';
|
||||
|
||||
/**
|
||||
* The type of (redux) action to update visitors in queue count.
|
||||
*
|
||||
* {
|
||||
* type: UPDATE_VISITORS_IN_QUEUE_COUNT,
|
||||
* count: number
|
||||
* }
|
||||
*/
|
||||
export const UPDATE_VISITORS_IN_QUEUE_COUNT = 'UPDATE_VISITORS_IN_QUEUE_COUNT';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which enables/disables visitors UI mode.
|
||||
*
|
||||
@@ -39,6 +49,16 @@ export const VISITOR_PROMOTION_REQUEST = 'VISITOR_PROMOTION_REQUEST';
|
||||
*/
|
||||
export const CLEAR_VISITOR_PROMOTION_REQUEST = 'CLEAR_VISITOR_PROMOTION_REQUEST';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which sets in visitor's queue.
|
||||
*
|
||||
* {
|
||||
* type: SET_IN_VISITORS_QUEUE,
|
||||
* value: boolean
|
||||
* }
|
||||
*/
|
||||
export const SET_IN_VISITORS_QUEUE = 'SET_IN_VISITORS_QUEUE';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which sets visitor demote actor.
|
||||
*
|
||||
|
||||
@@ -8,9 +8,11 @@ import { getLocalParticipant } from '../base/participants/functions';
|
||||
import {
|
||||
CLEAR_VISITOR_PROMOTION_REQUEST,
|
||||
I_AM_VISITOR_MODE,
|
||||
SET_IN_VISITORS_QUEUE,
|
||||
SET_VISITORS_SUPPORTED,
|
||||
SET_VISITOR_DEMOTE_ACTOR,
|
||||
UPDATE_VISITORS_COUNT,
|
||||
UPDATE_VISITORS_IN_QUEUE_COUNT,
|
||||
VISITOR_PROMOTION_REQUEST
|
||||
} from './actionTypes';
|
||||
import { IPromotionRequest } from './types';
|
||||
@@ -150,6 +152,21 @@ export function setIAmVisitor(enabled: boolean) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets in visitor's queue.
|
||||
*
|
||||
* @param {boolean} value - The new value.
|
||||
* @returns {{
|
||||
* type: SET_IN_VISITORS_QUEUE,
|
||||
* }}
|
||||
*/
|
||||
export function setInVisitorsQueue(value: boolean) {
|
||||
return {
|
||||
type: SET_IN_VISITORS_QUEUE,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets visitor demote actor.
|
||||
*
|
||||
@@ -194,3 +211,34 @@ export function updateVisitorsCount(count: number) {
|
||||
count
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Visitors in queue count has been updated.
|
||||
*
|
||||
* @param {number} count - The new visitors in queue count.
|
||||
* @returns {{
|
||||
* type: UPDATE_VISITORS_IN_QUEUE_COUNT,
|
||||
* }}
|
||||
*/
|
||||
export function updateVisitorsInQueueCount(count: number) {
|
||||
return {
|
||||
type: UPDATE_VISITORS_IN_QUEUE_COUNT,
|
||||
count
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the overflow menu if opened.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
export function goLive() {
|
||||
return (_: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const { conference } = getState()['features/base/conference'];
|
||||
|
||||
conference?.getMetadataHandler().setMetadata('visitors', {
|
||||
live: true
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { IReduxState } from '../../../app/types';
|
||||
import { IconUsers } from '../../../base/icons/svg';
|
||||
import Label from '../../../base/label/components/native/Label';
|
||||
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
|
||||
import { getVisitorsShortText, iAmVisitor } from '../../functions';
|
||||
import { getVisitorsCount, getVisitorsShortText, iAmVisitor } from '../../functions';
|
||||
|
||||
const styles = {
|
||||
raisedHandsCountLabel: {
|
||||
@@ -25,8 +25,7 @@ const styles = {
|
||||
|
||||
const VisitorsCountLabel = () => {
|
||||
const visitorsMode = useSelector((state: IReduxState) => iAmVisitor(state));
|
||||
const visitorsCount = useSelector((state: IReduxState) =>
|
||||
state['features/visitors'].count || 0);
|
||||
const visitorsCount = useSelector(getVisitorsCount);
|
||||
|
||||
return !visitorsMode && visitorsCount > 0 ? (
|
||||
<Label
|
||||
|
||||
27
react/features/visitors/components/native/VisitorsQueue.tsx
Normal file
27
react/features/visitors/components/native/VisitorsQueue.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Text, View } from 'react-native';
|
||||
|
||||
import LoadingIndicator from '../../../base/react/components/native/LoadingIndicator';
|
||||
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
|
||||
import styles from '../../../lobby/components/native/styles';
|
||||
|
||||
/**
|
||||
* The component that renders visitors queue UI.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
export default function VisitorsQueue() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<View style = { styles.lobbyWaitingFragmentContainer }>
|
||||
<Text style = { styles.lobbyTitle }>
|
||||
{ t('visitors.waitingMessage') }
|
||||
</Text>
|
||||
<LoadingIndicator
|
||||
color = { BaseTheme.palette.icon01 }
|
||||
style = { styles.loadingIndicator } />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { IReduxState } from '../../../app/types';
|
||||
import { IconUsers } from '../../../base/icons/svg';
|
||||
import Label from '../../../base/label/components/web/Label';
|
||||
import Tooltip from '../../../base/tooltip/components/Tooltip';
|
||||
import { getVisitorsShortText, iAmVisitor } from '../../functions';
|
||||
import { getVisitorsCount, getVisitorsShortText, iAmVisitor } from '../../functions';
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
@@ -21,8 +21,7 @@ const useStyles = makeStyles()(theme => {
|
||||
const VisitorsCountLabel = () => {
|
||||
const { classes: styles, theme } = useStyles();
|
||||
const visitorsMode = useSelector((state: IReduxState) => iAmVisitor(state));
|
||||
const visitorsCount = useSelector((state: IReduxState) =>
|
||||
state['features/visitors'].count || 0);
|
||||
const visitorsCount = useSelector(getVisitorsCount);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return !visitorsMode && visitorsCount > 0 ? (<Tooltip
|
||||
|
||||
93
react/features/visitors/components/web/VisitorsQueue.tsx
Normal file
93
react/features/visitors/components/web/VisitorsQueue.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { withPixelLineHeight } from '../../../base/styles/functions';
|
||||
import LoadingIndicator from '../../../base/ui/components/web/Spinner';
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
container: {
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
inset: '0 0 0 0',
|
||||
display: 'flex',
|
||||
backgroundColor: theme.palette.ui01,
|
||||
zIndex: 252,
|
||||
|
||||
'@media (max-width: 720px)': {
|
||||
flexDirection: 'column-reverse'
|
||||
}
|
||||
},
|
||||
content: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
flexShrink: 0,
|
||||
boxSizing: 'border-box',
|
||||
position: 'relative',
|
||||
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
zIndex: 252,
|
||||
|
||||
'@media (max-width: 720px)': {
|
||||
height: 'auto',
|
||||
margin: '0 auto'
|
||||
},
|
||||
|
||||
// mobile phone landscape
|
||||
'@media (max-width: 420px)': {
|
||||
padding: '16px 16px 0 16px',
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
'@media (max-width: 400px)': {
|
||||
padding: '16px'
|
||||
}
|
||||
},
|
||||
contentControls: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
margin: 'auto',
|
||||
width: '100%'
|
||||
},
|
||||
roomName: {
|
||||
...withPixelLineHeight(theme.typography.heading5),
|
||||
color: theme.palette.text01,
|
||||
marginBottom: theme.spacing(4),
|
||||
overflow: 'hidden',
|
||||
textAlign: 'center',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
width: '100%'
|
||||
},
|
||||
spinner: {
|
||||
margin: '8px'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* The component that renders visitors queue UI.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
export default function VisitorsQueue() {
|
||||
const { classes } = useStyles();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (<div className = { classes.container }>
|
||||
<div className = { classes.content }>
|
||||
<div className = { classes.contentControls }>
|
||||
<span className = { classes.roomName }>
|
||||
{ t('visitors.waitingMessage') }
|
||||
</span>
|
||||
<div className = { classes.spinner }>
|
||||
<LoadingIndicator size = 'large' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
@@ -45,3 +45,47 @@ export function iAmVisitor(stateful: IStateful) {
|
||||
export function getVisitorsCount(stateful: IStateful) {
|
||||
return toState(stateful)['features/visitors'].count ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of visitors that are waiting in queue.
|
||||
*
|
||||
* @param {Function|Object} stateful - The redux store or {@code getState}
|
||||
* function.
|
||||
* @returns {number} - The number of visitors in queue.
|
||||
*/
|
||||
export function getVisitorsInQueueCount(stateful: IStateful) {
|
||||
return toState(stateful)['features/visitors'].inQueueCount ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether visitor mode is supported.
|
||||
*
|
||||
* @param {Function|Object} stateful - The redux store or {@code getState}
|
||||
* function.
|
||||
* @returns {boolean} Whether visitor moder is supported.
|
||||
*/
|
||||
export function isVisitorsSupported(stateful: IStateful) {
|
||||
return toState(stateful)['features/visitors'].supported;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether visitor mode is live.
|
||||
*
|
||||
* @param {Function|Object} stateful - The redux store or {@code getState}
|
||||
* function.
|
||||
* @returns {boolean} Whether visitor moder is live.
|
||||
*/
|
||||
export function isVisitorsLive(stateful: IStateful) {
|
||||
return toState(stateful)['features/base/conference'].metadata?.visitors?.live;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to show visitor queue screen.
|
||||
*
|
||||
* @param {Function|Object} stateful - The redux store or {@code getState}
|
||||
* function.
|
||||
* @returns {boolean} Whether current participant is visitor and is in queue.
|
||||
*/
|
||||
export function showVisitorsQueue(stateful: IStateful) {
|
||||
return toState(stateful)['features/visitors'].inQueue;
|
||||
}
|
||||
|
||||
@@ -2,38 +2,55 @@ 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
|
||||
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 } from '../base/lib-jitsi-meet';
|
||||
import { JitsiConferenceEvents, JitsiConnectionErrors } from '../base/lib-jitsi-meet';
|
||||
import { PARTICIPANT_UPDATED } from '../base/participants/actionTypes';
|
||||
import { raiseHand } from '../base/participants/actions';
|
||||
import { getLocalParticipant, getParticipantById } from '../base/participants/functions';
|
||||
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
|
||||
updateVisitorsCount,
|
||||
updateVisitorsInQueueCount
|
||||
} from './actions';
|
||||
import { getPromotionRequests } from './functions';
|
||||
import { getPromotionRequests, getVisitorsCount, getVisitorsInQueueCount } from './functions';
|
||||
import logger from './logger';
|
||||
import { WebsocketClient } from './websocket-client';
|
||||
|
||||
MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
switch (action.type) {
|
||||
@@ -43,7 +60,7 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
conference.on(JitsiConferenceEvents.PROPERTIES_CHANGED, (properties: { 'visitor-count': number; }) => {
|
||||
const visitorCount = Number(properties?.['visitor-count']);
|
||||
|
||||
if (!isNaN(visitorCount) && getState()['features/visitors'].count !== visitorCount) {
|
||||
if (!isNaN(visitorCount) && getVisitorsCount(getState) !== visitorCount) {
|
||||
dispatch(updateVisitorsCount(visitorCount));
|
||||
}
|
||||
});
|
||||
@@ -130,17 +147,173 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
|
||||
if (data?.action === 'promotion-response' && data.approved) {
|
||||
const request = getPromotionRequests(getState())
|
||||
.find(r => r.from === data.id);
|
||||
.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, preferVisitor, visitors: visitorsConfig } = getState()['features/base/config'];
|
||||
const { locationURL } = 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();
|
||||
|
||||
let delay = 0;
|
||||
|
||||
// now let's connect to meeting
|
||||
if ('randomDelayMs' in msg) {
|
||||
delay = msg.randomDelayMs;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
dispatch(joinConference());
|
||||
dispatch(setInVisitorsQueue(false));
|
||||
}, 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.
|
||||
*
|
||||
|
||||
@@ -4,16 +4,20 @@ import ReducerRegistry from '../base/redux/ReducerRegistry';
|
||||
import {
|
||||
CLEAR_VISITOR_PROMOTION_REQUEST,
|
||||
I_AM_VISITOR_MODE,
|
||||
SET_IN_VISITORS_QUEUE,
|
||||
SET_VISITORS_SUPPORTED,
|
||||
SET_VISITOR_DEMOTE_ACTOR,
|
||||
UPDATE_VISITORS_COUNT,
|
||||
UPDATE_VISITORS_IN_QUEUE_COUNT,
|
||||
VISITOR_PROMOTION_REQUEST
|
||||
} from './actionTypes';
|
||||
import { IPromotionRequest } from './types';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
count: -1,
|
||||
count: 0,
|
||||
iAmVisitor: false,
|
||||
inQueue: false,
|
||||
inQueueCount: 0,
|
||||
showNotification: false,
|
||||
supported: false,
|
||||
promotionRequests: []
|
||||
@@ -23,6 +27,8 @@ export interface IVisitorsState {
|
||||
count?: number;
|
||||
demoteActorDisplayName?: string;
|
||||
iAmVisitor: boolean;
|
||||
inQueue: boolean;
|
||||
inQueueCount?: number;
|
||||
promotionRequests: IPromotionRequest[];
|
||||
supported: boolean;
|
||||
}
|
||||
@@ -49,12 +55,28 @@ ReducerRegistry.register<IVisitorsState>('features/visitors', (state = DEFAULT_S
|
||||
count: action.count
|
||||
};
|
||||
}
|
||||
case UPDATE_VISITORS_IN_QUEUE_COUNT: {
|
||||
if (state.count === action.count) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
inQueueCount: action.count
|
||||
};
|
||||
}
|
||||
case I_AM_VISITOR_MODE: {
|
||||
return {
|
||||
...state,
|
||||
iAmVisitor: action.enabled
|
||||
};
|
||||
}
|
||||
case SET_IN_VISITORS_QUEUE: {
|
||||
return {
|
||||
...state,
|
||||
inQueue: action.value
|
||||
};
|
||||
}
|
||||
case SET_VISITOR_DEMOTE_ACTOR: {
|
||||
return {
|
||||
...state,
|
||||
|
||||
138
react/features/visitors/websocket-client.ts
Normal file
138
react/features/visitors/websocket-client.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import { Client } from '@stomp/stompjs';
|
||||
|
||||
import logger from './logger';
|
||||
|
||||
interface QueueServiceResponse {
|
||||
conference: string;
|
||||
}
|
||||
export interface StateResponse extends QueueServiceResponse {
|
||||
randomDelayMs: number;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface VisitorResponse extends QueueServiceResponse {
|
||||
visitorsWaiting: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Websocket client impl, used for visitors queue.
|
||||
* Uses STOMP for authenticating (https://stomp.github.io/).
|
||||
*/
|
||||
export class WebsocketClient {
|
||||
private stompClient: Client | undefined;
|
||||
|
||||
private static instance: WebsocketClient;
|
||||
|
||||
private retriesCount = 0;
|
||||
|
||||
/**
|
||||
* WebsocketClient getInstance.
|
||||
*
|
||||
* @static
|
||||
* @returns {WebsocketClient} - WebsocketClient instance.
|
||||
*/
|
||||
static getInstance(): WebsocketClient {
|
||||
if (!this.instance) {
|
||||
this.instance = new WebsocketClient();
|
||||
}
|
||||
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to endpoint.
|
||||
*
|
||||
* @param {string} queueServiceURL - The service URL to use.
|
||||
* @param {string} endpoint - The endpoint to subscribe to.
|
||||
* @param {Function} callback - The callback to execute when we receive a message from the endpoint.
|
||||
* @param {string} token - The token, if any, to be used for authorization.
|
||||
* @param {Function?} connectCallback - The callback to execute when successfully connected.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
connect(queueServiceURL: string, // eslint-disable-line max-params
|
||||
endpoint: string,
|
||||
callback: (response: StateResponse | VisitorResponse) => void,
|
||||
token: string | undefined,
|
||||
connectCallback?: () => void): void {
|
||||
this.stompClient = new Client({
|
||||
brokerURL: queueServiceURL,
|
||||
forceBinaryWSFrames: true,
|
||||
appendMissingNULLonIncoming: true
|
||||
});
|
||||
|
||||
const errorConnecting = (error: any) => {
|
||||
if (this.retriesCount > 3) {
|
||||
this.stompClient?.deactivate();
|
||||
this.stompClient = undefined;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.retriesCount++;
|
||||
|
||||
logger.error(`Error connecting to ${queueServiceURL} ${JSON.stringify(error)}`);
|
||||
};
|
||||
|
||||
this.stompClient.onWebSocketError = errorConnecting;
|
||||
|
||||
this.stompClient.onStompError = frame => {
|
||||
errorConnecting(frame.headers.message);
|
||||
};
|
||||
|
||||
if (token) {
|
||||
this.stompClient.connectHeaders = {
|
||||
Authorization: `Bearer ${token}`
|
||||
};
|
||||
}
|
||||
|
||||
this.stompClient.onConnect = () => {
|
||||
if (!this.stompClient) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.retriesCount = 0;
|
||||
|
||||
logger.info(`Connected to:${endpoint}`);
|
||||
connectCallback?.();
|
||||
|
||||
this.stompClient.subscribe(endpoint, message => {
|
||||
try {
|
||||
callback(JSON.parse(message.body));
|
||||
} catch (e) {
|
||||
logger.error(`Error parsing response: ${message}`, e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
this.stompClient.activate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects the current stomp client instance and clears it.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
disconnect(): void {
|
||||
if (!this.stompClient) {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = this.stompClient.brokerURL;
|
||||
|
||||
this.stompClient.deactivate().then(() => {
|
||||
logger.info(`disconnected from: ${url}`);
|
||||
this.stompClient = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the instance is created and connected or in connecting state.
|
||||
*
|
||||
* @returns {boolean} Whether the connect method was executed.
|
||||
*/
|
||||
isActive() {
|
||||
return this.stompClient !== undefined;
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ local breakout_rooms_component_host = module:get_option_string('breakout_rooms_c
|
||||
|
||||
module:log("info", "Starting room metadata for %s", muc_component_host);
|
||||
|
||||
local main_muc_module;
|
||||
|
||||
-- Utility functions
|
||||
|
||||
@@ -148,6 +149,9 @@ function on_message(event)
|
||||
|
||||
broadcastMetadata(room);
|
||||
|
||||
-- fire and event for the change
|
||||
main_muc_module:fire_event('jitsi-metadata-updated', { room = room; actor = occupant; key = jsonData.key; });
|
||||
|
||||
return true;
|
||||
end
|
||||
|
||||
@@ -158,6 +162,8 @@ module:hook("message/host", on_message);
|
||||
|
||||
-- operates on already loaded main muc module
|
||||
function process_main_muc_loaded(main_muc, host_module)
|
||||
main_muc_module = host_module;
|
||||
|
||||
module:log('debug', 'Main muc loaded');
|
||||
module:log("info", "Hook to muc events on %s", muc_component_host);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
module:log('info', 'Starting visitors_component at %s', module.host);
|
||||
|
||||
local http = require 'net.http';
|
||||
local jid = require 'util.jid';
|
||||
local st = require 'util.stanza';
|
||||
local util = module:require 'util';
|
||||
@@ -18,6 +19,8 @@ local um_is_admin = require 'core.usermanager'.is_admin;
|
||||
local json = require 'cjson.safe';
|
||||
local inspect = require 'inspect';
|
||||
|
||||
local token_util = module:require 'token/util'.new(module);
|
||||
|
||||
local MUC_NS = 'http://jabber.org/protocol/muc';
|
||||
|
||||
local muc_domain_prefix = module:get_option_string('muc_mapper_domain_prefix', 'conference');
|
||||
@@ -36,6 +39,13 @@ local auto_allow_promotion = module:get_option_boolean('auto_allow_visitor_promo
|
||||
-- can be set to off and being controlled by another module, turning it on and off for rooms
|
||||
local always_visitors_enabled = module:get_option_boolean('always_visitors_enabled', true);
|
||||
|
||||
local visitors_queue_service = module:get_option_string('visitors_queue_service');
|
||||
local http_headers = {
|
||||
["User-Agent"] = "Prosody (" .. prosody.version .. "; " .. prosody.platform .. ")",
|
||||
["Content-Type"] = "application/json",
|
||||
["Accept"] = "application/json"
|
||||
};
|
||||
|
||||
local function is_admin(jid)
|
||||
return um_is_admin(jid, module.host);
|
||||
end
|
||||
@@ -294,6 +304,33 @@ local function process_promotion_response(room, id, approved)
|
||||
allow = approved }):up());
|
||||
end
|
||||
|
||||
local function go_live(room)
|
||||
-- let's inform the queue service
|
||||
local function cb(content_, code_, response_, request_)
|
||||
local room = room;
|
||||
if code_ == 200 then
|
||||
-- meeting went live ???
|
||||
module:log('info', 'live')
|
||||
else
|
||||
module:log('warn', 'External call to visitors_queue_service/golive failed. Code %s, Content %s',
|
||||
code_, content_)
|
||||
end
|
||||
end
|
||||
|
||||
local headers = http_headers or {};
|
||||
headers['Authorization'] = token_util:generateAsapToken();
|
||||
|
||||
local ev = {
|
||||
conference = internal_room_jid_match_rewrite(room.jid)
|
||||
};
|
||||
|
||||
http.request(visitors_queue_service..'/golive', {
|
||||
headers = headers,
|
||||
method = 'POST',
|
||||
body = json.encode(ev);
|
||||
}, cb);
|
||||
end
|
||||
|
||||
module:hook('iq/host', stanza_handler, 10);
|
||||
|
||||
process_host_module(muc_domain_prefix..'.'..muc_domain_base, function(host_module, host)
|
||||
@@ -451,6 +488,22 @@ process_host_module(muc_domain_prefix..'.'..muc_domain_base, function(host_modul
|
||||
|
||||
return true; -- halt processing, but return true that we handled it
|
||||
end);
|
||||
if visitors_queue_service then
|
||||
host_module:hook('muc-room-created', function (event)
|
||||
local room = event.room;
|
||||
if room.jitsiMetadata.visitors and room.jitsiMetadata.visitors.live then
|
||||
go_live(room);
|
||||
end
|
||||
end, -2); -- metadata hook on -1
|
||||
host_module:hook('jitsi-metadata-updated', function (event)
|
||||
if event.key == 'visitors' then
|
||||
local room = event.room;
|
||||
if room.jitsiMetadata.visitors and room.jitsiMetadata.visitors.live then
|
||||
go_live(room);
|
||||
end
|
||||
end
|
||||
end);
|
||||
end
|
||||
|
||||
if always_visitors_enabled then
|
||||
local visitorsEnabledField = {
|
||||
|
||||
@@ -26,6 +26,24 @@ local ssl = require "ssl";
|
||||
-- TODO: Figure out a less arbitrary default cache size.
|
||||
local cacheSize = module:get_option_number("jwt_pubkey_cache_size", 128);
|
||||
|
||||
-- the cache for generated asap jwt tokens
|
||||
local jwtKeyCache = require 'util.cache'.new(cacheSize);
|
||||
|
||||
local ASAPTTL_THRESHOLD = module:get_option_number('asap_ttl_threshold', 600);
|
||||
local ASAPTTL = module:get_option_number('asap_ttl', 3600);
|
||||
local ASAPIssuer = module:get_option_string('asap_issuer', 'jitsi');
|
||||
local ASAPAudience = module:get_option_string('asap_audience', 'jitsi');
|
||||
local ASAPKeyId = module:get_option_string('asap_key_id', 'jitsi');
|
||||
local ASAPKeyPath = module:get_option_string('asap_key_path', '/etc/prosody/certs/asap.key');
|
||||
|
||||
local ASAPKey;
|
||||
local f = io.open(ASAPKeyPath, 'r');
|
||||
|
||||
if f then
|
||||
ASAPKey = f:read('*all');
|
||||
f:close();
|
||||
end
|
||||
|
||||
local Util = {}
|
||||
Util.__index = Util
|
||||
|
||||
@@ -479,4 +497,48 @@ function Util:verify_room(session, room_address)
|
||||
end
|
||||
end
|
||||
|
||||
function Util:generateAsapToken(audience)
|
||||
if not ASAPKey then
|
||||
module:log('warn', 'No ASAP Key read, asap key generation is disabled');
|
||||
return ''
|
||||
end
|
||||
|
||||
audience = audience or ASAPAudience
|
||||
local t = os.time()
|
||||
local err
|
||||
local exp_key = 'asap_exp.'..audience
|
||||
local token_key = 'asap_token.'..audience
|
||||
local exp = jwtKeyCache:get(exp_key)
|
||||
local token = jwtKeyCache:get(token_key)
|
||||
|
||||
--if we find a token and it isn't too far from expiry, then use it
|
||||
if token ~= nil and exp ~= nil then
|
||||
exp = tonumber(exp)
|
||||
if (exp - t) > ASAPTTL_THRESHOLD then
|
||||
return token
|
||||
end
|
||||
end
|
||||
|
||||
--expiry is the current time plus TTL
|
||||
exp = t + ASAPTTL
|
||||
local payload = {
|
||||
iss = ASAPIssuer,
|
||||
aud = audience,
|
||||
nbf = t,
|
||||
exp = exp,
|
||||
}
|
||||
|
||||
-- encode
|
||||
local alg = 'RS256'
|
||||
token, err = jwt.encode(payload, ASAPKey, alg, { kid = ASAPKeyId })
|
||||
if not err then
|
||||
token = 'Bearer '..token
|
||||
jwtKeyCache:set(exp_key, exp)
|
||||
jwtKeyCache:set(token_key, token)
|
||||
return token
|
||||
else
|
||||
return ''
|
||||
end
|
||||
end
|
||||
|
||||
return Util;
|
||||
|
||||
Reference in New Issue
Block a user