1
0
Fork 0
mirror of https://github.com/muerwre/vault-frontend.git synced 2025-04-24 20:36:40 +07:00

fixed user hydration

This commit is contained in:
Fedor Katurov 2022-11-22 10:46:41 +06:00
parent ed9694c246
commit 75dc20ca0b
13 changed files with 194 additions and 95 deletions

View file

@ -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 classNames from 'classnames'; import classNames from 'classnames';
import { CommentForm } from '~/components/comment/CommentForm'; import { CommentForm } from '~/components/comment/CommentForm';
import { Authorized } from '~/components/containers/Authorized';
import { Group } from '~/components/containers/Group'; import { Group } from '~/components/containers/Group';
import { AudioPlayer } from '~/components/media/AudioPlayer'; import { AudioPlayer } from '~/components/media/AudioPlayer';
import { COMMENT_BLOCK_RENDERERS } from '~/constants/comment'; import { COMMENT_BLOCK_RENDERERS } from '~/constants/comment';
@ -28,7 +38,15 @@ interface IProps {
} }
const CommentContent: FC<IProps> = memo( 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 [isEditing, setIsEditing] = useState(false);
const startEditing = useCallback(() => setIsEditing(true), [setIsEditing]); const startEditing = useCallback(() => setIsEditing(true), [setIsEditing]);
@ -38,11 +56,13 @@ const CommentContent: FC<IProps> = memo(
() => () =>
reduce( reduce(
(group, file) => (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[]>, {} as Record<UploadType, IFile[]>,
comment.files comment.files,
), ),
[comment] [comment],
); );
const onLockClick = useCallback(() => { const onLockClick = useCallback(() => {
@ -50,8 +70,9 @@ const CommentContent: FC<IProps> = memo(
}, [comment, onDelete]); }, [comment, onDelete]);
const menu = useMemo( const menu = useMemo(
() => canEdit && <CommentMenu onDelete={onLockClick} onEdit={startEditing} />, () =>
[canEdit, startEditing, onLockClick] canEdit && <CommentMenu onDelete={onLockClick} onEdit={startEditing} />,
[canEdit, startEditing, onLockClick],
); );
const blocks = useMemo( const blocks = useMemo(
@ -59,7 +80,7 @@ const CommentContent: FC<IProps> = memo(
!!comment.text.trim() !!comment.text.trim()
? formatCommentText(path(['user', 'username'], comment), comment.text) ? formatCommentText(path(['user', 'username'], comment), comment.text)
: [], : [],
[comment] [comment],
); );
if (isEditing) { if (isEditing) {
@ -78,17 +99,22 @@ const CommentContent: FC<IProps> = memo(
{!!prefix && <div className={styles.prefix}>{prefix}</div>} {!!prefix && <div className={styles.prefix}>{prefix}</div>}
{comment.text.trim() && ( {comment.text.trim() && (
<Group className={classnames(styles.block, styles.block_text)}> <Group className={classnames(styles.block, styles.block_text)}>
{menu} <Authorized>{menu}</Authorized>
<Group className={styles.renderers}> <Group className={styles.renderers}>
{blocks.map( {blocks.map(
(block, key) => (block, key) =>
COMMENT_BLOCK_RENDERERS[block.type] && COMMENT_BLOCK_RENDERERS[block.type] &&
createElement(COMMENT_BLOCK_RENDERERS[block.type], { block, key }) createElement(COMMENT_BLOCK_RENDERERS[block.type], {
block,
key,
}),
)} )}
</Group> </Group>
<div className={styles.date}>{getPrettyDate(comment.created_at)}</div> <div className={styles.date}>
{getPrettyDate(comment.created_at)}
</div>
</Group> </Group>
)} )}
@ -102,32 +128,45 @@ const CommentContent: FC<IProps> = memo(
})} })}
> >
{groupped.image.map((file, index) => ( {groupped.image.map((file, index) => (
<div key={file.id} onClick={() => onShowImageModal(groupped.image, index)}> <div
<img src={getURL(file, ImagePresets['600'])} alt={file.name} /> key={file.id}
onClick={() => onShowImageModal(groupped.image, index)}
>
<img
src={getURL(file, ImagePresets['600'])}
alt={file.name}
/>
</div> </div>
))} ))}
</div> </div>
<div className={styles.date}>{getPrettyDate(comment.created_at)}</div> <div className={styles.date}>
{getPrettyDate(comment.created_at)}
</div>
</div> </div>
)} )}
{groupped.audio && groupped.audio.length > 0 && ( {groupped.audio && groupped.audio.length > 0 && (
<Fragment> <Fragment>
{groupped.audio.map(file => ( {groupped.audio.map((file) => (
<div className={classnames(styles.block, styles.block_audio)} key={file.id}> <div
className={classnames(styles.block, styles.block_audio)}
key={file.id}
>
{menu} {menu}
<AudioPlayer file={file} /> <AudioPlayer file={file} />
<div className={styles.date}>{getPrettyDate(comment.created_at)}</div> <div className={styles.date}>
{getPrettyDate(comment.created_at)}
</div>
</div> </div>
))} ))}
</Fragment> </Fragment>
)} )}
</div> </div>
); );
} },
); );
export { CommentContent }; export { CommentContent };

