From f0fc63f5730ea09a78b46577a8a48e0b7d892d10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B0=D0=BC=D1=8F=D0=BD=20=D0=9C=D0=B8=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=D0=B2?= Date: Fri, 28 Jun 2024 15:29:41 +0300 Subject: [PATCH] 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. --- lang/main.json | 7 +- package-lock.json | 21 +- package.json | 3 +- react/features/base/conference/reducer.ts | 3 + react/features/base/config/configType.ts | 1 + react/features/base/config/configWhitelist.ts | 1 + react/features/base/config/reducer.ts | 1 + .../base/connection/actions.native.ts | 10 +- .../conference/components/web/Conference.tsx | 16 +- .../components/RootNavigationContainer.tsx | 6 + react/features/mobile/navigation/routes.ts | 1 + .../mobile/navigation/screenOptions.ts | 5 + .../components/web/Notification.tsx | 19 +- react/features/notifications/constants.ts | 9 +- react/features/notifications/types.ts | 1 + react/features/overlay/middleware.ts | 4 +- .../components/native/VisitorsList.tsx | 37 +++- .../components/web/VisitorsList.tsx | 33 +++- react/features/prejoin/actions.native.ts | 13 +- react/features/visitors/actionTypes.ts | 20 ++ react/features/visitors/actions.ts | 48 +++++ .../components/native/VisitorsCountLabel.tsx | 5 +- .../components/native/VisitorsQueue.tsx | 27 +++ .../components/web/VisitorsCountLabel.tsx | 5 +- .../visitors/components/web/VisitorsQueue.tsx | 93 +++++++++ react/features/visitors/functions.ts | 44 +++++ react/features/visitors/middleware.ts | 187 +++++++++++++++++- react/features/visitors/reducer.ts | 24 ++- react/features/visitors/websocket-client.ts | 138 +++++++++++++ .../mod_room_metadata_component.lua | 6 + .../mod_visitors_component.lua | 53 +++++ resources/prosody-plugins/token/util.lib.lua | 62 ++++++ 32 files changed, 852 insertions(+), 51 deletions(-) create mode 100644 react/features/visitors/components/native/VisitorsQueue.tsx create mode 100644 react/features/visitors/components/web/VisitorsQueue.tsx create mode 100644 react/features/visitors/websocket-client.ts diff --git a/lang/main.json b/lang/main.json index c0584cb447..644e4daefa 100644 --- a/lang/main.json +++ b/lang/main.json @@ -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": { diff --git a/package-lock.json b/package-lock.json index c004b0deeb..af1e625c4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 5bb00ba87d..2c3cdbe348 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/react/features/base/conference/reducer.ts b/react/features/base/conference/reducer.ts index c35491a1ee..5cbd727f00 100644 --- a/react/features/base/conference/reducer.ts +++ b/react/features/base/conference/reducer.ts @@ -54,6 +54,9 @@ export interface IConferenceMetadata { recording?: { isTranscribingEnabled: boolean; }; + visitors?: { + live: boolean; + }; whiteboard?: { collabDetails: { roomId: string; diff --git a/react/features/base/config/configType.ts b/react/features/base/config/configType.ts index 5c2bb58515..79761ef137 100644 --- a/react/features/base/config/configType.ts +++ b/react/features/base/config/configType.ts @@ -478,6 +478,7 @@ export interface IConfig { peopleSearchQueryTypes?: string[]; peopleSearchUrl?: string; preferBosh?: boolean; + preferVisitor?: boolean; preferredTranscribeLanguage?: string; prejoinConfig?: { enabled?: boolean; diff --git a/react/features/base/config/configWhitelist.ts b/react/features/base/config/configWhitelist.ts index e57f467301..631e7ebfe4 100644 --- a/react/features/base/config/configWhitelist.ts +++ b/react/features/base/config/configWhitelist.ts @@ -197,6 +197,7 @@ export default [ 'participantsPane', 'pcStatsInterval', 'preferBosh', + 'preferVisitor', 'prejoinConfig', 'prejoinPageEnabled', 'recordingService', diff --git a/react/features/base/config/reducer.ts b/react/features/base/config/reducer.ts index 55fd34f61a..00812289cd 100644 --- a/react/features/base/config/reducer.ts +++ b/react/features/base/config/reducer.ts @@ -80,6 +80,7 @@ export interface IConfigState extends IConfig { audio?: boolean; video?: boolean; }; + queueService: string; }; } diff --git a/react/features/base/connection/actions.native.ts b/react/features/base/connection/actions.native.ts index e11a3a458f..0096381507 100644 --- a/react/features/base/connection/actions.native.ts +++ b/react/features/base/connection/actions.native.ts @@ -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); + } + }); } /** diff --git a/react/features/conference/components/web/Conference.tsx b/react/features/conference/components/web/Conference.tsx index c357e61183..e430aadad1 100644 --- a/react/features/conference/components/web/Conference.tsx +++ b/react/features/conference/components/web/Conference.tsx @@ -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 { _overflowDrawer, _showLobby, _showPrejoin, + _showVisitorsQueue, t } = this.props; @@ -257,8 +265,9 @@ class Conference extends AbstractConference { - { _showPrejoin && } - { _showLobby && } + { (_showPrejoin && !_showVisitorsQueue) && } + { (_showLobby && !_showVisitorsQueue) && } + { _showVisitorsQueue && } @@ -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) }; } diff --git a/react/features/mobile/navigation/components/RootNavigationContainer.tsx b/react/features/mobile/navigation/components/RootNavigationContainer.tsx index 47e8b76192..d09c29b146 100644 --- a/react/features/mobile/navigation/components/RootNavigationContainer.tsx +++ b/react/features/mobile/navigation/components/RootNavigationContainer.tsx @@ -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 } /> } + - + { !disableClosing && ( + + )} ); diff --git a/react/features/notifications/constants.ts b/react/features/notifications/constants.ts index c9c8188b77..7bf47d81f9 100644 --- a/react/features/notifications/constants.ts +++ b/react/features/notifications/constants.ts @@ -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. */ diff --git a/react/features/notifications/types.ts b/react/features/notifications/types.ts index a60148bd75..224077da0d 100644 --- a/react/features/notifications/types.ts +++ b/react/features/notifications/types.ts @@ -9,6 +9,7 @@ export interface INotificationProps { description?: string | React.ReactNode; descriptionArguments?: Object; descriptionKey?: string; + disableClosing?: boolean; hideErrorSupportLink?: boolean; icon?: string; maxLines?: number; diff --git a/react/features/overlay/middleware.ts b/react/features/overlay/middleware.ts index db70a7d017..58df9eed49 100644 --- a/react/features/overlay/middleware.ts +++ b/react/features/overlay/middleware.ts @@ -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, diff --git a/react/features/participants-pane/components/native/VisitorsList.tsx b/react/features/participants-pane/components/native/VisitorsList.tsx index 9c9a8c6265..3ae7ad93a1 100644 --- a/react/features/participants-pane/components/native/VisitorsList.tsx +++ b/react/features/participants-pane/components/native/VisitorsList.tsx @@ -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 ( <> @@ -41,7 +56,7 @@ const VisitorsList = () => { { title } { - requests.length > 1 && ( + requests.length > 1 && !showVisitorsInQueue && (