123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475 |
- // @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';
- 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 {
- getFirstLoadableAvatarUrl,
- getLocalParticipant,
- 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.
- *
- * @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:
- store.dispatch(localParticipantIdChanged(action.conference.myUserId()));
- break;
-
- case DOMINANT_SPEAKER_CHANGED: {
- // Ensure the raised hand state is cleared for the dominant speaker
- // and only if it was set when this is the local participant
-
- const { conference, id } = action.participant;
- const participant = getLocalParticipant(store.getState());
- const isLocal = participant && participant.id === id;
-
- if (isLocal && participant.raisedHand === undefined) {
- // if local was undefined, let's leave it like that
- // avoids sending unnecessary presence updates
- break;
- }
-
- participant
- && store.dispatch(participantUpdated({
- conference,
- id,
- local: isLocal,
- raisedHand: false
- }));
-
- break;
- }
-
- case GRANT_MODERATOR: {
- const { conference } = store.getState()['features/base/conference'];
-
- conference.grantOwner(action.id);
- break;
- }
-
- case KICK_PARTICIPANT: {
- const { conference } = store.getState()['features/base/conference'];
-
- conference.kickParticipant(action.id);
- break;
- }
-
- case MUTE_REMOTE_PARTICIPANT: {
- const { conference } = store.getState()['features/base/conference'];
-
- conference.muteParticipant(action.id);
- break;
- }
-
- // 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: {
- _maybePlaySounds(store, action);
-
- return _participantJoinedOrUpdated(store, next, action);
- }
-
- case PARTICIPANT_LEFT:
- _maybePlaySounds(store, action);
- break;
-
- 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));
- }
- });
-
- /**
- * 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
- * @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({
- 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.
- *
- * @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
- && (!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));
- }
- }
- }
-
- /**
- * 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)}.
- */
- function _participantJoinedOrUpdated(store, next, action) {
- const { dispatch, getState } = store;
- const { participant: { avatarURL, e2eeEnabled, email, id, local, name, raisedHand } } = action;
-
- // 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'];
-
- // Send raisedHand signalling only if there is a change
- if (conference && raisedHand !== getLocalParticipant(getState()).raisedHand) {
- conference.setLocalParticipantProperty('raisedHand', raisedHand);
- }
- }
- }
-
- // 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);
- }
- }
-
- // 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);
-
- const { disableThirdPartyRequests } = getState()['features/base/config'];
-
- if (!disableThirdPartyRequests && (avatarURL || email || id || name)) {
- const participantId = !id && local ? getLocalParticipant(getState()).id : id;
- const updatedParticipant = getParticipantById(getState(), participantId);
-
- getFirstLoadableAvatarUrl(updatedParticipant, store)
- .then(url => {
- dispatch(setLoadableAvatarUrl(participantId, url));
- });
- }
-
- // Notify external listeners of potential avatarURL changes.
- if (typeof APP === 'object') {
- const currentKnownId = local ? APP.conference.getMyUserId() : id;
-
- // Force update of local video getting a new id.
- APP.UI.refreshAvatarDisplay(currentKnownId);
- }
-
- return result;
- }
-
- /**
- * 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.
- *
- * @param {Store} store - The redux store.
- * @private
- * @returns {void}
- */
- function _registerSounds({ dispatch }) {
- dispatch(
- registerSound(PARTICIPANT_JOINED_SOUND_ID, PARTICIPANT_JOINED_FILE));
- dispatch(registerSound(PARTICIPANT_LEFT_SOUND_ID, PARTICIPANT_LEFT_FILE));
- }
-
- /**
- * Unregisters sounds related with the participants feature.
- *
- * @param {Store} store - The redux store.
- * @private
- * @returns {void}
- */
- function _unregisterSounds({ dispatch }) {
- dispatch(unregisterSound(PARTICIPANT_JOINED_SOUND_ID));
- dispatch(unregisterSound(PARTICIPANT_LEFT_SOUND_ID));
- }
|