theluyuan 38ba663466
Some checks failed
Close stale issues and PRs / stale (push) Has been cancelled
init
2025-09-02 14:49:16 +08:00

296 lines
8.2 KiB
TypeScript

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);