View file

@ -1,15 +1,20 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { observer } from 'mobx-react-lite';
import { useAuth } from '~/hooks/auth/useAuth'; 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 Authorized: FC<IProps> = observer(({ children, hydratedOnly }) => {
const { isUser } = useAuth(); const { isUser, fetched } = useAuth();
if (!isUser) return null; if (!isUser || (!hydratedOnly && !fetched)) return null;
return <>{children}</>; return <>{children}</>;
}; });
export { Authorized }; export { Authorized };

View file

@ -1,9 +1,11 @@
import React, { FC, Fragment } from 'react'; import React, { FC, Fragment } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { observer } from 'mobx-react-lite';
import { FlowCell } from '~/components/flow/FlowCell'; import { FlowCell } from '~/components/flow/FlowCell';
import { flowDisplayToPreset, URLS } from '~/constants/urls'; import { flowDisplayToPreset, URLS } from '~/constants/urls';
import { useAuth } from '~/hooks/auth/useAuth';
import { FlowDisplay, IFlowNode, INode } from '~/types'; import { FlowDisplay, IFlowNode, INode } from '~/types';
import { IUser } from '~/types/auth'; import { IUser } from '~/types/auth';
import { getURLFromString } from '~/utils/dom'; import { getURLFromString } from '~/utils/dom';
@ -17,28 +19,38 @@ interface Props {
onChangeCellView: (id: INode['id'], flow: FlowDisplay) => void; onChangeCellView: (id: INode['id'], flow: FlowDisplay) => void;
} }
export const FlowGrid: FC<Props> = ({ user, nodes, onChangeCellView }) => { export const FlowGrid: FC<Props> = observer(
({ user, nodes, onChangeCellView }) => {
const { fetched, isUser } = useAuth();
if (!nodes) { if (!nodes) {
return null; return null;
} }
return ( return (
<Fragment> <Fragment>
{nodes.map(node => ( {nodes.map((node) => (
<div className={classNames(styles.cell, styles[node.flow.display])} key={node.id}> <div
className={classNames(styles.cell, styles[node.flow.display])}
key={node.id}
>
<FlowCell <FlowCell
id={node.id} id={node.id}
color={node.flow.dominant_color} color={node.flow.dominant_color}
to={URLS.NODE_URL(node.id)} to={URLS.NODE_URL(node.id)}
image={getURLFromString(node.thumbnail, flowDisplayToPreset[node.flow.display])} image={getURLFromString(
node.thumbnail,
flowDisplayToPreset[node.flow.display],
)}
flow={node.flow} flow={node.flow}
text={node.description} text={node.description}
title={node.title} title={node.title}
canEdit={canEditNode(node, user)} canEdit={fetched && isUser && canEditNode(node, user)}
onChangeCellView={onChangeCellView} onChangeCellView={onChangeCellView}
/> />
</div> </div>
))} ))}
</Fragment> </Fragment>
); );
}; },
);

View file

