jiti-meet/react/features/base/participants/middleware.js

467 lines
15 KiB
JavaScript
Raw Normal View History

// @flow
import UIEvents from '../../../../service/UI/UIEvents';
import { NOTIFICATION_TIMEOUT, showNotification } from '../../notifications';
import { CALLING, INVITED } from '../../presence-status';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app';
import {
CONFERENCE_WILL_JOIN,
forEachConference,
getCurrentConference
} from '../conference';
import { JitsiConferenceEvents } from '../lib-jitsi-meet';
import { MiddlewareRegistry, StateListenerRegistry } from '../redux';
import { playSound, registerSound, unregisterSound } from '../sounds';
import {
DOMINANT_SPEAKER_CHANGED,
GRANT_MODERATOR,
KICK_PARTICIPANT,
MUTE_REMOTE_PARTICIPANT,
PARTICIPANT_DISPLAY_NAME_CHANGED,
PARTICIPANT_JOINED,
PARTICIPANT_LEFT,
PARTICIPANT_UPDATED
} from './actionTypes';
2020-05-20 10:57:03 +00:00
import {
localParticipantIdChanged,
localParticipantJoined,
localParticipantLeft,
participantLeft,
participantUpdated,
setLoadableAvatarUrl
} from './actions';
import {
LOCAL_PARTICIPANT_DEFAULT_ID,
PARTICIPANT_JOINED_SOUND_ID,
PARTICIPANT_LEFT_SOUND_ID
} from './constants';
import {
2019-06-26 14:08:23 +00:00
getFirstLoadableAvatarUrl,
getLocalParticipant,
2019-06-26 14:08:23 +00:00
getParticipantById,
getParticipantCount,
getParticipantDisplayName
} from './functions';
import { PARTICIPANT_JOINED_FILE, PARTICIPANT_LEFT_FILE } from './sounds';
declare var APP: Object;
/**
* Middleware that captures CONFERENCE_JOINED and CONFERENCE_LEFT actions and
* updates respectively ID of local participant.
*
2018-05-22 03:42:22 +00:00
* @param {Store} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case APP_WILL_MOUNT:
_registerSounds(store);
return _localParticipantJoined(store, next, action);
case APP_WILL_UNMOUNT:
_unregisterSounds(store);
return _localParticipantLeft(store, next, action);
case CONFERENCE_WILL_JOIN:
2016-12-12 00:29:13 +00:00
store.dispatch(localParticipantIdChanged(action.conference.myUserId()));
break;
case DOMINANT_SPEAKER_CHANGED: {
// Ensure the raised hand state is cleared for the dominant speaker.
Associate remote participant w/ JitsiConference (_UPDATED) The commit message of "Associate remote participant w/ JitsiConference (_JOINED)" explains the motivation for this commit. Practically, _JOINED and _LEFT combined with "Remove remote participants who are no longer of interest" should alleviate the problem with multiplying remote participants to an acceptable level of annoyance. Technically though, a remote participant cannot be identified by an ID only. The ID is (somewhat) "unique" in the context of a single JitsiConference instance. So in order to not have to scratch our heads over an obscure corner, racing case, it's better to always identify remote participants by the pair id-conference. Unfortunately, that's a bit of a high order given the existing source code. So I've implemented the cases which are the easiest so that new source code written with participantUpdated is more likely to identify a remote participant with the pair id-conference. Additionally, the commit "Reduce direct read access to the features/base/participants redux state" brings more control back to the functions of the feature base/participants so that one day we can (if we choose to) do something like, for example: If getParticipants is called with a conference, it returns the participants from features/base/participants who are associated with the specified conference. If no conference is specified in the function call, then default to the conference which is the primary focus of the app at the time of the function call. Added to the above, this should allow us to further reduce the cases in which we're identifying remote participants by id only and get us even closer to a more "predictable" behavior in corner, racing cases.
2018-05-22 21:47:43 +00:00
const { conference, id } = action.participant;
const participant = getLocalParticipant(store.getState());
Associate remote participant w/ JitsiConference (_UPDATED) The commit message of "Associate remote participant w/ JitsiConference (_JOINED)" explains the motivation for this commit. Practically, _JOINED and _LEFT combined with "Remove remote participants who are no longer of interest" should alleviate the problem with multiplying remote participants to an acceptable level of annoyance. Technically though, a remote participant cannot be identified by an ID only. The ID is (somewhat) "unique" in the context of a single JitsiConference instance. So in order to not have to scratch our heads over an obscure corner, racing case, it's better to always identify remote participants by the pair id-conference. Unfortunately, that's a bit of a high order given the existing source code. So I've implemented the cases which are the easiest so that new source code written with participantUpdated is more likely to identify a remote participant with the pair id-conference. Additionally, the commit "Reduce direct read access to the features/base/participants redux state" brings more control back to the functions of the feature base/participants so that one day we can (if we choose to) do something like, for example: If getParticipants is called with a conference, it returns the participants from features/base/participants who are associated with the specified conference. If no conference is specified in the function call, then default to the conference which is the primary focus of the app at the time of the function call. Added to the above, this should allow us to further reduce the cases in which we're identifying remote participants by id only and get us even closer to a more "predictable" behavior in corner, racing cases.
2018-05-22 21:47:43 +00:00
participant
&& store.dispatch(participantUpdated({
conference,
2018-05-22 03:42:22 +00:00
id,
local: participant.id === id,
raisedHand: false
}));
break;
}
case GRANT_MODERATOR: {
const { conference } = store.getState()['features/base/conference'];
conference.grantOwner(action.id);
break;
}
2018-05-22 03:42:22 +00:00
case KICK_PARTICIPANT: {
const { conference } = store.getState()['features/base/conference'];
conference.kickParticipant(action.id);
break;
2018-05-22 03:42:22 +00:00
}
case MUTE_REMOTE_PARTICIPANT: {
const { conference } = store.getState()['features/base/conference'];
conference.muteParticipant(action.id);
break;
2018-05-22 03:42:22 +00:00
}
// TODO Remove this middleware when the local display name update flow is
// fully brought into redux.
case PARTICIPANT_DISPLAY_NAME_CHANGED: {
if (typeof APP !== 'undefined') {
const participant = getLocalParticipant(store.getState());
if (participant && participant.id === action.id) {
APP.UI.emitEvent(UIEvents.NICKNAME_CHANGED, action.name);
}
}
break;
}
case PARTICIPANT_JOINED: {
2018-05-22 03:42:22 +00:00
_maybePlaySounds(store, action);
return _participantJoinedOrUpdated(store, next, action);
}
2018-05-22 03:42:22 +00:00
case PARTICIPANT_LEFT:
_maybePlaySounds(store, action);
break;
2018-05-22 03:42:22 +00:00
case PARTICIPANT_UPDATED:
return _participantJoinedOrUpdated(store, next, action);
}
return next(action);
});
/**
* Syncs the redux state features/base/participants up with the redux state
* features/base/conference by ensuring that the former does not contain remote
* participants no longer relevant to the latter. Introduced to address an issue
* with multiplying thumbnails in the filmstrip.
*/
StateListenerRegistry.register(
/* selector */ state => getCurrentConference(state),
/* listener */ (conference, { dispatch, getState }) => {
for (const p of getState()['features/base/participants']) {
!p.local
&& (!conference || p.conference !== conference)
&& dispatch(participantLeft(p.id, p.conference));
}
});
fix(base/participants): ensure default local id outside of conference Makes sure that whenever a conference is left or switched, the local participant's id will be equal to the default value. The problem fixed by this commit is a situation where the local participant may end up sharing the same ID with it's "ghost" when rejoining a disconnected conference. The most important and easiest to hit case is when the conference is left after the CONFERENCE_FAILED event. Another rare and harder to encounter in the real world issue is where CONFERENCE_LEFT may come with the delay due to it's asynchronous nature. The step by step scenario is as follows: trying to leave a conference, but the network is not doing well, so it takes time, requests are timing out. After getting back to the welcome page the the CONFERENCE_LEFT has not arrived yet. The same conference is joined again and the load config may timeout, but it will be read from the cache. Now the network gets better and conference is joining which results in our ghost participant added to the redux state. At this point there's the root issue: two participants with the same id, because the local one was neither cleared nor set to the new one yet (PARTICIPANT_JOINED come, before CONFERENCE_JOINED where we adjust the id). Then comes CONFERENCE_JOINED and we try to update our local id. We're updating the ID of both ghost and local participant. It could be also that the delayed CONFERENCE_LEFT comes for the old conference, but it's too late and it would update the id for both participants. The approach here reasons that the ID of the local participant may be reset as soon as the local participant and, respectively, her ID is no longer involved in a recoverable JitsiConference of interest to the user and, consequently, the app. Co-authored-by: Pawel Domas <pawel.domas@jitsi.org> Co-authored-by: Lyubo Marinov <lmarinov@atlassian.com>
2018-05-16 20:08:34 +00:00
/**
* Reset the ID of the local participant to
* {@link LOCAL_PARTICIPANT_DEFAULT_ID}. Such a reset is deemed possible only if
* the local participant and, respectively, her ID is not involved in a
* conference which is still of interest to the user and, consequently, the app.
* For example, a conference which is in the process of leaving is no longer of
* interest the user, is unrecoverable from the perspective of the user and,
* consequently, the app.
*/
StateListenerRegistry.register(
/* selector */ state => state['features/base/conference'],
/* listener */ ({ leaving }, { dispatch, getState }) => {
const state = getState();
const localParticipant = getLocalParticipant(state);
let id;
if (!localParticipant
|| (id = localParticipant.id)
=== LOCAL_PARTICIPANT_DEFAULT_ID) {
// The ID of the local participant has been reset already.
return;
}
// The ID of the local may be reset only if it is not in use.
const dispatchLocalParticipantIdChanged
= forEachConference(
state,
conference =>
conference === leaving || conference.myUserId() !== id);
dispatchLocalParticipantIdChanged
&& dispatch(
localParticipantIdChanged(LOCAL_PARTICIPANT_DEFAULT_ID));
});
/**
* Registers listeners for participant change events.
*/
StateListenerRegistry.register(
state => state['features/base/conference'].conference,
(conference, store) => {
if (conference) {
// We joined a conference
conference.on(
JitsiConferenceEvents.PARTICIPANT_PROPERTY_CHANGED,
(participant, propertyName, oldValue, newValue) => {
switch (propertyName) {
case 'e2eeEnabled':
_e2eeUpdated(store, conference, participant.getId(), newValue);
break;
case 'features_e2ee':
store.dispatch(participantUpdated({
conference,
id: participant.getId(),
e2eeSupported: newValue
}));
break;
case 'features_jigasi':
store.dispatch(participantUpdated({
conference,
id: participant.getId(),
isJigasi: newValue
}));
break;
case 'features_screen-sharing':
store.dispatch(participantUpdated({
conference,
id: participant.getId(),
features: { 'screen-sharing': true }
}));
break;
case 'raisedHand': {
_raiseHandUpdated(store, conference, participant.getId(), newValue);
break;
}
default:
// Ignore for now.
}
});
} else {
const localParticipantId = getLocalParticipant(store.getState).id;
// We left the conference, the local participant must be updated.
_e2eeUpdated(store, conference, localParticipantId, false);
_raiseHandUpdated(store, conference, localParticipantId, false);
}
}
);
/**
* Handles a E2EE enabled status update.
*
* @param {Function} dispatch - The Redux dispatch function.
* @param {Object} conference - The conference for which we got an update.
* @param {string} participantId - The ID of the participant from which we got an update.
* @param {boolean} newValue - The new value of the E2EE enabled status.
* @returns {void}
*/
function _e2eeUpdated({ dispatch }, conference, participantId, newValue) {
const e2eeEnabled = newValue === 'true';
dispatch(participantUpdated({
conference,
id: participantId,
e2eeEnabled
}));
}
/**
* Initializes the local participant and signals that it joined.
*
* @private
2018-05-22 03:42:22 +00:00
* @param {Store} store - The redux store.
* @param {Dispatch} next - The redux dispatch function to dispatch the
* specified action to the specified store.
* @param {Action} action - The redux action which is being dispatched
* in the specified store.
* @private
* @returns {Object} The value returned by {@code next(action)}.
*/
function _localParticipantJoined({ getState, dispatch }, next, action) {
const result = next(action);
const settings = getState()['features/base/settings'];
dispatch(localParticipantJoined({
avatarID: settings.avatarID,
avatarURL: settings.avatarURL,
email: settings.email,
name: settings.displayName
}));
return result;
}
/**
* Signals that the local participant has left.
*
* @param {Store} store - The redux store.
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
* specified {@code action} into the specified {@code store}.
* @param {Action} action - The redux action which is being dispatched in the
* specified {@code store}.
* @private
* @returns {Object} The value returned by {@code next(action)}.
*/
function _localParticipantLeft({ dispatch }, next, action) {
const result = next(action);
dispatch(localParticipantLeft());
return result;
}
/**
* Plays sounds when participants join/leave conference.
*
2018-05-22 03:42:22 +00:00
* @param {Store} store - The redux store.
* @param {Action} action - The redux action. Should be either
* {@link PARTICIPANT_JOINED} or {@link PARTICIPANT_LEFT}.
* @private
* @returns {void}
*/
function _maybePlaySounds({ getState, dispatch }, action) {
const state = getState();
const { startAudioMuted } = state['features/base/config'];
// We're not playing sounds for local participant
// nor when the user is joining past the "startAudioMuted" limit.
// The intention there was to not play user joined notification in big
// conferences where 100th person is joining.
if (!action.participant.local
2018-05-22 03:42:22 +00:00
&& (!startAudioMuted
|| getParticipantCount(state) < startAudioMuted)) {
if (action.type === PARTICIPANT_JOINED) {
const { presence } = action.participant;
// The sounds for the poltergeist are handled by features/invite.
if (presence !== INVITED && presence !== CALLING) {
dispatch(playSound(PARTICIPANT_JOINED_SOUND_ID));
}
} else if (action.type === PARTICIPANT_LEFT) {
dispatch(playSound(PARTICIPANT_LEFT_SOUND_ID));
}
}
}
2018-05-22 03:42:22 +00:00
/**
* Notifies the feature base/participants that the action
* {@code PARTICIPANT_JOINED} or {@code PARTICIPANT_UPDATED} is being dispatched
* within a specific redux store.
*
* @param {Store} store - The redux store in which the specified {@code action}
* is being dispatched.
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
* specified {@code action} in the specified {@code store}.
* @param {Action} action - The redux action {@code PARTICIPANT_JOINED} or
* {@code PARTICIPANT_UPDATED} which is being dispatched in the specified
* {@code store}.
* @private
* @returns {Object} The value returned by {@code next(action)}.
*/
2019-06-26 14:08:23 +00:00
function _participantJoinedOrUpdated({ dispatch, getState }, next, action) {
const { participant: { avatarURL, e2eeEnabled, email, id, local, name, raisedHand } } = action;
2018-05-22 03:42:22 +00:00
// Send an external update of the local participant's raised hand state
// if a new raised hand state is defined in the action.
if (typeof raisedHand !== 'undefined') {
if (local) {
const { conference } = getState()['features/base/conference'];
conference
&& conference.setLocalParticipantProperty(
'raisedHand',
raisedHand);
2018-05-22 03:42:22 +00:00
}
}
// Send an external update of the local participant's E2EE enabled state
// if a new state is defined in the action.
if (typeof e2eeEnabled !== 'undefined') {
if (local) {
const { conference } = getState()['features/base/conference'];
conference && conference.setLocalParticipantProperty('e2eeEnabled', e2eeEnabled);
}
}
2019-06-26 14:08:23 +00:00
// Allow the redux update to go through and compare the old avatar
// to the new avatar and emit out change events if necessary.
const result = next(action);
2018-05-22 03:42:22 +00:00
const { disableThirdPartyRequests } = getState()['features/base/config'];
if (!disableThirdPartyRequests && (avatarURL || email || id || name)) {
2019-06-26 14:08:23 +00:00
const participantId = !id && local ? getLocalParticipant(getState()).id : id;
const updatedParticipant = getParticipantById(getState(), participantId);
2018-05-22 03:42:22 +00:00
2019-06-26 14:08:23 +00:00
getFirstLoadableAvatarUrl(updatedParticipant)
.then(url => {
dispatch(setLoadableAvatarUrl(participantId, url));
});
}
2018-05-22 03:42:22 +00:00
2019-06-26 14:08:23 +00:00
// Notify external listeners of potential avatarURL changes.
if (typeof APP === 'object') {
const currentKnownId = local ? APP.conference.getMyUserId() : id;
2018-05-22 03:42:22 +00:00
2019-06-26 14:08:23 +00:00
// Force update of local video getting a new id.
APP.UI.refreshAvatarDisplay(currentKnownId);
2018-05-22 03:42:22 +00:00
}
2019-06-26 14:08:23 +00:00
return result;
2018-05-22 03:42:22 +00:00
}
/**
* Handles a raise hand status update.
*
* @param {Function} dispatch - The Redux dispatch function.
* @param {Object} conference - The conference for which we got an update.
* @param {string} participantId - The ID of the participant from which we got an update.
* @param {boolean} newValue - The new value of the raise hand status.
* @returns {void}
*/
function _raiseHandUpdated({ dispatch, getState }, conference, participantId, newValue) {
const raisedHand = newValue === 'true';
dispatch(participantUpdated({
conference,
id: participantId,
raisedHand
}));
if (raisedHand) {
dispatch(showNotification({
titleArguments: {
name: getParticipantDisplayName(getState, participantId)
},
titleKey: 'notify.raisedHand'
}, NOTIFICATION_TIMEOUT));
}
}
/**
* Registers sounds related with the participants feature.
*
2018-05-22 03:42:22 +00:00
* @param {Store} store - The redux store.
* @private
* @returns {void}
*/
function _registerSounds({ dispatch }) {
dispatch(
registerSound(PARTICIPANT_JOINED_SOUND_ID, PARTICIPANT_JOINED_FILE));
2018-05-22 03:42:22 +00:00
dispatch(registerSound(PARTICIPANT_LEFT_SOUND_ID, PARTICIPANT_LEFT_FILE));
}
/**
* Unregisters sounds related with the participants feature.
*
2018-05-22 03:42:22 +00:00
* @param {Store} store - The redux store.
* @private
* @returns {void}
*/
function _unregisterSounds({ dispatch }) {
2018-05-22 03:42:22 +00:00
dispatch(unregisterSound(PARTICIPANT_JOINED_SOUND_ID));
dispatch(unregisterSound(PARTICIPANT_LEFT_SOUND_ID));
}