feat: initial based avatars

This commit is contained in:
Bettenbuk Zoltan 2019-06-26 16:08:23 +02:00 committed by Zoltan Bettenbuk
parent 0734ce7ae3
commit 72137a2811
49 changed files with 902 additions and 700 deletions

14
css/_avatar.scss Normal file
View File

@ -0,0 +1,14 @@
.avatar {
align-items: center;
background-color: #AAA;
display: flex;
border-radius: 50%;
color: rgba(255, 255, 255, 0.6);
font-weight: 100;
justify-content: center;
object-fit: cover;
}
.defaultAvatar {
opacity: 0.6
}

View File

@ -493,7 +493,6 @@
}
#dominantSpeakerAvatarContainer,
#dominantSpeakerAvatar,
.dynamic-shadow {
width: 200px;
height: 200px;
@ -503,14 +502,9 @@
top: 50px;
margin: auto;
position: relative;
border-radius: 100px;
overflow: hidden;
visibility: inherit;
}
#dominantSpeakerAvatar {
background-color: #000000;
object-fit: cover;
}
.dynamic-shadow {
border-radius: 50%;
@ -524,7 +518,6 @@
.avatar-container {
@include maxSize(60px);
@include absoluteAligning();
border-radius: 50%;
display: flex;
justify-content: center;
height: 50%;

View File

@ -86,5 +86,6 @@ $flagsImagePath: "../images/";
@import 'navigate_section_list';
@import 'third-party-branding/google';
@import 'third-party-branding/microsoft';
@import 'avatar';
/* Modules END */

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -510,8 +510,8 @@ UI.dockToolbar = dock => APP.store.dispatch(dockToolbox(dock));
* @param {string} avatarURL - The URL to avatar image to display.
* @returns {void}
*/
UI.refreshAvatarDisplay = function(id, avatarURL) {
VideoLayout.changeUserAvatar(id, avatarURL);
UI.refreshAvatarDisplay = function(id) {
VideoLayout.changeUserAvatar(id);
};
/**

View File

@ -33,7 +33,7 @@ SharedVideoThumb.prototype.constructor = SharedVideoThumb;
SharedVideoThumb.prototype.setDeviceAvailabilityIcons = function() {};
// eslint-disable-next-line no-empty-function
SharedVideoThumb.prototype.avatarChanged = function() {};
SharedVideoThumb.prototype.initializeAvatar = function() {};
SharedVideoThumb.prototype.createContainer = function(spanId) {
const container = document.createElement('span');

View File

@ -5,11 +5,8 @@ import ReactDOM from 'react-dom';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import { Avatar } from '../../../react/features/base/avatar';
import { i18next } from '../../../react/features/base/i18n';
import {
Avatar,
getAvatarURLByParticipantId
} from '../../../react/features/base/participants';
import { PresenceLabel } from '../../../react/features/presence-status';
/* eslint-enable no-unused-vars */
@ -214,8 +211,7 @@ export default class LargeVideoManager {
container.setStream(id, stream, videoType);
// change the avatar url on large
this.updateAvatar(
getAvatarURLByParticipantId(APP.store.getState(), id));
this.updateAvatar();
// If the user's connection is disrupted then the avatar will be
// displayed in case we have no video image cached. That is if
@ -406,18 +402,16 @@ export default class LargeVideoManager {
/**
* Updates the src of the dominant speaker avatar
*/
updateAvatar(avatarUrl) {
if (avatarUrl) {
ReactDOM.render(
updateAvatar() {
ReactDOM.render(
<Provider store = { APP.store }>
<Avatar
id = "dominantSpeakerAvatar"
uri = { avatarUrl } />,
this._dominantSpeakerAvatarContainer
);
} else {
ReactDOM.unmountComponentAtNode(
this._dominantSpeakerAvatarContainer);
}
participantId = { this.id }
size = { 200 } />
</Provider>,
this._dominantSpeakerAvatarContainer
);
}
/**

View File

@ -7,9 +7,6 @@ import { Provider } from 'react-redux';
import { JitsiTrackEvents } from '../../../react/features/base/lib-jitsi-meet';
import { VideoTrack } from '../../../react/features/base/media';
import {
getAvatarURLByParticipantId
} from '../../../react/features/base/participants';
import { updateSettings } from '../../../react/features/base/settings';
import { getLocalVideoTrack } from '../../../react/features/base/tracks';
import { shouldDisplayTileView } from '../../../react/features/video-layout';
@ -55,8 +52,7 @@ function LocalVideo(VideoLayout, emitter, streamEndedCallback) {
// Initialize the avatar display with an avatar url selected from the redux
// state. Redux stores the local user with a hardcoded participant id of
// 'local' if no id has been assigned yet.
this.avatarChanged(
getAvatarURLByParticipantId(APP.store.getState(), this.id));
this.initializeAvatar();
this.addAudioLevelIndicator();
this.updateIndicators();

View File

@ -10,9 +10,8 @@ import { Provider } from 'react-redux';
import { i18next } from '../../../react/features/base/i18n';
import { AudioLevelIndicator }
from '../../../react/features/audio-level-indicator';
import { Avatar as AvatarDisplay } from '../../../react/features/base/avatar';
import {
Avatar as AvatarDisplay,
getAvatarURLByParticipantId,
getPinnedParticipant,
pinParticipant
} from '../../../react/features/base/participants';
@ -570,8 +569,7 @@ SmallVideo.prototype.updateView = function() {
if (!this.hasAvatar) {
if (this.id) {
// Init avatar
this.avatarChanged(
getAvatarURLByParticipantId(APP.store.getState(), this.id));
this.initializeAvatar();
} else {
logger.error('Unable to init avatar - no id', this);
@ -609,19 +607,22 @@ SmallVideo.prototype.updateView = function() {
* Updates the react component displaying the avatar with the passed in avatar
* url.
*
* @param {string} avatarUrl - The uri to the avatar image.
* @returns {void}
*/
SmallVideo.prototype.avatarChanged = function(avatarUrl) {
SmallVideo.prototype.initializeAvatar = function() {
const thumbnail = this.$avatar().get(0);
this.hasAvatar = true;
if (thumbnail) {
// Maybe add a special case for local participant, as on init of
// LocalVideo.js the id is set to "local" but will get updated later.
ReactDOM.render(
<AvatarDisplay
className = 'userAvatar'
uri = { avatarUrl } />,
<Provider store = { APP.store }>
<AvatarDisplay
className = 'userAvatar'
participantId = { this.id } />
</Provider>,
thumbnail
);
}

View File

@ -880,7 +880,7 @@ const VideoLayout = {
const smallVideo = VideoLayout.getSmallVideo(id);
if (smallVideo) {
smallVideo.avatarChanged(avatarUrl);
smallVideo.initializeAvatar();
} else {
logger.warn(
`Missed avatar update - no small video yet for ${id}`

4
package-lock.json generated
View File

@ -8738,8 +8738,8 @@
"integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls="
},
"js-utils": {
"version": "github:jitsi/js-utils#73a67a7a60d52f8e895f50939c8fcbd1f20fe7b5",
"from": "github:jitsi/js-utils#73a67a7a60d52f8e895f50939c8fcbd1f20fe7b5",
"version": "github:jitsi/js-utils#192b1c996e8c05530eb1f19e82a31069c3021e31",
"from": "github:jitsi/js-utils#192b1c996e8c05530eb1f19e82a31069c3021e31",
"requires": {
"bowser": "1.9.1",
"js-md5": "0.7.3"

View File

@ -49,7 +49,7 @@
"jquery-contextmenu": "2.4.5",
"jquery-i18next": "1.2.0",
"js-md5": "0.6.1",
"js-utils": "github:jitsi/js-utils#73a67a7a60d52f8e895f50939c8fcbd1f20fe7b5",
"js-utils": "github:jitsi/js-utils#192b1c996e8c05530eb1f19e82a31069c3021e31",
"jsrsasign": "8.0.12",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#9bcc2a26cc94683b8ed302418695a331b450df97",

View File

@ -0,0 +1,177 @@
// @flow
import { PureComponent } from 'react';
import { getParticipantById } from '../../participants';
import { getAvatarColor, getInitials } from '../functions';
export type Props = {
/**
* The string we base the initials on (this is generated from a list of precendences).
*/
_initialsBase: ?string,
/**
* An URL that we validated that it can be loaded.
*/
_loadableAvatarUrl: ?string,
/**
* A string to override the initials to generate a color of. This is handy if you don't want to make
* the background color match the string that the initials are generated from.
*/
colorBase?: string,
/**
* Display name of the entity to render an avatar for (if any). This is handy when we need
* an avatar for a non-participasnt entity (e.g. a recent list item).
*/
displayName?: string,
/**
* The ID of the participant to render an avatar for (if it's a participant avatar).
*/
participantId?: string,
/**
* The size of the avatar.
*/
size: number,
/**
* URI of the avatar, if any.
*/
uri: ?string,
}
type State = {
avatarFailed: boolean
}
export const DEFAULT_SIZE = 65;
/**
* Implements an abstract class to render avatars in the app.
*/
export default class AbstractAvatar<P: Props> extends PureComponent<P, State> {
/**
* Instantiates a new {@code Component}.
*
* @inheritdoc
*/
constructor(props: P) {
super(props);
this.state = {
avatarFailed: false
};
this._onAvatarLoadError = this._onAvatarLoadError.bind(this);
}
/**
* Implements {@code Component#componentDidUpdate}.
*
* @inheritdoc
*/
componentDidUpdate(prevProps: P) {
if (prevProps.uri !== this.props.uri) {
// URI changed, so we need to try to fetch it again.
// Eslint doesn't like this statement, but based on the React doc, it's safe if it's
// wrapped in a condition: https://reactjs.org/docs/react-component.html#componentdidupdate
// eslint-disable-next-line react/no-did-update-set-state
this.setState({
avatarFailed: false
});
}
}
/**
* Implements {@code Componenr#render}.
*
* @inheritdoc
*/
render() {
const {
_initialsBase,
_loadableAvatarUrl,
colorBase,
uri
} = this.props;
const { avatarFailed } = this.state;
// _loadableAvatarUrl is validated that it can be loaded, but uri (if present) is not, so
// we still need to do a check for that. And an explicitly provided URI is higher priority than
// an avatar URL anyhow.
if ((uri && !avatarFailed) || _loadableAvatarUrl) {
return this._renderURLAvatar((!avatarFailed && uri) || _loadableAvatarUrl);
}
const _initials = getInitials(_initialsBase);
if (_initials) {
return this._renderInitialsAvatar(_initials, getAvatarColor(colorBase || _initialsBase));
}
return this._renderDefaultAvatar();
}
_onAvatarLoadError: () => void;
/**
* Callback to handle the error while loading of the avatar URI.
*
* @returns {void}
*/
_onAvatarLoadError() {
this.setState({
avatarFailed: true
});
}
/**
* Function to render the actual, platform specific default avatar component.
*
* @returns {React$Element<*>}
*/
_renderDefaultAvatar: () => React$Element<*>
/**
* Function to render the actual, platform specific initials-based avatar component.
*
* @param {string} initials - The initials to use.
* @param {string} color - The color to use.
* @returns {React$Element<*>}
*/
_renderInitialsAvatar: (string, string) => React$Element<*>
/**
* Function to render the actual, platform specific URL-based avatar component.
*
* @param {string} uri - The URI of the avatar.
* @returns {React$Element<*>}
*/
_renderURLAvatar: ?string => React$Element<*>
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @param {Props} ownProps - The own props of the component.
* @returns {Props}
*/
export function _mapStateToProps(state: Object, ownProps: Props) {
const { displayName, participantId } = ownProps;
const _participant = participantId && getParticipantById(state, participantId);
const _initialsBase = (_participant && (_participant.name || _participant.email)) || displayName;
return {
_initialsBase,
_loadableAvatarUrl: _participant && _participant.loadableAvatarUrl
};
}

View File

@ -0,0 +1,3 @@
// @flow
export * from './native';

View File

@ -0,0 +1,3 @@
// @flow
export * from './web';

View File

@ -0,0 +1,106 @@
// @flow
import React from 'react';
import { Image, Text, View } from 'react-native';
import { connect } from '../../../redux';
import { type StyleType } from '../../../styles';
import AbstractAvatar, {
_mapStateToProps,
type Props as AbstractProps,
DEFAULT_SIZE
} from '../AbstractAvatar';
import RemoteAvatar, { DEFAULT_AVATAR } from './RemoteAvatar';
import styles from './styles';
type Props = AbstractProps & {
/**
* External style of the component.
*/
style?: StyleType
}
/**
* Implements an avatar component that has 4 ways to render an avatar:
*
* - Based on an explicit avatar URI, if provided
* - Gravatar, if there is any
* - Based on initials generated from name or email
* - Default avatar icon, if any of the above fails
*/
class Avatar extends AbstractAvatar<Props> {
_onAvatarLoadError: () => void;
/**
* Implements {@code AbstractAvatar#_renderDefaultAvatar}.
*
* @inheritdoc
*/
_renderDefaultAvatar() {
return this._wrapAvatar(
<Image
source = { DEFAULT_AVATAR }
style = { [
styles.avatarContent(this.props.size || DEFAULT_SIZE),
styles.staticAvatar
] } />
);
}
/**
* Implements {@code AbstractAvatar#_renderGravatar}.
*
* @inheritdoc
*/
_renderInitialsAvatar(initials, color) {
return this._wrapAvatar(
<View
style = { [
styles.initialsContainer,
{
backgroundColor: color
}
] }>
<Text style = { styles.initialsText(this.props.size || DEFAULT_SIZE) }> { initials } </Text>
</View>
);
}
/**
* Implements {@code AbstractAvatar#_renderGravatar}.
*
* @inheritdoc
*/
_renderURLAvatar(uri) {
return this._wrapAvatar(
<RemoteAvatar
onError = { this._onAvatarLoadError }
size = { this.props.size || DEFAULT_SIZE }
uri = { uri } />
);
}
/**
* Wraps an avatar into a common wrapper.
*
* @param {React#Component} avatar - The avatar component.
* @returns {React#Component}
*/
_wrapAvatar(avatar) {
return (
<View
style = { [
styles.avatarContainer(this.props.size || DEFAULT_SIZE),
this.props.style
] }>
{ avatar }
</View>
);
}
}
export default connect(_mapStateToProps)(Avatar);

View File

@ -0,0 +1,50 @@
// @flow
import React, { PureComponent } from 'react';
import { Image } from 'react-native';
import styles from './styles';
export const DEFAULT_AVATAR = require('../../../../../../images/avatar.png');
type Props = {
/**
* Callback for load errors.
*/
onError: Function,
/**
* Size of the avatar.
*/
size: number,
/**
* URI of the avatar to load.
*/
uri: string
};
/**
* Implements a private class that is used to fetch and render remote avatars based on an URI.
*/
export default class RemoteAvatar extends PureComponent<Props> {
/**
* Implements {@code Component#render}.
*
* @inheritdoc
*/
render() {
const { onError, size, uri } = this.props;
return (
<Image
defaultSource = { DEFAULT_AVATAR }
onError = { onError }
resizeMode = 'cover'
source = {{ uri }}
style = { styles.avatarContent(size) } />
);
}
}

View File

@ -0,0 +1,3 @@
// @flow
export { default as Avatar } from './Avatar';

View File

@ -0,0 +1,47 @@
// @flow
import { ColorPalette } from '../../../styles';
/**
* The styles of the feature base/participants.
*/
export default {
avatarContainer: (size: number) => {
return {
alignItems: 'center',
borderRadius: size / 2,
height: size,
justifyContent: 'center',
overflow: 'hidden',
width: size
};
},
avatarContent: (size: number) => {
return {
height: size,
width: size
};
},
initialsContainer: {
alignItems: 'center',
alignSelf: 'stretch',
flex: 1,
justifyContent: 'center'
},
initialsText: (size: number) => {
return {
color: 'rgba(255, 255, 255, 0.6)',
fontSize: size * 0.5,
fontWeight: '100'
};
},
staticAvatar: {
backgroundColor: ColorPalette.lightGrey,
opacity: 0.4
}
};

View File

@ -0,0 +1,98 @@
// @flow
import React from 'react';
import { connect } from '../../../redux';
import AbstractAvatar, {
_mapStateToProps,
type Props as AbstractProps
} from '../AbstractAvatar';
type Props = AbstractProps & {
className?: string,
id: string
};
/**
* Implements an avatar as a React/Web {@link Component}.
*/
class Avatar extends AbstractAvatar<Props> {
/**
* Constructs a style object to be used on the avatars.
*
* @param {string?} color - The desired background color.
* @returns {Object}
*/
_getAvatarStyle(color) {
const { size } = this.props;
return {
backgroundColor: color || undefined,
fontSize: size ? size * 0.5 : '180%',
height: size || '100%',
width: size || '100%'
};
}
/**
* Constructs a list of class names required for the avatar component.
*
* @param {string} additional - Any additional class to add.
* @returns {string}
*/
_getAvatarClassName(additional) {
return `avatar ${additional || ''} ${this.props.className || ''}`;
}
_onAvatarLoadError: () => void;
/**
* Implements {@code AbstractAvatar#_renderDefaultAvatar}.
*
* @inheritdoc
*/
_renderDefaultAvatar() {
return (
<img
className = { this._getAvatarClassName('defaultAvatar') }
id = { this.props.id }
src = '/images/avatar.png'
style = { this._getAvatarStyle() } />
);
}
/**
* Implements {@code AbstractAvatar#_renderGravatar}.
*
* @inheritdoc
*/
_renderInitialsAvatar(initials, color) {
return (
<div
className = { this._getAvatarClassName() }
id = { this.props.id }
style = { this._getAvatarStyle(color) }>
{ initials }
</div>
);
}
/**
* Implements {@code AbstractAvatar#_renderGravatar}.
*
* @inheritdoc
*/
_renderURLAvatar(uri) {
return (
<img
className = { this._getAvatarClassName() }
id = { this.props.id }
onError = { this._onAvatarLoadError }
src = { uri }
style = { this._getAvatarStyle() } />
);
}
}
export default connect(_mapStateToProps)(Avatar);

View File

@ -0,0 +1,3 @@
// @flow
export { default as Avatar } from './Avatar';

View File

@ -0,0 +1,54 @@
// @flow
import _ from 'lodash';
const AVATAR_COLORS = [
'232, 105, 156',
'255, 198, 115',
'128, 128, 255',
'105, 232, 194',
'234, 255, 128'
];
const AVATAR_OPACITY = 0.4;
/**
* Generates the background color of an initials based avatar.
*
* @param {string?} initials - The initials of the avatar.
* @returns {string}
*/
export function getAvatarColor(initials: ?string) {
let colorIndex = 0;
if (initials) {
let nameHash = 0;
for (const s of initials) {
nameHash += s.codePointAt(0);
}
colorIndex = nameHash % AVATAR_COLORS.length;
}
return `rgba(${AVATAR_COLORS[colorIndex]}, ${AVATAR_OPACITY})`;
}
/**
* Generates initials for a simple string.
*
* @param {string?} s - The string to generate initials for.
* @returns {string?}
*/
export function getInitials(s: ?string) {
// We don't want to use the domain part of an email address, if it is one
const initialsBasis = _.split(s, '@')[0];
const words = _.words(initialsBasis);
let initials = '';
for (const w of words) {
(initials.length < 2) && (initials += w.substr(0, 1).toUpperCase());
}
return initials;
}

View File

@ -0,0 +1,3 @@
// @flow
export * from './components';

View File

@ -1,3 +1,5 @@
// @flow
/**
* Create an action for when dominant speaker changes.
*
@ -132,3 +134,17 @@ export const HIDDEN_PARTICIPANT_JOINED = 'HIDDEN_PARTICIPANT_JOINED';
* }
*/
export const HIDDEN_PARTICIPANT_LEFT = 'HIDDEN_PARTICIPANT_LEFT';
/**
* The type of Redux action which notifies the app that the loadable avatar URL has changed.
*
* {
* type: SET_LOADABLE_AVATAR_URL,
* participant: {
* id: string,
loadableAvatarUrl: string
* }
* }
*/
export const SET_LOADABLE_AVATAR_URL = 'SET_LOADABLE_AVATAR_URL';

View File

@ -12,7 +12,8 @@ import {
PARTICIPANT_KICKED,
PARTICIPANT_LEFT,
PARTICIPANT_UPDATED,
PIN_PARTICIPANT
PIN_PARTICIPANT,
SET_LOADABLE_AVATAR_URL
} from './actionTypes';
import {
getLocalParticipant,
@ -454,3 +455,26 @@ export function pinParticipant(id) {
}
};
}
/**
* Creates an action which notifies the app that the loadable URL of the avatar of a participant got updated.
*
* @param {string} participantId - The ID of the participant.
* @param {string} url - The new URL.
* @returns {{
* type: SET_LOADABLE_AVATAR_URL,
* participant: {
* id: string,
* loadableAvatarUrl: string
* }
* }}
*/
export function setLoadableAvatarUrl(participantId, url) {
return {
type: SET_LOADABLE_AVATAR_URL,
participant: {
id: participantId,
loadableAvatarUrl: url
}
};
}

View File

@ -1,320 +0,0 @@
// @flow
import React, { Component, Fragment, PureComponent } from 'react';
import { Dimensions, Image, Platform, View } from 'react-native';
import FastImage, {
type CacheControls,
type Priorities
} from 'react-native-fast-image';
import { ColorPalette } from '../../styles';
import styles from './styles';
/**
* The default image/source to be used in case none is specified or the
* specified one fails to load.
*
* XXX The relative path to the default/stock (image) file is defined by the
* {@code const} {@code DEFAULT_AVATAR_RELATIVE_PATH}. Unfortunately, the
* packager of React Native cannot deal with it early enough for the following
* {@code require} to succeed at runtime. Anyway, be sure to synchronize the
* relative path on Web and mobile for the purposes of consistency.
*
* @private
* @type {string}
*/
const _DEFAULT_SOURCE = require('../../../../../images/avatar.png');
/**
* The type of the React {@link Component} props of {@link Avatar}.
*/
type Props = {
/**
* The size for the {@link Avatar}.
*/
size: number,
/**
* The URI of the {@link Avatar}.
*/
uri: string
};
/**
* The type of the React {@link Component} state of {@link Avatar}.
*/
type State = {
/**
* Background color for the locally generated avatar.
*/
backgroundColor: string,
/**
* Error indicator for non-local avatars.
*/
error: boolean,
/**
* Indicates if the non-local avatar was loaded or not.
*/
loaded: boolean,
/**
* Source for the non-local avatar.
*/
source: {
uri?: string,
headers?: Object,
priority?: Priorities,
cache?: CacheControls,
}
};
/**
* Implements a React Native/mobile {@link Component} wich renders the content
* of an Avatar.
*/
class AvatarContent extends Component<Props, State> {
/**
* Initializes a new Avatar instance.
*
* @param {Props} props - The read-only React Component props with which
* the new instance is to be initialized.
*/
constructor(props: Props) {
super(props);
// Set the image source. The logic for the character # below is as
// follows:
// - Technically, URI is supposed to start with a scheme and scheme
// cannot contain the character #.
// - Technically, the character # in a URI signals the start of the
// fragment/hash.
// - Technically, the fragment/hash does not imply a retrieval
// action.
// - Practically, the fragment/hash does not always mandate a
// retrieval action. For example, an HTML anchor with an href that
// starts with the character # does not cause a Web browser to
// initiate a retrieval action.
// So I'll use the character # at the start of URI to not initiate
// an image retrieval action.
const source = {};
if (props.uri && !props.uri.startsWith('#')) {
source.uri = props.uri;
}
this.state = {
backgroundColor: this._getBackgroundColor(props),
error: false,
loaded: false,
source
};
// Bind event handlers so they are only bound once per instance.
this._onAvatarLoaded = this._onAvatarLoaded.bind(this);
this._onAvatarLoadError = this._onAvatarLoadError.bind(this);
}
/**
* Computes if the default avatar (ie, locally generated) should be used
* or not.
*/
get useDefaultAvatar() {
const { error, loaded, source } = this.state;
return !source.uri || error || !loaded;
}
/**
* Computes a hash over the URI and returns a HSL background color. We use
* 75% as lightness, for nice pastel style colors.
*
* @param {Object} props - The read-only React {@code Component} props from
* which the background color is to be generated.
* @private
* @returns {string} - The HSL CSS property.
*/
_getBackgroundColor({ uri }) {
if (!uri) {
return ColorPalette.white;
}
let hash = 0;
/* eslint-disable no-bitwise */
for (let i = 0; i < uri.length; i++) {
hash = uri.charCodeAt(i) + ((hash << 5) - hash);
hash |= 0; // Convert to 32-bit integer
}
/* eslint-enable no-bitwise */
return `hsl(${hash % 360}, 100%, 75%)`;
}
/**
* Helper which computes the style for the {@code Image} / {@code FastImage}
* component.
*
* @private
* @returns {Object}
*/
_getImageStyle() {
const { size } = this.props;
return {
...styles.avatar,
borderRadius: size / 2,
height: size,
width: size
};
}
_onAvatarLoaded: () => void;
/**
* Handler called when the remote image loading finishes. This doesn't
* necessarily mean the load was successful.
*
* @private
* @returns {void}
*/
_onAvatarLoaded() {
this.setState({ loaded: true });
}
_onAvatarLoadError: () => void;
/**
* Handler called when the remote image loading failed.
*
* @private
* @returns {void}
*/
_onAvatarLoadError() {
this.setState({ error: true });
}
/**
* Renders a default, locally generated avatar image.
*
* @private
* @returns {ReactElement}
*/
_renderDefaultAvatar() {
// When using a local image, react-native-fastimage falls back to a
// regular Image, so we need to wrap it in a view to make it round.
// https://github.com/facebook/react-native/issues/3198
const { backgroundColor } = this.state;
const imageStyle = this._getImageStyle();
const viewStyle = {
...imageStyle,
backgroundColor,
// FIXME @lyubomir: Without the opacity below I feel like the
// avatar colors are too strong. Besides, we use opacity for the
// ToolbarButtons. That's where I copied the value from and we
// may want to think about "standardizing" the opacity in the
// app in a way similar to ColorPalette.
opacity: 0.1,
overflow: 'hidden'
};
return (
<View style = { viewStyle }>
<Image
// The Image adds a fade effect without asking, so lets
// explicitly disable it. More info here:
// https://github.com/facebook/react-native/issues/10194
fadeDuration = { 0 }
resizeMode = 'contain'
source = { _DEFAULT_SOURCE }
style = { imageStyle } />
</View>
);
}
/**
* Renders an avatar using a remote image.
*
* @private
* @returns {ReactElement}
*/
_renderAvatar() {
const { source } = this.state;
let extraStyle;
if (this.useDefaultAvatar) {
// On Android, the image loading indicators don't work unless the
// Glide image is actually created, so we cannot use display: none.
// Instead, render it off-screen, which does the trick.
if (Platform.OS === 'android') {
const windowDimensions = Dimensions.get('window');
extraStyle = {
bottom: -windowDimensions.height,
right: -windowDimensions.width
};
} else {
extraStyle = { display: 'none' };
}
}
return (
<FastImage
onError = { this._onAvatarLoadError }
onLoadEnd = { this._onAvatarLoaded }
resizeMode = 'contain'
source = { source }
style = { [ this._getImageStyle(), extraStyle ] } />
);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
*/
render() {
const { source } = this.state;
return (
<Fragment>
{ source.uri && this._renderAvatar() }
{ this.useDefaultAvatar && this._renderDefaultAvatar() }
</Fragment>
);
}
}
/* eslint-disable react/no-multi-comp */
/**
* Implements an avatar as a React Native/mobile {@link Component}.
*
* Note: we use `key` in order to trigger a new component creation in case
* the URI changes.
*/
export default class Avatar extends PureComponent<Props> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
*/
render() {
return (
<AvatarContent
key = { this.props.uri }
{ ...this.props } />
);
}
}

View File

@ -1,38 +0,0 @@
// @flow
import React, { Component } from 'react';
/**
* The type of the React {@link Component} props of {@link Avatar}.
*/
type Props = {
/**
* The URI of the {@link Avatar}.
*/
uri: string
};
/**
* Implements an avatar as a React/Web {@link Component}.
*/
export default class Avatar extends Component<Props> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
*/
render() {
// Propagate all props of this Avatar but the ones consumed by this
// Avatar to the img it renders.
// eslint-disable-next-line no-unused-vars
const { uri, ...props } = this.props;
return (
<img
{ ...props }
src = { uri } />
);
}
}

View File

@ -2,8 +2,8 @@
import React, { Component } from 'react';
import { Text, View } from 'react-native';
import FastImage from 'react-native-fast-image';
import { Avatar } from '../../avatar';
import { translate } from '../../i18n';
import { JitsiParticipantConnectionStatus } from '../../lib-jitsi-meet';
import {
@ -16,13 +16,7 @@ import { StyleType } from '../../styles';
import { TestHint } from '../../testing/components';
import { getTrackByMediaTypeAndParticipant } from '../../tracks';
import Avatar from './Avatar';
import {
getAvatarURL,
getParticipantById,
getParticipantDisplayName,
shouldRenderParticipantVideo
} from '../functions';
import { shouldRenderParticipantVideo } from '../functions';
import styles from './styles';
/**
@ -30,14 +24,6 @@ import styles from './styles';
*/
type Props = {
/**
* The source (e.g. URI, URL) of the avatar image of the participant with
* {@link #participantId}.
*
* @private
*/
_avatar: string,
/**
* The connection status of the participant. Her video will only be rendered
* if the connection status is 'active'; otherwise, the avatar will be
@ -192,7 +178,6 @@ class ParticipantView extends Component<Props> {
*/
render() {
const {
_avatar: avatar,
_connectionStatus: connectionStatus,
_renderVideo: renderVideo,
_videoTrack: videoTrack,
@ -202,9 +187,6 @@ class ParticipantView extends Component<Props> {
const waitForVideoStarted = false;
// Is the avatar to be rendered?
const renderAvatar = Boolean(!renderVideo && avatar);
// If the connection has problems, we will "tint" the video / avatar.
const connectionProblem
= connectionStatus !== JitsiParticipantConnectionStatus.ACTIVE;
@ -238,10 +220,12 @@ class ParticipantView extends Component<Props> {
zOrder = { this.props.zOrder }
zoomEnabled = { this.props.zoomEnabled } /> }
{ renderAvatar
&& <Avatar
size = { this.props.avatarSize }
uri = { avatar } /> }
{ !renderVideo
&& <View style = { styles.avatarContainer }>
<Avatar
participantId = { this.props.participantId }
size = { this.props.avatarSize } />
</View> }
{ useTint
@ -265,45 +249,14 @@ class ParticipantView extends Component<Props> {
* @param {Object} ownProps - The React {@code Component} props passed to the
* associated (instance of) {@code ParticipantView}.
* @private
* @returns {{
* _avatar: string,
* _connectionStatus: string,
* _participantName: string,
* _renderVideo: boolean,
* _videoTrack: Track
* }}
* @returns {Props}
*/
function _mapStateToProps(state, ownProps) {
const { participantId } = ownProps;
const participant = getParticipantById(state, participantId);
let avatar;
let connectionStatus;
let participantName;
if (participant) {
avatar = getAvatarURL(participant);
connectionStatus = participant.connectionStatus;
participantName = getParticipantDisplayName(state, participant.id);
// Avatar (on React Native) now has the ability to generate an
// automatically-colored default image when no URI/URL is specified or
// when it fails to load. In order to make the coloring permanent(ish)
// per participant, Avatar will need something permanent(ish) per
// perticipant, obviously. A participant's ID is such a piece of data.
// But the local participant changes her ID as she joins, leaves.
// TODO @lyubomir: The participants may change their avatar URLs at
// runtime which means that, if their old and new avatar URLs fail to
// download, Avatar will change their automatically-generated colors.
avatar || participant.local || (avatar = `#${participant.id}`);
// ParticipantView knows before Avatar that an avatar URL will be used
// so it's advisable to prefetch here.
avatar && !avatar.startsWith('#')
&& FastImage.preload([ { uri: avatar } ]);
}
return {
_avatar: avatar,
_connectionStatus:
connectionStatus
|| JitsiParticipantConnectionStatus.ACTIVE,

View File

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

View File

@ -1,15 +1,17 @@
import { BoxModel, ColorPalette, createStyleSheet } from '../../styles';
// @flow
import { BoxModel, ColorPalette } from '../../styles';
/**
* The styles of the feature base/participants.
*/
export default createStyleSheet({
export default {
/**
* The style of the avatar of the participant.
* Container for the avatar in the view.
*/
avatar: {
alignSelf: 'center',
flex: 0
avatarContainer: {
alignItems: 'center',
justifyContent: 'center'
},
/**
@ -42,4 +44,4 @@ export default createStyleSheet({
flex: 1,
justifyContent: 'center'
}
});
};

View File

@ -1,11 +1,15 @@
// @flow
import { getAvatarURL as _getAvatarURL } from 'js-utils/avatar';
import {
getAvatarURL as _getAvatarURL,
getGravatarURL
} from 'js-utils/avatar';
import { toState } from '../redux';
import { JitsiParticipantConnectionStatus } from '../lib-jitsi-meet';
import { MEDIA_TYPE, shouldRenderVideoTrack } from '../media';
import { getTrackByMediaTypeAndParticipant } from '../tracks';
import { createDeferred } from '../util';
import {
DEFAULT_AVATAR_RELATIVE_PATH,
@ -13,10 +17,27 @@ import {
MAX_DISPLAY_NAME_LENGTH,
PARTICIPANT_ROLE
} from './constants';
import { preloadImage } from './preloadImage';
declare var config: Object;
declare var interfaceConfig: Object;
/**
* Temp structures for avatar urls to be checked/preloaded.
*/
const AVATAR_QUEUE = [];
const AVATAR_CHECKED_URLS = new Map();
/* eslint-disable arrow-body-style */
const AVATAR_CHECKER_FUNCTIONS = [
participant => {
return participant && participant.avatarURL ? participant.avatarURL : null;
},
participant => {
return participant && participant.email ? getGravatarURL(participant.email) : null;
}
];
/* eslint-enable arrow-body-style */
/**
* Returns the URL of the image for the avatar of a specific participant.
*
@ -84,6 +105,36 @@ export function getAvatarURLByParticipantId(
return participant && getAvatarURL(participant);
}
/**
* Resolves the first loadable avatar URL for a participant.
*
* @param {Object} participant - The participant to resolve avatars for.
* @returns {Promise}
*/
export function getFirstLoadableAvatarUrl(participant: Object) {
const deferred = createDeferred();
const fullPromise = deferred.promise
.then(() => _getFirstLoadableAvatarUrl(participant))
.then(src => {
if (AVATAR_QUEUE.length) {
const next = AVATAR_QUEUE.shift();
next.resolve();
}
return src;
});
if (AVATAR_QUEUE.length) {
AVATAR_QUEUE.push(deferred);
} else {
deferred.resolve();
}
return fullPromise;
}
/**
* Returns local participant from Redux state.
*
@ -169,7 +220,6 @@ export function getParticipantCountWithFake(stateful: Object | Function) {
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
* {@code getState} function to be used to retrieve the state.
* @param {string} id - The ID of the participant's display name to retrieve.
* @private
* @returns {string}
*/
export function getParticipantDisplayName(
@ -346,3 +396,35 @@ export function shouldRenderParticipantVideo(
&& shouldRenderVideoTrack(videoTrack, waitForVideoStarted);
}
/**
* Resolves the first loadable avatar URL for a participant.
*
* @param {Object} participant - The participant to resolve avatars for.
* @returns {?string}
*/
async function _getFirstLoadableAvatarUrl(participant) {
for (let i = 0; i < AVATAR_CHECKER_FUNCTIONS.length; i++) {
const url = AVATAR_CHECKER_FUNCTIONS[i](participant);
if (url) {
if (AVATAR_CHECKED_URLS.has(url)) {
if (AVATAR_CHECKED_URLS.get(url)) {
return url;
}
} else {
try {
const finalUrl = await preloadImage(url);
AVATAR_CHECKED_URLS.set(finalUrl, true);
return finalUrl;
} catch (e) {
AVATAR_CHECKED_URLS.set(url, false);
}
}
}
}
return undefined;
}

View File

@ -20,7 +20,8 @@ import {
localParticipantJoined,
localParticipantLeft,
participantLeft,
participantUpdated
participantUpdated,
setLoadableAvatarUrl
} from './actions';
import {
DOMINANT_SPEAKER_CHANGED,
@ -37,8 +38,9 @@ import {
PARTICIPANT_LEFT_SOUND_ID
} from './constants';
import {
getAvatarURLByParticipantId,
getFirstLoadableAvatarUrl,
getLocalParticipant,
getParticipantById,
getParticipantCount,
getParticipantDisplayName
} from './functions';
@ -314,8 +316,8 @@ function _maybePlaySounds({ getState, dispatch }, action) {
* @private
* @returns {Object} The value returned by {@code next(action)}.
*/
function _participantJoinedOrUpdated({ getState }, next, action) {
const { participant: { id, local, raisedHand } } = action;
function _participantJoinedOrUpdated({ dispatch, getState }, next, action) {
const { participant: { avatarURL, email, id, local, name, raisedHand } } = action;
// Send an external update of the local participant's raised hand state
// if a new raised hand state is defined in the action.
@ -330,26 +332,29 @@ function _participantJoinedOrUpdated({ getState }, next, action) {
}
}
// Notify external listeners of potential avatarURL changes.
if (typeof APP === 'object') {
const oldAvatarURL = getAvatarURLByParticipantId(getState(), id);
// Allow the redux update to go through and compare the old avatar
// to the new avatar and emit out change events if necessary.
const result = next(action);
// Allow the redux update to go through and compare the old avatar
// to the new avatar and emit out change events if necessary.
const result = next(action);
const newAvatarURL = getAvatarURLByParticipantId(getState(), id);
if (avatarURL || email || id || name) {
const participantId = !id && local ? getLocalParticipant(getState()).id : id;
const updatedParticipant = getParticipantById(getState(), participantId);
if (oldAvatarURL !== newAvatarURL) {
const currentKnownId = local ? APP.conference.getMyUserId() : id;
APP.UI.refreshAvatarDisplay(currentKnownId, newAvatarURL);
APP.API.notifyAvatarChanged(currentKnownId, newAvatarURL);
}
return result;
getFirstLoadableAvatarUrl(updatedParticipant)
.then(url => {
dispatch(setLoadableAvatarUrl(participantId, url));
});
}
return next(action);
// Notify external listeners of potential avatarURL changes.
if (typeof APP === 'object') {
const currentKnownId = local ? APP.conference.getMyUserId() : id;
// Force update of local video getting a new id.
APP.UI.refreshAvatarDisplay(currentKnownId);
}
return result;
}
/**

View File

@ -0,0 +1,16 @@
// @flow
import { Image } from 'react-native';
/**
* Tries to preload an image.
*
* @param {string} src - Source of the avatar.
* @returns {Promise}
*/
export function preloadImage(src: string): Promise<string> {
return new Promise((resolve, reject) => {
Image.prefetch(src).then(() => resolve(src), reject);
});
}

View File

@ -0,0 +1,24 @@
// @flow
declare var config: Object;
/**
* Tries to preload an image.
*
* @param {string} src - Source of the avatar.
* @returns {Promise}
*/
export function preloadImage(src: string): Promise<string> {
if (typeof config === 'object' && config.disableThirdPartyRequests) {
return Promise.reject();
}
return new Promise((resolve, reject) => {
const image = document.createElement('img');
image.onload = () => resolve(src);
image.onerror = reject;
image.src = src;
});
}

View File

@ -10,7 +10,8 @@ import {
PARTICIPANT_JOINED,
PARTICIPANT_LEFT,
PARTICIPANT_UPDATED,
PIN_PARTICIPANT
PIN_PARTICIPANT,
SET_LOADABLE_AVATAR_URL
} from './actionTypes';
import { LOCAL_PARTICIPANT_DEFAULT_ID, PARTICIPANT_ROLE } from './constants';
@ -65,6 +66,7 @@ const PARTICIPANT_PROPS_TO_OMIT_WHEN_UPDATE = [
*/
ReducerRegistry.register('features/base/participants', (state = [], action) => {
switch (action.type) {
case SET_LOADABLE_AVATAR_URL:
case DOMINANT_SPEAKER_CHANGED:
case PARTICIPANT_ID_CHANGED:
case PARTICIPANT_UPDATED:
@ -133,6 +135,7 @@ function _participant(state: Object = {}, action) {
break;
}
case SET_LOADABLE_AVATAR_URL:
case PARTICIPANT_UPDATED: {
const { participant } = action; // eslint-disable-line no-shadow
let { id } = participant;
@ -186,6 +189,7 @@ function _participantJoined({ participant }) {
dominantSpeaker,
email,
isFakeParticipant,
loadableAvatarUrl,
local,
name,
pinned,
@ -221,6 +225,7 @@ function _participantJoined({ participant }) {
email,
id,
isFakeParticipant,
loadableAvatarUrl,
local: local || false,
name,
pinned: pinned || false,

View File

@ -3,8 +3,7 @@
import React, { Component } from 'react';
import { Text } from 'react-native';
import { Icon } from '../../../font-icons';
import { Avatar } from '../../../participants';
import { Avatar } from '../../../avatar';
import { StyleType } from '../../../styles';
import { type Item } from '../../Types';
@ -70,44 +69,6 @@ export default class AvatarListItem extends Component<Props> {
this._renderItemLine = this._renderItemLine.bind(this);
}
/**
* Helper function to render the content in the avatar container.
*
* @returns {React$Element}
*/
_getAvatarContent() {
const {
avatarSize = AVATAR_SIZE,
avatarTextStyle
} = this.props;
const { avatar, title } = this.props.item;
const isAvatarURL = Boolean(avatar && avatar.match(/^http[s]*:\/\//i));
if (isAvatarURL) {
return (
<Avatar
size = { avatarSize }
uri = { avatar } />
);
}
if (avatar && !isAvatarURL) {
return (
<Icon name = { avatar } />
);
}
return (
<Text
style = { [
styles.avatarContent,
avatarTextStyle
] }>
{ title.substr(0, 1).toUpperCase() }
</Text>
);
}
/**
* Implements {@code Component#render}.
*
@ -118,26 +79,19 @@ export default class AvatarListItem extends Component<Props> {
avatarSize = AVATAR_SIZE,
avatarStyle
} = this.props;
const { colorBase, lines, title } = this.props.item;
const avatarStyles = {
...styles.avatar,
...this._getAvatarColor(colorBase),
...avatarStyle,
borderRadius: avatarSize / 2,
height: avatarSize,
width: avatarSize
};
const { avatar, colorBase, lines, title } = this.props.item;
return (
<Container
onClick = { this.props.onPress }
style = { styles.listItem }
underlayColor = { UNDERLAY_COLOR }>
<Container style = { styles.avatarContainer }>
<Container style = { avatarStyles }>
{ this._getAvatarContent() }
</Container>
</Container>
<Avatar
colorBase = { colorBase }
displayName = { title }
size = { avatarSize }
style = { avatarStyle }
uri = { avatar } />
<Container style = { styles.listItemDetails }>
<Text
numberOfLines = { 1 }
@ -155,27 +109,6 @@ export default class AvatarListItem extends Component<Props> {
);
}
/**
* Returns a style (color) based on the string that determines the color of
* the avatar.
*
* @param {string} colorBase - The string that is the base of the color.
* @private
* @returns {Object}
*/
_getAvatarColor(colorBase) {
if (!colorBase) {
return null;
}
let nameHash = 0;
for (let i = 0; i < colorBase.length; i++) {
nameHash += colorBase.codePointAt(i);
}
return styles[`avatarColor${(nameHash % 5) + 1}`];
}
_renderItemLine: (string, number) => React$Node;
/**

View File

@ -2,7 +2,6 @@
import { BoxModel, ColorPalette, createStyleSheet } from '../../../styles';
const AVATAR_OPACITY = 0.4;
const OVERLAY_FONT_COLOR = 'rgba(255, 255, 255, 0.6)';
export const AVATAR_SIZE = 65;
@ -92,40 +91,6 @@ const PAGED_LIST_STYLES = {
};
const SECTION_LIST_STYLES = {
/**
* The style of the actual avatar.
*/
avatar: {
alignItems: 'center',
backgroundColor: `rgba(23, 160, 219, ${AVATAR_OPACITY})`,
justifyContent: 'center'
},
/**
* List of styles of the avatar of a remote meeting (not the default
* server). The number of colors are limited because they should match
* nicely.
*/
avatarColor1: {
backgroundColor: `rgba(232, 105, 156, ${AVATAR_OPACITY})`
},
avatarColor2: {
backgroundColor: `rgba(255, 198, 115, ${AVATAR_OPACITY})`
},
avatarColor3: {
backgroundColor: `rgba(128, 128, 255, ${AVATAR_OPACITY})`
},
avatarColor4: {
backgroundColor: `rgba(105, 232, 194, ${AVATAR_OPACITY})`
},
avatarColor5: {
backgroundColor: `rgba(234, 255, 128, ${AVATAR_OPACITY})`
},
/**
* The style of the avatar container that makes the avatar rounded.
*/

View File

@ -2,6 +2,22 @@
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* Creates a deferred object.
*
* @returns {{promise, resolve, reject}}
*/
export function createDeferred(): Object {
const deferred = {};
deferred.promise = new Promise((resolve, reject) => {
deferred.resolve = resolve;
deferred.reject = reject;
});
return deferred;
}
/**
* Returns the namespace for all global variables, functions, etc that we need.
*

View File

@ -3,7 +3,6 @@
import { PureComponent } from 'react';
import { getLocalizedDateFormatter } from '../../base/i18n';
import { getAvatarURLByParticipantId } from '../../base/participants';
/**
* Formatter string to display the message timestamp.
@ -15,11 +14,6 @@ const TIMESTAMP_FORMAT = 'H:mm';
*/
export type Props = {
/**
* The URL of the avatar of the participant.
*/
_avatarURL: string,
/**
* The representation of a chat message.
*/
@ -63,20 +57,3 @@ export default class AbstractChatMessage<P: Props> extends PureComponent<P> {
.format(TIMESTAMP_FORMAT);
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @param {Props} ownProps - The own props of the component.
* @returns {{
* _avatarURL: string
* }}
*/
export function _mapStateToProps(state: Object, ownProps: Props) {
const { message } = ownProps;
return {
_avatarURL: getAvatarURLByParticipantId(state, message.id)
};
}

View File

@ -3,14 +3,10 @@
import React from 'react';
import { Text, View } from 'react-native';
import { Avatar } from '../../../base/avatar';
import { translate } from '../../../base/i18n';
import { Avatar } from '../../../base/participants';
import { connect } from '../../../base/redux';
import AbstractChatMessage, {
_mapStateToProps,
type Props
} from '../AbstractChatMessage';
import AbstractChatMessage, { type Props } from '../AbstractChatMessage';
import styles from './styles';
/**
@ -81,8 +77,8 @@ class ChatMessage extends AbstractChatMessage<Props> {
return (
<View style = { styles.avatarWrapper }>
{ this.props.showAvatar && <Avatar
size = { styles.avatarWrapper.width }
uri = { this.props._avatarURL } />
participantId = { this.props.message.id }
size = { styles.avatarWrapper.width } />
}
</View>
);
@ -115,4 +111,4 @@ class ChatMessage extends AbstractChatMessage<Props> {
}
}
export default translate(connect(_mapStateToProps)(ChatMessage));
export default translate(ChatMessage);

View File

@ -9,8 +9,10 @@ import { NOTIFY_CAMERA_ERROR, NOTIFY_MIC_ERROR } from '../base/devices';
import { JitsiConferenceErrors } from '../base/lib-jitsi-meet';
import {
PARTICIPANT_KICKED,
SET_LOADABLE_AVATAR_URL,
getAvatarURLByParticipantId,
getLocalParticipant
getLocalParticipant,
getParticipantById
} from '../base/participants';
import { MiddlewareRegistry } from '../base/redux';
import { appendSuffix } from '../display-name';
@ -26,8 +28,31 @@ declare var interfaceConfig: Object;
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
// We need to do these before executing the rest of the middelware chain
switch (action.type) {
case SET_LOADABLE_AVATAR_URL: {
const { id, loadableAvatarUrl } = action.participant;
const participant = getParticipantById(
store.getState(),
id
);
const result = next(action);
if (participant.loadableAvatarUrl !== loadableAvatarUrl) {
APP.API.notifyAvatarChanged(
id,
loadableAvatarUrl
);
}
return result;
}
}
const result = next(action);
// These should happen after the rest of the middleware chain ran
switch (action.type) {
case CONFERENCE_FAILED: {
if (action.conference
@ -54,7 +79,6 @@ MiddlewareRegistry.register(store => next => action => {
avatarURL: getAvatarURLByParticipantId(state, id)
}
);
break;
}

View File

@ -2,10 +2,9 @@
import React, { Component } from 'react';
import { Avatar } from '../../../base/avatar';
import { MEDIA_TYPE } from '../../../base/media';
import {
Avatar,
getAvatarURL,
getParticipants,
getParticipantDisplayName,
getParticipantPresenceStatus
@ -23,7 +22,7 @@ import styles from './styles';
type Props = {
/**
* The callee's information such as avatar and display name.
* The callee's information such as display name.
*/
_callee: Object,
@ -46,7 +45,7 @@ class CalleeInfo extends Component<Props> {
*/
render() {
const {
avatar,
id,
name,
status = CALLING
} = this.props._callee;
@ -60,7 +59,7 @@ class CalleeInfo extends Component<Props> {
{ ...this._style('ringing__content') }>
<Avatar
{ ...this._style('ringing__avatar') }
uri = { avatar } />
participantId = { id } />
<Container { ...this._style('ringing__status') }>
<PresenceLabel
defaultPresence = { status }
@ -144,7 +143,7 @@ function _mapStateToProps(state) {
return {
_callee: {
avatar: getAvatarURL(poltergeist),
id,
name: getParticipantDisplayName(state, id),
status: getParticipantPresenceStatus(state, id)
},

View File

@ -4,8 +4,8 @@ import React, { Component } from 'react';
import { Image, Text, View } from 'react-native';
import LinearGradient from 'react-native-linear-gradient';
import { Avatar } from '../../../base/avatar';
import { translate } from '../../../base/i18n';
import { Avatar } from '../../../base/participants';
import { connect } from '../../../base/redux';
import AnswerButton from './AnswerButton';

View File

@ -2,11 +2,8 @@
import React, { Component } from 'react';
import {
Avatar,
getAvatarURL,
getLocalParticipant
} from '../../../base/participants';
import { Avatar } from '../../../base/avatar';
import { getLocalParticipant } from '../../../base/participants';
import { connect } from '../../../base/redux';
import OverlayFrame from './OverlayFrame';
@ -18,9 +15,9 @@ import OverlayFrame from './OverlayFrame';
type Props = {
/**
* The source (e.g. URI, URL) of the avatar image of the local participant.
* The ID of the local participant.
*/
_avatar: string,
_localParticipantId: string,
/**
* The children components to be displayed into the overlay frame for
@ -85,7 +82,7 @@ class FilmstripOnlyOverlayFrame extends Component<Props> {
}
</div>
<div className = 'inlay-filmstrip-only__avatar-container'>
<Avatar uri = { this.props._avatar } />
<Avatar participantId = { this.props._localParticipantId } />
{
this._renderIcon()
}
@ -103,12 +100,12 @@ class FilmstripOnlyOverlayFrame extends Component<Props> {
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _avatar: string
* _localParticipantId: string
* }}
*/
function _mapStateToProps(state) {
return {
_avatar: getAvatarURL(getLocalParticipant(state) || {})
_localParticipantId: (getLocalParticipant(state) || {}).id
};
}

View File

@ -3,15 +3,12 @@
import React, { Component } from 'react';
import { Text, View } from 'react-native';
import { Avatar } from '../../../base/avatar';
import { ColorSchemeRegistry } from '../../../base/color-scheme';
import {
BottomSheet
} from '../../../base/dialog';
import {
Avatar,
getAvatarURL,
getParticipantDisplayName
} from '../../../base/participants';
import { getParticipantDisplayName } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { StyleType } from '../../../base/styles';
@ -39,11 +36,6 @@ type Props = {
*/
participant: Object,
/**
* URL of the avatar of the participant.
*/
_avatarURL: string,
/**
* The color-schemed stylesheet of the BottomSheet.
*/
@ -76,10 +68,11 @@ class RemoteVideoMenu extends Component<Props> {
* @inheritdoc
*/
render() {
const { participant } = this.props;
const buttonProps = {
afterClick: this._onCancel,
showLabel: true,
participantID: this.props.participant.id,
participantID: participant.id,
styles: this.props._bottomSheetStyles
};
@ -87,8 +80,8 @@ class RemoteVideoMenu extends Component<Props> {
<BottomSheet onCancel = { this._onCancel }>
<View style = { styles.participantNameContainer }>
<Avatar
size = { AVATAR_SIZE }
uri = { this.props._avatarURL } />
participantId = { participant.id }
size = { AVATAR_SIZE } />
<Text style = { styles.participantNameLabel }>
{ this.props._participantDisplayName }
</Text>
@ -120,7 +113,6 @@ class RemoteVideoMenu extends Component<Props> {
* @param {Object} ownProps - Properties of component.
* @private
* @returns {{
* _avatarURL: string,
* _bottomSheetStyles: StyleType,
* _participantDisplayName: string
* }}
@ -129,7 +121,6 @@ function _mapStateToProps(state, ownProps) {
const { participant } = ownProps;
return {
_avatarURL: getAvatarURL(participant),
_bottomSheetStyles:
ColorSchemeRegistry.get(state, 'BottomSheet'),
_participantDisplayName: getParticipantDisplayName(

View File

@ -53,6 +53,11 @@ export type Props = {
* @extends Component
*/
class ProfileTab extends AbstractDialogTab<Props> {
static defaultProps = {
displayName: '',
email: ''
};
/**
* Initializes a new {@code ConnectedSettingsDialog} instance.
*

View File

@ -2,14 +2,10 @@
import React, { Component } from 'react';
import { Avatar } from '../../../base/avatar';
import { getLocalParticipant } from '../../../base/participants';
import { connect } from '../../../base/redux';
import {
Avatar,
getAvatarURL,
getLocalParticipant
} from '../../../base/participants';
declare var interfaceConfig: Object;
/**
@ -65,7 +61,6 @@ class OverflowMenuProfileItem extends Component<Props> {
const { _localParticipant, _unclickable } = this.props;
const classNames = `overflow-menu-item ${
_unclickable ? 'unclickable' : ''}`;
const avatarURL = getAvatarURL(_localParticipant);
let displayName;
if (_localParticipant && _localParticipant.name) {
@ -80,7 +75,9 @@ class OverflowMenuProfileItem extends Component<Props> {
className = { classNames }
onClick = { this._onClick }>
<span className = 'overflow-menu-item-icon'>
<Avatar uri = { avatarURL } />
<Avatar
participantId = { _localParticipant.id }
size = { 24 } />
</span>
<span className = 'profile-text'>
{ displayName }

View File

@ -3,9 +3,8 @@
import React, { Component } from 'react';
import { SafeAreaView, ScrollView, Text } from 'react-native';
import { Avatar } from '../../base/avatar';
import {
Avatar,
getAvatarURL,
getLocalParticipant,
getParticipantDisplayName
} from '../../base/participants';
@ -42,16 +41,16 @@ type Props = {
*/
dispatch: Function,
/**
* The avatar URL to be rendered.
*/
_avatarURL: string,
/**
* Display name of the local participant.
*/
_displayName: string,
/**
* ID of the local participant.
*/
_localParticipantId: string,
/**
* Sets the side bar visible or hidden.
*/
@ -90,9 +89,8 @@ class WelcomePageSideBar extends Component<Props> {
style = { styles.sideBar } >
<Header style = { styles.sideBarHeader }>
<Avatar
size = { SIDEBAR_AVATAR_SIZE }
style = { styles.avatar }
uri = { this.props._avatarURL } />
participantId = { this.props._localParticipantId }
size = { SIDEBAR_AVATAR_SIZE } />
<Text style = { styles.displayName }>
{ this.props._displayName }
</Text>
@ -155,18 +153,14 @@ class WelcomePageSideBar extends Component<Props> {
*
* @param {Object} state - The redux state.
* @protected
* @returns {{
* _avatarURL: string,
* _displayName: string,
* _visible: boolean
* }}
* @returns {Props}
*/
function _mapStateToProps(state: Object) {
const localParticipant = getLocalParticipant(state);
const _localParticipant = getLocalParticipant(state);
return {
_avatarURL: getAvatarURL(localParticipant),
_displayName: getParticipantDisplayName(state, localParticipant.id),
_displayName: getParticipantDisplayName(state, _localParticipant.id),
_localParticipantId: _localParticipant.id,
_visible: state['features/welcome'].sideBarVisible
};
}

View File

@ -38,14 +38,6 @@ export default createStyleSheet({
flexDirection: 'row'
},
/**
* Style of the avatar in te side bar.
*/
avatar: {
alignSelf: 'center',
flex: 0
},
/**
* Join button style.
*/