@ -4,7 +4,7 @@ import classNames from 'classnames';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
interface SeparatedMenuProps { export interface SeparatedMenuProps {
className?: string; className?: string;
} }
@ -14,7 +14,7 @@ const SeparatedMenu: FC<SeparatedMenuProps> = ({ children, className }) => {
return []; return [];
} }
return (Array.isArray(children) ? children : [children]).filter(it => it); return (Array.isArray(children) ? children : [children]).filter((it) => it);
}, [children]); }, [children]);
return ( return (

View file

@ -2,6 +2,7 @@ import React, { memo, VFC } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { Authorized } from '~/components/containers/Authorized';
import { Icon } from '~/components/input/Icon'; import { Icon } from '~/components/input/Icon';
import { SeparatedMenu } from '~/components/menu/SeparatedMenu'; import { SeparatedMenu } from '~/components/menu/SeparatedMenu';
import { NodeEditMenu } from '~/components/node/NodeEditMenu'; import { NodeEditMenu } from '~/components/node/NodeEditMenu';
@ -76,6 +77,7 @@ const NodeTitle: VFC<IProps> = memo(
)} )}
</div> </div>
<Authorized>
<SeparatedMenu className={styles.buttons}> <SeparatedMenu className={styles.buttons}>
{canEdit && ( {canEdit && (
<NodeEditMenu <NodeEditMenu
@ -107,6 +109,7 @@ const NodeTitle: VFC<IProps> = memo(
</div> </div>
)} )}
</SeparatedMenu> </SeparatedMenu>
</Authorized>
</div> </div>
</div> </div>
); );

View file

