From 75dc20ca0b5f6737074f11d59a3a591aa6d89d24 Mon Sep 17 00:00:00 2001
From: Fedor Katurov <fedor.katurov@playrix.com>
Date: Tue, 22 Nov 2022 10:46:41 +0600
Subject: [PATCH] fixed user hydration

---
 .../comment/CommentContent/index.tsx          | 75 ++++++++++++++-----
 .../containers/Authorized/index.tsx           | 15 ++--
 src/components/flow/FlowGrid/index.tsx        | 60 +++++++++------
 src/components/menu/SeparatedMenu/index.tsx   |  4 +-
 src/components/node/NodeTitle/index.tsx       | 61 ++++++++-------
 src/containers/main/Header/index.tsx          | 12 ++-
 src/containers/main/Header/styles.module.scss | 10 ++-
 src/hooks/auth/useAuth.ts                     |  1 +
 src/hooks/auth/useUser.ts                     | 20 +++--
 src/hooks/node/useNodePermissions.ts          | 18 ++++-
 src/layouts/NodeLayout/index.tsx              |  6 +-
 src/store/auth/AuthStore.ts                   |  5 ++
 src/utils/providers/AuthProvider.tsx          |  2 +
 13 files changed, 194 insertions(+), 95 deletions(-)

diff --git a/src/components/comment/CommentContent/index.tsx b/src/components/comment/CommentContent/index.tsx
index 51c9f96e..17f1eac1 100644
--- a/src/components/comment/CommentContent/index.tsx
+++ b/src/components/comment/CommentContent/index.tsx
@@ -1,9 +1,19 @@
-import React, { createElement, FC, Fragment, memo, ReactNode, useCallback, useMemo, useState } from 'react';
+import React, {
+  createElement,
+  FC,
+  Fragment,
+  memo,
+  ReactNode,
+  useCallback,
+  useMemo,
+  useState,
+} from 'react';
 
 import classnames from 'classnames';
 import classNames from 'classnames';
 
 import { CommentForm } from '~/components/comment/CommentForm';
+import { Authorized } from '~/components/containers/Authorized';
 import { Group } from '~/components/containers/Group';
 import { AudioPlayer } from '~/components/media/AudioPlayer';
 import { COMMENT_BLOCK_RENDERERS } from '~/constants/comment';
