feat: initial based avatars
This commit is contained in:
parent
0734ce7ae3
commit
72137a2811
|
@ -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
|
||||
}
|
|
@ -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%;
|
||||
|
|
|
@ -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 |
|
@ -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);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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}`
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
// @flow
|
||||
|
||||
export * from './native';
|
|
@ -0,0 +1,3 @@
|
|||
// @flow
|
||||
|
||||
export * from './web';
|
|
@ -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);
|
|
@ -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) } />
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
// @flow
|
||||
|
||||
export { default as Avatar } from './Avatar';
|
|
@ -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
|
||||
}
|
||||
};
|
|
@ -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);
|
|
@ -0,0 +1,3 @@
|
|||
// @flow
|
||||
|
||||
export { default as Avatar } from './Avatar';
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
// @flow
|
||||
|
||||
export * from './components';
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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 } />
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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 } />
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
export { default as Avatar } from './Avatar';
|
||||
// @flow
|
||||
|
||||
export { default as ParticipantView } from './ParticipantView';
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
|
@ -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;
|
||||
});
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
||||
/**
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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)
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
},
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -53,6 +53,11 @@ export type Props = {
|
|||
* @extends Component
|
||||
*/
|
||||
class ProfileTab extends AbstractDialogTab<Props> {
|
||||
static defaultProps = {
|
||||
displayName: '',
|
||||
email: ''
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a new {@code ConnectedSettingsDialog} instance.
|
||||
*
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue