mirror of https://github.com/jitsi/jitsi-meet
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 |
||||
} |
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 3.4 KiB |
@ -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,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 } /> |
||||
); |
||||
} |
||||
} |
@ -1,2 +1,3 @@ |
||||
export { default as Avatar } from './Avatar'; |
||||
// @flow
|
||||
|
||||
export { default as ParticipantView } from './ParticipantView'; |
||||
|
@ -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; |
||||
}); |
||||
} |
Loading…
Reference in new issue