This commit is contained in:
295
react/features/base/avatar/components/Avatar.tsx
Normal file
295
react/features/base/avatar/components/Avatar.tsx
Normal file
@@ -0,0 +1,295 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { IconUser } from '../../icons/svg';
|
||||
import { getParticipantById } from '../../participants/functions';
|
||||
import { IParticipant } from '../../participants/types';
|
||||
import { getAvatarColor, getInitials, isCORSAvatarURL } from '../functions';
|
||||
import { IAvatarProps as AbstractProps } from '../types';
|
||||
|
||||
import { StatelessAvatar } from './';
|
||||
|
||||
export interface IProps {
|
||||
|
||||
/**
|
||||
* The URL patterns for URLs that needs to be handled with CORS.
|
||||
*/
|
||||
_corsAvatarURLs?: Array<string>;
|
||||
|
||||
/**
|
||||
* Custom avatar backgrounds from branding.
|
||||
*/
|
||||
_customAvatarBackgrounds?: Array<string>;
|
||||
|
||||
/**
|
||||
* The string we base the initials on (this is generated from a list of precedences).
|
||||
*/
|
||||
_initialsBase?: string;
|
||||
|
||||
/**
|
||||
* An URL that we validated that it can be loaded.
|
||||
*/
|
||||
_loadableAvatarUrl?: string;
|
||||
|
||||
/**
|
||||
* Indicates whether _loadableAvatarUrl should use CORS or not.
|
||||
*/
|
||||
_loadableAvatarUrlUseCORS?: boolean;
|
||||
|
||||
/**
|
||||
* A prop to maintain compatibility with web.
|
||||
*/
|
||||
className?: 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;
|
||||
|
||||
/**
|
||||
* Indicates the default icon for the avatar.
|
||||
*/
|
||||
defaultIcon?: string;
|
||||
|
||||
/**
|
||||
* Display name of the entity to render an avatar for (if any). This is handy when we need
|
||||
* an avatar for a non-participant entity (e.g. A recent list item).
|
||||
*/
|
||||
displayName?: string;
|
||||
|
||||
/**
|
||||
* Whether or not to update the background color of the avatar.
|
||||
*/
|
||||
dynamicColor?: boolean;
|
||||
|
||||
/**
|
||||
* ID of the element, if any.
|
||||
*/
|
||||
id?: 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;
|
||||
|
||||
/**
|
||||
* One of the expected status strings (e.g. 'available') to render a badge on the avatar, if necessary.
|
||||
*/
|
||||
status?: string;
|
||||
|
||||
/**
|
||||
* TestId of the element, if any.
|
||||
*/
|
||||
testId?: string;
|
||||
|
||||
/**
|
||||
* URL of the avatar, if any.
|
||||
*/
|
||||
url?: string;
|
||||
|
||||
/**
|
||||
* Indicates whether to load the avatar using CORS or not.
|
||||
*/
|
||||
useCORS?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
avatarFailed: boolean;
|
||||
isUsingCORS: boolean;
|
||||
}
|
||||
|
||||
export const DEFAULT_SIZE = 65;
|
||||
|
||||
/**
|
||||
* Implements a class to render avatars in the app.
|
||||
*/
|
||||
class Avatar<P extends IProps> extends PureComponent<P, IState> {
|
||||
/**
|
||||
* Default values for {@code Avatar} component's properties.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static defaultProps = {
|
||||
defaultIcon: IconUser,
|
||||
dynamicColor: true
|
||||
};
|
||||
|
||||
/**
|
||||
* Instantiates a new {@code Component}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: P) {
|
||||
super(props);
|
||||
|
||||
const {
|
||||
_corsAvatarURLs,
|
||||
url,
|
||||
useCORS
|
||||
} = props;
|
||||
|
||||
this.state = {
|
||||
avatarFailed: false,
|
||||
isUsingCORS: Boolean(useCORS) || Boolean(url && isCORSAvatarURL(url, _corsAvatarURLs))
|
||||
};
|
||||
|
||||
this._onAvatarLoadError = this._onAvatarLoadError.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@code Component#componentDidUpdate}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override componentDidUpdate(prevProps: P) {
|
||||
const { _corsAvatarURLs, url } = this.props;
|
||||
|
||||
if (prevProps.url !== url) {
|
||||
|
||||
// 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,
|
||||
isUsingCORS: Boolean(this.props.useCORS) || Boolean(url && isCORSAvatarURL(url, _corsAvatarURLs))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@code Componenr#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override render() {
|
||||
const {
|
||||
_customAvatarBackgrounds,
|
||||
_initialsBase,
|
||||
_loadableAvatarUrl,
|
||||
_loadableAvatarUrlUseCORS,
|
||||
className,
|
||||
colorBase,
|
||||
defaultIcon,
|
||||
dynamicColor,
|
||||
id,
|
||||
size,
|
||||
status,
|
||||
testId,
|
||||
url
|
||||
} = this.props;
|
||||
const { avatarFailed, isUsingCORS } = this.state;
|
||||
|
||||
const avatarProps: AbstractProps & {
|
||||
className?: string;
|
||||
iconUser?: any;
|
||||
id?: string;
|
||||
status?: string;
|
||||
testId?: string;
|
||||
url?: string;
|
||||
useCORS?: boolean;
|
||||
} = {
|
||||
className,
|
||||
color: undefined,
|
||||
id,
|
||||
initials: undefined,
|
||||
onAvatarLoadError: undefined,
|
||||
onAvatarLoadErrorParams: undefined,
|
||||
size,
|
||||
status,
|
||||
testId,
|
||||
url: undefined,
|
||||
useCORS: isUsingCORS
|
||||
};
|
||||
|
||||
// _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.
|
||||
const useReduxLoadableAvatarURL = avatarFailed || !url;
|
||||
const effectiveURL = useReduxLoadableAvatarURL ? _loadableAvatarUrl : url;
|
||||
|
||||
if (effectiveURL) {
|
||||
avatarProps.onAvatarLoadError = this._onAvatarLoadError;
|
||||
if (useReduxLoadableAvatarURL) {
|
||||
avatarProps.onAvatarLoadErrorParams = { dontRetry: true };
|
||||
avatarProps.useCORS = _loadableAvatarUrlUseCORS;
|
||||
}
|
||||
avatarProps.url = effectiveURL;
|
||||
}
|
||||
|
||||
const initials = getInitials(_initialsBase);
|
||||
|
||||
if (initials) {
|
||||
if (dynamicColor) {
|
||||
avatarProps.color = getAvatarColor(colorBase || _initialsBase, _customAvatarBackgrounds ?? []);
|
||||
}
|
||||
|
||||
avatarProps.initials = initials;
|
||||
}
|
||||
|
||||
if (navigator.product !== 'ReactNative') {
|
||||
avatarProps.iconUser = defaultIcon;
|
||||
}
|
||||
|
||||
return (
|
||||
<StatelessAvatar
|
||||
{ ...avatarProps } />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to handle the error while loading of the avatar URI.
|
||||
*
|
||||
* @param {Object} params - An object with parameters.
|
||||
* @param {boolean} params.dontRetry - If false we will retry to load the Avatar with different CORS mode.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onAvatarLoadError(params: { dontRetry?: boolean; } = {}) {
|
||||
const { dontRetry = false } = params;
|
||||
|
||||
if (Boolean(this.props.useCORS) === this.state.isUsingCORS && !dontRetry) {
|
||||
// try different mode of loading the avatar.
|
||||
this.setState({
|
||||
isUsingCORS: !this.state.isUsingCORS
|
||||
});
|
||||
} else {
|
||||
// we already have tried loading the avatar with and without CORS and it failed.
|
||||
this.setState({
|
||||
avatarFailed: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps part of the Redux state to the props of this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @param {IProps} ownProps - The own props of the component.
|
||||
* @returns {IProps}
|
||||
*/
|
||||
export function _mapStateToProps(state: IReduxState, ownProps: IProps) {
|
||||
const { colorBase, displayName, participantId } = ownProps;
|
||||
const _participant: IParticipant | undefined = participantId ? getParticipantById(state, participantId) : undefined;
|
||||
const _initialsBase = _participant?.name ?? displayName;
|
||||
const { corsAvatarURLs } = state['features/base/config'];
|
||||
|
||||
return {
|
||||
_customAvatarBackgrounds: state['features/dynamic-branding'].avatarBackgrounds,
|
||||
_corsAvatarURLs: corsAvatarURLs,
|
||||
_initialsBase,
|
||||
_loadableAvatarUrl: _participant?.loadableAvatarUrl,
|
||||
_loadableAvatarUrlUseCORS: _participant?.loadableAvatarUrlUseCORS,
|
||||
colorBase
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(Avatar);
|
||||
1
react/features/base/avatar/components/index.native.ts
Normal file
1
react/features/base/avatar/components/index.native.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as StatelessAvatar } from './native/StatelessAvatar';
|
||||
1
react/features/base/avatar/components/index.web.ts
Normal file
1
react/features/base/avatar/components/index.web.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as StatelessAvatar } from './web/StatelessAvatar';
|
||||
200
react/features/base/avatar/components/native/StatelessAvatar.tsx
Normal file
200
react/features/base/avatar/components/native/StatelessAvatar.tsx
Normal file
@@ -0,0 +1,200 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Image, Text, TextStyle, View, ViewStyle } from 'react-native';
|
||||
|
||||
import Icon from '../../../icons/components/Icon';
|
||||
import { StyleType } from '../../../styles/functions.native';
|
||||
import { isIcon } from '../../functions';
|
||||
import { IAvatarProps } from '../../types';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const DEFAULT_AVATAR = require('../../../../../../images/avatar.png');
|
||||
|
||||
interface IProps extends IAvatarProps {
|
||||
|
||||
/**
|
||||
* One of the expected status strings (e.g. 'available') to render a badge on the avatar, if necessary.
|
||||
*/
|
||||
status?: string;
|
||||
|
||||
/**
|
||||
* External style passed to the component.
|
||||
*/
|
||||
style?: StyleType;
|
||||
|
||||
/**
|
||||
* The URL of the avatar to render.
|
||||
*/
|
||||
url?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a stateless avatar component that renders an avatar purely from what gets passed through
|
||||
* props.
|
||||
*/
|
||||
export default class StatelessAvatar extends Component<IProps> {
|
||||
|
||||
/**
|
||||
* Instantiates a new {@code Component}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this._onAvatarLoadError = this._onAvatarLoadError.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@code Component#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override render() {
|
||||
const { initials, size, style, url } = this.props;
|
||||
|
||||
let avatar;
|
||||
|
||||
if (isIcon(url)) {
|
||||
avatar = this._renderIconAvatar(url);
|
||||
} else if (url) {
|
||||
avatar = this._renderURLAvatar();
|
||||
} else if (initials) {
|
||||
avatar = this._renderInitialsAvatar();
|
||||
} else {
|
||||
avatar = this._renderDefaultAvatar();
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View
|
||||
style = { [
|
||||
styles.avatarContainer(size) as ViewStyle,
|
||||
style
|
||||
] }>
|
||||
{ avatar }
|
||||
</View>
|
||||
{ this._renderAvatarStatus() }
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a badge representing the avatar status.
|
||||
*
|
||||
* @returns {React$Elementaa}
|
||||
*/
|
||||
_renderAvatarStatus() {
|
||||
const { size, status } = this.props;
|
||||
|
||||
if (!status) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style = { styles.badgeContainer }>
|
||||
<View style = { styles.badge(size, status) as ViewStyle } />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the default avatar.
|
||||
*
|
||||
* @returns {React$Element<*>}
|
||||
*/
|
||||
_renderDefaultAvatar() {
|
||||
const { size } = this.props;
|
||||
|
||||
return (
|
||||
<Image
|
||||
source = { DEFAULT_AVATAR }
|
||||
style = { [
|
||||
styles.avatarContent(size),
|
||||
styles.staticAvatar
|
||||
] } />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the icon avatar.
|
||||
*
|
||||
* @param {Object} icon - The icon component to render.
|
||||
* @returns {React$Element<*>}
|
||||
*/
|
||||
_renderIconAvatar(icon: Function) {
|
||||
const { color, size } = this.props;
|
||||
|
||||
return (
|
||||
<View
|
||||
style = { [
|
||||
styles.initialsContainer as ViewStyle,
|
||||
{
|
||||
backgroundColor: color
|
||||
}
|
||||
] }>
|
||||
<Icon
|
||||
src = { icon }
|
||||
style = { styles.initialsText(size) } />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the initials-based avatar.
|
||||
*
|
||||
* @returns {React$Element<*>}
|
||||
*/
|
||||
_renderInitialsAvatar() {
|
||||
const { color, initials, size } = this.props;
|
||||
|
||||
return (
|
||||
<View
|
||||
style = { [
|
||||
styles.initialsContainer as ViewStyle,
|
||||
{
|
||||
backgroundColor: color
|
||||
}
|
||||
] }>
|
||||
<Text style = { styles.initialsText(size) as TextStyle }> { initials } </Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the url-based avatar.
|
||||
*
|
||||
* @returns {React$Element<*>}
|
||||
*/
|
||||
_renderURLAvatar() {
|
||||
const { onAvatarLoadError, size, url } = this.props;
|
||||
|
||||
return (
|
||||
<Image
|
||||
defaultSource = { DEFAULT_AVATAR }
|
||||
|
||||
// @ts-ignore
|
||||
onError = { onAvatarLoadError }
|
||||
resizeMode = 'cover'
|
||||
source = {{ uri: url }}
|
||||
style = { styles.avatarContent(size) } />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles avatar load errors.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onAvatarLoadError() {
|
||||
const { onAvatarLoadError, onAvatarLoadErrorParams = {} } = this.props;
|
||||
|
||||
if (onAvatarLoadError) {
|
||||
onAvatarLoadError({
|
||||
...onAvatarLoadErrorParams,
|
||||
dontRetry: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
82
react/features/base/avatar/components/native/styles.ts
Normal file
82
react/features/base/avatar/components/native/styles.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
import { ColorPalette } from '../../../styles/components/styles/ColorPalette';
|
||||
import { PRESENCE_AVAILABLE_COLOR, PRESENCE_AWAY_COLOR, PRESENCE_BUSY_COLOR, PRESENCE_IDLE_COLOR } from '../styles';
|
||||
|
||||
const DEFAULT_SIZE = 65;
|
||||
|
||||
/**
|
||||
* The styles of the feature base/participants.
|
||||
*/
|
||||
export default {
|
||||
|
||||
avatarContainer: (size: number = DEFAULT_SIZE) => {
|
||||
return {
|
||||
alignItems: 'center',
|
||||
borderRadius: size / 2,
|
||||
height: size,
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
width: size
|
||||
};
|
||||
},
|
||||
|
||||
avatarContent: (size: number = DEFAULT_SIZE) => {
|
||||
return {
|
||||
height: size,
|
||||
width: size
|
||||
};
|
||||
},
|
||||
|
||||
badge: (size: number = DEFAULT_SIZE, status: string) => {
|
||||
let color;
|
||||
|
||||
switch (status) {
|
||||
case 'available':
|
||||
color = PRESENCE_AVAILABLE_COLOR;
|
||||
break;
|
||||
case 'away':
|
||||
color = PRESENCE_AWAY_COLOR;
|
||||
break;
|
||||
case 'busy':
|
||||
color = PRESENCE_BUSY_COLOR;
|
||||
break;
|
||||
case 'idle':
|
||||
color = PRESENCE_IDLE_COLOR;
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
backgroundColor: color,
|
||||
borderRadius: size / 2,
|
||||
bottom: 0,
|
||||
height: size * 0.3,
|
||||
position: 'absolute',
|
||||
width: size * 0.3
|
||||
};
|
||||
},
|
||||
|
||||
badgeContainer: {
|
||||
...StyleSheet.absoluteFillObject
|
||||
},
|
||||
|
||||
initialsContainer: {
|
||||
alignItems: 'center',
|
||||
alignSelf: 'stretch',
|
||||
flex: 1,
|
||||
justifyContent: 'center'
|
||||
},
|
||||
|
||||
initialsText: (size: number = DEFAULT_SIZE) => {
|
||||
return {
|
||||
color: 'white',
|
||||
fontSize: size * 0.45,
|
||||
fontWeight: '100'
|
||||
};
|
||||
},
|
||||
|
||||
staticAvatar: {
|
||||
backgroundColor: ColorPalette.lightGrey,
|
||||
opacity: 0.4
|
||||
}
|
||||
};
|
||||
5
react/features/base/avatar/components/styles.ts
Normal file
5
react/features/base/avatar/components/styles.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
// Colors for avatar status badge
|
||||
export const PRESENCE_AVAILABLE_COLOR = 'rgb(110, 176, 5)';
|
||||
export const PRESENCE_AWAY_COLOR = 'rgb(250, 201, 20)';
|
||||
export const PRESENCE_BUSY_COLOR = 'rgb(233, 0, 27)';
|
||||
export const PRESENCE_IDLE_COLOR = 'rgb(172, 172, 172)';
|
||||
220
react/features/base/avatar/components/web/StatelessAvatar.tsx
Normal file
220
react/features/base/avatar/components/web/StatelessAvatar.tsx
Normal file
@@ -0,0 +1,220 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import Icon from '../../../icons/components/Icon';
|
||||
import { pixelsToRem } from '../../../ui/functions.any';
|
||||
import { isIcon } from '../../functions';
|
||||
import { IAvatarProps } from '../../types';
|
||||
import { PRESENCE_AVAILABLE_COLOR, PRESENCE_AWAY_COLOR, PRESENCE_BUSY_COLOR, PRESENCE_IDLE_COLOR } from '../styles';
|
||||
|
||||
interface IProps extends IAvatarProps {
|
||||
|
||||
/**
|
||||
* External class name passed through props.
|
||||
*/
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* The default avatar URL if we want to override the app bundled one (e.g. AlwaysOnTop).
|
||||
*/
|
||||
defaultAvatar?: string;
|
||||
|
||||
/**
|
||||
* ID of the component to be rendered.
|
||||
*/
|
||||
id?: string;
|
||||
|
||||
/**
|
||||
* One of the expected status strings (e.g. 'available') to render a badge on the avatar, if necessary.
|
||||
*/
|
||||
status?: string;
|
||||
|
||||
/**
|
||||
* TestId of the element, if any.
|
||||
*/
|
||||
testId?: string;
|
||||
|
||||
/**
|
||||
* The URL of the avatar to render.
|
||||
*/
|
||||
url?: string | Function;
|
||||
|
||||
/**
|
||||
* Indicates whether to load the avatar using CORS or not.
|
||||
*/
|
||||
useCORS?: boolean;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
avatar: {
|
||||
backgroundColor: '#AAA',
|
||||
borderRadius: '50%',
|
||||
color: theme.palette?.text01 || '#fff',
|
||||
...(theme.typography?.heading1 ?? {}),
|
||||
fontSize: 'inherit',
|
||||
objectFit: 'cover',
|
||||
textAlign: 'center',
|
||||
overflow: 'hidden',
|
||||
|
||||
'&.avatar-small': {
|
||||
height: '28px !important',
|
||||
width: '28px !important'
|
||||
},
|
||||
|
||||
'&.avatar-xsmall': {
|
||||
height: '16px !important',
|
||||
width: '16px !important'
|
||||
},
|
||||
|
||||
'& .jitsi-icon': {
|
||||
transform: 'translateY(50%)'
|
||||
},
|
||||
|
||||
'& .avatar-svg': {
|
||||
height: '100%',
|
||||
width: '100%'
|
||||
}
|
||||
},
|
||||
|
||||
initialsContainer: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
},
|
||||
|
||||
badge: {
|
||||
position: 'relative',
|
||||
|
||||
'&.avatar-badge:after': {
|
||||
borderRadius: '50%',
|
||||
content: '""',
|
||||
display: 'block',
|
||||
height: '35%',
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
width: '35%'
|
||||
},
|
||||
|
||||
'&.avatar-badge-available:after': {
|
||||
backgroundColor: PRESENCE_AVAILABLE_COLOR
|
||||
},
|
||||
|
||||
'&.avatar-badge-away:after': {
|
||||
backgroundColor: PRESENCE_AWAY_COLOR
|
||||
},
|
||||
|
||||
'&.avatar-badge-busy:after': {
|
||||
backgroundColor: PRESENCE_BUSY_COLOR
|
||||
},
|
||||
|
||||
'&.avatar-badge-idle:after': {
|
||||
backgroundColor: PRESENCE_IDLE_COLOR
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const StatelessAvatar = ({
|
||||
className,
|
||||
color,
|
||||
iconUser,
|
||||
id,
|
||||
initials,
|
||||
onAvatarLoadError,
|
||||
onAvatarLoadErrorParams,
|
||||
size,
|
||||
status,
|
||||
testId,
|
||||
url,
|
||||
useCORS
|
||||
}: IProps) => {
|
||||
const { classes, cx } = useStyles();
|
||||
|
||||
const _getAvatarStyle = (backgroundColor?: string) => {
|
||||
return {
|
||||
background: backgroundColor || undefined,
|
||||
fontSize: size ? pixelsToRem(size * 0.4) : '180%',
|
||||
height: size || '100%',
|
||||
width: size || '100%'
|
||||
};
|
||||
};
|
||||
|
||||
const _getAvatarClassName = (additional?: string) => cx('avatar', additional, className, classes.avatar);
|
||||
|
||||
const _getBadgeClassName = () => {
|
||||
if (status) {
|
||||
return cx('avatar-badge', `avatar-badge-${status}`, classes.badge);
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const _onAvatarLoadError = useCallback(() => {
|
||||
if (typeof onAvatarLoadError === 'function') {
|
||||
onAvatarLoadError(onAvatarLoadErrorParams);
|
||||
}
|
||||
}, [ onAvatarLoadError, onAvatarLoadErrorParams ]);
|
||||
|
||||
if (isIcon(url)) {
|
||||
return (
|
||||
<div
|
||||
className = { cx(_getAvatarClassName(), _getBadgeClassName()) }
|
||||
data-testid = { testId }
|
||||
id = { id }
|
||||
style = { _getAvatarStyle(color) }>
|
||||
<Icon
|
||||
size = '50%'
|
||||
src = { url } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (url) {
|
||||
return (
|
||||
<div className = { _getBadgeClassName() }>
|
||||
<img
|
||||
alt = 'avatar'
|
||||
className = { _getAvatarClassName() }
|
||||
crossOrigin = { useCORS ? '' : undefined }
|
||||
data-testid = { testId }
|
||||
id = { id }
|
||||
onError = { _onAvatarLoadError }
|
||||
src = { url }
|
||||
style = { _getAvatarStyle() } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (initials) {
|
||||
return (
|
||||
<div
|
||||
className = { cx(_getAvatarClassName(), _getBadgeClassName()) }
|
||||
data-testid = { testId }
|
||||
id = { id }
|
||||
style = { _getAvatarStyle(color) }>
|
||||
<div className = { classes.initialsContainer }>
|
||||
{initials}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// default avatar
|
||||
return (
|
||||
<div
|
||||
className = { cx(_getAvatarClassName('defaultAvatar'), _getBadgeClassName()) }
|
||||
data-testid = { testId }
|
||||
id = { id }
|
||||
style = { _getAvatarStyle() }>
|
||||
<Icon
|
||||
size = { '50%' }
|
||||
src = { iconUser } />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default StatelessAvatar;
|
||||
4
react/features/base/avatar/constants.ts
Normal file
4
react/features/base/avatar/constants.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* The base URL for gravatar images.
|
||||
*/
|
||||
export const GRAVATAR_BASE_URL = 'https://www.gravatar.com/avatar/';
|
||||
95
react/features/base/avatar/functions.ts
Normal file
95
react/features/base/avatar/functions.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import GraphemeSplitter from 'grapheme-splitter';
|
||||
import { split } from 'lodash-es';
|
||||
|
||||
const AVATAR_COLORS = [
|
||||
'#6A50D3',
|
||||
'#FF9B42',
|
||||
'#DF486F',
|
||||
'#73348C',
|
||||
'#B23683',
|
||||
'#F96E57',
|
||||
'#4380E2',
|
||||
'#238561',
|
||||
'#00A8B3'
|
||||
];
|
||||
const wordSplitRegex = (/\s+|\.+|_+|;+|-+|,+|\|+|\/+|\\+|"+|'+|\(+|\)+|#+|&+/);
|
||||
const splitter = new GraphemeSplitter();
|
||||
|
||||
/**
|
||||
* Generates the background color of an initials based avatar.
|
||||
*
|
||||
* @param {string?} initials - The initials of the avatar.
|
||||
* @param {Array<string>} customAvatarBackgrounds - Custom avatar background values.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getAvatarColor(initials: string | undefined, customAvatarBackgrounds: Array<string>) {
|
||||
const hasCustomAvatarBackgronds = customAvatarBackgrounds?.length;
|
||||
const colorsBase = hasCustomAvatarBackgronds ? customAvatarBackgrounds : AVATAR_COLORS;
|
||||
|
||||
let colorIndex = 0;
|
||||
|
||||
if (initials) {
|
||||
let nameHash = 0;
|
||||
|
||||
for (const s of initials) {
|
||||
nameHash += Number(s.codePointAt(0));
|
||||
}
|
||||
|
||||
colorIndex = nameHash % colorsBase.length;
|
||||
}
|
||||
|
||||
return colorsBase[colorIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first grapheme from a word, uppercased.
|
||||
*
|
||||
* @param {string} word - The string to get grapheme from.
|
||||
* @returns {string}
|
||||
*/
|
||||
function getFirstGraphemeUpper(word: string) {
|
||||
if (!word?.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return splitter.splitGraphemes(word)[0].toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 [ firstWord, ...remainingWords ] = initialsBasis.split(wordSplitRegex).filter(Boolean);
|
||||
|
||||
return getFirstGraphemeUpper(firstWord) + getFirstGraphemeUpper(remainingWords.pop() || '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the passed URL should be loaded with CORS.
|
||||
*
|
||||
* @param {string | Function} url - The URL (on mobile we use a specific Icon component for avatars).
|
||||
* @param {Array<string>} corsURLs - The URL pattern that matches a URL that needs to be handled with CORS.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isCORSAvatarURL(url: string | Function, corsURLs: Array<string> = []): boolean {
|
||||
if (typeof url === 'function') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return corsURLs.some(pattern => url.startsWith(pattern));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the passed prop is a loaded icon or not.
|
||||
*
|
||||
* @param {string? | Object?} iconProp - The prop to check.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isIcon(iconProp?: string | Function): iconProp is Function {
|
||||
return Boolean(iconProp) && (typeof iconProp === 'object' || typeof iconProp === 'function');
|
||||
}
|
||||
32
react/features/base/avatar/types.ts
Normal file
32
react/features/base/avatar/types.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
export interface IAvatarProps {
|
||||
|
||||
/**
|
||||
* Color of the (initials based) avatar, if needed.
|
||||
*/
|
||||
color?: string;
|
||||
|
||||
/**
|
||||
* The user icon(browser only).
|
||||
*/
|
||||
iconUser?: any;
|
||||
|
||||
/**
|
||||
* Initials to be used to render the initials based avatars.
|
||||
*/
|
||||
initials?: string;
|
||||
|
||||
/**
|
||||
* Callback to signal the failure of the loading of the URL.
|
||||
*/
|
||||
onAvatarLoadError?: Function;
|
||||
|
||||
/**
|
||||
* Additional parameters to be passed to onAvatarLoadError function.
|
||||
*/
|
||||
onAvatarLoadErrorParams?: Object;
|
||||
|
||||
/**
|
||||
* Expected size of the avatar.
|
||||
*/
|
||||
size?: number;
|
||||
}
|
||||
Reference in New Issue
Block a user