diff --git a/conference.js b/conference.js index 36d54c591..9469f55a8 100644 --- a/conference.js +++ b/conference.js @@ -20,7 +20,11 @@ import analytics from './modules/analytics/analytics'; import EventEmitter from "events"; -import { conferenceFailed } from './react/features/base/conference'; +import { + CONFERENCE_JOINED, + conferenceFailed, + conferenceLeft +} from './react/features/base/conference'; import { isFatalJitsiConnectionError } from './react/features/base/lib-jitsi-meet'; @@ -29,6 +33,15 @@ import { suspendDetected } from './react/features/overlay'; +import { + changeParticipantAvatarID, + changeParticipantAvatarURL, + changeParticipantEmail, + participantJoined, + participantLeft, + participantRoleChanged +} from './react/features/base/participants'; + const ConnectionEvents = JitsiMeetJS.events.connection; const ConnectionErrors = JitsiMeetJS.errors.connection; @@ -150,6 +163,29 @@ function sendData (command, value) { room.sendCommand(command, {value: value}); } +/** + * Setups initially the properties for the local participant - email, avatarId, + * avatarUrl, displayName, etc. + */ +function _setupLocalParticipantProperties() { + const email = APP.settings.getEmail(); + email && sendData(commands.EMAIL, email); + + const avatarUrl = APP.settings.getAvatarUrl(); + avatarUrl && sendData(commands.AVATAR_URL, avatarUrl); + + if(!email && !avatarUrl) { + sendData(commands.AVATAR_ID, APP.settings.getAvatarId()); + } + + let nick = APP.settings.getDisplayName(); + if (config.useNicks && !nick) { + nick = APP.UI.askForNickname(); + APP.settings.setDisplayName(nick); + } + nick && room.setDisplayName(nick); +} + /** * Get user nickname by user id. * @param {string} id user id @@ -875,21 +911,7 @@ export default { this.invite = new Invite(room); this._room = room; // FIXME do not use this - let email = APP.settings.getEmail(); - email && sendData(this.commands.defaults.EMAIL, email); - - let avatarUrl = APP.settings.getAvatarUrl(); - avatarUrl && sendData(this.commands.defaults.AVATAR_URL, - avatarUrl); - !email && sendData( - this.commands.defaults.AVATAR_ID, APP.settings.getAvatarId()); - - let nick = APP.settings.getDisplayName(); - if (config.useNicks && !nick) { - nick = APP.UI.askForNickname(); - APP.settings.setDisplayName(nick); - } - nick && room.setDisplayName(nick); + _setupLocalParticipantProperties(); this._setupListeners(); }, @@ -1116,11 +1138,18 @@ export default { _setupListeners () { // add local streams when joined to the conference room.on(ConferenceEvents.CONFERENCE_JOINED, () => { + APP.store.dispatch({ + type: CONFERENCE_JOINED, + conference: room + }); APP.UI.mucJoined(); APP.API.notifyConferenceJoined(APP.conference.roomName); APP.UI.markVideoInterrupted(false); }); + room.on(ConferenceEvents.CONFERENCE_LEFT, + (...args) => APP.store.dispatch(conferenceLeft(room, ...args))); + room.on( ConferenceEvents.AUTH_STATUS_CHANGED, function (authEnabled, authLogin) { @@ -1134,6 +1163,12 @@ export default { if (user.isHidden()) return; + APP.store.dispatch(participantJoined({ + id, + name: user.getDisplayName(), + role: user.getRole() + })); + logger.log('USER %s connnected', id, user); APP.API.notifyUserJoined(id); APP.UI.addUser(user); @@ -1142,6 +1177,7 @@ export default { APP.UI.updateUserRole(user); }); room.on(ConferenceEvents.USER_LEFT, (id, user) => { + APP.store.dispatch(participantLeft(id, user)); logger.log('USER %s LEFT', id, user); APP.API.notifyUserLeft(id); APP.UI.removeUser(id, user.getDisplayName()); @@ -1150,6 +1186,7 @@ export default { room.on(ConferenceEvents.USER_ROLE_CHANGED, (id, role) => { + APP.store.dispatch(participantRoleChanged(id, role)); if (this.isLocalId(id)) { logger.info(`My role changed, new role: ${role}`); if (this.isModerator !== room.isModerator()) { @@ -1411,17 +1448,22 @@ export default { APP.UI.addListener(UIEvents.EMAIL_CHANGED, this.changeLocalEmail); room.addCommandListener(this.commands.defaults.EMAIL, (data, from) => { + APP.store.dispatch(changeParticipantEmail(from, data.value)); APP.UI.setUserEmail(from, data.value); }); room.addCommandListener( this.commands.defaults.AVATAR_URL, (data, from) => { + APP.store.dispatch( + changeParticipantAvatarURL(from, data.value)); APP.UI.setUserAvatarUrl(from, data.value); }); room.addCommandListener(this.commands.defaults.AVATAR_ID, (data, from) => { + APP.store.dispatch( + changeParticipantAvatarID(from, data.value)); APP.UI.setUserAvatarID(from, data.value); }); @@ -1832,6 +1874,7 @@ export default { if (email === APP.settings.getEmail()) { return; } + APP.store.dispatch(changeParticipantEmail(room.myUserId(), email)); APP.settings.setEmail(email); APP.UI.setUserEmail(room.myUserId(), email); @@ -1848,6 +1891,7 @@ export default { if (url === APP.settings.getAvatarUrl()) { return; } + APP.store.dispatch(changeParticipantAvatarURL(room.myUserId(), url)); APP.settings.setAvatarUrl(url); APP.UI.setUserAvatarUrl(room.myUserId(), url); diff --git a/modules/UI/avatar/Avatar.js b/modules/UI/avatar/Avatar.js index 311546033..d3dbb322f 100644 --- a/modules/UI/avatar/Avatar.js +++ b/modules/UI/avatar/Avatar.js @@ -21,8 +21,9 @@ * SOFTWARE. */ -/* global MD5, config, interfaceConfig, APP */ -const logger = require("jitsi-meet-logger").getLogger(__filename); +/* global APP */ + +import { getAvatarURL } from '../../../react/features/base/participants'; let users = {}; @@ -64,7 +65,7 @@ export default { * @param url the url for the avatar */ setUserAvatarUrl: function (id, url) { - this._setUserProp(id, "url", url); + this._setUserProp(id, "avatarUrl", url); }, /** @@ -82,57 +83,14 @@ export default { * @param {string} userId user id */ getAvatarUrl: function (userId) { - if (config.disableThirdPartyRequests) { - return 'images/avatar2.png'; - } - + let user; if (!userId || APP.conference.isLocalId(userId)) { - userId = "local"; + user = users.local; + userId = APP.conference.getMyUserId(); + } else { + user = users[userId]; } - let avatarId = null; - const user = users[userId]; - - // The priority is url, email and lowest is avatarId - if(user) { - if(user.url) - return user.url; - - if (user.email) - avatarId = user.email; - else { - avatarId = user.avatarId; - } - } - - // If the ID looks like an email, we'll use gravatar. - // Otherwise, it's a random avatar, and we'll use the configured - // URL. - let random = !avatarId || avatarId.indexOf('@') < 0; - - if (!avatarId) { - logger.warn( - `No avatar stored yet for ${userId} - using ID as avatar ID`); - avatarId = userId; - } - avatarId = MD5.hexdigest(avatarId.trim().toLowerCase()); - - - let urlPref = null; - let urlSuf = null; - if (!random) { - urlPref = 'https://www.gravatar.com/avatar/'; - urlSuf = "?d=wavatar&size=200"; - } - else if (random && interfaceConfig.RANDOM_AVATAR_URL_PREFIX) { - urlPref = interfaceConfig.RANDOM_AVATAR_URL_PREFIX; - urlSuf = interfaceConfig.RANDOM_AVATAR_URL_SUFFIX; - } - else { - urlPref = 'https://api.adorable.io/avatars/200/'; - urlSuf = ".png"; - } - - return urlPref + avatarId + urlSuf; + return getAvatarURL(userId, user); } }; diff --git a/react/features/app/components/AbstractApp.js b/react/features/app/components/AbstractApp.js index f4818d75e..f15655020 100644 --- a/react/features/app/components/AbstractApp.js +++ b/react/features/app/components/AbstractApp.js @@ -78,7 +78,11 @@ export class AbstractApp extends Component { dispatch(appWillMount(this)); - dispatch(localParticipantJoined()); + dispatch(localParticipantJoined({ + avatarId: APP.settings.getAvatarId(), + avatarUrl: APP.settings.getAvatarUrl(), + email: APP.settings.getEmail() + })); // If a URL was explicitly specified to this React Component, then open // it; otherwise, use a default. diff --git a/react/features/base/conference/actions.js b/react/features/base/conference/actions.js index 6a0761e6e..430c6113e 100644 --- a/react/features/base/conference/actions.js +++ b/react/features/base/conference/actions.js @@ -38,7 +38,7 @@ function _addConferenceListeners(conference, dispatch) { (...args) => dispatch(_conferenceJoined(conference, ...args))); conference.on( JitsiConferenceEvents.CONFERENCE_LEFT, - (...args) => dispatch(_conferenceLeft(conference, ...args))); + (...args) => dispatch(conferenceLeft(conference, ...args))); conference.on( JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED, @@ -131,7 +131,7 @@ function _conferenceJoined(conference) { * conference: JitsiConference * }} */ -function _conferenceLeft(conference) { +export function conferenceLeft(conference) { return { type: CONFERENCE_LEFT, conference diff --git a/react/features/base/participants/actions.js b/react/features/base/participants/actions.js index 2ca491d85..d3fd91e03 100644 --- a/react/features/base/participants/actions.js +++ b/react/features/base/participants/actions.js @@ -8,6 +8,52 @@ import { } from './actionTypes'; import { getLocalParticipant } from './functions'; +/** + * Action to update a participant's avatar id. + * + * @param {string} id - Participant's id. + * @param {string} avatarId - Participant's avatar id. + * @returns {{ + * type: PARTICIPANT_UPDATED, + * participant: { + * id: string, + * avatarId: string, + * } + * }} + */ +export function changeParticipantAvatarID(id, avatarId) { + return { + type: PARTICIPANT_UPDATED, + participant: { + id, + avatarId + } + }; +} + +/** + * Action to update a participant's avatar URL. + * + * @param {string} id - Participant's id. + * @param {string} url - Participant's avatar url. + * @returns {{ + * type: PARTICIPANT_UPDATED, + * participant: { + * id: string, + * url: string, + * } + * }} + */ +export function changeParticipantAvatarURL(id, url) { + return { + type: PARTICIPANT_UPDATED, + participant: { + id, + url + } + }; +} + /** * Action to update a participant's email. * @@ -17,7 +63,6 @@ import { getLocalParticipant } from './functions'; * type: PARTICIPANT_UPDATED, * participant: { * id: string, - * avatar: string, * email: string * } * }} diff --git a/react/features/base/participants/components/Avatar.web.js b/react/features/base/participants/components/Avatar.web.js new file mode 100644 index 000000000..01d9ee2d2 --- /dev/null +++ b/react/features/base/participants/components/Avatar.web.js @@ -0,0 +1,32 @@ +import React, { Component } from 'react'; + +/** + * Display a participant avatar. + */ +export default class Avatar extends Component { + /** + * Avatar component's property types. + * + * @static + */ + static propTypes = { + /** + * The URL for the avatar. + * + * @type {string} + */ + uri: React.PropTypes.string + } + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + */ + render() { + return ( + + ); + } +} diff --git a/react/features/base/participants/components/index.js b/react/features/base/participants/components/index.js index 59f6ab78b..afa6b246d 100644 --- a/react/features/base/participants/components/index.js +++ b/react/features/base/participants/components/index.js @@ -1 +1,2 @@ +export { default as Avatar } from './Avatar'; export { default as ParticipantView } from './ParticipantView'; diff --git a/react/features/base/participants/functions.js b/react/features/base/participants/functions.js index 1d0ecf0d9..d74e830d9 100644 --- a/react/features/base/participants/functions.js +++ b/react/features/base/participants/functions.js @@ -1,3 +1,8 @@ +/* global MD5 */ + +declare var config: Object; +declare var interfaceConfig: Object; + /** * Returns local participant from Redux state. * @@ -45,3 +50,70 @@ function _getParticipants(participantsOrGetState) { return participants || []; } + +/** + * Returns the URL of the image for the avatar of a particular participant + * identified by their id and/or e-mail address. + * + * @param {string} [participantId] - Participant's id. + * @param {Object} [options] - The optional arguments. + * @param {string} [options.avatarId] - Participant's avatar id. + * @param {string} [options.avatarUrl] - Participant's avatar url. + * @param {string} [options.email] - Participant's email. + * @returns {string} The URL of the image for the avatar of the participant + * identified by the specified participantId and/or email. + * + * @public + */ +export function getAvatarURL(participantId, options = {}) { + // If disableThirdPartyRequests is enabled we shouldn't use third party + // avatar services, we are returning one of our images. + if (typeof config === 'object' && config.disableThirdPartyRequests) { + return 'images/avatar2.png'; + } + + const { avatarId, avatarUrl, email } = options; + + // If we have avatarUrl we don't need to generate new one. + if (avatarUrl) { + return avatarUrl; + } + + let avatarKey = null; + + if (email) { + avatarKey = email; + } else { + avatarKey = avatarId; + } + + // If the ID looks like an email, we'll use gravatar. + // Otherwise, it's a random avatar, and we'll use the configured + // URL. + const isEmail = avatarKey && avatarKey.indexOf('@') > 0; + + if (!avatarKey) { + avatarKey = participantId; + } + + avatarKey = MD5.hexdigest(avatarKey.trim().toLowerCase()); + + let urlPref = null; + let urlSuf = null; + + // gravatar doesn't support random avatars that's why we need to use other + // services for the use case when the email is undefined. + if (isEmail) { + urlPref = 'https://www.gravatar.com/avatar/'; + urlSuf = '?d=wavatar&size=200'; + } else if (typeof interfaceConfig === 'object' + && interfaceConfig.RANDOM_AVATAR_URL_PREFIX) { // custom avatar service + urlPref = interfaceConfig.RANDOM_AVATAR_URL_PREFIX; + urlSuf = interfaceConfig.RANDOM_AVATAR_URL_SUFFIX; + } else { // default avatar service + urlPref = 'https://api.adorable.io/avatars/200/'; + urlSuf = '.png'; + } + + return urlPref + avatarKey + urlSuf; +} diff --git a/react/features/base/participants/reducer.js b/react/features/base/participants/reducer.js index beca0eaf5..fdd9cc7dc 100644 --- a/react/features/base/participants/reducer.js +++ b/react/features/base/participants/reducer.js @@ -1,5 +1,3 @@ -/* global MD5 */ - import { ReducerRegistry, setStateProperty } from '../redux'; import { @@ -14,6 +12,7 @@ import { LOCAL_PARTICIPANT_DEFAULT_ID, PARTICIPANT_ROLE } from './constants'; +import { getAvatarURL } from './functions'; /** * Participant object. @@ -64,11 +63,16 @@ function _participant(state, action) { case PARTICIPANT_ID_CHANGED: if (state.id === action.oldValue) { const id = action.newValue; + const { avatarId, avatarUrl, email } = state; return { ...state, id, - avatar: state.avatar || _getAvatarURL(id, state.email) + avatar: state.avatar || getAvatarURL(id, { + avatarId, + avatarUrl, + email + }) }; } break; @@ -81,8 +85,14 @@ function _participant(state, action) { const id = participant.id || (participant.local && LOCAL_PARTICIPANT_DEFAULT_ID); + const { avatarId, avatarUrl, email } = participant; const avatar - = participant.avatar || _getAvatarURL(id, participant.email); + = participant.avatar + || getAvatarURL(id, { + avatarId, + avatarUrl, + email + }); // TODO Get these names from config/localized. const name @@ -90,7 +100,7 @@ function _participant(state, action) { return { avatar, - email: participant.email, + email, id, local: participant.local || false, name, @@ -113,8 +123,13 @@ function _participant(state, action) { } if (!newState.avatar) { - newState.avatar - = _getAvatarURL(action.participant.id, newState.email); + const { avatarId, avatarUrl, email } = newState; + + newState.avatar = getAvatarURL(action.participant.id, { + avatarId, + avatarUrl, + email + }); } return newState; @@ -162,43 +177,3 @@ ReducerRegistry.register('features/base/participants', (state = [], action) => { return state; } }); - -/** - * Returns the URL of the image for the avatar of a particular participant - * identified by their id and/or e-mail address. - * - * @param {string} participantId - Participant's id. - * @param {string} [email] - Participant's email. - * @returns {string} The URL of the image for the avatar of the participant - * identified by the specified participantId and/or email. - */ -function _getAvatarURL(participantId, email) { - // TODO: Use disableThirdPartyRequests config. - - let avatarId = email || participantId; - - // If the ID looks like an email, we'll use gravatar. Otherwise, it's a - // random avatar and we'll use the configured URL. - const random = !avatarId || avatarId.indexOf('@') < 0; - - if (!avatarId) { - avatarId = participantId; - } - - // MD5 is provided by Strophe - avatarId = MD5.hexdigest(avatarId.trim().toLowerCase()); - - let urlPref = null; - let urlSuf = null; - - if (random) { - // TODO: Use RANDOM_AVATAR_URL_PREFIX from interface config. - urlPref = 'https://robohash.org/'; - urlSuf = '.png?size=200x200'; - } else { - urlPref = 'https://www.gravatar.com/avatar/'; - urlSuf = '?d=wavatar&size=200'; - } - - return urlPref + avatarId + urlSuf; -}