diff --git a/conference.js b/conference.js index cb48ec56b..081d2e094 100644 --- a/conference.js +++ b/conference.js @@ -70,6 +70,7 @@ import { } from './react/features/base/media'; import { dominantSpeakerChanged, + getAvatarURLByParticipantId, getLocalParticipant, getParticipantById, localParticipantConnectionStatusChanged, @@ -2092,7 +2093,6 @@ export default { id: from, avatarURL: data.value })); - APP.UI.setUserAvatarUrl(from, data.value); }); room.addCommandListener(this.commands.defaults.AVATAR_ID, @@ -2102,7 +2102,6 @@ export default { id: from, avatarID: data.value })); - APP.UI.setUserAvatarID(from, data.value); }); APP.UI.addListener(UIEvents.NICKNAME_CHANGED, @@ -2414,7 +2413,8 @@ export default { formattedDisplayName: appendSuffix( displayName, interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME), - avatarURL: APP.UI.getAvatarUrl() + avatarURL: getAvatarURLByParticipantId( + APP.store.getState(), this._room.myUserId()) } ); APP.UI.markVideoInterrupted(false); @@ -2704,7 +2704,7 @@ export default { APP.store.dispatch(participantUpdated({ id: localId, local: true, - formattedEmail + email: formattedEmail })); APP.settings.setEmail(formattedEmail); @@ -2732,7 +2732,6 @@ export default { })); APP.settings.setAvatarUrl(url); - APP.UI.setUserAvatarUrl(id, url); sendData(commands.AVATAR_URL, url); }, diff --git a/modules/UI/UI.js b/modules/UI/UI.js index 400f66e23..62eabaf2e 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -6,7 +6,6 @@ const UI = {}; import Chat from './side_pannels/chat/Chat'; import SidePanels from './side_pannels/SidePanels'; -import Avatar from './avatar/Avatar'; import SideContainerToggler from './side_pannels/SideContainerToggler'; import messageHandler from './util/MessageHandler'; import UIUtil from './util/UIUtil'; @@ -255,7 +254,7 @@ UI.setLocalRaisedHandStatus */ UI.initConference = function() { const { dispatch, getState } = APP.store; - const { avatarID, email, id, name } = getLocalParticipant(getState); + const { email, id, name } = getLocalParticipant(getState); // Update default button states before showing the toolbar // if local role changes buttons state will be again updated. @@ -272,8 +271,6 @@ UI.initConference = function() { // Make sure we configure our avatar id, before creating avatar for us if (email) { UI.setUserEmail(id, email); - } else { - UI.setUserAvatarID(id, avatarID); } dispatch(checkAutoEnableDesktopSharing()); @@ -789,65 +786,26 @@ UI.showToolbar = timeout => APP.store.dispatch(showToolbox(timeout)); // Used by torture. UI.dockToolbar = dock => APP.store.dispatch(dockToolbox(dock)); -/** - * Updates the avatar for participant. - * @param {string} id user id - * @param {string} avatarUrl the URL for the avatar - */ -function changeAvatar(id, avatarUrl) { - VideoLayout.changeUserAvatar(id, avatarUrl); - if (APP.conference.isLocalId(id)) { - Profile.changeAvatar(avatarUrl); - } -} - -/** - * Returns the avatar URL for a given user. - * - * @param {string} id - The id of the user. - * @returns {string} The avatar URL. - */ -UI.getAvatarUrl = function(id) { - return Avatar.getAvatarUrl(id); -}; - /** * Update user email. * @param {string} id user id * @param {string} email user email */ UI.setUserEmail = function(id, email) { - // update avatar - Avatar.setUserEmail(id, email); - - changeAvatar(id, Avatar.getAvatarUrl(id)); if (APP.conference.isLocalId(id)) { Profile.changeEmail(email); } }; /** - * Update user avtar id. - * @param {string} id user id - * @param {string} avatarId user's avatar id + * Updates the displayed avatar for participant. + * + * @param {string} id - User id whose avatar should be updated. + * @param {string} avatarURL - The URL to avatar image to display. + * @returns {void} */ -UI.setUserAvatarID = function(id, avatarId) { - // update avatar - Avatar.setUserAvatarID(id, avatarId); - - changeAvatar(id, Avatar.getAvatarUrl(id)); -}; - -/** - * Update user avatar URL. - * @param {string} id user id - * @param {string} url user avatar url - */ -UI.setUserAvatarUrl = function(id, url) { - // update avatar - Avatar.setUserAvatarUrl(id, url); - - changeAvatar(id, Avatar.getAvatarUrl(id)); +UI.refreshAvatarDisplay = function(id, avatarURL) { + VideoLayout.changeUserAvatar(id, avatarURL); }; /** diff --git a/modules/UI/avatar/Avatar.js b/modules/UI/avatar/Avatar.js deleted file mode 100644 index 0b164f309..000000000 --- a/modules/UI/avatar/Avatar.js +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Adorable Avatars service used at the end of this file is released under the - * terms of the MIT License. - * - * Copyright (c) 2014 Adorable IO LLC - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -/* global APP */ - -import { getAvatarURL } from '../../../react/features/base/participants'; - -const users = {}; - -export default { - /** - * Sets prop in users object. - * @param id {string} user id or undefined for the local user. - * @param prop {string} name of the prop - * @param val {string} value to be set - */ - _setUserProp(id, prop, val) { - // FIXME: Fixes the issue with not be able to return avatar for the - // local user when the conference has been left. Maybe there is beter - // way to solve it. - if (!id || APP.conference.isLocalId(id)) { - id = 'local';// eslint-disable-line no-param-reassign - } - if (!val || (users[id] && users[id][prop] === val)) { - return; - } - if (!users[id]) { - users[id] = {}; - } - users[id][prop] = val; - APP.API.notifyAvatarChanged( - id === 'local' ? APP.conference.getMyUserId() : id, - this.getAvatarUrl(id) - ); - }, - - /** - * Sets the user's avatar in the settings menu(if local user), contact list - * and thumbnail - * @param id id of the user - * @param email email or nickname to be used as a hash - */ - setUserEmail(id, email) { - this._setUserProp(id, 'email', email); - }, - - /** - * Sets the user's avatar in the settings menu(if local user), contact list - * and thumbnail - * @param id id of the user - * @param url the url for the avatar - */ - setUserAvatarUrl(id, url) { - this._setUserProp(id, 'avatarUrl', url); - }, - - /** - * Sets the user's avatar id. - * @param id id of the user - * @param avatarId an id to be used for the avatar - */ - setUserAvatarID(id, avatarId) { - this._setUserProp(id, 'avatarId', avatarId); - }, - - /** - * Returns the URL of the image for the avatar of a particular user, - * identified by its id. - * @param {string} userId user id - */ - getAvatarUrl(userId) { - let user; - - if (!userId || APP.conference.isLocalId(userId)) { - user = users.local; - // eslint-disable-next-line no-param-reassign - userId = APP.conference.getMyUserId(); - } else { - user = users[userId]; - } - - return getAvatarURL({ - avatarID: user ? user.avatarId : undefined, - avatarURL: user ? user.avatarUrl : undefined, - email: user ? user.email : undefined, - id: userId - }); - } -}; diff --git a/modules/UI/side_pannels/profile/Profile.js b/modules/UI/side_pannels/profile/Profile.js index b9b3bdd82..2669b5294 100644 --- a/modules/UI/side_pannels/profile/Profile.js +++ b/modules/UI/side_pannels/profile/Profile.js @@ -142,14 +142,6 @@ export default { $('#setDisplayName').val(newDisplayName); }, - /** - * Change user avatar in the settings menu. - * @param {string} avatarUrl url of the new avatar - */ - changeAvatar(avatarUrl) { - $('#avatar').attr('src', avatarUrl); - }, - /** * Change the value of the field for the user email. * @param {string} email the new value that will be displayed in the field. diff --git a/modules/UI/videolayout/LargeVideoManager.js b/modules/UI/videolayout/LargeVideoManager.js index 97d0c3e45..9ddafa45a 100644 --- a/modules/UI/videolayout/LargeVideoManager.js +++ b/modules/UI/videolayout/LargeVideoManager.js @@ -12,11 +12,12 @@ const logger = require('jitsi-meet-logger').getLogger(__filename); import { JitsiParticipantConnectionStatus } from '../../../react/features/base/lib-jitsi-meet'; +import { + getAvatarURLByParticipantId +} from '../../../react/features/base/participants'; import { updateKnownLargeVideoResolution } from '../../../react/features/large-video'; - -import Avatar from '../avatar/Avatar'; import { createDeferred } from '../../util/helpers'; import UIEvents from '../../../service/UI/UIEvents'; import UIUtil from '../util/UIUtil'; @@ -219,7 +220,8 @@ export default class LargeVideoManager { container.setStream(id, stream, videoType); // change the avatar url on large - this.updateAvatar(Avatar.getAvatarUrl(id)); + this.updateAvatar( + getAvatarURLByParticipantId(APP.store.getState(), id)); // If the user's connection is disrupted then the avatar will be // displayed in case we have no video image cached. That is if diff --git a/modules/UI/videolayout/LocalVideo.js b/modules/UI/videolayout/LocalVideo.js index 9fa4e1ad5..86f86b146 100644 --- a/modules/UI/videolayout/LocalVideo.js +++ b/modules/UI/videolayout/LocalVideo.js @@ -7,6 +7,9 @@ import { Provider } from 'react-redux'; import { JitsiTrackEvents } from '../../../react/features/base/lib-jitsi-meet'; import { VideoTrack } from '../../../react/features/base/media'; +import { + getAvatarURLByParticipantId +} from '../../../react/features/base/participants'; /* eslint-enable no-unused-vars */ const logger = require('jitsi-meet-logger').getLogger(__filename); @@ -46,6 +49,12 @@ function LocalVideo(VideoLayout, emitter) { // Set default display name. this.setDisplayName(); + // Initialize the avatar display with an avatar url selected from the redux + // state. Redux stores the local user with a hardcoded participant id of + // 'local' if no id has been assigned yet. + this.avatarChanged( + getAvatarURLByParticipantId(APP.store.getState(), this.id)); + this.addAudioLevelIndicator(); this.updateIndicators(); diff --git a/modules/UI/videolayout/SmallVideo.js b/modules/UI/videolayout/SmallVideo.js index e20bb2998..2211cb18d 100644 --- a/modules/UI/videolayout/SmallVideo.js +++ b/modules/UI/videolayout/SmallVideo.js @@ -11,7 +11,8 @@ import { i18next } from '../../../react/features/base/i18n'; import { AudioLevelIndicator } from '../../../react/features/audio-level-indicator'; import { - Avatar as AvatarDisplay + Avatar as AvatarDisplay, + getAvatarURLByParticipantId } from '../../../react/features/base/participants'; import { ConnectionIndicator @@ -28,7 +29,6 @@ import { const logger = require('jitsi-meet-logger').getLogger(__filename); -import Avatar from '../avatar/Avatar'; import UIUtil from '../util/UIUtil'; import UIEvents from '../../../service/UI/UIEvents'; @@ -590,7 +590,8 @@ SmallVideo.prototype.updateView = function() { if (!this.hasAvatar) { if (this.id) { // Init avatar - this.avatarChanged(Avatar.getAvatarUrl(this.id)); + this.avatarChanged( + getAvatarURLByParticipantId(APP.store.getState(), this.id)); } else { logger.error('Unable to init avatar - no id', this); diff --git a/react/features/base/participants/functions.js b/react/features/base/participants/functions.js index 3cf8ae194..bf0a1fa08 100644 --- a/react/features/base/participants/functions.js +++ b/react/features/base/participants/functions.js @@ -3,7 +3,10 @@ import md5 from 'js-md5'; import { toState } from '../redux'; -import { DEFAULT_AVATAR_RELATIVE_PATH } from './constants'; +import { + DEFAULT_AVATAR_RELATIVE_PATH, + LOCAL_PARTICIPANT_DEFAULT_ID +} from './constants'; declare var config: Object; declare var interfaceConfig: Object; @@ -74,6 +77,29 @@ export function getAvatarURL({ avatarID, avatarURL, email, id }: { return urlPrefix + md5.hex(key.trim().toLowerCase()) + urlSuffix; } +/** + * Returns the avatarURL for the participant associated with the passed in + * participant ID. + * + * @param {(Function|Object|Participant[])} stateful - The redux state + * features/base/participants, the (whole) redux state, or redux's + * {@code getState} function to be used to retrieve the state + * features/base/participants. + * @param {string} id - The ID of the participant to retrieve. + * @param {boolean} isLocal - An optional parameter indicating whether or not + * the partcipant id is for the local user. If true, a different logic flow is + * used find the local user, ignoring the id value as it can change through the + * beginning and end of a call. + * @returns {(string|undefined)} + */ +export function getAvatarURLByParticipantId( + stateful: Object | Function, + id: string = LOCAL_PARTICIPANT_DEFAULT_ID) { + const participant = getParticipantById(stateful, id); + + return participant && getAvatarURL(participant); +} + /** * Returns local participant from Redux state. * diff --git a/react/features/base/participants/middleware.js b/react/features/base/participants/middleware.js index 3f2194558..f9c9a8a80 100644 --- a/react/features/base/participants/middleware.js +++ b/react/features/base/participants/middleware.js @@ -12,10 +12,15 @@ import { localParticipantIdChanged } from './actions'; import { KICK_PARTICIPANT, MUTE_REMOTE_PARTICIPANT, - PARTICIPANT_DISPLAY_NAME_CHANGED + PARTICIPANT_DISPLAY_NAME_CHANGED, + PARTICIPANT_JOINED, + PARTICIPANT_UPDATED } from './actionTypes'; import { LOCAL_PARTICIPANT_DEFAULT_ID } from './constants'; -import { getLocalParticipant } from './functions'; +import { + getAvatarURLByParticipantId, + getLocalParticipant +} from './functions'; declare var APP: Object; @@ -59,6 +64,38 @@ MiddlewareRegistry.register(store => next => action => { break; } + + case PARTICIPANT_JOINED: + case PARTICIPANT_UPDATED: { + if (typeof APP !== 'undefined') { + const participant = action.participant; + const { id, local } = participant; + + 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; + } + + break; + } } return next(action); diff --git a/react/features/toolbox/components/ProfileButton.web.js b/react/features/toolbox/components/ProfileButton.web.js index c427c65bf..a02032860 100644 --- a/react/features/toolbox/components/ProfileButton.web.js +++ b/react/features/toolbox/components/ProfileButton.web.js @@ -5,7 +5,10 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { TOOLBAR_PROFILE_TOGGLED, sendAnalyticsEvent } from '../../analytics'; -import { DEFAULT_AVATAR_RELATIVE_PATH } from '../../base/participants'; +import { + getAvatarURL, + getLocalParticipant +} from '../../base/participants'; import UIEvents from '../../../../service/UI/UIEvents'; import ToolbarButton from './ToolbarButton'; @@ -39,6 +42,11 @@ class ProfileButton extends Component<*> { * @static */ static propTypes = { + /** + * The redux representation of the local participant. + */ + _localParticipant: PropTypes.object, + /** * Whether the button support clicking or not. */ @@ -76,7 +84,12 @@ class ProfileButton extends Component<*> { * @returns {ReactElement} */ render() { - const { _unclickable, tooltipPosition, toggled } = this.props; + const { + _localParticipant, + _unclickable, + tooltipPosition, + toggled + } = this.props; const buttonConfiguration = { ...DEFAULT_BUTTON_CONFIGURATION, unclickable: _unclickable, @@ -90,7 +103,7 @@ class ProfileButton extends Component<*> { tooltipPosition = { tooltipPosition }> + src = { getAvatarURL(_localParticipant) } /> ); } @@ -115,11 +128,13 @@ class ProfileButton extends Component<*> { * @param {Object} state - The Redux state. * @private * @returns {{ + * _localParticipant: Object, * _unclickable: boolean * }} */ function _mapStateToProps(state) { return { + _localParticipant: getLocalParticipant(state), _unclickable: !state['features/base/jwt'].isGuest }; }