diff --git a/react/features/base/conference/middleware.js b/react/features/base/conference/middleware.js index ad97874bf..64295047a 100644 --- a/react/features/base/conference/middleware.js +++ b/react/features/base/conference/middleware.js @@ -20,6 +20,7 @@ import UIEvents from '../../../../service/UI/UIEvents'; import { TRACK_ADDED, TRACK_REMOVED } from '../tracks'; import { + conferenceLeft, createConference, setAudioOnly, setLastN, @@ -32,8 +33,10 @@ import { DATA_CHANNEL_OPENED, SET_AUDIO_ONLY, SET_LASTN, - SET_RECEIVE_VIDEO_QUALITY + SET_RECEIVE_VIDEO_QUALITY, + SET_ROOM } from './actionTypes'; +import { JITSI_CONFERENCE_URL_KEY } from './constants'; import { _addLocalTracksToConference, _handleParticipantError, @@ -77,6 +80,9 @@ MiddlewareRegistry.register(store => next => action => { case SET_RECEIVE_VIDEO_QUALITY: return _setReceiveVideoQuality(store, next, action); + case SET_ROOM: + return _setRoom(store, next, action); + case TRACK_ADDED: case TRACK_REMOVED: return _trackAddedOrRemoved(store, next, action); @@ -330,6 +336,62 @@ function _setReceiveVideoQuality({ dispatch, getState }, next, action) { return next(action); } +/** + * Notifies the feature {@code base/conference} that the redix action + * {@link SET_ROOM} is being dispatched within a specific redux store. + * + * @param {Store} store - The redux store in which the specified action is being + * dispatched. + * @param {Dispatch} next - The redux dispatch function to dispatch the + * specified action to the specified store. + * @param {Action} action - The redux action {@code SET_ROOM} which is being + * dispatched in the specified store. + * @private + * @returns {Object} The value returned by {@code next(action)}. + */ +function _setRoom({ dispatch, getState }, next, action) { + const result = next(action); + + // By the time SET_ROOM is dispatched, base/connection's locationURL and + // base/conference's leaving should be the only conference-related sources + // of truth. + const state = getState(); + const { + leaving, + ...stateFeaturesBaseConference + } = state['features/base/conference']; + const { locationURL } = state['features/base/connection']; + const dispatchConferenceLeft = new Set(); + + // Figure out which of the JitsiConferences referenced by base/conference + // have not dispatched or are not likely to dispatch CONFERENCE_FAILED and + // CONFERENCE_LEFT. + + // eslint-disable-next-line guard-for-in + for (const p in stateFeaturesBaseConference) { + const v = stateFeaturesBaseConference[p]; + + // Does the value of the base/conference's property look like a + // JitsiConference? + if (v && typeof v === 'object') { + const url = v[JITSI_CONFERENCE_URL_KEY]; + + if (url && v !== leaving && url !== locationURL) { + dispatchConferenceLeft.add(v); + } + } + } + + // Dispatch CONFERENCE_LEFT for the JitsiConferences referenced by + // base/conference which have not dispatched or are not likely to dispatch + // CONFERENCE_FAILED or CONFERENCE_LEFT. + for (const conference of dispatchConferenceLeft) { + dispatch(conferenceLeft(conference)); + } + + return result; +} + /** * Synchronizes local tracks from state with local tracks in JitsiConference * instance. diff --git a/react/features/base/connection/actions.native.js b/react/features/base/connection/actions.native.js index caca1b67f..82619bd7c 100644 --- a/react/features/base/connection/actions.native.js +++ b/react/features/base/connection/actions.native.js @@ -3,7 +3,7 @@ import _ from 'lodash'; import type { Dispatch } from 'redux'; -import { conferenceWillLeave } from '../conference'; +import { conferenceLeft, conferenceWillLeave } from '../conference'; import JitsiMeetJS, { JitsiConnectionEvents } from '../lib-jitsi-meet'; import { parseStandardURIString } from '../util'; @@ -281,7 +281,16 @@ export function disconnect() { // intention to leave the conference. dispatch(conferenceWillLeave(conference_)); - promise = conference_.leave(); + promise + = conference_.leave() + .catch(() => { + // The library lib-jitsi-meet failed to make the + // JitsiConference leave. Which may be because + // JitsiConference thinks it has already left. + // Regardless of the failure reason, continue in + // jitsi-meet as if the leave has succeeded. + dispatch(conferenceLeft(conference_)); + }); } else { promise = Promise.resolve(); } diff --git a/react/features/mobile/external-api/middleware.js b/react/features/mobile/external-api/middleware.js index a9aa95811..59d5dc0ad 100644 --- a/react/features/mobile/external-api/middleware.js +++ b/react/features/mobile/external-api/middleware.js @@ -125,11 +125,13 @@ function _getSymbolDescription(symbol: Symbol) { */ function _sendConferenceEvent( store: Object, - { conference, type, ...data }: { + action: { conference: Object, type: Symbol, url: ?string }) { + const { conference, type, ...data } = action; + // For these (redux) actions, conference identifies a JitsiConference // instance. The external API cannot transport such an object so we have to // transport an "equivalent". @@ -137,7 +139,8 @@ function _sendConferenceEvent( data.url = toURLString(conference[JITSI_CONFERENCE_URL_KEY]); } - _sendEvent(store, _getSymbolDescription(type), data); + _swallowEvent(store, action, data) + || _sendEvent(store, _getSymbolDescription(type), data); } /** @@ -169,3 +172,68 @@ function _sendEvent( } } } + +/** + * Determines whether to not send a {@code CONFERENCE_LEFT} event to the native + * counterpart of the External API. + * + * @param {Object} store - The redux store. + * @param {Action} action - The redux action which is causing the sending of the + * event. + * @param {Object} data - The details/specifics of the event to send determined + * by/associated with the specified {@code action}. + * @returns {boolean} If the specified event is to not be sent, {@code true}; + * otherwise, {@code false}. + */ +function _swallowConferenceLeft({ getState }, action, { url }) { + // XXX Internally, we work with JitsiConference instances. Externally + // though, we deal with URL strings. The relation between the two is many to + // one so it's technically and practically possible (by externally loading + // the same URL string multiple times) to try to send CONFERENCE_LEFT + // externally for a URL string which identifies a JitsiConference that the + // app is internally legitimately working with. + + if (url) { + const stateFeaturesBaseConference + = getState()['features/base/conference']; + + // eslint-disable-next-line guard-for-in + for (const p in stateFeaturesBaseConference) { + const v = stateFeaturesBaseConference[p]; + + // Does the value of the base/conference's property look like a + // JitsiConference? + if (v && typeof v === 'object') { + const vURL = v[JITSI_CONFERENCE_URL_KEY]; + + if (vURL && vURL.toString() === url) { + return true; + } + } + } + } + + return false; +} + +/** + * Determines whether to not send a specific event to the native counterpart of + * the External API. + * + * @param {Object} store - The redux store. + * @param {Action} action - The redux action which is causing the sending of the + * event. + * @param {Object} data - The details/specifics of the event to send determined + * by/associated with the specified {@code action}. + * @returns {boolean} If the specified event is to not be sent, {@code true}; + * otherwise, {@code false}. + */ +function _swallowEvent(store, action, data) { + switch (action.type) { + case CONFERENCE_LEFT: + return _swallowConferenceLeft(store, action, data); + + default: + return false; + } +}