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 { 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);

View File

@ -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);
}
};

View File

@ -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.

View File

@ -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

View File

@ -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
* }
* }}

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';

View File

@ -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;
}

View File

@ -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;
}