@@ -28,7 +38,15 @@ interface IProps {
 }
 
 const CommentContent: FC<IProps> = memo(
-  ({ comment, canEdit, nodeId, saveComment, onDelete, onShowImageModal, prefix }) => {
+  ({
+    comment,
+    canEdit,
+    nodeId,
+    saveComment,
+    onDelete,
+    onShowImageModal,
+    prefix,
+  }) => {
     const [isEditing, setIsEditing] = useState(false);
 
     const startEditing = useCallback(() => setIsEditing(true), [setIsEditing]);
@@ -38,11 +56,13 @@ const CommentContent: FC<IProps> = memo(
       () =>
         reduce(
           (group, file) =>
-            file.type ? assocPath([file.type], append(file, group[file.type]), group) : group,
+            file.type
+              ? assocPath([file.type], append(file, group[file.type]), group)
+              : group,
           {} as Record<UploadType, IFile[]>,
-          comment.files
+          comment.files,
         ),
-      [comment]
+      [comment],
     );
 
     const onLockClick = useCallback(() => {
@@ -50,8 +70,9 @@ const CommentContent: FC<IProps> = memo(
     }, [comment, onDelete]);
 
     const menu = useMemo(
-      () => canEdit && <CommentMenu onDelete={onLockClick} onEdit={startEditing} />,
-      [canEdit, startEditing, onLockClick]
+      () =>
+        canEdit && <CommentMenu onDelete={onLockClick} onEdit={startEditing} />,
+      [canEdit, startEditing, onLockClick],
     );
 
     const blocks = useMemo(
@@ -59,7 +80,7 @@ const CommentContent: FC<IProps> = memo(
         !!comment.text.trim()
           ? formatCommentText(path(['user', 'username'], comment), comment.text)
           : [],
-      [comment]
+      [comment],
     );
 
     if (isEditing) {
@@ -78,17 +99,22 @@ const CommentContent: FC<IProps> = memo(
         {!!prefix && <div className={styles.prefix}>{prefix}</div>}
         {comment.text.trim() && (
           <Group className={classnames(styles.block, styles.block_text)}>
-            {menu}
+            <Authorized>{menu}</Authorized>
 
             <Group className={styles.renderers}>
               {blocks.map(
                 (block, key) =>
                   COMMENT_BLOCK_RENDERERS[block.type] &&
-                  createElement(COMMENT_BLOCK_RENDERERS[block.type], { block, key })
+                  createElement(COMMENT_BLOCK_RENDERERS[block.type], {
+                    block,
+                    key,
+                  }),
               )}
             </Group>
 
-            <div className={styles.date}>{getPrettyDate(comment.created_at)}</div>
+            <div className={styles.date}>
+              {getPrettyDate(comment.created_at)}
+            </div>
           </Group>
         )}
 
@@ -102,32 +128,45 @@ const CommentContent: FC<IProps> = memo(
               })}
             >
               {groupped.image.map((file, index) => (
-                <div key={file.id} onClick={() => onShowImageModal(groupped.image, index)}>
-                  <img src={getURL(file, ImagePresets['600'])} alt={file.name} />
+                <div
+                  key={file.id}
+                  onClick={() => onShowImageModal(groupped.image, index)}
+                >
+                  <img
+                    src={getURL(file, ImagePresets['600'])}
+                    alt={file.name}
+                  />
                 </div>
               ))}
             </div>
 
-            <div className={styles.date}>{getPrettyDate(comment.created_at)}</div>
+            <div className={styles.date}>
+              {getPrettyDate(comment.created_at)}
+            </div>
           </div>
         )}
 
         {groupped.audio && groupped.audio.length > 0 && (
           <Fragment>
-            {groupped.audio.map(file => (
-              <div className={classnames(styles.block, styles.block_audio)} key={file.id}>
+            {groupped.audio.map((file) => (
+              <div
+                className={classnames(styles.block, styles.block_audio)}
+                key={file.id}
+              >
                 {menu}
 
                 <AudioPlayer file={file} />
 
-                <div className={styles.date}>{getPrettyDate(comment.created_at)}</div>
+                <div className={styles.date}>
+                  {getPrettyDate(comment.created_at)}
+                </div>
               </div>
             ))}
           </Fragment>
         )}
       </div>
     );
-  }
+  },
 );
 
 export { CommentContent };
diff --git a/src/components/containers/Authorized/index.tsx b/src/components/containers/Authorized/index.tsx
index 739e1ec8..d38a9ea6 100644
--- a/src/components/containers/Authorized/index.tsx
+++ b/src/components/containers/Authorized/index.tsx
@@ -1,15 +1,20 @@
 import React, { FC } from 'react';
 
+import { observer } from 'mobx-react-lite';
+
 import { useAuth } from '~/hooks/auth/useAuth';
 
-interface IProps {}
+interface IProps {
+  // don't wait for user refetch, trust hydration
+  hydratedOnly?: boolean;
+}
 
-const Authorized: FC<IProps> = ({ children }) => {
-  const { isUser } = useAuth();
+const Authorized: FC<IProps> = observer(({ children, hydratedOnly }) => {
+  const { isUser, fetched } = useAuth();
 
-  if (!isUser) return null;
+  if (!isUser || (!hydratedOnly && !fetched)) return null;
 
   return <>{children}</>;
-};
+});
 
 export { Authorized };
diff --git a/src/components/flow/FlowGrid/index.tsx b/src/components/flow/FlowGrid/index.tsx
index 3d451b54..5aac50b5 100644
--- a/src/components/flow/FlowGrid/index.tsx
+++ b/src/components/flow/FlowGrid/index.tsx
@@ -1,9 +1,11 @@
 import React, { FC, Fragment } from 'react';
 
 import classNames from 'classnames';
+import { observer } from 'mobx-react-lite';
 
 import { FlowCell } from '~/components/flow/FlowCell';
 import { flowDisplayToPreset, URLS } from '~/constants/urls';
+import { useAuth } from '~/hooks/auth/useAuth';
 import { FlowDisplay, IFlowNode, INode } from '~/types';
 import { IUser } from '~/types/auth';
 import { getURLFromString } from '~/utils/dom';
@@ -17,28 +19,38 @@ interface Props {
   onChangeCellView: (id: INode['id'], flow: FlowDisplay) => void;
 }
 
-export const FlowGrid: FC<Props> = ({ user, nodes, onChangeCellView }) => {
-  if (!nodes) {
-    return null;
-  }
+export const FlowGrid: FC<Props> = observer(
+  ({ user, nodes, onChangeCellView }) => {
+    const { fetched, isUser } = useAuth();
 
-  return (
-    <Fragment>
-      {nodes.map(node => (
-        <div className={classNames(styles.cell, styles[node.flow.display])} key={node.id}>
-          <FlowCell
-            id={node.id}
-            color={node.flow.dominant_color}
-            to={URLS.NODE_URL(node.id)}
-            image={getURLFromString(node.thumbnail, flowDisplayToPreset[node.flow.display])}
-            flow={node.flow}
-            text={node.description}
-            title={node.title}
-            canEdit={canEditNode(node, user)}
-            onChangeCellView={onChangeCellView}
-          />
-        </div>
-      ))}
-    </Fragment>
-  );
-};
+    if (!nodes) {
+      return null;
+    }
+
+    return (
+      <Fragment>
+        {nodes.map((node) => (
+          <div
+            className={classNames(styles.cell, styles[node.flow.display])}
+            key={node.id}
+          >
+            <FlowCell
+              id={node.id}
+              color={node.flow.dominant_color}
+              to={URLS.NODE_URL(node.id)}
+              image={getURLFromString(
+                node.thumbnail,
+                flowDisplayToPreset[node.flow.display],
+              )}
+              flow={node.flow}
+              text={node.description}
+              title={node.title}
+              canEdit={fetched && isUser && canEditNode(node, user)}
+              onChangeCellView={onChangeCellView}
+            />
+          </div>
+        ))}
+      </Fragment>
+    );
+  },
+);
diff --git a/src/components/menu/SeparatedMenu/index.tsx b/src/components/menu/SeparatedMenu/index.tsx
index 19a4c9e3..c030220b 100644
--- a/src/components/menu/SeparatedMenu/index.tsx
+++ b/src/components/menu/SeparatedMenu/index.tsx
@@ -4,7 +4,7 @@ import classNames from 'classnames';
 
 import styles from './styles.module.scss';
 
-interface SeparatedMenuProps {
+export interface SeparatedMenuProps {
   className?: string;
 }
 
@@ -14,7 +14,7 @@ const SeparatedMenu: FC<SeparatedMenuProps> = ({ children, className }) => {
       return [];
     }
 
-    return (Array.isArray(children) ? children : [children]).filter(it => it);
+    return (Array.isArray(children) ? children : [children]).filter((it) => it);
   }, [children]);
 
   return (
diff --git a/src/components/node/NodeTitle/index.tsx b/src/components/node/NodeTitle/index.tsx
index d33a4bab..99ae85ae 100644
--- a/src/components/node/NodeTitle/index.tsx
+++ b/src/components/node/NodeTitle/index.tsx
@@ -2,6 +2,7 @@ import React, { memo, VFC } from 'react';
 
 import classNames from 'classnames';
 
+import { Authorized } from '~/components/containers/Authorized';
 import { Icon } from '~/components/input/Icon';
 import { SeparatedMenu } from '~/components/menu/SeparatedMenu';
 import { NodeEditMenu } from '~/components/node/NodeEditMenu';
@@ -76,37 +77,39 @@ const NodeTitle: VFC<IProps> = memo(
             )}
           </div>
 
-          <SeparatedMenu className={styles.buttons}>
-            {canEdit && (
-              <NodeEditMenu
-                className={styles.button}
-                canStar={canStar}
-                isHeroic={isHeroic}
-                isLocked={isLocked}
-                onStar={onStar}
-                onLock={onLock}
-                onEdit={onEdit}
-              />
-            )}
+          <Authorized>
+            <SeparatedMenu className={styles.buttons}>
+              {canEdit && (
+                <NodeEditMenu
+                  className={styles.button}
+                  canStar={canStar}
+                  isHeroic={isHeroic}
+                  isLocked={isLocked}
+                  onStar={onStar}
+                  onLock={onLock}
+                  onEdit={onEdit}
+                />
+              )}
 
-            {canLike && (
-              <div
-                className={classNames(styles.button, styles.like, {
-                  [styles.is_liked]: isLiked,
-                })}
-              >
-                {isLiked ? (
-                  <Icon icon="heart_full" size={24} onClick={onLike} />
-                ) : (
-                  <Icon icon="heart" size={24} onClick={onLike} />
-                )}
+              {canLike && (
+                <div
+                  className={classNames(styles.button, styles.like, {
+                    [styles.is_liked]: isLiked,
+                  })}
+                >
+                  {isLiked ? (
+                    <Icon icon="heart_full" size={24} onClick={onLike} />
+                  ) : (
+                    <Icon icon="heart" size={24} onClick={onLike} />
+                  )}
 
-                {!!likeCount && likeCount > 0 && (
-                  <div className={styles.like_count}>{likeCount}</div>
-                )}
-              </div>
-            )}
-          </SeparatedMenu>
+                  {!!likeCount && likeCount > 0 && (
+                    <div className={styles.like_count}>{likeCount}</div>
+                  )}
+                </div>
+              )}
+            </SeparatedMenu>
+          </Authorized>
         </div>
       </div>
     );
