feat(Avatar): Implement Avatar for web
This commit is contained in:
parent
2e4b39c19c
commit
814bd26c07
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
* }
|
||||
* }}
|
||||
|
|
|
@ -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 } />
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1 +1,2 @@
|
|||
export { default as Avatar } from './Avatar';
|
||||
export { default as ParticipantView } from './ParticipantView';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue