From 342dd4ceca7be21c78ba83cf7d464a779c8e699e Mon Sep 17 00:00:00 2001 From: hmuresan Date: Fri, 11 Jun 2021 11:58:45 +0300 Subject: [PATCH] feat(replace-participant): Replace participant with same jwt in the conf - update lib-jitsi-meet to version with support for replacing participant --- conference.js | 25 ++++++++++++++++--- modules/UI/authentication/AuthHandler.js | 5 +++- package-lock.json | 4 +-- package.json | 2 +- react/features/base/conference/actions.js | 7 ++++-- react/features/base/conference/functions.js | 9 +++++-- react/features/base/config/configWhitelist.js | 1 + react/features/base/participants/actions.js | 12 ++++++--- .../features/base/participants/middleware.js | 8 +++--- react/features/base/participants/reducer.js | 2 ++ react/features/conference/actions.native.js | 6 +++++ react/features/conference/actions.web.js | 4 +++ react/features/external-api/middleware.js | 2 +- .../AbstractKnockingParticipantList.js | 3 ++- react/features/notifications/middleware.js | 21 +++++++++------- 15 files changed, 83 insertions(+), 28 deletions(-) diff --git a/conference.js b/conference.js index 6076f5174..27bea0ab6 100644 --- a/conference.js +++ b/conference.js @@ -304,10 +304,14 @@ class ConferenceConnector { // not enough rights to create conference case JitsiConferenceErrors.AUTHENTICATION_REQUIRED: { + const { replaceParticipant } + = APP.store.getState()['features/base/config']; + + // Schedule reconnect to check if someone else created the room. this.reconnectTimeout = setTimeout(() => { APP.store.dispatch(conferenceWillJoin(room)); - room.join(); + room.join(null, replaceParticipant); }, 5000); const { password } @@ -393,8 +397,10 @@ class ConferenceConnector { * */ connect() { + const { replaceParticipant } = APP.store.getState()['features/base/config']; + // the local storage overrides here and in connection.js can be used by jibri - room.join(jitsiLocalStorage.getItem('xmpp_conference_password_override')); + room.join(jitsiLocalStorage.getItem('xmpp_conference_password_override'), replaceParticipant); } } @@ -2167,7 +2173,20 @@ export default { JitsiConferenceEvents.LOCK_STATE_CHANGED, (...args) => APP.store.dispatch(lockStateChanged(room, ...args))); - room.on(JitsiConferenceEvents.KICKED, participant => { + room.on(JitsiConferenceEvents.KICKED, (participant, reason, isReplaced) => { + if (isReplaced) { + // this event triggers when the local participant is kicked, `participant` + // is the kicker. In replace participant case, kicker is undefined, + // as the server initiated it. We mark in store the local participant + // as being replaced based on jwt. + const localParticipant = getLocalParticipant(APP.store.getState()); + + APP.store.dispatch(participantUpdated({ + conference: room, + id: localParticipant.id, + isReplaced + })); + } APP.store.dispatch(kickedOut(room, participant)); }); diff --git a/modules/UI/authentication/AuthHandler.js b/modules/UI/authentication/AuthHandler.js index b3d79cba0..24c1de694 100644 --- a/modules/UI/authentication/AuthHandler.js +++ b/modules/UI/authentication/AuthHandler.js @@ -209,7 +209,10 @@ function logout(room: Object) { }).then(url => { // de-authenticate conference on the fly if (room.isJoined()) { - room.join(); + const { replaceParticipant } + = APP.store.getState()['features/base/config']; + + room.join(null, replaceParticipant); } return url; diff --git a/package-lock.json b/package-lock.json index e10af3f07..4d951938f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11071,8 +11071,8 @@ } }, "lib-jitsi-meet": { - "version": "github:jitsi/lib-jitsi-meet#2259d4418574841a782e98df27c44370bb84df45", - "from": "github:jitsi/lib-jitsi-meet#2259d4418574841a782e98df27c44370bb84df45", + "version": "github:jitsi/lib-jitsi-meet#fad985e95a9e8a4fb8a1b8b1ad2cfef75370c866", + "from": "github:jitsi/lib-jitsi-meet#fad985e95a9e8a4fb8a1b8b1ad2cfef75370c866", "requires": { "@jitsi/js-utils": "1.0.2", "@jitsi/sdp-interop": "github:jitsi/sdp-interop#5fc4af6dcf8a6e6af9fedbcd654412fd47b1b4ae", diff --git a/package.json b/package.json index 747710b65..53d368713 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "jquery-i18next": "1.2.1", "js-md5": "0.6.1", "jwt-decode": "2.2.0", - "lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#2259d4418574841a782e98df27c44370bb84df45", + "lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#fad985e95a9e8a4fb8a1b8b1ad2cfef75370c866", "libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d", "lodash": "4.17.21", "moment": "2.29.1", diff --git a/react/features/base/conference/actions.js b/react/features/base/conference/actions.js index e97a121b1..8d7eb3c1c 100644 --- a/react/features/base/conference/actions.js +++ b/react/features/base/conference/actions.js @@ -460,7 +460,7 @@ export function createConference() { sendLocalParticipant(state, conference); - conference.join(password); + conference.join(password, config.replaceParticipant); }; } @@ -477,8 +477,11 @@ export function checkIfCanJoin() { const { authRequired, password } = getState()['features/base/conference']; + const { replaceParticipant } + = getState()['features/base/config']; + authRequired && dispatch(_conferenceWillJoin(authRequired)); - authRequired && authRequired.join(password); + authRequired && authRequired.join(password, replaceParticipant); }; } diff --git a/react/features/base/conference/functions.js b/react/features/base/conference/functions.js index b87f0b8d2..d80e71fb2 100644 --- a/react/features/base/conference/functions.js +++ b/react/features/base/conference/functions.js @@ -87,6 +87,8 @@ export function commonUserJoinedHandling( if (user.isHidden()) { dispatch(hiddenParticipantJoined(id, displayName)); } else { + const isReplacing = user.isReplacing && user.isReplacing(); + dispatch(participantJoined({ botType: user.getBotType(), connectionStatus: user.getConnectionStatus(), @@ -94,7 +96,8 @@ export function commonUserJoinedHandling( id, name: displayName, presence: user.getStatus(), - role: user.getRole() + role: user.getRole(), + isReplacing })); } } @@ -119,7 +122,9 @@ export function commonUserLeftHandling( if (user.isHidden()) { dispatch(hiddenParticipantLeft(id)); } else { - dispatch(participantLeft(id, conference)); + const isReplaced = user.isReplaced && user.isReplaced(); + + dispatch(participantLeft(id, conference, isReplaced)); } } diff --git a/react/features/base/config/configWhitelist.js b/react/features/base/config/configWhitelist.js index 3b002151a..ddef03739 100644 --- a/react/features/base/config/configWhitelist.js +++ b/react/features/base/config/configWhitelist.js @@ -152,6 +152,7 @@ export default [ 'requireDisplayName', 'remoteVideoMenu', 'roomPasswordNumberOfDigits', + 'replaceParticipant', 'resolution', 'startAudioMuted', 'startAudioOnly', diff --git a/react/features/base/participants/actions.js b/react/features/base/participants/actions.js index 5ba4a6abc..5cdd8f957 100644 --- a/react/features/base/participants/actions.js +++ b/react/features/base/participants/actions.js @@ -366,6 +366,7 @@ export function hiddenParticipantLeft(id) { * with the participant identified by the specified {@code id}. Only the local * participant is allowed to not specify an associated {@code JitsiConference} * instance. + * @param {boolean} isReplaced - Whether the participant is to be replaced in the meeting. * @returns {{ * type: PARTICIPANT_LEFT, * participant: { @@ -374,12 +375,13 @@ export function hiddenParticipantLeft(id) { * } * }} */ -export function participantLeft(id, conference) { +export function participantLeft(id, conference, isReplaced) { return { type: PARTICIPANT_LEFT, participant: { conference, - id + id, + isReplaced } }; } @@ -490,9 +492,13 @@ export function participantKicked(kicker, kicked) { dispatch({ type: PARTICIPANT_KICKED, kicked: kicked.getId(), - kicker: kicker.getId() + kicker: kicker?.getId() }); + if (kicked.isReplaced && kicked.isReplaced()) { + return; + } + dispatch(showNotification({ titleArguments: { kicked: diff --git a/react/features/base/participants/middleware.js b/react/features/base/participants/middleware.js index cddc83e3f..d2d0ee8e0 100644 --- a/react/features/base/participants/middleware.js +++ b/react/features/base/participants/middleware.js @@ -161,7 +161,7 @@ StateListenerRegistry.register( for (const p of getState()['features/base/participants']) { !p.local && (!conference || p.conference !== conference) - && dispatch(participantLeft(p.id, p.conference)); + && dispatch(participantLeft(p.id, p.conference, p.isReplaced)); } }); @@ -356,14 +356,16 @@ function _maybePlaySounds({ getState, dispatch }, action) { if (!action.participant.local && (!startAudioMuted || getParticipantCount(state) < startAudioMuted)) { + const { isReplacing, isReplaced } = action.participant; + if (action.type === PARTICIPANT_JOINED) { const { presence } = action.participant; // The sounds for the poltergeist are handled by features/invite. - if (presence !== INVITED && presence !== CALLING) { + if (presence !== INVITED && presence !== CALLING && !isReplacing) { dispatch(playSound(PARTICIPANT_JOINED_SOUND_ID)); } - } else if (action.type === PARTICIPANT_LEFT) { + } else if (action.type === PARTICIPANT_LEFT && !isReplaced) { dispatch(playSound(PARTICIPANT_LEFT_SOUND_ID)); } } diff --git a/react/features/base/participants/reducer.js b/react/features/base/participants/reducer.js index 48e224da9..7467fbf55 100644 --- a/react/features/base/participants/reducer.js +++ b/react/features/base/participants/reducer.js @@ -187,6 +187,7 @@ function _participantJoined({ participant }) { dominantSpeaker, email, isFakeParticipant, + isReplacing, isJigasi, loadableAvatarUrl, local, @@ -218,6 +219,7 @@ function _participantJoined({ participant }) { email, id, isFakeParticipant, + isReplacing, isJigasi, loadableAvatarUrl, local: local || false, diff --git a/react/features/conference/actions.native.js b/react/features/conference/actions.native.js index ff3b0c60c..40b48e569 100644 --- a/react/features/conference/actions.native.js +++ b/react/features/conference/actions.native.js @@ -18,6 +18,12 @@ import { getParticipantDisplayName } from '../base/participants'; */ export function notifyKickedOut(participant: Object, submit: ?Function) { return (dispatch: Dispatch, getState: Function) => { + if (!participant || (participant.isReplaced && participant.isReplaced())) { + submit && submit(); + + return; + } + dispatch(openDialog(AlertDialog, { contentKey: { key: 'dialog.kickTitle', diff --git a/react/features/conference/actions.web.js b/react/features/conference/actions.web.js index 24637e259..413416c00 100644 --- a/react/features/conference/actions.web.js +++ b/react/features/conference/actions.web.js @@ -18,6 +18,10 @@ import { */ export function notifyKickedOut(participant: Object, _: ?Function) { // eslint-disable-line no-unused-vars return (dispatch: Dispatch, getState: Function) => { + if (!participant || (participant.isReplaced && participant.isReplaced())) { + return; + } + const args = { participantDisplayName: getParticipantDisplayName(getState, participant.getId()) diff --git a/react/features/external-api/middleware.js b/react/features/external-api/middleware.js index bfd45e433..6d9ee1cbf 100644 --- a/react/features/external-api/middleware.js +++ b/react/features/external-api/middleware.js @@ -113,7 +113,7 @@ MiddlewareRegistry.register(store => next => action => { id: getLocalParticipant(store.getState()).id, local: true }, - { id: action.participant.getId() } + { id: action.participant ? action.participant.getId() : undefined } ); break; diff --git a/react/features/lobby/components/AbstractKnockingParticipantList.js b/react/features/lobby/components/AbstractKnockingParticipantList.js index b49fa8765..c2ea807f1 100644 --- a/react/features/lobby/components/AbstractKnockingParticipantList.js +++ b/react/features/lobby/components/AbstractKnockingParticipantList.js @@ -71,6 +71,7 @@ export function mapStateToProps(state: Object): $Shape { return { _participants: knockingParticipants, - _visible: lobbyEnabled && isLocalParticipantModerator(state) && Boolean(knockingParticipants.length) + _visible: lobbyEnabled && isLocalParticipantModerator(state) + && Boolean(knockingParticipants && knockingParticipants.length) }; } diff --git a/react/features/notifications/middleware.js b/react/features/notifications/middleware.js index d30e1e1a8..aeb36b1b4 100644 --- a/react/features/notifications/middleware.js +++ b/react/features/notifications/middleware.js @@ -34,7 +34,7 @@ MiddlewareRegistry.register(store => next => action => { const { participant: p } = action; const { dispatch, getState } = store; - if (!p.local && !joinLeaveNotificationsDisabled()) { + if (!p.local && !joinLeaveNotificationsDisabled() && !p.isReplacing) { dispatch(showParticipantJoinedNotification( getParticipantDisplayName(getState, p.id) )); @@ -45,13 +45,15 @@ MiddlewareRegistry.register(store => next => action => { // Do not show the notification for mobile and also when the focus indicator is disabled. const displayName = getParticipantDisplayName(getState, p.id); - dispatch(showNotification({ - descriptionArguments: { to: displayName || '$t(notify.somebody)' }, - descriptionKey: 'notify.grantedTo', - titleKey: 'notify.somebody', - title: displayName - }, - NOTIFICATION_TIMEOUT)); + if (!p.isReplacing) { + dispatch(showNotification({ + descriptionArguments: { to: displayName || '$t(notify.somebody)' }, + descriptionKey: 'notify.grantedTo', + titleKey: 'notify.somebody', + title: displayName + }, + NOTIFICATION_TIMEOUT)); + } } return result; @@ -65,7 +67,8 @@ MiddlewareRegistry.register(store => next => action => { if (typeof interfaceConfig === 'object' && participant - && !participant.local) { + && !participant.local + && !action.participant.isReplaced) { store.dispatch(showNotification({ descriptionKey: 'notify.disconnected', titleKey: 'notify.somebody',