UserPicture.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. import {
  2. type JSX,
  3. type ReactNode,
  4. forwardRef,
  5. memo,
  6. useCallback,
  7. useRef,
  8. } from 'react';
  9. import type { IUser, Ref } from '@growi/core';
  10. import { pagePathUtils } from '@growi/core/dist/utils';
  11. import dynamic from 'next/dynamic';
  12. import { useRouter } from 'next/router';
  13. import type { UncontrolledTooltipProps } from 'reactstrap';
  14. import styles from './UserPicture.module.scss';
  15. const moduleClass = styles['user-picture'];
  16. const moduleTooltipClass = styles['user-picture-tooltip'];
  17. const UncontrolledTooltip = dynamic<UncontrolledTooltipProps>(
  18. () => import('reactstrap').then((mod) => mod.UncontrolledTooltip),
  19. { ssr: false },
  20. );
  21. const DEFAULT_IMAGE = '/images/icons/user.svg';
  22. type UserPictureSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
  23. type BaseUserPictureRootProps = {
  24. displayName: string;
  25. children: ReactNode;
  26. size?: UserPictureSize;
  27. className?: string;
  28. };
  29. type UserPictureRootWithoutLinkProps = BaseUserPictureRootProps;
  30. type UserPictureRootWithLinkProps = BaseUserPictureRootProps & {
  31. username: string;
  32. };
  33. const UserPictureRootWithoutLink = forwardRef<
  34. HTMLSpanElement,
  35. UserPictureRootWithoutLinkProps
  36. >((props, ref) => {
  37. return (
  38. <span ref={ref} className={props.className}>
  39. {props.children}
  40. </span>
  41. );
  42. });
  43. const UserPictureRootWithLink = forwardRef<
  44. HTMLSpanElement,
  45. UserPictureRootWithLinkProps
  46. >((props, ref) => {
  47. const router = useRouter();
  48. const { username } = props;
  49. const clickHandler = useCallback(() => {
  50. const href = pagePathUtils.userHomepagePath({ username });
  51. router.push(href);
  52. }, [router, username]);
  53. // Using <span> tag here instead of <a> tag because UserPicture is used in SearchResultList which is essentially a anchor tag.
  54. // Nested anchor tags causes a warning.
  55. // https://stackoverflow.com/questions/13052598/creating-anchor-tag-inside-anchor-taga
  56. return (
  57. <span
  58. ref={ref}
  59. className={props.className}
  60. onClick={clickHandler}
  61. onKeyDown={() => {}}
  62. style={{ cursor: 'pointer' }}
  63. >
  64. {props.children}
  65. </span>
  66. );
  67. });
  68. // wrapper with Tooltip
  69. const withTooltip =
  70. <P extends BaseUserPictureRootProps>(
  71. UserPictureSpanElm: React.ForwardRefExoticComponent<
  72. P & React.RefAttributes<HTMLSpanElement>
  73. >,
  74. ) =>
  75. (props: P): JSX.Element => {
  76. const { displayName, size } = props;
  77. const username = 'username' in props ? props.username : undefined;
  78. const tooltipClassName = `${moduleTooltipClass} user-picture-tooltip-${size ?? 'md'}`;
  79. const userPictureRef = useRef<HTMLSpanElement>(null);
  80. return (
  81. <>
  82. <UserPictureSpanElm ref={userPictureRef} {...props} />
  83. <UncontrolledTooltip
  84. placement="bottom"
  85. target={userPictureRef}
  86. popperClassName={tooltipClassName}
  87. delay={0}
  88. fade={false}
  89. >
  90. {username ? (
  91. <>
  92. {`@${username}`}
  93. <br />
  94. </>
  95. ) : null}
  96. {displayName}
  97. </UncontrolledTooltip>
  98. </>
  99. );
  100. };
  101. /**
  102. * type guard to determine whether the specified object is IUser
  103. */
  104. const hasUsername = (
  105. obj: Partial<IUser> | Ref<IUser> | null | undefined,
  106. ): obj is { username: string } => {
  107. return obj != null && typeof obj !== 'string' && 'username' in obj;
  108. };
  109. /**
  110. * Type guard to determine whether tooltip should be shown
  111. */
  112. const hasName = (
  113. obj: Partial<IUser> | Ref<IUser> | null | undefined,
  114. ): obj is { name: string } => {
  115. return obj != null && typeof obj === 'object' && 'name' in obj;
  116. };
  117. /**
  118. * type guard to determine whether the specified object is IUser
  119. */
  120. const hasProfileImage = (
  121. obj: Partial<IUser> | Ref<IUser> | null | undefined,
  122. ): obj is { imageUrlCached: string } => {
  123. return obj != null && typeof obj === 'object' && 'imageUrlCached' in obj;
  124. };
  125. type Props = {
  126. user?: Partial<IUser> | Ref<IUser> | null;
  127. size?: UserPictureSize;
  128. noLink?: boolean;
  129. noTooltip?: boolean;
  130. className?: string;
  131. };
  132. export const UserPicture = memo((userProps: Props): JSX.Element => {
  133. const {
  134. user,
  135. size,
  136. noLink,
  137. noTooltip,
  138. className: additionalClassName,
  139. } = userProps;
  140. // Extract user information
  141. const username = hasUsername(user) ? user.username : undefined;
  142. const displayName = hasName(user) ? user.name : 'someone';
  143. const src = hasProfileImage(user)
  144. ? (user.imageUrlCached ?? DEFAULT_IMAGE)
  145. : DEFAULT_IMAGE;
  146. const showTooltip = !noTooltip && hasName(user);
  147. // Build className
  148. const className = [
  149. moduleClass,
  150. 'user-picture',
  151. 'rounded-circle',
  152. size && `user-picture-${size}`,
  153. additionalClassName,
  154. ]
  155. .filter(Boolean)
  156. .join(' ');
  157. const imgElement = <img src={src} alt={displayName} className={className} />;
  158. const baseProps = { displayName, size, children: imgElement };
  159. if (username == null || noLink) {
  160. const Component = showTooltip
  161. ? withTooltip(UserPictureRootWithoutLink)
  162. : UserPictureRootWithoutLink;
  163. return <Component {...baseProps} />;
  164. }
  165. const Component = showTooltip
  166. ? withTooltip(UserPictureRootWithLink)
  167. : UserPictureRootWithLink;
  168. return <Component {...baseProps} username={username} />;
  169. });
  170. UserPicture.displayName = 'UserPicture';