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; 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, 'children' | 'title'>; uploadId?: string | null | undefined; }; type Props = BaseProps; type State = { hasLoaded: boolean; loadError: boolean; showBackupAvatar: boolean; }; class BaseAvatar extends Component { 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 ( ); } if (type === 'upload') { return ( ); } if (type === 'background') { return this.renderBackgroundAvatar(); } return this.renderLetterAvatar(); }; renderLetterAvatar() { const {title, letterId, round, suggested} = this.props; return ( ); } renderBackgroundAvatar() { const {round, suggested} = this.props; return ; } 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 ( {this.state.showBackupAvatar && this.renderBackupAvatar()} {this.renderImg()} ); } } export default BaseAvatar; const Image = styled('img')` ${imageStyle}; `;