baseAvatar.tsx 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. import {useCallback, useState} 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 type {TooltipProps} from 'sentry/components/tooltip';
  8. import {Tooltip} from 'sentry/components/tooltip';
  9. import type {Avatar} from 'sentry/types/core';
  10. import Gravatar from './gravatar';
  11. import type {ImageStyleProps} from './styles';
  12. import {imageStyle} from './styles';
  13. type AllowedSize = 20 | 32 | 36 | 48 | 52 | 64 | 80 | 96 | 120;
  14. const DEFAULT_REMOTE_SIZE = 120 satisfies AllowedSize;
  15. interface BaseAvatarProps extends React.HTMLAttributes<HTMLSpanElement> {
  16. backupAvatar?: React.ReactNode;
  17. className?: string;
  18. forwardedRef?: React.Ref<HTMLSpanElement>;
  19. gravatarId?: string;
  20. /**
  21. * Enable to display tooltips.
  22. */
  23. hasTooltip?: boolean;
  24. letterId?: string;
  25. /**
  26. * Should avatar be round instead of a square
  27. */
  28. round?: boolean;
  29. size?: number;
  30. suggested?: boolean;
  31. title?: string;
  32. /**
  33. * The content for the tooltip. Requires hasTooltip to display
  34. */
  35. tooltip?: React.ReactNode;
  36. /**
  37. * Additional props for the tooltip
  38. */
  39. tooltipOptions?: Omit<TooltipProps, 'children' | 'title'>;
  40. /**
  41. * The type of avatar being rendered.
  42. */
  43. type?: Avatar['avatarType'];
  44. /**
  45. * Full URL to the uploaded avatar's image.
  46. */
  47. uploadUrl?: string | null | undefined;
  48. }
  49. function BaseAvatar({
  50. backupAvatar,
  51. className,
  52. forwardedRef,
  53. gravatarId,
  54. letterId,
  55. size,
  56. style,
  57. suggested,
  58. title,
  59. tooltip,
  60. tooltipOptions,
  61. uploadUrl,
  62. hasTooltip = false,
  63. round = false,
  64. type = 'letter_avatar',
  65. ...props
  66. }: BaseAvatarProps) {
  67. const [hasError, setError] = useState<boolean | null>(null);
  68. const handleError = useCallback(() => setError(true), []);
  69. const handleLoad = useCallback(() => setError(false), []);
  70. const resolvedUploadUrl = uploadUrl
  71. ? `${uploadUrl}?${qs.stringify({s: DEFAULT_REMOTE_SIZE})}`
  72. : '';
  73. const letterAvatar = (
  74. <LetterAvatar
  75. round={round}
  76. displayName={title === '[Filtered]' ? '?' : title}
  77. identifier={letterId}
  78. suggested={suggested}
  79. />
  80. );
  81. const imageAvatar =
  82. type === 'upload' ? (
  83. <ImageAvatar
  84. src={resolvedUploadUrl}
  85. round={round}
  86. suggested={suggested}
  87. onLoad={handleLoad}
  88. onError={handleError}
  89. />
  90. ) : type === 'gravatar' ? (
  91. <Gravatar
  92. gravatarId={gravatarId}
  93. remoteSize={DEFAULT_REMOTE_SIZE}
  94. round={round}
  95. suggested={suggested}
  96. onLoad={handleLoad}
  97. onError={handleError}
  98. />
  99. ) : type === 'background' ? (
  100. <BackgroundAvatar round={round} suggested={suggested} />
  101. ) : (
  102. letterAvatar
  103. );
  104. const backup = backupAvatar ?? letterAvatar;
  105. const sizeStyle: React.CSSProperties = !size
  106. ? {}
  107. : {
  108. height: size,
  109. width: size,
  110. };
  111. const avatarComponent = (
  112. <StyledBaseAvatar
  113. data-test-id={`${type}-avatar`}
  114. ref={forwardedRef}
  115. className={classNames('avatar', className)}
  116. round={!!round}
  117. suggested={!!suggested}
  118. style={{...sizeStyle, ...style}}
  119. title={title}
  120. {...props}
  121. >
  122. {hasError ? backup : imageAvatar}
  123. </StyledBaseAvatar>
  124. );
  125. return hasTooltip ? (
  126. <Tooltip title={tooltip} {...tooltipOptions}>
  127. {avatarComponent}
  128. </Tooltip>
  129. ) : (
  130. avatarComponent
  131. );
  132. }
  133. export {BaseAvatar, type BaseAvatarProps};
  134. // Note: Avatar will not always be a child of a flex layout, but this seems like a
  135. // sensible default.
  136. const StyledBaseAvatar = styled('span')<{
  137. round: boolean;
  138. suggested: boolean;
  139. }>`
  140. flex-shrink: 0;
  141. border-radius: ${p => (p.round ? '50%' : '3px')};
  142. border: ${p => (p.suggested ? `1px dashed ${p.theme.subText}` : 'none')};
  143. background-color: ${p => (p.suggested ? p.theme.background : 'none')};
  144. `;
  145. const ImageAvatar = styled('img')<ImageStyleProps>`
  146. ${imageStyle};
  147. `;