Merge pull request #3009 from jitsi/state-listener

[RN] Prevent multiplying remote thumbnails
This commit is contained in:
Zoltan Bettenbuk 2018-05-23 18:48:16 +02:00 committed by GitHub
commit 480fe53001
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 546 additions and 247 deletions

View File

@ -1688,6 +1688,7 @@ export default {
const displayName = user.getDisplayName();
APP.store.dispatch(participantJoined({
conference: room,
id,
name: displayName,
role: user.getRole()
@ -1709,7 +1710,7 @@ export default {
if (user.isHidden()) {
return;
}
APP.store.dispatch(participantLeft(id, user));
APP.store.dispatch(participantLeft(id, room));
logger.log('USER %s LEFT', id, user);
APP.API.notifyUserLeft(id);
APP.UI.removeUser(id, user.getDisplayName());
@ -1803,9 +1804,9 @@ export default {
APP.UI.participantConnectionStatusChanged(id);
});
room.on(JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED, id => {
APP.store.dispatch(dominantSpeakerChanged(id));
});
room.on(
JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED,
id => APP.store.dispatch(dominantSpeakerChanged(id, room)));
if (!interfaceConfig.filmStripOnly) {
room.on(JitsiConferenceEvents.CONNECTION_INTERRUPTED, () => {
@ -1882,6 +1883,7 @@ export default {
= displayName.substr(0, MAX_DISPLAY_NAME_LENGTH);
APP.store.dispatch(participantUpdated({
conference: room,
id,
name: formattedDisplayName
}));
@ -1915,6 +1917,7 @@ export default {
switch (name) {
case 'raisedHand':
APP.store.dispatch(participantUpdated({
conference: room,
id: participant.getId(),
raisedHand: newValue === 'true'
}));
@ -2014,6 +2017,7 @@ export default {
APP.UI.addListener(UIEvents.EMAIL_CHANGED, this.changeLocalEmail);
room.addCommandListener(this.commands.defaults.EMAIL, (data, from) => {
APP.store.dispatch(participantUpdated({
conference: room,
id: from,
email: data.value
}));
@ -2025,6 +2029,7 @@ export default {
(data, from) => {
APP.store.dispatch(
participantUpdated({
conference: room,
id: from,
avatarURL: data.value
}));
@ -2034,6 +2039,7 @@ export default {
(data, from) => {
APP.store.dispatch(
participantUpdated({
conference: room,
id: from,
avatarID: data.value
}));
@ -2578,6 +2584,12 @@ export default {
const localId = localParticipant.id;
APP.store.dispatch(participantUpdated({
// XXX Only the local participant is allowed to update without
// stating the JitsiConference instance (i.e. participant property
// `conference` for a remote participant) because the local
// participant is uniquely identified by the very fact that there is
// only one local participant.
id: localId,
local: true,
email: formattedEmail
@ -2605,6 +2617,12 @@ export default {
}
APP.store.dispatch(participantUpdated({
// XXX Only the local participant is allowed to update without
// stating the JitsiConference instance (i.e. participant property
// `conference` for a remote participant) because the local
// participant is uniquely identified by the very fact that there is
// only one local participant.
id,
local: true,
avatarURL: formattedUrl
@ -2661,6 +2679,12 @@ export default {
}
APP.store.dispatch(participantUpdated({
// XXX Only the local participant is allowed to update without
// stating the JitsiConference instance (i.e. participant property
// `conference` for a remote participant) because the local
// participant is uniquely identified by the very fact that there is
// only one local participant.
id,
local: true,
name: formattedNickname

View File

@ -306,6 +306,7 @@ export default class SharedVideoManager {
SHARED_VIDEO_CONTAINER_TYPE, self.sharedVideo);
APP.store.dispatch(participantJoined({
conference: APP.conference,
id: self.url,
isBot: true,
name: 'YouTube'
@ -516,7 +517,7 @@ export default class SharedVideoManager {
UIEvents.UPDATE_SHARED_VIDEO, null, 'removed');
});
APP.store.dispatch(participantLeft(this.url));
APP.store.dispatch(participantLeft(this.url, APP.conference));
this.url = null;
this.isSharedVideoShown = false;

21
package-lock.json generated
View File

@ -10556,13 +10556,13 @@
}
},
"react-redux": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-5.0.6.tgz",
"integrity": "sha512-8taaaGu+J7PMJQDJrk/xiWEYQmdo3mkXw6wPr3K3LxvXis3Fymiq7c13S+Tpls/AyNUAsoONkU81AP0RA6y6Vw==",
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-5.0.7.tgz",
"integrity": "sha512-5VI8EV5hdgNgyjfmWzBbdrqUkrVRKlyTKk1sGH3jzM2M2Mhj/seQgPXaz6gVAj2lz/nz688AdTqMO18Lr24Zhg==",
"requires": {
"hoist-non-react-statics": "2.5.0",
"invariant": "2.2.3",
"lodash": "4.17.4",
"lodash": "4.17.10",
"lodash-es": "4.17.5",
"loose-envify": "1.3.1",
"prop-types": "15.6.0"
@ -10572,6 +10572,11 @@
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.0.tgz",
"integrity": "sha512-6Bl6XsDT1ntE0lHbIhr4Kp2PGcleGZ66qu5Jqk8lc0Xc/IeG6gVLmwUGs/K0Us+L8VWoKgj0uWdPMataOsm31w=="
},
"lodash": {
"version": "4.17.10",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz",
"integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg=="
}
}
},
@ -10764,12 +10769,10 @@
}
},
"redux": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz",
"integrity": "sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/redux/-/redux-4.0.0.tgz",
"integrity": "sha512-NnnHF0h0WVE/hXyrB6OlX67LYRuaf/rJcbWvnHHEPCF/Xa/AZpwhs/20WyqzQae5x4SD2F9nPObgBh2rxAgLiA==",
"requires": {
"lodash": "4.17.4",
"lodash-es": "4.17.5",
"loose-envify": "1.3.1",
"symbol-observable": "1.2.0"
}

View File

@ -67,8 +67,8 @@
"react-native-sound": "0.10.9",
"react-native-vector-icons": "4.4.2",
"react-native-webrtc": "github:jitsi/react-native-webrtc#52fe4646401408e0569e972cabf08f3c21b7a107",
"react-redux": "5.0.6",
"redux": "3.7.2",
"react-redux": "5.0.7",
"redux": "4.0.0",
"redux-thunk": "2.2.0",
"strophe.js": "github:jitsi/strophejs#1.2.14-1",
"strophejs-plugin-disco": "0.0.2",

View File

@ -10,7 +10,11 @@ import Thunk from 'redux-thunk';
import { i18next } from '../../base/i18n';
import { localParticipantLeft } from '../../base/participants';
import { Fragment, RouteRegistry } from '../../base/react';
import { MiddlewareRegistry, ReducerRegistry } from '../../base/redux';
import {
MiddlewareRegistry,
ReducerRegistry,
StateListenerRegistry
} from '../../base/redux';
import { SoundCollection } from '../../base/sounds';
import { PersistenceRegistry } from '../../base/storage';
import { toURLString } from '../../base/util';
@ -386,14 +390,13 @@ export class AbstractApp extends Component {
* @returns {Store} - The redux store to be used by this
* {@code AbstractApp}.
*/
_maybeCreateStore(props) {
_maybeCreateStore({ store }) {
// The application Jitsi Meet is architected with redux. However, I do
// not want consumers of the App React Component to be forced into
// dealing with redux. If the consumer did not provide an external redux
// store, utilize an internal redux store.
let store = props.store;
if (typeof store === 'undefined') {
// eslint-disable-next-line no-param-reassign
store = this._createStore();
// This is temporary workaround to be able to dispatch actions from
@ -405,6 +408,9 @@ export class AbstractApp extends Component {
}
}
// StateListenerRegistry
store && StateListenerRegistry.subscribe(store);
return store;
}

View File

@ -125,13 +125,14 @@ function _addConferenceListeners(conference, dispatch) {
conference.on(
JitsiConferenceEvents.DISPLAY_NAME_CHANGED,
(id, displayName) => dispatch(participantUpdated({
conference,
id,
name: displayName.substr(0, MAX_DISPLAY_NAME_LENGTH)
})));
conference.on(
JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED,
(...args) => dispatch(dominantSpeakerChanged(...args)));
id => dispatch(dominantSpeakerChanged(id, conference)));
conference.on(
JitsiConferenceEvents.PARTICIPANT_CONN_STATUS_CHANGED,
@ -140,13 +141,14 @@ function _addConferenceListeners(conference, dispatch) {
conference.on(
JitsiConferenceEvents.USER_JOINED,
(id, user) => dispatch(participantJoined({
conference,
id,
name: user.getDisplayName(),
role: user.getRole()
})));
conference.on(
JitsiConferenceEvents.USER_LEFT,
(...args) => dispatch(participantLeft(...args)));
id => dispatch(participantLeft(id, conference)));
conference.on(
JitsiConferenceEvents.USER_ROLE_CHANGED,
(...args) => dispatch(participantRoleChanged(...args)));
@ -154,18 +156,21 @@ function _addConferenceListeners(conference, dispatch) {
conference.addCommandListener(
AVATAR_ID_COMMAND,
(data, id) => dispatch(participantUpdated({
conference,
id,
avatarID: data.value
})));
conference.addCommandListener(
AVATAR_URL_COMMAND,
(data, id) => dispatch(participantUpdated({
conference,
id,
avatarURL: data.value
})));
conference.addCommandListener(
EMAIL_COMMAND,
(data, id) => dispatch(participantUpdated({
conference,
id,
email: data.value
})));

View File

@ -143,7 +143,10 @@ function _overwriteLocalParticipant(
if ((avatarURL || email || name)
&& (localParticipant = getLocalParticipant(getState))) {
const newProperties: Object = { id: localParticipant.id };
const newProperties: Object = {
id: localParticipant.id,
local: true
};
if (avatarURL) {
newProperties.avatarURL = avatarURL;
@ -264,7 +267,10 @@ function _undoOverwriteLocalParticipant(
if ((avatarURL || name || email)
&& (localParticipant = getLocalParticipant(getState))) {
const newProperties: Object = { id: localParticipant.id };
const newProperties: Object = {
id: localParticipant.id,
local: true
};
if (avatarURL === localParticipant.avatarURL) {
newProperties.avatarURL = undefined;

View File

@ -22,17 +22,23 @@ import { getLocalParticipant } from './functions';
* Create an action for when dominant speaker changes.
*
* @param {string} id - Participant's ID.
* @param {JitsiConference} conference - The {@code JitsiConference} associated
* with the participant identified by the specified {@code id}. Only the local
* participant is allowed to not specify an associated {@code JitsiConference}
* instance.
* @returns {{
* type: DOMINANT_SPEAKER_CHANGED,
* participant: {
* conference: JitsiConference,
* id: string
* }
* }}
*/
export function dominantSpeakerChanged(id) {
export function dominantSpeakerChanged(id, conference) {
return {
type: DOMINANT_SPEAKER_CHANGED,
participant: {
conference,
id
}
};
@ -123,7 +129,19 @@ export function localParticipantLeft() {
const participant = getLocalParticipant(getState);
if (participant) {
return dispatch(participantLeft(participant.id));
return (
dispatch(
participantLeft(
participant.id,
// XXX Only the local participant is allowed to leave
// without stating the JitsiConference instance because
// the local participant is uniquely identified by the
// very fact that there is only one local participant
// (and the fact that the local participant "joins" at
// the beginning of the app and "leaves" at the end of
// the app).
undefined)));
}
};
}
@ -214,11 +232,16 @@ export function participantDisplayNameChanged(id, displayName = '') {
*
* @param {Participant} participant - Information about participant.
* @returns {{
* type: PARTICIPANT_JOINED,
* participant: Participant
* type: PARTICIPANT_JOINED,
* participant: Participant
* }}
*/
export function participantJoined(participant) {
if (!participant.local && !participant.conference) {
throw Error(
'A remote participant must be associated with a JitsiConference!');
}
return {
type: PARTICIPANT_JOINED,
participant
@ -229,17 +252,23 @@ export function participantJoined(participant) {
* Action to signal that a participant has left.
*
* @param {string} id - Participant's ID.
* @param {JitsiConference} conference - The {@code JitsiConference} associated
* with the participant identified by the specified {@code id}. Only the local
* participant is allowed to not specify an associated {@code JitsiConference}
* instance.
* @returns {{
* type: PARTICIPANT_LEFT,
* participant: {
* conference: JitsiConference,
* id: string
* }
* }}
*/
export function participantLeft(id) {
export function participantLeft(id, conference) {
return {
type: PARTICIPANT_LEFT,
participant: {
conference,
id
}
};
@ -393,7 +422,5 @@ export function showParticipantJoinedNotification(displayName) {
joinedParticipantsNames.push(
displayName || interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME);
return dispatch => {
_throttledNotifyParticipantConnected(dispatch);
};
return dispatch => _throttledNotifyParticipantConnected(dispatch);
}

View File

@ -313,10 +313,7 @@ function _toBoolean(value, undefinedValue) {
*/
function _mapStateToProps(state, ownProps) {
const { participantId } = ownProps;
const participant
= getParticipantById(
state['features/base/participants'],
participantId);
const participant = getParticipantById(state, participantId);
let avatar;
let connectionStatus;
let participantName;

View File

@ -127,9 +127,7 @@ export function getLocalParticipant(stateful: Object | Function) {
* @private
* @returns {(Participant|undefined)}
*/
export function getParticipantById(
stateful: Object | Function,
id: string) {
export function getParticipantById(stateful: Object | Function, id: string) {
const participants = _getAllParticipants(stateful);
return participants.find(p => p.id === id);
@ -242,11 +240,8 @@ export function isLocalParticipantModerator(stateful: Object | Function) {
return false;
}
const isModerator = localParticipant.role === PARTICIPANT_ROLE.MODERATOR;
if (state['features/base/config'].enableUserRolesBasedOnToken) {
return isModerator && !state['features/base/jwt'].isGuest;
}
return isModerator;
return (
localParticipant.role === PARTICIPANT_ROLE.MODERATOR
&& (!state['features/base/config'].enableUserRolesBasedOnToken
|| !state['features/base/jwt'].isGuest));
}

View File

@ -1,18 +1,15 @@
// @flow
import UIEvents from '../../../../service/UI/UIEvents';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../app';
import {
CONFERENCE_WILL_JOIN,
CONFERENCE_LEFT
} from '../conference';
import { MiddlewareRegistry } from '../redux';
import { CONFERENCE_LEFT, CONFERENCE_WILL_JOIN } from '../conference';
import { MiddlewareRegistry, StateListenerRegistry } from '../redux';
import UIEvents from '../../../../service/UI/UIEvents';
import { playSound, registerSound, unregisterSound } from '../sounds';
import {
localParticipantIdChanged,
localParticipantJoined,
participantLeft,
participantUpdated
} from './actions';
import {
@ -43,17 +40,10 @@ declare var APP: Object;
* Middleware that captures CONFERENCE_JOINED and CONFERENCE_LEFT actions and
* updates respectively ID of local participant.
*
* @param {Store} store - Redux store.
* @param {Store} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
const { conference } = store.getState()['features/base/conference'];
if (action.type === PARTICIPANT_JOINED
|| action.type === PARTICIPANT_LEFT) {
_maybePlaySounds(store, action);
}
switch (action.type) {
case APP_WILL_MOUNT:
_registerSounds(store);
@ -74,32 +64,36 @@ MiddlewareRegistry.register(store => next => action => {
case DOMINANT_SPEAKER_CHANGED: {
// Ensure the raised hand state is cleared for the dominant speaker.
const { conference, id } = action.participant;
const participant = getLocalParticipant(store.getState());
if (participant) {
const local = participant.id === action.participant.id;
store.dispatch(participantUpdated({
id: action.participant.id,
local,
participant
&& store.dispatch(participantUpdated({
conference,
id,
local: participant.id === id,
raisedHand: false
}));
}
if (typeof APP === 'object') {
APP.UI.markDominantSpeaker(action.participant.id);
}
typeof APP === 'object' && APP.UI.markDominantSpeaker(id);
break;
}
case KICK_PARTICIPANT:
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'];
case MUTE_REMOTE_PARTICIPANT:
conference.muteParticipant(action.id);
break;
}
// TODO Remove this middleware when the local display name update flow is
// fully brought into redux.
@ -116,73 +110,46 @@ MiddlewareRegistry.register(store => next => action => {
}
case PARTICIPANT_JOINED:
case PARTICIPANT_UPDATED: {
const { participant } = action;
const { id, local, raisedHand } = participant;
_maybePlaySounds(store, 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) {
conference.setLocalParticipantProperty(
'raisedHand',
raisedHand);
}
if (typeof APP === 'object') {
if (local) {
APP.UI.onLocalRaiseHandChanged(raisedHand);
APP.UI.setLocalRaisedHandStatus(raisedHand);
} else {
const remoteParticipant
= getParticipantById(store.getState(), id);
remoteParticipant
&& APP.UI.setRaisedHandStatus(
remoteParticipant.id,
remoteParticipant.name,
raisedHand);
}
}
}
// Notify external listeners of potential avatarURL changes.
if (typeof APP === 'object') {
const preUpdateAvatarURL
= getAvatarURLByParticipantId(store.getState(), id);
// 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 postUpdateAvatarURL
= getAvatarURLByParticipantId(store.getState(), id);
if (preUpdateAvatarURL !== postUpdateAvatarURL) {
const currentKnownId = local
? APP.conference.getMyUserId() : id;
APP.UI.refreshAvatarDisplay(
currentKnownId, postUpdateAvatarURL);
APP.API.notifyAvatarChanged(
currentKnownId, postUpdateAvatarURL);
}
return result;
}
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 => {
const { conference, joining } = state['features/base/conference'];
return conference || joining;
},
/* listener */ (conference, { dispatch, getState }) => {
for (const p of getState()['features/base/participants']) {
!p.local
&& (!conference || p.conference !== conference)
&& dispatch(participantLeft(p.id, p.conference));
}
});
/**
* Initializes the local participant and signals that it joined.
*
* @private
* @param {Store} store - The Redux store.
* @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
@ -192,15 +159,15 @@ MiddlewareRegistry.register(store => next => action => {
*/
function _localParticipantJoined({ getState, dispatch }, next, action) {
const result = next(action);
const settings = getState()['features/base/settings'];
const localParticipant = {
dispatch(localParticipantJoined({
avatarID: settings.avatarID,
avatarURL: settings.avatarURL,
email: settings.email,
name: settings.displayName
};
dispatch(localParticipantJoined(localParticipant));
}));
return result;
}
@ -208,8 +175,8 @@ function _localParticipantJoined({ getState, dispatch }, next, action) {
/**
* Plays sounds when participants join/leave conference.
*
* @param {Store} store - The Redux store.
* @param {Action} action - The Redux action. Should be either
* @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}
@ -223,8 +190,8 @@ function _maybePlaySounds({ getState, dispatch }, action) {
// 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)) {
&& (!startAudioMuted
|| getParticipantCount(state) < startAudioMuted)) {
if (action.type === PARTICIPANT_JOINED) {
dispatch(playSound(PARTICIPANT_JOINED_SOUND_ID));
} else if (action.type === PARTICIPANT_LEFT) {
@ -233,30 +200,95 @@ function _maybePlaySounds({ getState, dispatch }, action) {
}
}
/**
* 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({ getState }, next, action) {
const { participant: { id, local, 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'];
conference
&& conference.setLocalParticipantProperty(
'raisedHand',
raisedHand);
}
if (typeof APP === 'object') {
if (local) {
APP.UI.onLocalRaiseHandChanged(raisedHand);
APP.UI.setLocalRaisedHandStatus(raisedHand);
} else {
const remoteParticipant = getParticipantById(getState(), id);
remoteParticipant
&& APP.UI.setRaisedHandStatus(
remoteParticipant.id,
remoteParticipant.name,
raisedHand);
}
}
}
// Notify external listeners of potential avatarURL changes.
if (typeof APP === 'object') {
const oldAvatarURL = getAvatarURLByParticipantId(getState(), id);
// 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 newAvatarURL = getAvatarURLByParticipantId(getState(), id);
if (oldAvatarURL !== newAvatarURL) {
const currentKnownId = local ? APP.conference.getMyUserId() : id;
APP.UI.refreshAvatarDisplay(currentKnownId, newAvatarURL);
APP.API.notifyAvatarChanged(currentKnownId, newAvatarURL);
}
return result;
}
return next(action);
}
/**
* Registers sounds related with the participants feature.
*
* @param {Store} store - The Redux store.
* @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));
dispatch(registerSound(PARTICIPANT_LEFT_SOUND_ID, PARTICIPANT_LEFT_FILE));
}
/**
* Unregisters sounds related with the participants feature.
*
* @param {Store} store - The Redux store.
* @param {Store} store - The redux store.
* @private
* @returns {void}
*/
function _unregisterSounds({ dispatch }) {
dispatch(
unregisterSound(PARTICIPANT_JOINED_SOUND_ID));
dispatch(
unregisterSound(PARTICIPANT_LEFT_SOUND_ID));
dispatch(unregisterSound(PARTICIPANT_JOINED_SOUND_ID));
dispatch(unregisterSound(PARTICIPANT_LEFT_SOUND_ID));
}

View File

@ -32,12 +32,65 @@ import { LOCAL_PARTICIPANT_DEFAULT_ID, PARTICIPANT_ROLE } from './constants';
declare var APP: Object;
/**
* These properties should not be bulk assigned when updating a particular
* @see Participant.
* The participant properties which cannot be updated through
* {@link PARTICIPANT_UPDATED}. They either identify the participant or can only
* be modified through property-dedicated actions.
*
* @type {string[]}
*/
const PARTICIPANT_PROPS_TO_OMIT_WHEN_UPDATE
= [ 'dominantSpeaker', 'id', 'local', 'pinned' ];
const PARTICIPANT_PROPS_TO_OMIT_WHEN_UPDATE = [
// The following properties identify the participant:
'conference',
'id',
'local',
// The following properties can only be modified through property-dedicated
// actions:
'dominantSpeaker',
'pinned'
];
/**
* Listen for actions which add, remove, or update the set of participants in
* the conference.
*
* @param {Participant[]} state - List of participants to be modified.
* @param {Object} action - Action object.
* @param {string} action.type - Type of action.
* @param {Participant} action.participant - Information about participant to be
* added/removed/modified.
* @returns {Participant[]}
*/
ReducerRegistry.register('features/base/participants', (state = [], action) => {
switch (action.type) {
case DOMINANT_SPEAKER_CHANGED:
case PARTICIPANT_ID_CHANGED:
case PARTICIPANT_UPDATED:
case PIN_PARTICIPANT:
return state.map(p => _participant(p, action));
case PARTICIPANT_JOINED:
return [ ...state, _participantJoined(action) ];
case PARTICIPANT_LEFT: {
// XXX A remote participant is uniquely identified by their id in a
// specific JitsiConference instance. The local participant is uniquely
// identified by the very fact that there is only one local participant
// (and the fact that the local participant "joins" at the beginning of
// the app and "leaves" at the end of the app).
const { conference, id } = action.participant;
return state.filter(p =>
!(
p.id === id
&& (p.local
|| (conference && p.conference === conference))));
}
}
return state;
});
/**
* Reducer function for a single participant.
@ -67,52 +120,6 @@ function _participant(state: Object = {}, action) {
}
break;
case PARTICIPANT_JOINED: {
const { participant } = action; // eslint-disable-line no-shadow
const {
avatarURL,
connectionStatus,
dominantSpeaker,
email,
isBot,
local,
name,
pinned,
role
} = participant;
let { avatarID, id } = participant;
// avatarID
//
// TODO Get the avatarID of the local participant from localStorage.
if (!avatarID && local) {
avatarID = randomHexString(32);
}
// id
//
// XXX The situation of not having an ID for a remote participant should
// not happen. Maybe we should raise an error in this case or generate a
// random ID.
if (!id && local) {
id = LOCAL_PARTICIPANT_DEFAULT_ID;
}
return {
avatarID,
avatarURL,
connectionStatus,
dominantSpeaker: dominantSpeaker || false,
email,
id,
isBot,
local: local || false,
name,
pinned: pinned || false,
role: role || PARTICIPANT_ROLE.NONE
};
}
case PARTICIPANT_UPDATED: {
const { participant } = action; // eslint-disable-line no-shadow
let { id } = participant;
@ -147,31 +154,60 @@ function _participant(state: Object = {}, action) {
}
/**
* Listen for actions which add, remove, or update the set of participants in
* the conference.
* Reduces a specific redux action of type {@link PARTICIPANT_JOINED} in the
* feature base/participants.
*
* @param {Participant[]} state - List of participants to be modified.
* @param {Object} action - Action object.
* @param {string} action.type - Type of action.
* @param {Participant} action.participant - Information about participant to be
* added/removed/modified.
* @returns {Participant[]}
* @param {Action} action - The redux action of type {@code PARTICIPANT_JOINED}
* to reduce.
* @private
* @returns {Object} The new participant derived from the payload of the
* specified {@code action} to be added into the redux state of the feature
* base/participants after the reduction of the specified
* {@code action}.
*/
ReducerRegistry.register('features/base/participants', (state = [], action) => {
switch (action.type) {
case DOMINANT_SPEAKER_CHANGED:
case PARTICIPANT_ID_CHANGED:
case PARTICIPANT_UPDATED:
case PIN_PARTICIPANT:
return state.map(p => _participant(p, action));
function _participantJoined({ participant }) {
const {
avatarURL,
connectionStatus,
dominantSpeaker,
email,
isBot,
local,
name,
pinned,
role
} = participant;
let { avatarID, conference, id } = participant;
case PARTICIPANT_JOINED:
return [ ...state, _participant(undefined, action) ];
if (local) {
// avatarID
//
// TODO Get the avatarID of the local participant from localStorage.
avatarID || (avatarID = randomHexString(32));
case PARTICIPANT_LEFT:
return state.filter(p => p.id !== action.participant.id);
// conference
//
// XXX The local participant is not identified in association with a
// JitsiConference because it is identified by the very fact that it is
// the local participant.
conference = undefined;
default:
return state;
// id
id || (id = LOCAL_PARTICIPANT_DEFAULT_ID);
}
});
return {
avatarID,
avatarURL,
conference,
connectionStatus,
dominantSpeaker: dominantSpeaker || false,
email,
id,
isBot,
local: local || false,
name,
pinned: pinned || false,
role: role || PARTICIPANT_ROLE.NONE
};
}

View File

@ -0,0 +1,159 @@
// @flow
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* The type listener supported for registration with
* {@link StateListenerRegistry} in association with a {@link Selector}.
*
* @param {any} selection - The value derived from the redux store/state by the
* associated {@code Selector}. Immutable!
* @param {Store} store - The redux store. Provided in case the {@code Listener}
* needs to {@code dispatch} or {@code getState}. The latter is advisable only
* if the {@code Listener} is not to respond to changes to that state.
* @param {any} prevSelection - The value previously derived from the redux
* store/state by the associated {@code Selector}. The {@code Listener} is
* invoked only if {@code prevSelection} and {@code selection} are different.
* Immutable!
*/
type Listener = (selection: any, store: Store, prevSelection: any) => void;
/**
* The type selector supported for registration with
* {@link StateListenerRegistry} in association with a {@link Listener}.
*
* @param {Object} state - The redux state from which the {@code Selector} is to
* derive data.
* @param {any} prevSelection - The value previously derived from the redux
* store/state by the {@code Selector}. Provided in case the {@code Selector}
* needs to derive the returned value from the specified {@code state} and
* {@code prevSelection}. Immutable!
* @returns {any} The value derived from the specified {@code state} and/or
* {@code prevSelection}. The associated {@code Listener} will only be invoked
* if the returned value is other than {@code prevSelection}.
*/
type Selector = (state: Object, prevSelection: any) => any;
/**
* A type of a {@link Selector}-{@link Listener} association in which the
* {@code Listener} listens to changes in the values derived from a redux
* store/state by the {@code Selector}.
*/
type SelectorListener = {
/**
* The {@code Listener} which listens to changes in the values selected by
* {@link selector}.
*/
listener: Listener,
/**
* The {@code Selector} which selects values whose changes are listened to
* by {@link listener}.
*/
selector: Selector
};
/**
* A registry listeners which listen to changes in a redux store/state.
*/
class StateListenerRegistry {
/**
* The {@link Listener}s registered with this {@code StateListenerRegistry}
* to be notified when the values derived by associated {@link Selector}s
* from a redux store/state change.
*/
_selectorListeners: Set<SelectorListener> = new Set();
_listener: (Store) => void;
/**
* Invoked by a specific redux store any time an action is dispatched, and
* some part of the state (tree) may potentially have changed.
*
* @param {Object} context - The redux store invoking the listener and the
* private state of this {@code StateListenerRegistry} associated with the
* redux store.
* @returns {void}
*/
_listener({ prevSelections, store }: {
prevSelections: Map<SelectorListener, any>,
store: Store
}) {
for (const selectorListener of this._selectorListeners) {
const prevSelection = prevSelections.get(selectorListener);
try {
const selection
= selectorListener.selector(
store.getState(),
prevSelection);
if (prevSelection !== selection) {
prevSelections.set(selectorListener, selection);
selectorListener.listener(selection, store, prevSelection);
}
} catch (e) {
// Don't let one faulty listener prevent other listeners from
// being notified about their associated changes.
logger.error(e);
}
}
}
/**
* Registers a specific listener to be notified when the value derived by a
* specific {@code selector} from a redux store/state changes.
*
* @param {Function} selector - The pure {@code Function} of the redux
* store/state (and the previous selection of made by {@code selector})
* which selects the value listened to by the specified {@code listener}.
* @param {Function} listener - The listener to register with this
* {@code StateListenerRegistry} so that it gets invoked when the value
* returned by the specified {@code selector} changes.
* @returns {void}
*/
register(selector: Selector, listener: Listener) {
this._selectorListeners.add({
listener,
selector
});
}
/**
* Subscribes to a specific redux store (so that this instance gets notified
* any time an action is dispatched, and some part of the state (tree) of
* the specified redux store may potentially have changed).
*
* @param {Store} store - The redux store to which this
* {@code StateListenerRegistry} is to {@code subscribe}.
* @returns {void}
*/
subscribe(store: Store) {
// XXX If StateListenerRegistry is not utilized by the app to listen to
// state changes, do not bother subscribing to the store at all.
if (this._selectorListeners.size) {
store.subscribe(
this._listener.bind(
this,
{
/**
* The previous selections of the {@code Selector}s
* registered with this {@code StateListenerRegistry}.
*
* @type Map<any>
*/
prevSelections: new Map(),
/**
* The redux store.
*
* @type Store
*/
store
}));
}
}
}
export default new StateListenerRegistry();

View File

@ -1,3 +1,4 @@
export * from './functions';
export { default as MiddlewareRegistry } from './MiddlewareRegistry';
export { default as ReducerRegistry } from './ReducerRegistry';
export { default as StateListenerRegistry } from './StateListenerRegistry';

View File

@ -10,6 +10,7 @@ import { appNavigate } from '../../app';
import { connect, disconnect } from '../../base/connection';
import { DialogContainer } from '../../base/dialog';
import { CalleeInfoContainer } from '../../base/jwt';
import { getParticipantCount } from '../../base/participants';
import { Container, LoadingIndicator, TintedView } from '../../base/react';
import { TestConnectionInfo } from '../../base/testing';
import { createDesiredLocalTracks } from '../../base/tracks';
@ -383,7 +384,6 @@ function _mapStateToProps(state) {
const { connecting, connection } = state['features/base/connection'];
const { conference, joining, leaving } = state['features/base/conference'];
const { reducedUI } = state['features/base/responsive-ui'];
const participants = state['features/base/participants'];
// XXX There is a window of time between the successful establishment of the
// XMPP connection and the subsequent commencement of joining the MUC during
@ -415,7 +415,7 @@ function _mapStateToProps(state) {
* @private
* @type {number}
*/
_participantCount: participants.length,
_participantCount: getParticipantCount(state),
/**
* The indicator which determines whether the UI is reduced (to

View File

@ -1,6 +1,9 @@
// @flow
import { getPinnedParticipant } from '../base/participants';
import {
getParticipantCount,
getPinnedParticipant
} from '../base/participants';
declare var interfaceConfig: Object;
@ -13,8 +16,7 @@ declare var interfaceConfig: Object;
* in the filmstrip, then {@code true}; otherwise, {@code false}.
*/
export function shouldRemoteVideosBeVisible(state: Object) {
const participants = state['features/base/participants'];
const participantCount = participants.length;
const participantCount = getParticipantCount(state);
let pinnedParticipant;
return Boolean(
@ -26,7 +28,7 @@ export function shouldRemoteVideosBeVisible(state: Object) {
|| (participantCount > 1
&& (state['features/filmstrip'].hovered
|| state['features/toolbox'].visible
|| ((pinnedParticipant = getPinnedParticipant(participants))
|| ((pinnedParticipant = getPinnedParticipant(state))
&& pinnedParticipant.local)))
|| (typeof interfaceConfig === 'object'

View File

@ -236,10 +236,10 @@ function _mapStateToProps(state) {
return {
_dialIn: state['features/invite'],
_disableAutoShow: state['features/base/config'].iAmRecorder,
_liveStreamViewURL: currentLiveStreamingSession
&& currentLiveStreamingSession.liveStreamViewURL,
_participantCount:
getParticipantCount(state['features/base/participants']),
_liveStreamViewURL:
currentLiveStreamingSession
&& currentLiveStreamingSession.liveStreamViewURL,
_participantCount: getParticipantCount(state),
_toolboxVisible: state['features/toolbox'].visible
};
}

View File

@ -118,15 +118,13 @@ MiddlewareRegistry.register(store => next => action => {
* @returns {string} - The presence status.
*/
function _getParticipantPresence(state, id) {
if (!id) {
return undefined;
}
const participants = state['features/base/participants'];
const participantById = getParticipantById(participants, id);
if (id) {
const participantById = getParticipantById(state, id);
if (!participantById) {
return undefined;
if (participantById) {
return participantById.presence;
}
}
return participantById.presence;
return undefined;
}

View File

@ -1,7 +1,6 @@
/* @flow */
import { APP_WILL_MOUNT } from '../../app';
import { CONFERENCE_FAILED, CONFERENCE_LEFT } from '../../base/conference';
import {
getAvatarURL,
getLocalParticipant,
@ -37,8 +36,15 @@ const _PREFETCH_AVATAR_URLS = false;
MiddlewareRegistry.register(({ getState }) => next => action => {
switch (action.type) {
case APP_WILL_MOUNT:
case CONFERENCE_FAILED:
case CONFERENCE_LEFT:
// XXX CONFERENCE_FAILED/LEFT are no longer used here because they
// are tricky to get right as detectors of the moments in time at which
// CachedImage is not used. Anyway, if ImageCache is to be cleared from
// time to time, SET_LOCATION_URL is a much easier detector of such
// opportune times. Fixes at least one 100%-reproducible case of
// "TypeError: Cannot read property handlers of undefined." Anyway, in
// order to reduce the re-downloading of the same avatars, eventually we
// decided to not clear during the runtime of the app (other that at the
// beginning that is).
ImageCache && ImageCache.get().clear();
break;

View File

@ -99,9 +99,7 @@ class PresenceLabel extends Component {
* }}
*/
function _mapStateToProps(state, ownProps) {
const participant
= getParticipantById(
state['features/base/participants'], ownProps.participantID);
const participant = getParticipantById(state, ownProps.participantID);
return {
_presence: participant && participant.presence

View File

@ -159,10 +159,7 @@ class RemoteControlAuthorizationDialog extends Component<*> {
*/
function _mapStateToProps(state, ownProps) {
const { _displayName, participantId } = ownProps;
const participant
= getParticipantById(
state['features/base/participants'],
participantId);
const participant = getParticipantById(state, participantId);
return {
_displayName: participant ? participant.name : _displayName

View File

@ -505,6 +505,12 @@ class Toolbox extends Component<Props> {
const { _localParticipantID, _raisedHand } = this.props;
this.props.dispatch(participantUpdated({
// XXX Only the local participant is allowed to update without
// stating the JitsiConference instance (i.e. participant property
// `conference` for a remote participant) because the local
// participant is uniquely identified by the very fact that there is
// only one local participant.
id: _localParticipantID,
local: true,
raisedHand: !_raisedHand

View File

@ -156,11 +156,11 @@ class WelcomePageSideBar extends Component<Props> {
* @returns {Object}
*/
function _mapStateToProps(state: Object) {
const _localParticipant = getLocalParticipant(state);
const localParticipant = getLocalParticipant(state);
return {
_avatar: getAvatarURL(_localParticipant),
_displayName: getParticipantDisplayName(state, _localParticipant.id),
_avatar: getAvatarURL(localParticipant),
_displayName: getParticipantDisplayName(state, localParticipant.id),
_visible: state['features/welcome'].sideBarVisible
};
}