@ -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 classNames from 'classnames';
import isBefore from 'date-fns/isBefore'; import isBefore from 'date-fns/isBefore';
@ -16,7 +16,6 @@ import { URLS } from '~/constants/urls';
import { useAuth } from '~/hooks/auth/useAuth'; import { useAuth } from '~/hooks/auth/useAuth';
import { useScrollTop } from '~/hooks/dom/useScrollTop'; import { useScrollTop } from '~/hooks/dom/useScrollTop';
import { useFlow } from '~/hooks/flow/useFlow'; import { useFlow } from '~/hooks/flow/useFlow';
import { useGetLabStats } from '~/hooks/lab/useGetLabStats';
import { useModal } from '~/hooks/modal/useModal'; import { useModal } from '~/hooks/modal/useModal';
import { useUpdates } from '~/hooks/updates/useUpdates'; import { useUpdates } from '~/hooks/updates/useUpdates';
import { useSidebar } from '~/utils/providers/SidebarProvider'; import { useSidebar } from '~/utils/providers/SidebarProvider';
@ -66,8 +65,13 @@ const Header: FC<HeaderProps> = observer(() => {
<Filler className={styles.filler} /> <Filler className={styles.filler} />
<nav className={styles.plugs}> <nav
<Authorized> className={classNames(styles.plugs, {
// [styles.active]: isHydrated && fetched,
[styles.active]: true,
})}
>
<Authorized hydratedOnly>
<Anchor <Anchor
className={classNames(styles.item, { className={classNames(styles.item, {
[styles.has_dot]: hasFlowUpdates, [styles.has_dot]: hasFlowUpdates,

View file

@ -2,10 +2,10 @@
@keyframes appear { @keyframes appear {
from { from {
transform: translate(0, -$header_height); opacity: 0;
} }
to { to {
transform: translate(0, 0); opacity: 1;
} }
} }
@ -55,6 +55,12 @@
user-select: none; user-select: none;
text-transform: uppercase; text-transform: uppercase;
align-items: center; align-items: center;
opacity: 0;
transition: all 250ms;
&.active {
opacity: 1;
}
@include tablet { @include tablet {
flex: 1; flex: 1;

View file

@ -16,5 +16,6 @@ export const useAuth = () => {
setToken: auth.setToken, setToken: auth.setToken,
isTester: auth.isTester, isTester: auth.isTester,
setIsTester: auth.setIsTester, setIsTester: auth.setIsTester,
fetched: auth.fetched,
}; };
}; };

View file

@ -10,11 +10,19 @@ import { IUser } from '~/types/auth';
import { showErrorToast } from '~/utils/errors/showToast'; import { showErrorToast } from '~/utils/errors/showToast';
export const useUser = () => { export const useUser = () => {
const { token, setUser } = useAuthStore(); const { token, setUser, setFetched, user } = useAuthStore();
const { data, mutate } = useSWR(token ? API.USER.ME : null, () => apiAuthGetUser(), { const { data, mutate } = useSWR(
onSuccess: data => setUser(data?.user || EMPTY_USER), token ? API.USER.ME : null,
onError: error => showErrorToast(error), () => apiAuthGetUser(),
}); {
onSuccess: (data) => {
setUser(data?.user || EMPTY_USER);
setFetched(true);
},
onError: (error) => showErrorToast(error),
fallbackData: { user },
},
);
const update = useCallback( const update = useCallback(
async (user: Partial<IUser>, revalidate?: boolean) => { async (user: Partial<IUser>, revalidate?: boolean) => {
@ -25,7 +33,7 @@ export const useUser = () => {
await mutate({ ...data, user: { ...data.user, ...user } }, revalidate); await mutate({ ...data, user: { ...data.user, ...user } }, revalidate);
}, },
[data, mutate] [data, mutate],
); );
return { user: data?.user || EMPTY_USER, update }; return { user: data?.user || EMPTY_USER, update };

View file

@ -4,12 +4,24 @@ import { useUser } from '~/hooks/auth/useUser';
import { INode } from '~/types'; import { INode } from '~/types';
import { canEditNode, canLikeNode, canStarNode } from '~/utils/node'; import { canEditNode, canLikeNode, canStarNode } from '~/utils/node';
import { useAuth } from '../auth/useAuth';
export const useNodePermissions = (node?: INode) => { export const useNodePermissions = (node?: INode) => {
const { user } = useUser(); const { user } = useUser();
const { fetched, isUser } = useAuth();
const edit = useMemo(() => canEditNode(node, user), [node, user]); const edit = useMemo(
const like = useMemo(() => canLikeNode(node, user), [node, user]); () => fetched && isUser && canEditNode(node, user),
const star = useMemo(() => canStarNode(node, user), [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]; return [edit, like, star];
}; };

View file

@ -1,5 +1,7 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { observer } from 'mobx-react-lite';
import { Superpower } from '~/components/boris/Superpower'; import { Superpower } from '~/components/boris/Superpower';
import { ScrollHelperBottom } from '~/components/common/ScrollHelperBottom'; import { ScrollHelperBottom } from '~/components/common/ScrollHelperBottom';
import { Card } from '~/components/containers/Card'; import { Card } from '~/components/containers/Card';
@ -18,7 +20,7 @@ import styles from './styles.module.scss';
type IProps = {}; type IProps = {};
const NodeLayout: FC<IProps> = () => { const NodeLayout: FC<IProps> = observer(() => {
const { node, isLoading, update } = useNodeContext(); const { node, isLoading, update } = useNodeContext();
const { head, block } = useNodeBlocks(node, isLoading); const { head, block } = useNodeBlocks(node, isLoading);
const [canEdit, canLike, canStar] = useNodePermissions(node); const [canEdit, canLike, canStar] = useNodePermissions(node);
@ -70,6 +72,6 @@ const NodeLayout: FC<IProps> = () => {
</Superpower> </Superpower>
</div> </div>
); );
}; });
export { NodeLayout }; export { NodeLayout };

View file

@ -9,6 +9,7 @@ export class AuthStore {
token: string = ''; token: string = '';
user: IUser = EMPTY_USER; user: IUser = EMPTY_USER;
isTesterInternal: boolean = false; isTesterInternal: boolean = false;
fetched = false;
constructor() { constructor() {
makeAutoObservable(this); makeAutoObservable(this);
@ -46,4 +47,8 @@ export class AuthStore {
this.token = ''; this.token = '';
this.setUser(EMPTY_USER); this.setUser(EMPTY_USER);
}; };
setFetched = (fetched: boolean) => {
this.fetched = fetched;
};
} }

View file

@ -1,6 +1,7 @@
import { createContext, FC, useContext } from 'react'; import { createContext, FC, useContext } from 'react';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { boolean } from 'yup';
import { EMPTY_USER } from '~/constants/auth'; import { EMPTY_USER } from '~/constants/auth';
import { useAuth } from '~/hooks/auth/useAuth'; import { useAuth } from '~/hooks/auth/useAuth';
@ -18,6 +19,7 @@ const AuthContext = createContext<AuthProviderContextType>({
logout: () => {}, logout: () => {},
login: async () => EMPTY_USER, login: async () => EMPTY_USER,
setToken: () => {}, setToken: () => {},
fetched: false,
}); });
export const AuthProvider: FC = observer(({ children }) => { export const AuthProvider: FC = observer(({ children }) => {