diff --git a/src/containers/main/Header/index.tsx b/src/containers/main/Header/index.tsx
index 893737d3..113409f3 100644
--- a/src/containers/main/Header/index.tsx
+++ b/src/containers/main/Header/index.tsx
@@ -1,4 +1,4 @@
-import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
+import { FC, useCallback, useEffect, useMemo, useState } from 'react';
 
 import classNames from 'classnames';
 import isBefore from 'date-fns/isBefore';
@@ -16,7 +16,6 @@ import { URLS } from '~/constants/urls';
 import { useAuth } from '~/hooks/auth/useAuth';
 import { useScrollTop } from '~/hooks/dom/useScrollTop';
 import { useFlow } from '~/hooks/flow/useFlow';
-import { useGetLabStats } from '~/hooks/lab/useGetLabStats';
 import { useModal } from '~/hooks/modal/useModal';
 import { useUpdates } from '~/hooks/updates/useUpdates';
 import { useSidebar } from '~/utils/providers/SidebarProvider';
@@ -66,8 +65,13 @@ const Header: FC<HeaderProps> = observer(() => {
 
         <Filler className={styles.filler} />
 
-        <nav className={styles.plugs}>
-          <Authorized>
+        <nav
+          className={classNames(styles.plugs, {
+            // [styles.active]: isHydrated && fetched,
+            [styles.active]: true,
+          })}
+        >
+          <Authorized hydratedOnly>
             <Anchor
               className={classNames(styles.item, {
                 [styles.has_dot]: hasFlowUpdates,
diff --git a/src/containers/main/Header/styles.module.scss b/src/containers/main/Header/styles.module.scss
index fd01a468..3d2b0401 100644
--- a/src/containers/main/Header/styles.module.scss
+++ b/src/containers/main/Header/styles.module.scss
@@ -2,10 +2,10 @@
 
 @keyframes appear {
   from {
-    transform: translate(0, -$header_height);
+    opacity: 0;
   }
   to {
-    transform: translate(0, 0);
+    opacity: 1;
   }
 }
 
@@ -55,6 +55,12 @@
   user-select: none;
   text-transform: uppercase;
   align-items: center;
+  opacity: 0;
+  transition: all 250ms;
+
+  &.active {
+    opacity: 1;
+  }
 
   @include tablet {
     flex: 1;
diff --git a/src/hooks/auth/useAuth.ts b/src/hooks/auth/useAuth.ts
index c15a2ab7..91649d00 100644
--- a/src/hooks/auth/useAuth.ts
+++ b/src/hooks/auth/useAuth.ts
@@ -16,5 +16,6 @@ export const useAuth = () => {
     setToken: auth.setToken,
     isTester: auth.isTester,
     setIsTester: auth.setIsTester,
+    fetched: auth.fetched,
   };
 };
diff --git a/src/hooks/auth/useUser.ts b/src/hooks/auth/useUser.ts
index 23fa3efc..e362ede7 100644
--- a/src/hooks/auth/useUser.ts
+++ b/src/hooks/auth/useUser.ts
@@ -10,11 +10,19 @@ import { IUser } from '~/types/auth';
 import { showErrorToast } from '~/utils/errors/showToast';
 
 export const useUser = () => {
-  const { token, setUser } = useAuthStore();
-  const { data, mutate } = useSWR(token ? API.USER.ME : null, () => apiAuthGetUser(), {
-    onSuccess: data => setUser(data?.user || EMPTY_USER),
-    onError: error => showErrorToast(error),
-  });
+  const { token, setUser, setFetched, user } = useAuthStore();
+  const { data, mutate } = useSWR(
+    token ? API.USER.ME : null,
+    () => apiAuthGetUser(),
+    {
+      onSuccess: (data) => {
+        setUser(data?.user || EMPTY_USER);
+        setFetched(true);
+      },
+      onError: (error) => showErrorToast(error),
+      fallbackData: { user },
+    },
+  );
 
   const update = useCallback(
     async (user: Partial<IUser>, revalidate?: boolean) => {
@@ -25,7 +33,7 @@ export const useUser = () => {
 
       await mutate({ ...data, user: { ...data.user, ...user } }, revalidate);
     },
-    [data, mutate]
+    [data, mutate],
   );
 
   return { user: data?.user || EMPTY_USER, update };
diff --git a/src/hooks/node/useNodePermissions.ts b/src/hooks/node/useNodePermissions.ts
index a0825141..b1b9c7be 100644
--- a/src/hooks/node/useNodePermissions.ts
+++ b/src/hooks/node/useNodePermissions.ts
@@ -4,12 +4,24 @@ import { useUser } from '~/hooks/auth/useUser';
 import { INode } from '~/types';
 import { canEditNode, canLikeNode, canStarNode } from '~/utils/node';
 
+import { useAuth } from '../auth/useAuth';
+
 export const useNodePermissions = (node?: INode) => {
   const { user } = useUser();
+  const { fetched, isUser } = useAuth();
 
-  const edit = useMemo(() => canEditNode(node, user), [node, user]);
-  const like = useMemo(() => canLikeNode(node, user), [node, user]);
-  const star = useMemo(() => canStarNode(node, user), [node, user]);
+  const edit = useMemo(
+    () => fetched && isUser && canEditNode(node, user),
+    [node, user, fetched, isUser],
+  );
+  const like = useMemo(
+    () => fetched && isUser && canLikeNode(node, user),
+    [node, user, fetched, isUser],
+  );
+  const star = useMemo(
+    () => fetched && isUser && canStarNode(node, user),
+    [node, user, fetched, isUser],
+  );
 
   return [edit, like, star];
 };
diff --git a/src/layouts/NodeLayout/index.tsx b/src/layouts/NodeLayout/index.tsx
index 0e600c8b..c4c7611f 100644
--- a/src/layouts/NodeLayout/index.tsx
+++ b/src/layouts/NodeLayout/index.tsx
@@ -1,5 +1,7 @@
 import React, { FC } from 'react';
 
+import { observer } from 'mobx-react-lite';
+
 import { Superpower } from '~/components/boris/Superpower';
 import { ScrollHelperBottom } from '~/components/common/ScrollHelperBottom';
 import { Card } from '~/components/containers/Card';
@@ -18,7 +20,7 @@ import styles from './styles.module.scss';
 
 type IProps = {};
 
-const NodeLayout: FC<IProps> = () => {
+const NodeLayout: FC<IProps> = observer(() => {
   const { node, isLoading, update } = useNodeContext();
   const { head, block } = useNodeBlocks(node, isLoading);
   const [canEdit, canLike, canStar] = useNodePermissions(node);
@@ -70,6 +72,6 @@ const NodeLayout: FC<IProps> = () => {
       </Superpower>
     </div>
   );
-};
+});
 
 export { NodeLayout };
diff --git a/src/store/auth/AuthStore.ts b/src/store/auth/AuthStore.ts
index 52cb63c3..e80806bc 100644
--- a/src/store/auth/AuthStore.ts
+++ b/src/store/auth/AuthStore.ts
@@ -9,6 +9,7 @@ export class AuthStore {
   token: string = '';
   user: IUser = EMPTY_USER;
   isTesterInternal: boolean = false;
+  fetched = false;
 
   constructor() {
     makeAutoObservable(this);
@@ -46,4 +47,8 @@ export class AuthStore {
     this.token = '';
     this.setUser(EMPTY_USER);
   };
+
+  setFetched = (fetched: boolean) => {
+    this.fetched = fetched;
+  };
 }
diff --git a/src/utils/providers/AuthProvider.tsx b/src/utils/providers/AuthProvider.tsx
index 34aa4200..89d569e3 100644
--- a/src/utils/providers/AuthProvider.tsx
+++ b/src/utils/providers/AuthProvider.tsx
@@ -1,6 +1,7 @@
 import { createContext, FC, useContext } from 'react';
 
 import { observer } from 'mobx-react-lite';
+import { boolean } from 'yup';
 
 import { EMPTY_USER } from '~/constants/auth';
 import { useAuth } from '~/hooks/auth/useAuth';
@@ -18,6 +19,7 @@ const AuthContext = createContext<AuthProviderContextType>({
   logout: () => {},
   login: async () => EMPTY_USER,
   setToken: () => {},
+  fetched: false,
 });
 
 export const AuthProvider: FC = observer(({ children }) => {