Răsfoiți Sursa

Merge pull request #9898 from weseek/fix/user-picture

imprv: User picture tooltip (2)
mergify[bot] 11 luni în urmă
părinte
comite
8930094e31

+ 1 - 1
apps/app/src/client/components/PageEditor/EditorNavbar/EditingUserList.tsx

@@ -32,7 +32,7 @@ export const EditingUserList: FC<Props> = ({ clientList }) => {
         {firstFourUsers.map(editingClient => (
           <div key={editingClient.clientId} className="ms-1">
             <UserPicture
-              user={editingClient.userId}
+              user={editingClient}
               noLink
               className="border border-info"
             />

+ 1 - 1
packages/core/src/utils/page-path-utils/index.ts

@@ -128,7 +128,7 @@ export const isCreatablePage = (path: string): boolean => {
  * return user's homepage path
  * @param user
  */
-export const userHomepagePath = (user: IUser | null | undefined): string => {
+export const userHomepagePath = (user: { username: string } | null | undefined): string => {
   if (user?.username == null) {
     return '';
   }

+ 3 - 1
packages/editor/src/client/stores/use-collaborative-editor-mode.ts

@@ -67,8 +67,10 @@ export const useCollaborativeEditorMode = (
 
       const userLocalState: EditingClient = {
         clientId: primaryDoc.clientID,
-        name: user?.name ? `${user.name}` : `Guest User ${Math.floor(Math.random() * 100)}`,
+        name: user?.name ?? `Guest User ${Math.floor(Math.random() * 100)}`,
         userId: user?._id,
+        username: user?.username,
+        imageUrlCached: user?.imageUrlCached,
         color: userColor.color,
         colorLight: userColor.light,
       };

+ 3 - 2
packages/editor/src/interfaces/editing-client.ts

@@ -1,6 +1,7 @@
-export type EditingClient = {
+import type { IUser } from '@growi/core';
+
+export type EditingClient = Pick<IUser, 'name'> & Partial<Pick<IUser, 'username' | 'imageUrlCached'>> & {
   clientId: number;
-  name: string;
   userId?: string;
   color: string;
   colorLight: string;

+ 70 - 59
packages/ui/src/components/UserPicture.tsx

@@ -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';