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(); const displayName = user.getDisplayName();
APP.store.dispatch(participantJoined({ APP.store.dispatch(participantJoined({
conference: room,
id, id,
name: displayName, name: displayName,
role: user.getRole() role: user.getRole()
@ -1709,7 +1710,7 @@ export default {
if (user.isHidden()) { if (user.isHidden()) {
return; return;
} }
APP.store.dispatch(participantLeft(id, user)); APP.store.dispatch(participantLeft(id, room));
logger.log('USER %s LEFT', id, user); logger.log('USER %s LEFT', id, user);
APP.API.notifyUserLeft(id); APP.API.notifyUserLeft(id);
APP.UI.removeUser(id, user.getDisplayName()); APP.UI.removeUser(id, user.getDisplayName());
@ -1803,9 +1804,9 @@ export default {
APP.UI.participantConnectionStatusChanged(id); APP.UI.participantConnectionStatusChanged(id);
}); });
room.on(JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED, id => { room.on(
APP.store.dispatch(dominantSpeakerChanged(id)); JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED,
}); id => APP.store.dispatch(dominantSpeakerChanged(id, room)));
if (!interfaceConfig.filmStripOnly) { if (!interfaceConfig.filmStripOnly) {
room.on(JitsiConferenceEvents.CONNECTION_INTERRUPTED, () => { room.on(JitsiConferenceEvents.CONNECTION_INTERRUPTED, () => {
@ -1882,6 +1883,7 @@ export default {
= displayName.substr(0, MAX_DISPLAY_NAME_LENGTH); = displayName.substr(0, MAX_DISPLAY_NAME_LENGTH);
APP.store.dispatch(participantUpdated({ APP.store.dispatch(participantUpdated({
conference: room,
id, id,
name: formattedDisplayName name: formattedDisplayName
})); }));
@ -1915,6 +1917,7 @@ export default {
switch (name) { switch (name) {
case 'raisedHand': case 'raisedHand':
APP.store.dispatch(participantUpdated({ APP.store.dispatch(participantUpdated({
conference: room,
id: participant.getId(), id: participant.getId(),
raisedHand: newValue === 'true' raisedHand: newValue === 'true'
})); }));
@ -2014,6 +2017,7 @@ export default {
APP.UI.addListener(UIEvents.EMAIL_CHANGED, this.changeLocalEmail); APP.UI.addListener(UIEvents.EMAIL_CHANGED, this.changeLocalEmail);
room.addCommandListener(this.commands.defaults.EMAIL, (data, from) => { room.addCommandListener(this.commands.defaults.EMAIL, (data, from) => {
APP.store.dispatch(participantUpdated({ APP.store.dispatch(participantUpdated({
conference: room,
id: from, id: from,
email: data.value email: data.value
})); }));
@ -2025,6 +2029,7 @@ export default {
(data, from) => { (data, from) => {
APP.store.dispatch( APP.store.dispatch(
participantUpdated({ participantUpdated({
conference: room,
id: from, id: from,
avatarURL: data.value avatarURL: data.value
})); }));
@ -2034,6 +2039,7 @@ export default {
(data, from) => { (data, from) => {
APP.store.dispatch( APP.store.dispatch(
participantUpdated({ participantUpdated({
conference: room,
id: from, id: from,
avatarID: data.value avatarID: data.value
})); }));
@ -2578,6 +2584,12 @@ export default {
const localId = localParticipant.id; const localId = localParticipant.id;
APP.store.dispatch(participantUpdated({ 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, id: localId,
local: true, local: true,
email: formattedEmail email: formattedEmail
@ -2605,6 +2617,12 @@ export default {
} }
APP.store.dispatch(participantUpdated({ 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, id,
local: true, local: true,
avatarURL: formattedUrl avatarURL: formattedUrl
@ -2661,6 +2679,12 @@ export default {
} }
APP.store.dispatch(participantUpdated({ 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, id,
local: true, local: true,
name: formattedNickname name: formattedNickname

View File

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

21
package-lock.json generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,17 +22,23 @@ import { getLocalParticipant } from './functions';
* Create an action for when dominant speaker changes. * Create an action for when dominant speaker changes.
* *
* @param {string} id - Participant's ID. * @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 {{ * @returns {{
* type: DOMINANT_SPEAKER_CHANGED, * type: DOMINANT_SPEAKER_CHANGED,
* participant: { * participant: {
* conference: JitsiConference,
* id: string * id: string
* } * }
* }} * }}
*/ */
export function dominantSpeakerChanged(id) { export function dominantSpeakerChanged(id, conference) {
return { return {
type: DOMINANT_SPEAKER_CHANGED, type: DOMINANT_SPEAKER_CHANGED,
participant: { participant: {
conference,
id id
} }
}; };
@ -123,7 +129,19 @@ export function localParticipantLeft() {
const participant = getLocalParticipant(getState); const participant = getLocalParticipant(getState);
if (participant) { 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. * @param {Participant} participant - Information about participant.
* @returns {{ * @returns {{
* type: PARTICIPANT_JOINED, * type: PARTICIPANT_JOINED,
* participant: Participant * participant: Participant
* }} * }}
*/ */
export function participantJoined(participant) { export function participantJoined(participant) {
if (!participant.local && !participant.conference) {
throw Error(
'A remote participant must be associated with a JitsiConference!');
}
return { return {
type: PARTICIPANT_JOINED, type: PARTICIPANT_JOINED,
participant participant
@ -229,17 +252,23 @@ export function participantJoined(participant) {
* Action to signal that a participant has left. * Action to signal that a participant has left.
* *
* @param {string} id - Participant's ID. * @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 {{ * @returns {{
* type: PARTICIPANT_LEFT, * type: PARTICIPANT_LEFT,
* participant: { * participant: {
* conference: JitsiConference,
* id: string * id: string
* } * }
* }} * }}
*/ */
export function participantLeft(id) { export function participantLeft(id, conference) {
return { return {
type: PARTICIPANT_LEFT, type: PARTICIPANT_LEFT,
participant: { participant: {
conference,
id id
} }
}; };
@ -393,7 +422,5 @@ export function showParticipantJoinedNotification(displayName) {
joinedParticipantsNames.push( joinedParticipantsNames.push(
displayName || interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME); displayName || interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME);
return dispatch => { return dispatch => _throttledNotifyParticipantConnected(dispatch);
_throttledNotifyParticipantConnected(dispatch);
};
} }

View File

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

View File

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

View File

@ -1,18 +1,15 @@
// @flow // @flow
import UIEvents from '../../../../service/UI/UIEvents';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../app'; import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../app';
import { import { CONFERENCE_LEFT, CONFERENCE_WILL_JOIN } from '../conference';
CONFERENCE_WILL_JOIN, import { MiddlewareRegistry, StateListenerRegistry } from '../redux';
CONFERENCE_LEFT import UIEvents from '../../../../service/UI/UIEvents';
} from '../conference';
import { MiddlewareRegistry } from '../redux';
import { playSound, registerSound, unregisterSound } from '../sounds'; import { playSound, registerSound, unregisterSound } from '../sounds';
import { import {
localParticipantIdChanged, localParticipantIdChanged,
localParticipantJoined, localParticipantJoined,
participantLeft,
participantUpdated participantUpdated
} from './actions'; } from './actions';
import { import {
@ -43,17 +40,10 @@ declare var APP: Object;
* Middleware that captures CONFERENCE_JOINED and CONFERENCE_LEFT actions and * Middleware that captures CONFERENCE_JOINED and CONFERENCE_LEFT actions and
* updates respectively ID of local participant. * updates respectively ID of local participant.
* *
* @param {Store} store - Redux store. * @param {Store} store - The redux store.
* @returns {Function} * @returns {Function}
*/ */
MiddlewareRegistry.register(store => next => action => { 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) { switch (action.type) {
case APP_WILL_MOUNT: case APP_WILL_MOUNT:
_registerSounds(store); _registerSounds(store);
@ -74,32 +64,36 @@ MiddlewareRegistry.register(store => next => action => {
case DOMINANT_SPEAKER_CHANGED: { case DOMINANT_SPEAKER_CHANGED: {
// Ensure the raised hand state is cleared for the dominant speaker. // Ensure the raised hand state is cleared for the dominant speaker.
const { conference, id } = action.participant;
const participant = getLocalParticipant(store.getState()); const participant = getLocalParticipant(store.getState());
if (participant) { participant
const local = participant.id === action.participant.id; && store.dispatch(participantUpdated({
conference,
store.dispatch(participantUpdated({ id,
id: action.participant.id, local: participant.id === id,
local,
raisedHand: false raisedHand: false
})); }));
}
if (typeof APP === 'object') { typeof APP === 'object' && APP.UI.markDominantSpeaker(id);
APP.UI.markDominantSpeaker(action.participant.id);
}
break; break;
} }
case KICK_PARTICIPANT: case KICK_PARTICIPANT: {
const { conference } = store.getState()['features/base/conference'];
conference.kickParticipant(action.id); conference.kickParticipant(action.id);
break; break;
}
case MUTE_REMOTE_PARTICIPANT: {
const { conference } = store.getState()['features/base/conference'];
case MUTE_REMOTE_PARTICIPANT:
conference.muteParticipant(action.id); conference.muteParticipant(action.id);
break; break;
}
// TODO Remove this middleware when the local display name update flow is // TODO Remove this middleware when the local display name update flow is
// fully brought into redux. // fully brought into redux.
@ -116,73 +110,46 @@ MiddlewareRegistry.register(store => next => action => {
} }
case PARTICIPANT_JOINED: case PARTICIPANT_JOINED:
case PARTICIPANT_UPDATED: { _maybePlaySounds(store, action);
const { participant } = action;
const { id, local, raisedHand } = participant;
// Send an external update of the local participant's raised hand state return _participantJoinedOrUpdated(store, next, action);
// 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;
}
case PARTICIPANT_LEFT:
_maybePlaySounds(store, action);
break; break;
}
case PARTICIPANT_UPDATED:
return _participantJoinedOrUpdated(store, next, action);
} }
return 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. * Initializes the local participant and signals that it joined.
* *
* @private * @private
* @param {Store} store - The Redux store. * @param {Store} store - The redux store.
* @param {Dispatch} next - The redux dispatch function to dispatch the * @param {Dispatch} next - The redux dispatch function to dispatch the
* specified action to the specified store. * specified action to the specified store.
* @param {Action} action - The redux action which is being dispatched * @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) { function _localParticipantJoined({ getState, dispatch }, next, action) {
const result = next(action); const result = next(action);
const settings = getState()['features/base/settings']; const settings = getState()['features/base/settings'];
const localParticipant = {
dispatch(localParticipantJoined({
avatarID: settings.avatarID, avatarID: settings.avatarID,
avatarURL: settings.avatarURL, avatarURL: settings.avatarURL,
email: settings.email, email: settings.email,
name: settings.displayName name: settings.displayName
}; }));
dispatch(localParticipantJoined(localParticipant));
return result; return result;
} }
@ -208,8 +175,8 @@ function _localParticipantJoined({ getState, dispatch }, next, action) {
/** /**
* Plays sounds when participants join/leave conference. * Plays sounds when participants join/leave conference.
* *
* @param {Store} store - The Redux store. * @param {Store} store - The redux store.
* @param {Action} action - The Redux action. Should be either * @param {Action} action - The redux action. Should be either
* {@link PARTICIPANT_JOINED} or {@link PARTICIPANT_LEFT}. * {@link PARTICIPANT_JOINED} or {@link PARTICIPANT_LEFT}.
* @private * @private
* @returns {void} * @returns {void}
@ -223,8 +190,8 @@ function _maybePlaySounds({ getState, dispatch }, action) {
// The intention there was to not play user joined notification in big // The intention there was to not play user joined notification in big
// conferences where 100th person is joining. // conferences where 100th person is joining.
if (!action.participant.local if (!action.participant.local
&& (!startAudioMuted && (!startAudioMuted
|| getParticipantCount(state) < startAudioMuted)) { || getParticipantCount(state) < startAudioMuted)) {
if (action.type === PARTICIPANT_JOINED) { if (action.type === PARTICIPANT_JOINED) {
dispatch(playSound(PARTICIPANT_JOINED_SOUND_ID)); dispatch(playSound(PARTICIPANT_JOINED_SOUND_ID));
} else if (action.type === PARTICIPANT_LEFT) { } 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. * Registers sounds related with the participants feature.
* *
* @param {Store} store - The Redux store. * @param {Store} store - The redux store.
* @private * @private
* @returns {void} * @returns {void}
*/ */
function _registerSounds({ dispatch }) { function _registerSounds({ dispatch }) {
dispatch( dispatch(
registerSound(PARTICIPANT_JOINED_SOUND_ID, PARTICIPANT_JOINED_FILE)); registerSound(PARTICIPANT_JOINED_SOUND_ID, PARTICIPANT_JOINED_FILE));
dispatch( dispatch(registerSound(PARTICIPANT_LEFT_SOUND_ID, PARTICIPANT_LEFT_FILE));
registerSound(PARTICIPANT_LEFT_SOUND_ID, PARTICIPANT_LEFT_FILE));
} }
/** /**
* Unregisters sounds related with the participants feature. * Unregisters sounds related with the participants feature.
* *
* @param {Store} store - The Redux store. * @param {Store} store - The redux store.
* @private * @private
* @returns {void} * @returns {void}
*/ */
function _unregisterSounds({ dispatch }) { function _unregisterSounds({ dispatch }) {
dispatch( dispatch(unregisterSound(PARTICIPANT_JOINED_SOUND_ID));
unregisterSound(PARTICIPANT_JOINED_SOUND_ID)); dispatch(unregisterSound(PARTICIPANT_LEFT_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; declare var APP: Object;
/** /**
* These properties should not be bulk assigned when updating a particular * The participant properties which cannot be updated through
* @see Participant. * {@link PARTICIPANT_UPDATED}. They either identify the participant or can only
* be modified through property-dedicated actions.
*
* @type {string[]} * @type {string[]}
*/ */
const PARTICIPANT_PROPS_TO_OMIT_WHEN_UPDATE const PARTICIPANT_PROPS_TO_OMIT_WHEN_UPDATE = [
= [ 'dominantSpeaker', 'id', 'local', 'pinned' ];
// 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. * Reducer function for a single participant.
@ -67,52 +120,6 @@ function _participant(state: Object = {}, action) {
} }
break; 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: { case PARTICIPANT_UPDATED: {
const { participant } = action; // eslint-disable-line no-shadow const { participant } = action; // eslint-disable-line no-shadow
let { id } = participant; 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 * Reduces a specific redux action of type {@link PARTICIPANT_JOINED} in the
* the conference. * feature base/participants.
* *
* @param {Participant[]} state - List of participants to be modified. * @param {Action} action - The redux action of type {@code PARTICIPANT_JOINED}
* @param {Object} action - Action object. * to reduce.
* @param {string} action.type - Type of action. * @private
* @param {Participant} action.participant - Information about participant to be * @returns {Object} The new participant derived from the payload of the
* added/removed/modified. * specified {@code action} to be added into the redux state of the feature
* @returns {Participant[]} * base/participants after the reduction of the specified
* {@code action}.
*/ */
ReducerRegistry.register('features/base/participants', (state = [], action) => { function _participantJoined({ participant }) {
switch (action.type) { const {
case DOMINANT_SPEAKER_CHANGED: avatarURL,
case PARTICIPANT_ID_CHANGED: connectionStatus,
case PARTICIPANT_UPDATED: dominantSpeaker,
case PIN_PARTICIPANT: email,
return state.map(p => _participant(p, action)); isBot,
local,
name,
pinned,
role
} = participant;
let { avatarID, conference, id } = participant;
case PARTICIPANT_JOINED: if (local) {
return [ ...state, _participant(undefined, action) ]; // avatarID
//
// TODO Get the avatarID of the local participant from localStorage.
avatarID || (avatarID = randomHexString(32));
case PARTICIPANT_LEFT: // conference
return state.filter(p => p.id !== action.participant.id); //
// 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: // id
return state; 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 * from './functions';
export { default as MiddlewareRegistry } from './MiddlewareRegistry'; export { default as MiddlewareRegistry } from './MiddlewareRegistry';
export { default as ReducerRegistry } from './ReducerRegistry'; 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 { connect, disconnect } from '../../base/connection';
import { DialogContainer } from '../../base/dialog'; import { DialogContainer } from '../../base/dialog';
import { CalleeInfoContainer } from '../../base/jwt'; import { CalleeInfoContainer } from '../../base/jwt';
import { getParticipantCount } from '../../base/participants';
import { Container, LoadingIndicator, TintedView } from '../../base/react'; import { Container, LoadingIndicator, TintedView } from '../../base/react';
import { TestConnectionInfo } from '../../base/testing'; import { TestConnectionInfo } from '../../base/testing';
import { createDesiredLocalTracks } from '../../base/tracks'; import { createDesiredLocalTracks } from '../../base/tracks';
@ -383,7 +384,6 @@ function _mapStateToProps(state) {
const { connecting, connection } = state['features/base/connection']; const { connecting, connection } = state['features/base/connection'];
const { conference, joining, leaving } = state['features/base/conference']; const { conference, joining, leaving } = state['features/base/conference'];
const { reducedUI } = state['features/base/responsive-ui']; 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 // XXX There is a window of time between the successful establishment of the
// XMPP connection and the subsequent commencement of joining the MUC during // XMPP connection and the subsequent commencement of joining the MUC during
@ -415,7 +415,7 @@ function _mapStateToProps(state) {
* @private * @private
* @type {number} * @type {number}
*/ */
_participantCount: participants.length, _participantCount: getParticipantCount(state),
/** /**
* The indicator which determines whether the UI is reduced (to * The indicator which determines whether the UI is reduced (to

View File

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

View File

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

View File

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

View File

@ -1,7 +1,6 @@
/* @flow */ /* @flow */
import { APP_WILL_MOUNT } from '../../app'; import { APP_WILL_MOUNT } from '../../app';
import { CONFERENCE_FAILED, CONFERENCE_LEFT } from '../../base/conference';
import { import {
getAvatarURL, getAvatarURL,
getLocalParticipant, getLocalParticipant,
@ -37,8 +36,15 @@ const _PREFETCH_AVATAR_URLS = false;
MiddlewareRegistry.register(({ getState }) => next => action => { MiddlewareRegistry.register(({ getState }) => next => action => {
switch (action.type) { switch (action.type) {
case APP_WILL_MOUNT: case APP_WILL_MOUNT:
case CONFERENCE_FAILED: // XXX CONFERENCE_FAILED/LEFT are no longer used here because they
case CONFERENCE_LEFT: // 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(); ImageCache && ImageCache.get().clear();
break; break;

View File

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

View File

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

View File

@ -505,6 +505,12 @@ class Toolbox extends Component<Props> {
const { _localParticipantID, _raisedHand } = this.props; const { _localParticipantID, _raisedHand } = this.props;
this.props.dispatch(participantUpdated({ 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, id: _localParticipantID,
local: true, local: true,
raisedHand: !_raisedHand raisedHand: !_raisedHand

View File

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