123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272 |
- import {Component} from 'react';
- import styled from '@emotion/styled';
- import classNames from 'classnames';
- import * as qs from 'query-string';
- import BackgroundAvatar from 'sentry/components/avatar/backgroundAvatar';
- import LetterAvatar from 'sentry/components/letterAvatar';
- import Tooltip from 'sentry/components/tooltip';
- import {Avatar} from 'sentry/types';
- import Gravatar from './gravatar';
- import {imageStyle, ImageStyleProps} from './styles';
- const DEFAULT_GRAVATAR_SIZE = 64;
- const ALLOWED_SIZES = [20, 32, 36, 48, 52, 64, 80, 96, 120];
- const DEFAULT_REMOTE_SIZE = 120;
- // Note: Avatar will not always be a child of a flex layout, but this seems like a
- // sensible default.
- const StyledBaseAvatar = styled('span')<{
- loaded: boolean;
- round: boolean;
- suggested: boolean;
- }>`
- flex-shrink: 0;
- border-radius: ${p => (p.round ? '50%' : '3px')};
- border: ${p => (p.suggested ? `2px solid ${p.theme.background}` : 'none')};
- background-color: ${p => (p.suggested ? p.theme.background : 'none')};
- `;
- const defaultProps: DefaultProps = {
- // No default size to ease transition from CSS defined sizes
- // size: 64,
- style: {},
- /**
- * Enable to display tooltips.
- */
- hasTooltip: false,
- /**
- * The type of avatar being rendered.
- */
- type: 'letter_avatar',
- /**
- * Path to uploaded avatar (differs based on model type)
- */
- uploadPath: 'avatar',
- /**
- * Should avatar be round instead of a square
- */
- round: false,
- };
- type DefaultProps = {
- /**
- * Enable to display tooltips.
- */
- hasTooltip?: boolean;
- /**
- * Should avatar be round instead of a square
- */
- round?: boolean;
- style?: React.CSSProperties;
- suggested?: boolean;
- /**
- * The type of avatar being rendered.
- */
- type?: Avatar['avatarType'];
- /**
- * Path to uploaded avatar (differs based on model type)
- */
- uploadPath?:
- | 'avatar'
- | 'team-avatar'
- | 'organization-avatar'
- | 'project-avatar'
- | 'sentry-app-avatar'
- | 'doc-integration-avatar';
- };
- type BaseProps = DefaultProps & {
- backupAvatar?: React.ReactNode;
- className?: string;
- /**
- * Default gravatar to display
- */
- default?: string;
- forwardedRef?: React.Ref<HTMLSpanElement>;
- gravatarId?: string;
- letterId?: string;
- /**
- * This is the size of the remote image to request.
- */
- remoteImageSize?: typeof ALLOWED_SIZES[number];
- size?: number;
- title?: string;
- /**
- * The content for the tooltip. Requires hasTooltip to display
- */
- tooltip?: React.ReactNode;
- /**
- * Additional props for the tooltip
- */
- tooltipOptions?: Omit<React.ComponentProps<typeof Tooltip>, 'children' | 'title'>;
- uploadId?: string | null | undefined;
- };
- type Props = BaseProps;
- type State = {
- hasLoaded: boolean;
- loadError: boolean;
- showBackupAvatar: boolean;
- };
- class BaseAvatar extends Component<Props, State> {
- static defaultProps = defaultProps;
- constructor(props: Props) {
- super(props);
- this.state = {
- showBackupAvatar: false,
- hasLoaded: props.type !== 'upload',
- loadError: false,
- };
- }
- getRemoteImageSize = () => {
- const {remoteImageSize, size} = this.props;
- // Try to make sure remote image size is >= requested size
- // If requested size > allowed size then use the largest allowed size
- const allowed =
- size &&
- (ALLOWED_SIZES.find(allowedSize => allowedSize >= size) ||
- ALLOWED_SIZES[ALLOWED_SIZES.length - 1]);
- return remoteImageSize || allowed || DEFAULT_GRAVATAR_SIZE;
- };
- buildUploadUrl = () => {
- const {uploadPath, uploadId} = this.props;
- return `/${uploadPath || 'avatar'}/${uploadId}/?${qs.stringify({
- s: DEFAULT_REMOTE_SIZE,
- })}`;
- };
- handleLoad = () => {
- this.setState({showBackupAvatar: false, hasLoaded: true});
- };
- handleError = () => {
- this.setState({showBackupAvatar: true, loadError: true, hasLoaded: true});
- };
- renderImg = () => {
- if (this.state.loadError) {
- return null;
- }
- const {type, round, gravatarId, suggested} = this.props;
- const eventProps = {
- onError: this.handleError,
- onLoad: this.handleLoad,
- };
- if (type === 'gravatar') {
- return (
- <Gravatar
- placeholder={this.props.default}
- gravatarId={gravatarId}
- round={round}
- remoteSize={DEFAULT_REMOTE_SIZE}
- suggested={suggested}
- {...eventProps}
- />
- );
- }
- if (type === 'upload') {
- return (
- <Image
- round={round}
- src={this.buildUploadUrl()}
- {...eventProps}
- suggested={suggested}
- />
- );
- }
- if (type === 'background') {
- return this.renderBackgroundAvatar();
- }
- return this.renderLetterAvatar();
- };
- renderLetterAvatar() {
- const {title, letterId, round, suggested} = this.props;
- return (
- <LetterAvatar
- round={round}
- displayName={title}
- identifier={letterId}
- suggested={suggested}
- />
- );
- }
- renderBackgroundAvatar() {
- const {round, suggested} = this.props;
- return <BackgroundAvatar round={round} suggested={suggested} />;
- }
- renderBackupAvatar() {
- const {backupAvatar} = this.props;
- return backupAvatar ?? this.renderLetterAvatar();
- }
- render() {
- const {
- className,
- style,
- round,
- hasTooltip,
- size,
- suggested,
- tooltip,
- tooltipOptions,
- forwardedRef,
- type,
- ...props
- } = this.props;
- let sizeStyle = {};
- if (size) {
- sizeStyle = {
- width: `${size}px`,
- height: `${size}px`,
- };
- }
- return (
- <Tooltip title={tooltip} disabled={!hasTooltip} {...tooltipOptions}>
- <StyledBaseAvatar
- data-test-id={`${type}-avatar`}
- ref={forwardedRef}
- loaded={this.state.hasLoaded}
- className={classNames('avatar', className)}
- round={!!round}
- suggested={!!suggested}
- style={{
- ...sizeStyle,
- ...style,
- }}
- {...props}
- >
- {this.state.showBackupAvatar && this.renderBackupAvatar()}
- {this.renderImg()}
- </StyledBaseAvatar>
- </Tooltip>
- );
- }
- }
- export default BaseAvatar;
- const Image = styled('img')<ImageStyleProps>`
- ${imageStyle};
- `;
|