diff --git a/conference.js b/conference.js index 9469f55a8..dc2d02dc8 100644 --- a/conference.js +++ b/conference.js @@ -21,18 +21,13 @@ import analytics from './modules/analytics/analytics'; import EventEmitter from "events"; import { - CONFERENCE_JOINED, + conferenceJoined, conferenceFailed, conferenceLeft } from './react/features/base/conference'; import { isFatalJitsiConnectionError } from './react/features/base/lib-jitsi-meet'; -import { - mediaPermissionPromptVisibilityChanged, - suspendDetected -} from './react/features/overlay'; - import { changeParticipantAvatarID, changeParticipantAvatarURL, @@ -41,6 +36,10 @@ import { participantLeft, participantRoleChanged } from './react/features/base/participants'; +import { + mediaPermissionPromptVisibilityChanged, + suspendDetected +} from './react/features/overlay'; const ConnectionEvents = JitsiMeetJS.events.connection; const ConnectionErrors = JitsiMeetJS.errors.connection; @@ -164,8 +163,8 @@ function sendData (command, value) { } /** - * Setups initially the properties for the local participant - email, avatarId, - * avatarUrl, displayName, etc. + * Sets up initially the properties of the local participant - email, avatarID, + * avatarURL, displayName, etc. */ function _setupLocalParticipantProperties() { const email = APP.settings.getEmail(); @@ -174,7 +173,7 @@ function _setupLocalParticipantProperties() { const avatarUrl = APP.settings.getAvatarUrl(); avatarUrl && sendData(commands.AVATAR_URL, avatarUrl); - if(!email && !avatarUrl) { + if (!email && !avatarUrl) { sendData(commands.AVATAR_ID, APP.settings.getAvatarId()); } @@ -1138,16 +1137,15 @@ 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.store.dispatch(conferenceJoined(room)); + APP.UI.mucJoined(); APP.API.notifyConferenceJoined(APP.conference.roomName); APP.UI.markVideoInterrupted(false); }); - room.on(ConferenceEvents.CONFERENCE_LEFT, + room.on( + ConferenceEvents.CONFERENCE_LEFT, (...args) => APP.store.dispatch(conferenceLeft(room, ...args))); room.on( diff --git a/modules/UI/avatar/Avatar.js b/modules/UI/avatar/Avatar.js index d3dbb322f..e29619dee 100644 --- a/modules/UI/avatar/Avatar.js +++ b/modules/UI/avatar/Avatar.js @@ -91,6 +91,11 @@ export default { user = users[userId]; } - return getAvatarURL(userId, user); + return getAvatarURL({ + avatarID: user ? user.avatarId : undefined, + avatarURL: user ? user.avatarUrl : undefined, + email: user ? user.email : undefined, + id: userId + }); } }; diff --git a/react/features/app/components/AbstractApp.js b/react/features/app/components/AbstractApp.js index f15655020..fe518c025 100644 --- a/react/features/app/components/AbstractApp.js +++ b/react/features/app/components/AbstractApp.js @@ -1,5 +1,3 @@ -/* global APP */ - import React, { Component } from 'react'; import { I18nextProvider } from 'react-i18next'; import { Provider } from 'react-redux'; @@ -20,6 +18,8 @@ import { appWillUnmount } from '../actions'; +declare var APP: Object; + /** * Base (abstract) class for main App component. * @@ -78,11 +78,20 @@ export class AbstractApp extends Component { dispatch(appWillMount(this)); - dispatch(localParticipantJoined({ - avatarId: APP.settings.getAvatarId(), - avatarUrl: APP.settings.getAvatarUrl(), - email: APP.settings.getEmail() - })); + // FIXME I believe it makes more sense for a middleware to dispatch + // localParticipantJoined on APP_WILL_MOUNT because the order of actions + // is important, not the call site. Moreover, we've got localParticipant + // business logic in the React Component (i.e. UI) AbstractApp now. + let localParticipant; + + if (typeof APP !== 'undefined') { + localParticipant = { + avatarID: APP.settings.getAvatarId(), + avatarURL: APP.settings.getAvatarUrl(), + email: APP.settings.getEmail() + }; + } + dispatch(localParticipantJoined(localParticipant)); // 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 430c6113e..65d301f12 100644 --- a/react/features/base/conference/actions.js +++ b/react/features/base/conference/actions.js @@ -35,7 +35,7 @@ function _addConferenceListeners(conference, dispatch) { (...args) => dispatch(conferenceFailed(conference, ...args))); conference.on( JitsiConferenceEvents.CONFERENCE_JOINED, - (...args) => dispatch(_conferenceJoined(conference, ...args))); + (...args) => dispatch(conferenceJoined(conference, ...args))); conference.on( JitsiConferenceEvents.CONFERENCE_LEFT, (...args) => dispatch(conferenceLeft(conference, ...args))); @@ -103,7 +103,7 @@ export function conferenceFailed(conference, error) { * joined by the local participant. * @returns {Function} */ -function _conferenceJoined(conference) { +export function conferenceJoined(conference) { return (dispatch, getState) => { const localTracks = getState()['features/base/tracks'] diff --git a/react/features/base/participants/actions.js b/react/features/base/participants/actions.js index d3fd91e03..02f8f2c65 100644 --- a/react/features/base/participants/actions.js +++ b/react/features/base/participants/actions.js @@ -9,24 +9,24 @@ import { import { getLocalParticipant } from './functions'; /** - * Action to update a participant's avatar id. + * Action to update a participant's avatar ID. * - * @param {string} id - Participant's id. - * @param {string} avatarId - 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, - * } + * type: PARTICIPANT_UPDATED, + * participant: { + * id: string, + * avatarID: string, + * } * }} */ -export function changeParticipantAvatarID(id, avatarId) { +export function changeParticipantAvatarID(id, avatarID) { return { type: PARTICIPANT_UPDATED, participant: { id, - avatarId + avatarID } }; } @@ -34,22 +34,22 @@ export function changeParticipantAvatarID(id, avatarId) { /** * Action to update a participant's avatar URL. * - * @param {string} id - Participant's id. - * @param {string} url - Participant's avatar url. + * @param {string} id - Participant's ID. + * @param {string} avatarURL - Participant's avatar URL. * @returns {{ - * type: PARTICIPANT_UPDATED, - * participant: { - * id: string, - * url: string, - * } + * type: PARTICIPANT_UPDATED, + * participant: { + * id: string, + * avatarURL: string, + * } * }} */ -export function changeParticipantAvatarURL(id, url) { +export function changeParticipantAvatarURL(id, avatarURL) { return { type: PARTICIPANT_UPDATED, participant: { id, - url + avatarURL } }; } @@ -57,14 +57,14 @@ export function changeParticipantAvatarURL(id, url) { /** * Action to update a participant's email. * - * @param {string} id - Participant's id. + * @param {string} id - Participant's ID. * @param {string} email - Participant's email. * @returns {{ - * type: PARTICIPANT_UPDATED, - * participant: { - * id: string, - * email: string - * } + * type: PARTICIPANT_UPDATED, + * participant: { + * id: string, + * email: string + * } * }} */ export function changeParticipantEmail(id, email) { @@ -80,12 +80,12 @@ export function changeParticipantEmail(id, email) { /** * Create an action for when dominant speaker changes. * - * @param {string} id - Participant id. + * @param {string} id - Participant's ID. * @returns {{ - * type: DOMINANT_SPEAKER_CHANGED, - * participant: { - * id: string - * } + * type: DOMINANT_SPEAKER_CHANGED, + * participant: { + * id: string + * } * }} */ export function dominantSpeakerChanged(id) { @@ -103,9 +103,9 @@ export function dominantSpeakerChanged(id) { * * @param {string} id - New ID for local participant. * @returns {{ - * type: PARTICIPANT_ID_CHANGED, - * newValue: string, - * oldValue: string + * type: PARTICIPANT_ID_CHANGED, + * newValue: string, + * oldValue: string * }} */ export function localParticipantIdChanged(id) { @@ -127,8 +127,8 @@ export function localParticipantIdChanged(id) { * * @param {Participant} participant={} - Information about participant. * @returns {{ - * type: PARTICIPANT_JOINED, - * participant: Participant + * type: PARTICIPANT_JOINED, + * participant: Participant * }} */ export function localParticipantJoined(participant = {}) { @@ -170,14 +170,14 @@ export function participantJoined(participant) { } /** - * Action to handle case when participant lefts. + * Action to signal that a participant has left. * - * @param {string} id - Participant id. + * @param {string} id - Participant's ID. * @returns {{ - * type: PARTICIPANT_LEFT, - * participant: { - * id: string - * } + * type: PARTICIPANT_LEFT, + * participant: { + * id: string + * } * }} */ export function participantLeft(id) { @@ -190,16 +190,16 @@ export function participantLeft(id) { } /** - * Action to handle case when participant's role changes. + * Action to signal that a participant's role has changed. * - * @param {string} id - Participant id. + * @param {string} id - Participant's ID. * @param {PARTICIPANT_ROLE} role - Participant's new role. * @returns {{ - * type: PARTICIPANT_UPDATED, - * participant: { - * id: string, - * role: PARTICIPANT_ROLE - * } + * type: PARTICIPANT_UPDATED, + * participant: { + * id: string, + * role: PARTICIPANT_ROLE + * } * }} */ export function participantRoleChanged(id, role) { @@ -218,10 +218,10 @@ export function participantRoleChanged(id, role) { * @param {string|null} id - The ID of the conference participant to pin or null * if none of the conference's participants are to be pinned. * @returns {{ - * type: PIN_PARTICIPANT, - * participant: { - * id: string - * } + * type: PIN_PARTICIPANT, + * participant: { + * id: string + * } * }} */ export function pinParticipant(id) { diff --git a/react/features/base/participants/components/Avatar.web.js b/react/features/base/participants/components/Avatar.web.js index 01d9ee2d2..61aaa61f3 100644 --- a/react/features/base/participants/components/Avatar.web.js +++ b/react/features/base/participants/components/Avatar.web.js @@ -24,9 +24,6 @@ export default class Avatar extends Component { * @inheritdoc */ render() { - return ( - - ); + return ; } } diff --git a/react/features/base/participants/functions.js b/react/features/base/participants/functions.js index d74e830d9..28e85a2ff 100644 --- a/react/features/base/participants/functions.js +++ b/react/features/base/participants/functions.js @@ -1,7 +1,67 @@ -/* global MD5 */ - declare var config: Object; declare var interfaceConfig: Object; +declare var MD5: Object; + +/** + * Returns the URL of the image for the avatar of a specific participant. + * + * @param {Participant} [participant] - The participant to return the avatar URL + * of. + * @param {string} [participant.avatarID] - Participant's avatar ID. + * @param {string} [participant.avatarURL] - Participant's avatar URL. + * @param {string} [participant.email] - Participant's e-mail address. + * @param {string} [participant.id] - Participant's ID. + * @returns {string} The URL of the image for the avatar of the specified + * participant. + * + * @public + */ +export function getAvatarURL(participant) { + // If disableThirdPartyRequests disables third-party avatar services, we are + // restricted to a stock image of ours. + if (typeof config === 'object' && config.disableThirdPartyRequests) { + return 'images/avatar2.png'; + } + + const { avatarID, avatarURL, email, id } = participant; + + // If an avatarURL is specified, then obviously there's nothing to generate. + if (avatarURL) { + return avatarURL; + } + + let key = email || avatarID; + let urlPrefix; + let urlSuffix; + + // If the ID looks like an e-mail address, we'll use Gravatar because it + // supports e-mail addresses. + if (key && key.indexOf('@') > 0) { + urlPrefix = 'https://www.gravatar.com/avatar/'; + urlSuffix = '?d=wavatar&size=200'; + } else { + // Otherwise, we do not have much a choice but a random avatar (fetched + // from a configured avatar service). + if (!key) { + key = id; + } + + // The deployment is allowed to choose the avatar service which is to + // generate the random avatars. + urlPrefix + = typeof interfaceConfig === 'object' + && interfaceConfig.RANDOM_AVATAR_URL_PREFIX; + if (urlPrefix) { + urlSuffix = interfaceConfig.RANDOM_AVATAR_URL_SUFFIX; + } else { + // Otherwise, use a default (of course). + urlPrefix = 'https://api.adorable.io/avatars/200/'; + urlSuffix = '.png'; + } + } + + return urlPrefix + MD5.hexdigest(key.trim().toLowerCase()) + urlSuffix; +} /** * Returns local participant from Redux state. @@ -50,70 +110,3 @@ 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 fdd9cc7dc..8878bc638 100644 --- a/react/features/base/participants/reducer.js +++ b/react/features/base/participants/reducer.js @@ -63,79 +63,82 @@ function _participant(state, action) { case PARTICIPANT_ID_CHANGED: if (state.id === action.oldValue) { const id = action.newValue; - const { avatarId, avatarUrl, email } = state; - - return { + const newState = { ...state, - id, - avatar: state.avatar || getAvatarURL(id, { - avatarId, - avatarUrl, - email - }) + id }; - } - break; - - case PARTICIPANT_JOINED: { - const participant = action.participant; // eslint-disable-line no-shadow - // 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. - const id - = participant.id - || (participant.local && LOCAL_PARTICIPANT_DEFAULT_ID); - const { avatarId, avatarUrl, email } = participant; - const avatar - = participant.avatar - || getAvatarURL(id, { - avatarId, - avatarUrl, - email - }); - - // TODO Get these names from config/localized. - const name - = participant.name || (participant.local ? 'me' : 'Fellow Jitster'); - - return { - avatar, - email, - id, - local: participant.local || false, - name, - pinned: participant.pinned || false, - role: participant.role || PARTICIPANT_ROLE.NONE, - dominantSpeaker: participant.dominantSpeaker || false - }; - } - - case PARTICIPANT_UPDATED: - if (state.id === action.participant.id) { - const newState = { ...state }; - - for (const key in action.participant) { - if (action.participant.hasOwnProperty(key) - && PARTICIPANT_PROPS_TO_OMIT_WHEN_UPDATE.indexOf(key) - === -1) { - newState[key] = action.participant[key]; - } - } if (!newState.avatar) { - const { avatarId, avatarUrl, email } = newState; - - newState.avatar = getAvatarURL(action.participant.id, { - avatarId, - avatarUrl, - email - }); + newState.avatar = getAvatarURL(newState); } return newState; } break; + case PARTICIPANT_JOINED: { + const participant = action.participant; // eslint-disable-line no-shadow + const { avatar, dominantSpeaker, email, local, pinned, role } + = participant; + let { id, name } = participant; + + // 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; + } + + // name + if (!name) { + // TODO Get the from config and/or localized. + name = local ? 'me' : 'Fellow Jitster'; + } + + const newState = { + avatar, + dominantSpeaker: dominantSpeaker || false, + email, + id, + local: local || false, + name, + pinned: pinned || false, + role: role || PARTICIPANT_ROLE.NONE + }; + + if (!newState.avatar) { + newState.avatar = getAvatarURL(newState); + } + + return newState; + } + + case PARTICIPANT_UPDATED: { + const participant = action.participant; // eslint-disable-line no-shadow + const { id } = participant; + + if (state.id === id) { + const newState = { ...state }; + + for (const key in participant) { + if (participant.hasOwnProperty(key) + && PARTICIPANT_PROPS_TO_OMIT_WHEN_UPDATE.indexOf(key) + === -1) { + newState[key] = participant[key]; + } + } + + if (!newState.avatar) { + newState.avatar = getAvatarURL(newState); + } + + return newState; + } + break; + } + case PIN_PARTICIPANT: // Currently, only one pinned participant is allowed. return (