|
|
@@ -19,28 +19,34 @@ const UncontrolledTooltip = dynamic<UncontrolledTooltipProps>(() => import('reac
|
|
|
const DEFAULT_IMAGE = '/images/icons/user.svg';
|
|
|
|
|
|
|
|
|
-type UserPitureSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
|
+type UserPictureSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
|
|
|
|
-type UserPictureRootProps = {
|
|
|
- user: IUser,
|
|
|
- size?: UserPitureSize,
|
|
|
+type BaseUserPictureRootProps = {
|
|
|
+ displayName: string,
|
|
|
+ children: ReactNode,
|
|
|
+ size?: UserPictureSize,
|
|
|
className?: string,
|
|
|
- children?: ReactNode,
|
|
|
}
|
|
|
|
|
|
-const UserPictureRootWithoutLink = forwardRef<HTMLSpanElement, UserPictureRootProps>((props, ref) => {
|
|
|
+type UserPictureRootWithoutLinkProps = BaseUserPictureRootProps;
|
|
|
+
|
|
|
+type UserPictureRootWithLinkProps = BaseUserPictureRootProps & {
|
|
|
+ username: string,
|
|
|
+}
|
|
|
+
|
|
|
+const UserPictureRootWithoutLink = forwardRef<HTMLSpanElement, UserPictureRootWithoutLinkProps>((props, ref) => {
|
|
|
return <span ref={ref} className={props.className}>{props.children}</span>;
|
|
|
});
|
|
|
|
|
|
-const UserPictureRootWithLink = forwardRef<HTMLSpanElement, UserPictureRootProps>((props, ref) => {
|
|
|
+const UserPictureRootWithLink = forwardRef<HTMLSpanElement, UserPictureRootWithLinkProps>((props, ref) => {
|
|
|
const router = useRouter();
|
|
|
|
|
|
- const { user } = props;
|
|
|
+ const { username } = props;
|
|
|
|
|
|
const clickHandler = useCallback(() => {
|
|
|
- const href = pagePathUtils.userHomepagePath(user);
|
|
|
+ const href = pagePathUtils.userHomepagePath({ username });
|
|
|
router.push(href);
|
|
|
- }, [router, user]);
|
|
|
+ }, [router, username]);
|
|
|
|
|
|
// Using <span> tag here instead of <a> tag because UserPicture is used in SearchResultList which is essentially a anchor tag.
|
|
|
// Nested anchor tags causes a warning.
|
|
|
@@ -50,91 +56,96 @@ const UserPictureRootWithLink = forwardRef<HTMLSpanElement, UserPictureRootProps
|
|
|
|
|
|
|
|
|
// wrapper with Tooltip
|
|
|
-const withTooltip = (UserPictureSpanElm: React.ForwardRefExoticComponent<UserPictureRootProps & React.RefAttributes<HTMLSpanElement>>) => {
|
|
|
- return (props: UserPictureRootProps) => {
|
|
|
- const { user, size } = props;
|
|
|
+const withTooltip = <P extends BaseUserPictureRootProps>(
|
|
|
+ UserPictureSpanElm: React.ForwardRefExoticComponent<P & React.RefAttributes<HTMLSpanElement>>,
|
|
|
+) => (props: P): JSX.Element => {
|
|
|
+ const { displayName, size } = props;
|
|
|
+ const username = 'username' in props ? props.username : undefined;
|
|
|
|
|
|
const tooltipClassName = `${moduleTooltipClass} user-picture-tooltip-${size ?? 'md'}`;
|
|
|
-
|
|
|
const userPictureRef = useRef<HTMLSpanElement>(null);
|
|
|
|
|
|
return (
|
|
|
<>
|
|
|
- <UserPictureSpanElm ref={userPictureRef} user={user}>{props.children}</UserPictureSpanElm>
|
|
|
+ <UserPictureSpanElm ref={userPictureRef} {...props} />
|
|
|
<UncontrolledTooltip
|
|
|
placement="bottom"
|
|
|
target={userPictureRef}
|
|
|
popperClassName={tooltipClassName}
|
|
|
delay={0}
|
|
|
fade={false}
|
|
|
- show
|
|
|
>
|
|
|
- @{user.username}<br />
|
|
|
- {user.name}
|
|
|
+ {username ? <>{`@${username}`}<br /></> : null}
|
|
|
+ {displayName}
|
|
|
</UncontrolledTooltip>
|
|
|
</>
|
|
|
);
|
|
|
};
|
|
|
+
|
|
|
+
|
|
|
+/**
|
|
|
+ * type guard to determine whether the specified object is IUser
|
|
|
+ */
|
|
|
+const hasUsername = (obj: Partial<IUser> | Ref<IUser> | null | undefined): obj is { username: string } => {
|
|
|
+ return obj != null && typeof obj !== 'string' && 'username' in obj;
|
|
|
};
|
|
|
|
|
|
+/**
|
|
|
+ * Type guard to determine whether tooltip should be shown
|
|
|
+ */
|
|
|
+const hasName = (obj: Partial<IUser> | Ref<IUser> | null | undefined): obj is { name: string } => {
|
|
|
+ return obj != null && typeof obj === 'object' && 'name' in obj;
|
|
|
+};
|
|
|
|
|
|
/**
|
|
|
* type guard to determine whether the specified object is IUser
|
|
|
*/
|
|
|
-const isUserObj = (obj: Partial<IUser> | Ref<IUser>): obj is IUser => {
|
|
|
- return typeof obj !== 'string' && 'username' in obj;
|
|
|
+const hasProfileImage = (obj: Partial<IUser> | Ref<IUser> | null | undefined): obj is { imageUrlCached: string } => {
|
|
|
+ return obj != null && typeof obj === 'object' && 'imageUrlCached' in obj;
|
|
|
};
|
|
|
|
|
|
|
|
|
type Props = {
|
|
|
user?: Partial<IUser> | Ref<IUser> | null,
|
|
|
- size?: UserPitureSize,
|
|
|
+ size?: UserPictureSize,
|
|
|
noLink?: boolean,
|
|
|
noTooltip?: boolean,
|
|
|
className?: string
|
|
|
};
|
|
|
|
|
|
-export const UserPicture = memo((props: Props): JSX.Element => {
|
|
|
-
|
|
|
+export const UserPicture = memo((userProps: Props): JSX.Element => {
|
|
|
const {
|
|
|
user, size, noLink, noTooltip, className: additionalClassName,
|
|
|
- } = props;
|
|
|
-
|
|
|
- const classNames = [moduleClass, 'user-picture', 'rounded-circle'];
|
|
|
- if (size != null) {
|
|
|
- classNames.push(`user-picture-${size}`);
|
|
|
- }
|
|
|
- if (additionalClassName != null) {
|
|
|
- classNames.push(additionalClassName);
|
|
|
- }
|
|
|
- const className = classNames.join(' ');
|
|
|
-
|
|
|
- if (user == null || !isUserObj(user)) {
|
|
|
- return (
|
|
|
- <img
|
|
|
- src={DEFAULT_IMAGE}
|
|
|
- alt="someone"
|
|
|
- className={className}
|
|
|
- />
|
|
|
- );
|
|
|
+ } = userProps;
|
|
|
+
|
|
|
+ // Extract user information
|
|
|
+ const username = hasUsername(user) ? user.username : undefined;
|
|
|
+ const displayName = hasName(user) ? user.name : 'someone';
|
|
|
+ const src = hasProfileImage(user) ? user.imageUrlCached ?? DEFAULT_IMAGE : DEFAULT_IMAGE;
|
|
|
+ const showTooltip = !noTooltip && hasName(user);
|
|
|
+
|
|
|
+ // Build className
|
|
|
+ const className = [
|
|
|
+ moduleClass,
|
|
|
+ 'user-picture',
|
|
|
+ 'rounded-circle',
|
|
|
+ size && `user-picture-${size}`,
|
|
|
+ additionalClassName,
|
|
|
+ ].filter(Boolean).join(' ');
|
|
|
+
|
|
|
+ const imgElement = <img src={src} alt={displayName} className={className} />;
|
|
|
+ const baseProps = { displayName, size, children: imgElement };
|
|
|
+
|
|
|
+ if (username == null || noLink) {
|
|
|
+ const Component = showTooltip
|
|
|
+ ? withTooltip(UserPictureRootWithoutLink)
|
|
|
+ : UserPictureRootWithoutLink;
|
|
|
+ return <Component {...baseProps} />;
|
|
|
}
|
|
|
|
|
|
- // determine RootElm
|
|
|
- const UserPictureSpanElm = noLink ? UserPictureRootWithoutLink : UserPictureRootWithLink;
|
|
|
- const UserPictureRootElm = noTooltip
|
|
|
- ? UserPictureSpanElm
|
|
|
- : withTooltip(UserPictureSpanElm);
|
|
|
-
|
|
|
- const userPictureSrc = user.imageUrlCached ?? DEFAULT_IMAGE;
|
|
|
-
|
|
|
- return (
|
|
|
- <UserPictureRootElm user={user} size={size}>
|
|
|
- <img
|
|
|
- src={userPictureSrc}
|
|
|
- alt={user.username}
|
|
|
- className={className}
|
|
|
- />
|
|
|
- </UserPictureRootElm>
|
|
|
- );
|
|
|
+ const Component = showTooltip
|
|
|
+ ? withTooltip(UserPictureRootWithLink)
|
|
|
+ : UserPictureRootWithLink;
|
|
|
+ return <Component {...baseProps} username={username} />;
|
|
|
});
|
|
|
UserPicture.displayName = 'UserPicture';
|