feat(Avatar): Implement Avatar for web

This commit is contained in:
hristoterezov 2017-02-27 15:42:28 -06:00 committed by Lyubo Marinov
parent 2e4b39c19c
commit 814bd26c07
9 changed files with 250 additions and 119 deletions

View File

@ -20,7 +20,11 @@ import analytics from './modules/analytics/analytics';
import EventEmitter from "events"; import EventEmitter from "events";
import { conferenceFailed } from './react/features/base/conference'; import {
CONFERENCE_JOINED,
conferenceFailed,
conferenceLeft
} from './react/features/base/conference';
import { import {
isFatalJitsiConnectionError isFatalJitsiConnectionError
} from './react/features/base/lib-jitsi-meet'; } from './react/features/base/lib-jitsi-meet';
@ -29,6 +33,15 @@ import {
suspendDetected suspendDetected
} from './react/features/overlay'; } from './react/features/overlay';
import {
changeParticipantAvatarID,
changeParticipantAvatarURL,
changeParticipantEmail,
participantJoined,
participantLeft,
participantRoleChanged
} from './react/features/base/participants';
const ConnectionEvents = JitsiMeetJS.events.connection; const ConnectionEvents = JitsiMeetJS.events.connection;
const ConnectionErrors = JitsiMeetJS.errors.connection; const ConnectionErrors = JitsiMeetJS.errors.connection;
@ -150,6 +163,29 @@ function sendData (command, value) {
room.sendCommand(command, {value: 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. * Get user nickname by user id.
* @param {string} id user id * @param {string} id user id
@ -875,21 +911,7 @@ export default {
this.invite = new Invite(room); this.invite = new Invite(room);
this._room = room; // FIXME do not use this this._room = room; // FIXME do not use this
let email = APP.settings.getEmail(); _setupLocalParticipantProperties();
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);
this._setupListeners(); this._setupListeners();
}, },
@ -1116,11 +1138,18 @@ export default {
_setupListeners () { _setupListeners () {
// add local streams when joined to the conference // add local streams when joined to the conference
room.on(ConferenceEvents.CONFERENCE_JOINED, () => { room.on(ConferenceEvents.CONFERENCE_JOINED, () => {
APP.store.dispatch({
type: CONFERENCE_JOINED,
conference: room
});
APP.UI.mucJoined(); APP.UI.mucJoined();
APP.API.notifyConferenceJoined(APP.conference.roomName); APP.API.notifyConferenceJoined(APP.conference.roomName);
APP.UI.markVideoInterrupted(false); APP.UI.markVideoInterrupted(false);
}); });
room.on(ConferenceEvents.CONFERENCE_LEFT,
(...args) => APP.store.dispatch(conferenceLeft(room, ...args)));
room.on( room.on(
ConferenceEvents.AUTH_STATUS_CHANGED, ConferenceEvents.AUTH_STATUS_CHANGED,
function (authEnabled, authLogin) { function (authEnabled, authLogin) {
@ -1134,6 +1163,12 @@ export default {
if (user.isHidden()) if (user.isHidden())
return; return;
APP.store.dispatch(participantJoined({
id,
name: user.getDisplayName(),
role: user.getRole()
}));
logger.log('USER %s connnected', id, user); logger.log('USER %s connnected', id, user);
APP.API.notifyUserJoined(id); APP.API.notifyUserJoined(id);
APP.UI.addUser(user); APP.UI.addUser(user);
@ -1142,6 +1177,7 @@ export default {
APP.UI.updateUserRole(user); APP.UI.updateUserRole(user);
}); });
room.on(ConferenceEvents.USER_LEFT, (id, user) => { room.on(ConferenceEvents.USER_LEFT, (id, user) => {
APP.store.dispatch(participantLeft(id, user));
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());
@ -1150,6 +1186,7 @@ export default {
room.on(ConferenceEvents.USER_ROLE_CHANGED, (id, role) => { room.on(ConferenceEvents.USER_ROLE_CHANGED, (id, role) => {
APP.store.dispatch(participantRoleChanged(id, role));
if (this.isLocalId(id)) { if (this.isLocalId(id)) {
logger.info(`My role changed, new role: ${role}`); logger.info(`My role changed, new role: ${role}`);
if (this.isModerator !== room.isModerator()) { if (this.isModerator !== room.isModerator()) {
@ -1411,17 +1448,22 @@ 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(changeParticipantEmail(from, data.value));
APP.UI.setUserEmail(from, data.value); APP.UI.setUserEmail(from, data.value);
}); });
room.addCommandListener( room.addCommandListener(
this.commands.defaults.AVATAR_URL, this.commands.defaults.AVATAR_URL,
(data, from) => { (data, from) => {
APP.store.dispatch(
changeParticipantAvatarURL(from, data.value));
APP.UI.setUserAvatarUrl(from, data.value); APP.UI.setUserAvatarUrl(from, data.value);
}); });
room.addCommandListener(this.commands.defaults.AVATAR_ID, room.addCommandListener(this.commands.defaults.AVATAR_ID,
(data, from) => { (data, from) => {
APP.store.dispatch(
changeParticipantAvatarID(from, data.value));
APP.UI.setUserAvatarID(from, data.value); APP.UI.setUserAvatarID(from, data.value);
}); });
@ -1832,6 +1874,7 @@ export default {
if (email === APP.settings.getEmail()) { if (email === APP.settings.getEmail()) {
return; return;
} }
APP.store.dispatch(changeParticipantEmail(room.myUserId(), email));
APP.settings.setEmail(email); APP.settings.setEmail(email);
APP.UI.setUserEmail(room.myUserId(), email); APP.UI.setUserEmail(room.myUserId(), email);
@ -1848,6 +1891,7 @@ export default {
if (url === APP.settings.getAvatarUrl()) { if (url === APP.settings.getAvatarUrl()) {
return; return;
} }
APP.store.dispatch(changeParticipantAvatarURL(room.myUserId(), url));
APP.settings.setAvatarUrl(url); APP.settings.setAvatarUrl(url);
APP.UI.setUserAvatarUrl(room.myUserId(), url); APP.UI.setUserAvatarUrl(room.myUserId(), url);

View File

@ -21,8 +21,9 @@
* SOFTWARE. * SOFTWARE.
*/ */
/* global MD5, config, interfaceConfig, APP */ /* global APP */
const logger = require("jitsi-meet-logger").getLogger(__filename);
import { getAvatarURL } from '../../../react/features/base/participants';
let users = {}; let users = {};
@ -64,7 +65,7 @@ export default {
* @param url the url for the avatar * @param url the url for the avatar
*/ */
setUserAvatarUrl: function (id, url) { 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 * @param {string} userId user id
*/ */
getAvatarUrl: function (userId) { getAvatarUrl: function (userId) {
if (config.disableThirdPartyRequests) { let user;
return 'images/avatar2.png';
}
if (!userId || APP.conference.isLocalId(userId)) { if (!userId || APP.conference.isLocalId(userId)) {
userId = "local"; user = users.local;
userId = APP.conference.getMyUserId();
} else {
user = users[userId];
} }
let avatarId = null; return getAvatarURL(userId, user);
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;
} }
}; };

View File

@ -78,7 +78,11 @@ export class AbstractApp extends Component {
dispatch(appWillMount(this)); 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 // If a URL was explicitly specified to this React Component, then open
// it; otherwise, use a default. // it; otherwise, use a default.

View File

@ -38,7 +38,7 @@ function _addConferenceListeners(conference, dispatch) {
(...args) => dispatch(_conferenceJoined(conference, ...args))); (...args) => dispatch(_conferenceJoined(conference, ...args)));
conference.on( conference.on(
JitsiConferenceEvents.CONFERENCE_LEFT, JitsiConferenceEvents.CONFERENCE_LEFT,
(...args) => dispatch(_conferenceLeft(conference, ...args))); (...args) => dispatch(conferenceLeft(conference, ...args)));
conference.on( conference.on(
JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED, JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED,
@ -131,7 +131,7 @@ function _conferenceJoined(conference) {
* conference: JitsiConference * conference: JitsiConference
* }} * }}
*/ */
function _conferenceLeft(conference) { export function conferenceLeft(conference) {
return { return {
type: CONFERENCE_LEFT, type: CONFERENCE_LEFT,
conference conference

View File

@ -8,6 +8,52 @@ import {
} from './actionTypes'; } from './actionTypes';
import { getLocalParticipant } from './functions'; 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. * Action to update a participant's email.
* *
@ -17,7 +63,6 @@ import { getLocalParticipant } from './functions';
* type: PARTICIPANT_UPDATED, * type: PARTICIPANT_UPDATED,
* participant: { * participant: {
* id: string, * id: string,
* avatar: string,
* email: string * email: string
* } * }
* }} * }}

View File

@ -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 (
<img
src = { this.props.uri } />
);
}
}

View File

@ -1 +1,2 @@
export { default as Avatar } from './Avatar';
export { default as ParticipantView } from './ParticipantView'; export { default as ParticipantView } from './ParticipantView';

View File

@ -1,3 +1,8 @@
/* global MD5 */
declare var config: Object;
declare var interfaceConfig: Object;
/** /**
* Returns local participant from Redux state. * Returns local participant from Redux state.
* *
@ -45,3 +50,70 @@ function _getParticipants(participantsOrGetState) {
return participants || []; 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;
}

View File

@ -1,5 +1,3 @@
/* global MD5 */
import { ReducerRegistry, setStateProperty } from '../redux'; import { ReducerRegistry, setStateProperty } from '../redux';
import { import {
@ -14,6 +12,7 @@ import {
LOCAL_PARTICIPANT_DEFAULT_ID, LOCAL_PARTICIPANT_DEFAULT_ID,
PARTICIPANT_ROLE PARTICIPANT_ROLE
} from './constants'; } from './constants';
import { getAvatarURL } from './functions';
/** /**
* Participant object. * Participant object.
@ -64,11 +63,16 @@ function _participant(state, action) {
case PARTICIPANT_ID_CHANGED: case PARTICIPANT_ID_CHANGED:
if (state.id === action.oldValue) { if (state.id === action.oldValue) {
const id = action.newValue; const id = action.newValue;
const { avatarId, avatarUrl, email } = state;
return { return {
...state, ...state,
id, id,
avatar: state.avatar || _getAvatarURL(id, state.email) avatar: state.avatar || getAvatarURL(id, {
avatarId,
avatarUrl,
email
})
}; };
} }
break; break;
@ -81,8 +85,14 @@ function _participant(state, action) {
const id const id
= participant.id = participant.id
|| (participant.local && LOCAL_PARTICIPANT_DEFAULT_ID); || (participant.local && LOCAL_PARTICIPANT_DEFAULT_ID);
const { avatarId, avatarUrl, email } = participant;
const avatar const avatar
= participant.avatar || _getAvatarURL(id, participant.email); = participant.avatar
|| getAvatarURL(id, {
avatarId,
avatarUrl,
email
});
// TODO Get these names from config/localized. // TODO Get these names from config/localized.
const name const name
@ -90,7 +100,7 @@ function _participant(state, action) {
return { return {
avatar, avatar,
email: participant.email, email,
id, id,
local: participant.local || false, local: participant.local || false,
name, name,
@ -113,8 +123,13 @@ function _participant(state, action) {
} }
if (!newState.avatar) { if (!newState.avatar) {
newState.avatar const { avatarId, avatarUrl, email } = newState;
= _getAvatarURL(action.participant.id, newState.email);
newState.avatar = getAvatarURL(action.participant.id, {
avatarId,
avatarUrl,
email
});
} }
return newState; return newState;
@ -162,43 +177,3 @@ ReducerRegistry.register('features/base/participants', (state = [], action) => {
return state; 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;
}