baseAvatar.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. import * as React from 'react';
  2. import styled from '@emotion/styled';
  3. import classNames from 'classnames';
  4. import * as qs from 'query-string';
  5. import BackgroundAvatar from 'sentry/components/avatar/backgroundAvatar';
  6. import LetterAvatar from 'sentry/components/letterAvatar';
  7. import Tooltip from 'sentry/components/tooltip';
  8. import {Avatar} from 'sentry/types';
  9. import Gravatar from './gravatar';
  10. import {imageStyle, ImageStyleProps} from './styles';
  11. const DEFAULT_GRAVATAR_SIZE = 64;
  12. const ALLOWED_SIZES = [20, 32, 36, 48, 52, 64, 80, 96, 120];
  13. const DEFAULT_REMOTE_SIZE = 120;
  14. // Note: Avatar will not always be a child of a flex layout, but this seems like a
  15. // sensible default.
  16. const StyledBaseAvatar = styled('span')<{
  17. round: boolean;
  18. loaded: boolean;
  19. suggested: boolean;
  20. }>`
  21. flex-shrink: 0;
  22. border-radius: ${p => (p.round ? '50%' : '3px')};
  23. border: ${p => (p.suggested ? `1px dashed ${p.theme.gray400}` : 'none')};
  24. background-color: ${p =>
  25. p.loaded ? p.theme.background : 'background-color: rgba(200, 200, 200, 0.1);'};
  26. `;
  27. const defaultProps: DefaultProps = {
  28. // No default size to ease transition from CSS defined sizes
  29. // size: 64,
  30. style: {},
  31. /**
  32. * Enable to display tooltips.
  33. */
  34. hasTooltip: false,
  35. /**
  36. * The type of avatar being rendered.
  37. */
  38. type: 'letter_avatar',
  39. /**
  40. * Path to uploaded avatar (differs based on model type)
  41. */
  42. uploadPath: 'avatar',
  43. /**
  44. * Should avatar be round instead of a square
  45. */
  46. round: false,
  47. };
  48. type DefaultProps = {
  49. style?: React.CSSProperties;
  50. suggested?: boolean;
  51. /**
  52. * Enable to display tooltips.
  53. */
  54. hasTooltip?: boolean;
  55. /**
  56. * The type of avatar being rendered.
  57. */
  58. type?: Avatar['avatarType'];
  59. /**
  60. * Should avatar be round instead of a square
  61. */
  62. round?: boolean;
  63. /**
  64. * Path to uploaded avatar (differs based on model type)
  65. */
  66. uploadPath?:
  67. | 'avatar'
  68. | 'team-avatar'
  69. | 'organization-avatar'
  70. | 'project-avatar'
  71. | 'sentry-app-avatar';
  72. };
  73. type BaseProps = DefaultProps & {
  74. size?: number;
  75. /**
  76. * This is the size of the remote image to request.
  77. */
  78. remoteImageSize?: typeof ALLOWED_SIZES[number];
  79. /**
  80. * Default gravatar to display
  81. */
  82. default?: string;
  83. uploadId?: string | null | undefined;
  84. gravatarId?: string;
  85. letterId?: string;
  86. title?: string;
  87. /**
  88. * The content for the tooltip. Requires hasTooltip to display
  89. */
  90. tooltip?: React.ReactNode;
  91. /**
  92. * Additional props for the tooltip
  93. */
  94. tooltipOptions?: Omit<Tooltip['props'], 'children' | 'title'>;
  95. className?: string;
  96. forwardedRef?: React.Ref<HTMLSpanElement>;
  97. };
  98. type Props = BaseProps;
  99. type State = {
  100. showBackupAvatar: boolean;
  101. hasLoaded: boolean;
  102. loadError: boolean;
  103. };
  104. class BaseAvatar extends React.Component<Props, State> {
  105. static defaultProps = defaultProps;
  106. constructor(props: Props) {
  107. super(props);
  108. this.state = {
  109. showBackupAvatar: false,
  110. hasLoaded: props.type !== 'upload',
  111. loadError: false,
  112. };
  113. }
  114. getRemoteImageSize = () => {
  115. const {remoteImageSize, size} = this.props;
  116. // Try to make sure remote image size is >= requested size
  117. // If requested size > allowed size then use the largest allowed size
  118. const allowed =
  119. size &&
  120. (ALLOWED_SIZES.find(allowedSize => allowedSize >= size) ||
  121. ALLOWED_SIZES[ALLOWED_SIZES.length - 1]);
  122. return remoteImageSize || allowed || DEFAULT_GRAVATAR_SIZE;
  123. };
  124. buildUploadUrl = () => {
  125. const {uploadPath, uploadId} = this.props;
  126. return `/${uploadPath || 'avatar'}/${uploadId}/?${qs.stringify({
  127. s: DEFAULT_REMOTE_SIZE,
  128. })}`;
  129. };
  130. handleLoad = () => {
  131. this.setState({showBackupAvatar: false, hasLoaded: true});
  132. };
  133. handleError = () => {
  134. this.setState({showBackupAvatar: true, loadError: true, hasLoaded: true});
  135. };
  136. renderImg = () => {
  137. if (this.state.loadError) {
  138. return null;
  139. }
  140. const {type, round, gravatarId, suggested} = this.props;
  141. const eventProps = {
  142. onError: this.handleError,
  143. onLoad: this.handleLoad,
  144. };
  145. if (type === 'gravatar') {
  146. return (
  147. <Gravatar
  148. placeholder={this.props.default}
  149. gravatarId={gravatarId}
  150. round={round}
  151. remoteSize={DEFAULT_REMOTE_SIZE}
  152. suggested={suggested}
  153. grayscale={suggested}
  154. {...eventProps}
  155. />
  156. );
  157. }
  158. if (type === 'upload') {
  159. return (
  160. <Image
  161. round={round}
  162. src={this.buildUploadUrl()}
  163. {...eventProps}
  164. suggested={suggested}
  165. grayscale={suggested}
  166. />
  167. );
  168. }
  169. if (type === 'background') {
  170. return this.renderBackgroundAvatar();
  171. }
  172. return this.renderLetterAvatar();
  173. };
  174. renderLetterAvatar() {
  175. const {title, letterId, round, suggested} = this.props;
  176. return (
  177. <LetterAvatar
  178. round={round}
  179. displayName={title}
  180. identifier={letterId}
  181. suggested={suggested}
  182. />
  183. );
  184. }
  185. renderBackgroundAvatar() {
  186. const {round, suggested} = this.props;
  187. return <BackgroundAvatar round={round} suggested={suggested} />;
  188. }
  189. render() {
  190. const {
  191. className,
  192. style,
  193. round,
  194. hasTooltip,
  195. size,
  196. suggested,
  197. tooltip,
  198. tooltipOptions,
  199. forwardedRef,
  200. type,
  201. ...props
  202. } = this.props;
  203. let sizeStyle = {};
  204. if (size) {
  205. sizeStyle = {
  206. width: `${size}px`,
  207. height: `${size}px`,
  208. };
  209. }
  210. return (
  211. <Tooltip title={tooltip} disabled={!hasTooltip} {...tooltipOptions}>
  212. <StyledBaseAvatar
  213. data-test-id={`${type}-avatar`}
  214. ref={forwardedRef}
  215. loaded={this.state.hasLoaded}
  216. className={classNames('avatar', className)}
  217. round={!!round}
  218. suggested={!!suggested}
  219. style={{
  220. ...sizeStyle,
  221. ...style,
  222. }}
  223. {...props}
  224. >
  225. {this.state.showBackupAvatar && this.renderLetterAvatar()}
  226. {this.renderImg()}
  227. </StyledBaseAvatar>
  228. </Tooltip>
  229. );
  230. }
  231. }
  232. export default BaseAvatar;
  233. const Image = styled('img')<ImageStyleProps>`
  234. ${imageStyle};
  235. `;