mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-24 20:36:40 +07:00
removed almost all node sagas
This commit is contained in:
parent
f76a5a4798
commit
168ba8cc04
30 changed files with 268 additions and 448 deletions
|
@ -14,6 +14,7 @@ type IProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
group: ICommentGroup;
|
group: ICommentGroup;
|
||||||
isSame?: boolean;
|
isSame?: boolean;
|
||||||
canEdit?: boolean;
|
canEdit?: boolean;
|
||||||
|
saveComment: (data: IComment) => Promise<unknown>;
|
||||||
onDelete: (id: IComment['id'], isLocked: boolean) => void;
|
onDelete: (id: IComment['id'], isLocked: boolean) => void;
|
||||||
onShowImageModal: (images: IFile[], index: number) => void;
|
onShowImageModal: (images: IFile[], index: number) => void;
|
||||||
};
|
};
|
||||||
|
@ -29,6 +30,7 @@ const Comment: FC<IProps> = memo(
|
||||||
canEdit,
|
canEdit,
|
||||||
onDelete,
|
onDelete,
|
||||||
onShowImageModal,
|
onShowImageModal,
|
||||||
|
saveComment,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
|
@ -50,6 +52,7 @@ const Comment: FC<IProps> = memo(
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommentContent
|
<CommentContent
|
||||||
|
saveComment={saveComment}
|
||||||
nodeId={nodeId}
|
nodeId={nodeId}
|
||||||
comment={comment}
|
comment={comment}
|
||||||
key={comment.id}
|
key={comment.id}
|
||||||
|
|
|
@ -18,12 +18,13 @@ interface IProps {
|
||||||
nodeId: number;
|
nodeId: number;
|
||||||
comment: IComment;
|
comment: IComment;
|
||||||
canEdit: boolean;
|
canEdit: boolean;
|
||||||
|
saveComment: (data: IComment) => Promise<unknown>;
|
||||||
onDelete: (id: IComment['id'], isLocked: boolean) => void;
|
onDelete: (id: IComment['id'], isLocked: boolean) => void;
|
||||||
onShowImageModal: (images: IFile[], index: number) => void;
|
onShowImageModal: (images: IFile[], index: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CommentContent: FC<IProps> = memo(
|
const CommentContent: FC<IProps> = memo(
|
||||||
({ comment, canEdit, nodeId, onDelete, onShowImageModal }) => {
|
({ comment, canEdit, nodeId, saveComment, onDelete, onShowImageModal }) => {
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
|
||||||
const startEditing = useCallback(() => setIsEditing(true), [setIsEditing]);
|
const startEditing = useCallback(() => setIsEditing(true), [setIsEditing]);
|
||||||
|
@ -58,7 +59,14 @@ const CommentContent: FC<IProps> = memo(
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
return <CommentForm nodeId={nodeId} comment={comment} onCancelEdit={stopEditing} />;
|
return (
|
||||||
|
<CommentForm
|
||||||
|
saveComment={saveComment}
|
||||||
|
nodeId={nodeId}
|
||||||
|
comment={comment}
|
||||||
|
onCancelEdit={stopEditing}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -20,17 +20,24 @@ import { Filler } from '~/components/containers/Filler';
|
||||||
interface IProps {
|
interface IProps {
|
||||||
comment?: IComment;
|
comment?: IComment;
|
||||||
nodeId: INode['id'];
|
nodeId: INode['id'];
|
||||||
|
saveComment: (data: IComment) => Promise<unknown>;
|
||||||
onCancelEdit?: () => void;
|
onCancelEdit?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CommentForm: FC<IProps> = ({ comment, nodeId, onCancelEdit }) => {
|
const CommentForm: FC<IProps> = ({ comment, nodeId, saveComment, onCancelEdit }) => {
|
||||||
const [textarea, setTextarea] = useState<HTMLTextAreaElement>();
|
const [textarea, setTextarea] = useState<HTMLTextAreaElement>();
|
||||||
const uploader = useFileUploader(
|
const uploader = useFileUploader(
|
||||||
UPLOAD_SUBJECTS.COMMENT,
|
UPLOAD_SUBJECTS.COMMENT,
|
||||||
UPLOAD_TARGETS.COMMENTS,
|
UPLOAD_TARGETS.COMMENTS,
|
||||||
comment?.files
|
comment?.files
|
||||||
);
|
);
|
||||||
const formik = useCommentFormFormik(comment || EMPTY_COMMENT, nodeId, uploader, onCancelEdit);
|
const formik = useCommentFormFormik(
|
||||||
|
comment || EMPTY_COMMENT,
|
||||||
|
nodeId,
|
||||||
|
uploader,
|
||||||
|
saveComment,
|
||||||
|
onCancelEdit
|
||||||
|
);
|
||||||
const isLoading = formik.isSubmitting || uploader.isUploading;
|
const isLoading = formik.isSubmitting || uploader.isUploading;
|
||||||
const isEditing = !!comment?.id;
|
const isEditing = !!comment?.id;
|
||||||
|
|
||||||
|
|
|
@ -1,27 +1,21 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { CommentWrapper } from '~/components/containers/CommentWrapper';
|
import { CommentWrapper } from '~/components/containers/CommentWrapper';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { selectAuthUser } from '~/redux/auth/selectors';
|
|
||||||
import { CommentForm } from '~/components/comment/CommentForm';
|
import { CommentForm } from '~/components/comment/CommentForm';
|
||||||
import { INode } from '~/redux/types';
|
import { IComment } from '~/redux/types';
|
||||||
|
import { IUser } from '~/redux/auth/types';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
interface NodeCommentFormProps {
|
||||||
user: selectAuthUser(state),
|
user: IUser;
|
||||||
});
|
nodeId?: number;
|
||||||
|
saveComment: (comment: IComment) => Promise<unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
type IProps = ReturnType<typeof mapStateToProps> & {
|
const NodeCommentForm: FC<NodeCommentFormProps> = ({ user, nodeId, saveComment }) => {
|
||||||
isBefore?: boolean;
|
|
||||||
nodeId: INode['id'];
|
|
||||||
};
|
|
||||||
|
|
||||||
const NodeCommentFormUnconnected: FC<IProps> = ({ user, isBefore, nodeId }) => {
|
|
||||||
return (
|
return (
|
||||||
<CommentWrapper user={user} isForm>
|
<CommentWrapper user={user} isForm>
|
||||||
<CommentForm nodeId={nodeId} />
|
<CommentForm nodeId={nodeId} saveComment={saveComment} />
|
||||||
</CommentWrapper>
|
</CommentWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const NodeCommentForm = connect(mapStateToProps)(NodeCommentFormUnconnected);
|
|
||||||
|
|
||||||
export { NodeCommentForm };
|
export { NodeCommentForm };
|
||||||
|
|
|
@ -27,7 +27,7 @@ export const API = {
|
||||||
GET_DIFF: '/flow/diff',
|
GET_DIFF: '/flow/diff',
|
||||||
GET_NODE: (id: number | string) => `/node/${id}`,
|
GET_NODE: (id: number | string) => `/node/${id}`,
|
||||||
|
|
||||||
COMMENT: (id: INode['id']) => `/node/${id}/comment`,
|
COMMENT: (id: INode['id'] | string) => `/node/${id}/comment`,
|
||||||
RELATED: (id: INode['id']) => `/node/${id}/related`,
|
RELATED: (id: INode['id']) => `/node/${id}/related`,
|
||||||
UPDATE_TAGS: (id: INode['id']) => `/node/${id}/tags`,
|
UPDATE_TAGS: (id: INode['id']) => `/node/${id}/tags`,
|
||||||
DELETE_TAG: (id: INode['id'], tagId: ITag['ID']) => `/node/${id}/tags/${tagId}`,
|
DELETE_TAG: (id: INode['id'], tagId: ITag['ID']) => `/node/${id}/tags/${tagId}`,
|
||||||
|
|
|
@ -16,24 +16,28 @@ const BorisComments: FC<IProps> = () => {
|
||||||
const {
|
const {
|
||||||
isLoading,
|
isLoading,
|
||||||
comments,
|
comments,
|
||||||
|
onSaveComment,
|
||||||
onLoadMoreComments,
|
onLoadMoreComments,
|
||||||
onDeleteComment,
|
onDeleteComment,
|
||||||
onShowImageModal,
|
onShowImageModal,
|
||||||
count,
|
hasMore,
|
||||||
} = useCommentContext();
|
} = useCommentContext();
|
||||||
const { node } = useNodeContext();
|
const { node } = useNodeContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Group className={styles.grid}>
|
<Group className={styles.grid}>
|
||||||
{user.is_user && <NodeCommentForm isBefore nodeId={node.id} />}
|
{user.is_user && (
|
||||||
|
<NodeCommentForm user={user} nodeId={node.id} saveComment={onSaveComment} />
|
||||||
|
)}
|
||||||
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<NodeNoComments is_loading count={7} />
|
<NodeNoComments is_loading count={7} />
|
||||||
) : (
|
) : (
|
||||||
<CommentContextProvider
|
<CommentContextProvider
|
||||||
|
onSaveComment={onSaveComment}
|
||||||
comments={comments}
|
comments={comments}
|
||||||
count={count}
|
hasMore={hasMore}
|
||||||
onDeleteComment={onDeleteComment}
|
onDeleteComment={onDeleteComment}
|
||||||
onLoadMoreComments={onLoadMoreComments}
|
onLoadMoreComments={onLoadMoreComments}
|
||||||
onShowImageModal={onShowImageModal}
|
onShowImageModal={onShowImageModal}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { useHistory, useRouteMatch } from 'react-router';
|
||||||
import { ModalWrapper } from '~/components/dialogs/ModalWrapper';
|
import { ModalWrapper } from '~/components/dialogs/ModalWrapper';
|
||||||
import { LoaderCircle } from '~/components/input/LoaderCircle';
|
import { LoaderCircle } from '~/components/input/LoaderCircle';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { useGetNode } from '~/hooks/node/useGetNode';
|
import { useLoadNode } from '~/hooks/node/useLoadNode';
|
||||||
import { useUpdateNode } from '~/hooks/node/useUpdateNode';
|
import { useUpdateNode } from '~/hooks/node/useUpdateNode';
|
||||||
import { INode } from '~/redux/types';
|
import { INode } from '~/redux/types';
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ const EditorEditDialog: FC = () => {
|
||||||
history.replace(backUrl);
|
history.replace(backUrl);
|
||||||
}, [backUrl, history]);
|
}, [backUrl, history]);
|
||||||
|
|
||||||
const { node, isLoading } = useGetNode(parseInt(id, 10));
|
const { node, isLoading } = useLoadNode(parseInt(id, 10));
|
||||||
const updateNode = useUpdateNode(parseInt(id, 10));
|
const updateNode = useUpdateNode(parseInt(id, 10));
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(
|
||||||
|
|
|
@ -21,9 +21,9 @@ interface IProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const NodeBottomBlock: FC<IProps> = ({ commentsOrder }) => {
|
const NodeBottomBlock: FC<IProps> = ({ commentsOrder }) => {
|
||||||
const { is_user: isUser } = useUserContext();
|
const user = useUserContext();
|
||||||
const { node, isLoading } = useNodeContext();
|
const { node, isLoading } = useNodeContext();
|
||||||
const { comments, isLoading: isLoadingComments } = useCommentContext();
|
const { comments, isLoading: isLoadingComments, onSaveComment } = useCommentContext();
|
||||||
const { related, isLoading: isLoadingRelated } = useNodeRelatedContext();
|
const { related, isLoading: isLoadingRelated } = useNodeRelatedContext();
|
||||||
const { inline } = useNodeBlocks(node, isLoading);
|
const { inline } = useNodeBlocks(node, isLoading);
|
||||||
|
|
||||||
|
@ -44,7 +44,9 @@ const NodeBottomBlock: FC<IProps> = ({ commentsOrder }) => {
|
||||||
<NodeComments order={commentsOrder} />
|
<NodeComments order={commentsOrder} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isUser && !isLoading && <NodeCommentForm nodeId={node.id} />}
|
{user.is_user && !isLoading && (
|
||||||
|
<NodeCommentForm nodeId={node.id} saveComment={onSaveComment} user={user} />
|
||||||
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<div className={styles.panel}>
|
<div className={styles.panel}>
|
||||||
|
|
|
@ -21,27 +21,24 @@ const NodeComments: FC<IProps> = memo(({ order }) => {
|
||||||
|
|
||||||
const {
|
const {
|
||||||
comments,
|
comments,
|
||||||
count,
|
hasMore,
|
||||||
lastSeenCurrent,
|
lastSeenCurrent,
|
||||||
onLoadMoreComments,
|
onLoadMoreComments,
|
||||||
onDeleteComment,
|
onDeleteComment,
|
||||||
onShowImageModal,
|
onShowImageModal,
|
||||||
|
onSaveComment,
|
||||||
} = useCommentContext();
|
} = useCommentContext();
|
||||||
|
|
||||||
const left = useMemo(() => Math.max(0, count - comments.length), [comments, count]);
|
|
||||||
|
|
||||||
const groupped: ICommentGroup[] = useGrouppedComments(comments, order, lastSeenCurrent);
|
const groupped: ICommentGroup[] = useGrouppedComments(comments, order, lastSeenCurrent);
|
||||||
|
|
||||||
const more = useMemo(
|
const more = useMemo(
|
||||||
() =>
|
() =>
|
||||||
left > 0 && (
|
hasMore && (
|
||||||
<div className={styles.more} onClick={onLoadMoreComments}>
|
<div className={styles.more} onClick={onLoadMoreComments}>
|
||||||
Показать ещё{' '}
|
Показать ещё комментарии
|
||||||
{plural(Math.min(left, COMMENTS_DISPLAY), 'комментарий', 'комментария', 'комментариев')}
|
|
||||||
{left > COMMENTS_DISPLAY ? ` из ${left} оставшихся` : ''}
|
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
[left, onLoadMoreComments]
|
[hasMore, onLoadMoreComments]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!node?.id) {
|
if (!node?.id) {
|
||||||
|
@ -61,6 +58,7 @@ const NodeComments: FC<IProps> = memo(({ order }) => {
|
||||||
onDelete={onDeleteComment}
|
onDelete={onDeleteComment}
|
||||||
onShowImageModal={onShowImageModal}
|
onShowImageModal={onShowImageModal}
|
||||||
isSame={group.user.id === user.id}
|
isSame={group.user.id === user.id}
|
||||||
|
saveComment={onSaveComment}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,7 @@ const ProfileLayoutUnconnected: FC<IProps> = ({ history, nodeSetCoverImage }) =>
|
||||||
|
|
||||||
<Grid className={styles.content}>
|
<Grid className={styles.content}>
|
||||||
<div className={styles.comments}>
|
<div className={styles.comments}>
|
||||||
<CommentForm nodeId={0} />
|
<CommentForm nodeId={0} saveComment={async () => console.log()} />
|
||||||
<NodeNoComments is_loading={false} />
|
<NodeNoComments is_loading={false} />
|
||||||
</div>
|
</div>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
|
@ -3,21 +3,23 @@ import { useCallback, useEffect, useRef } from 'react';
|
||||||
import { FormikHelpers, useFormik, useFormikContext } from 'formik';
|
import { FormikHelpers, useFormik, useFormikContext } from 'formik';
|
||||||
import { array, object, string } from 'yup';
|
import { array, object, string } from 'yup';
|
||||||
import { FileUploader } from '~/hooks/data/useFileUploader';
|
import { FileUploader } from '~/hooks/data/useFileUploader';
|
||||||
import { useDispatch } from 'react-redux';
|
import { showErrorToast } from '~/utils/errors/showToast';
|
||||||
import { nodePostLocalComment } from '~/redux/node/actions';
|
import { hasPath, path } from 'ramda';
|
||||||
|
|
||||||
const validationSchema = object().shape({
|
const validationSchema = object().shape({
|
||||||
text: string(),
|
text: string(),
|
||||||
files: array(),
|
files: array(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSuccess = ({ resetForm, setStatus, setSubmitting }: FormikHelpers<IComment>) => (
|
const onSuccess = ({ resetForm, setSubmitting, setErrors }: FormikHelpers<IComment>) => (
|
||||||
e?: string
|
error?: unknown
|
||||||
) => {
|
) => {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
|
|
||||||
if (e) {
|
if (hasPath(['response', 'data', 'error'], error)) {
|
||||||
setStatus(e);
|
const message = path(['response', 'data', 'error'], error) as string;
|
||||||
|
setErrors({ text: message });
|
||||||
|
showErrorToast(error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,26 +32,23 @@ export const useCommentFormFormik = (
|
||||||
values: IComment,
|
values: IComment,
|
||||||
nodeId: INode['id'],
|
nodeId: INode['id'],
|
||||||
uploader: FileUploader,
|
uploader: FileUploader,
|
||||||
|
sendData: (data: IComment) => Promise<unknown>,
|
||||||
stopEditing?: () => void
|
stopEditing?: () => void
|
||||||
) => {
|
) => {
|
||||||
const dispatch = useDispatch();
|
|
||||||
const { current: initialValues } = useRef(values);
|
const { current: initialValues } = useRef(values);
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(
|
||||||
(values: IComment, helpers: FormikHelpers<IComment>) => {
|
async (values: IComment, helpers: FormikHelpers<IComment>) => {
|
||||||
helpers.setSubmitting(true);
|
try {
|
||||||
dispatch(
|
helpers.setSubmitting(true);
|
||||||
nodePostLocalComment(
|
await sendData(values);
|
||||||
nodeId,
|
onSuccess(helpers)();
|
||||||
{
|
} catch (error) {
|
||||||
...values,
|
console.log('error', error);
|
||||||
files: uploader.files,
|
onSuccess(helpers)(error);
|
||||||
},
|
}
|
||||||
onSuccess(helpers)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
[dispatch, nodeId, uploader.files]
|
[sendData]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onReset = useCallback(() => {
|
const onReset = useCallback(() => {
|
||||||
|
|
52
src/hooks/comments/useGetComments.ts
Normal file
52
src/hooks/comments/useGetComments.ts
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import { KeyLoader } from 'swr';
|
||||||
|
import { IComment } from '~/redux/types';
|
||||||
|
import { API } from '~/constants/api';
|
||||||
|
import { flatten, isNil } from 'ramda';
|
||||||
|
import useSWRInfinite from 'swr/infinite';
|
||||||
|
import { apiGetNodeComments } from '~/redux/node/api';
|
||||||
|
import { COMMENTS_DISPLAY } from '~/redux/node/constants';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
const getKey: (nodeId: number) => KeyLoader<IComment[]> = (nodeId: number) => (
|
||||||
|
pageIndex,
|
||||||
|
previousPageData
|
||||||
|
) => {
|
||||||
|
if (pageIndex > 0 && !previousPageData?.length) return null;
|
||||||
|
return `${API.NODE.COMMENT(nodeId)}?page=${pageIndex}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const extractKey = (key: string) => {
|
||||||
|
const re = new RegExp(`${API.NODE.COMMENT('\\d+')}\\?page=(\\d+)`);
|
||||||
|
const match = key.match(re);
|
||||||
|
|
||||||
|
if (!match || !Array.isArray(match) || isNil(match[1])) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseInt(match[1], 10) || 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetComments = (nodeId: number) => {
|
||||||
|
// TODO: const postedCommentsLength = Math.min(0, data[data.length - 1] - COMMENTS_DISPLAY);
|
||||||
|
|
||||||
|
const { data, isValidating, setSize, size, mutate } = useSWRInfinite(
|
||||||
|
getKey(nodeId),
|
||||||
|
async (key: string) => {
|
||||||
|
const result = await apiGetNodeComments({
|
||||||
|
id: nodeId,
|
||||||
|
take: COMMENTS_DISPLAY,
|
||||||
|
skip: extractKey(key) * COMMENTS_DISPLAY, // TODO: - postedCommentsLength,
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.comments;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const comments = flatten(data || []);
|
||||||
|
const hasMore =
|
||||||
|
!!data?.[data?.length - 1].length && data[data.length - 1].length === COMMENTS_DISPLAY;
|
||||||
|
|
||||||
|
const onLoadMoreComments = useCallback(() => setSize(size + 1), [setSize, size]);
|
||||||
|
|
||||||
|
return { comments, hasMore, onLoadMoreComments, isLoading: !data && isValidating, mutate, data };
|
||||||
|
};
|
62
src/hooks/comments/useNodeComments.ts
Normal file
62
src/hooks/comments/useNodeComments.ts
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { IComment } from '~/redux/types';
|
||||||
|
import { useGetComments } from '~/hooks/comments/useGetComments';
|
||||||
|
import { apiLockComment, apiPostComment } from '~/redux/node/api';
|
||||||
|
import { showErrorToast } from '~/utils/errors/showToast';
|
||||||
|
|
||||||
|
export const useNodeComments = (nodeId: number) => {
|
||||||
|
const { comments, isLoading, onLoadMoreComments, hasMore, data, mutate } = useGetComments(nodeId);
|
||||||
|
|
||||||
|
const onDelete = useCallback(
|
||||||
|
async (id: IComment['id'], isLocked: boolean) => {
|
||||||
|
try {
|
||||||
|
const { deleted_at } = await apiLockComment({ id, nodeId, isLocked });
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await mutate(
|
||||||
|
prev =>
|
||||||
|
prev?.map(list =>
|
||||||
|
list.map(comment => (comment.id === id ? { ...comment, deleted_at } : comment))
|
||||||
|
),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
showErrorToast(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[data, mutate, nodeId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onEdit = useCallback(
|
||||||
|
async (comment: IComment) => {
|
||||||
|
const result = await apiPostComment({ id: nodeId, data: comment });
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comment was created
|
||||||
|
if (!comment.id) {
|
||||||
|
await mutate(
|
||||||
|
data.map((list, index) => (index === 0 ? [result.comment, ...list] : list)),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await mutate(
|
||||||
|
prev =>
|
||||||
|
prev?.map(list =>
|
||||||
|
list.map(it => (it.id === result.comment.id ? { ...it, ...result.comment } : it))
|
||||||
|
),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[data, mutate, nodeId]
|
||||||
|
);
|
||||||
|
|
||||||
|
return { onLoadMoreComments, onDelete, comments, hasMore, isLoading, onEdit };
|
||||||
|
};
|
|
@ -1,20 +0,0 @@
|
||||||
import { useShallowSelect } from '~/hooks/data/useShallowSelect';
|
|
||||||
import { selectNode } from '~/redux/node/selectors';
|
|
||||||
import { useLoadNode } from '~/hooks/node/useLoadNode';
|
|
||||||
import { useOnNodeSeen } from '~/hooks/node/useOnNodeSeen';
|
|
||||||
|
|
||||||
export const useFullNode = (id: string) => {
|
|
||||||
const {
|
|
||||||
is_loading: isLoading,
|
|
||||||
current: node,
|
|
||||||
comments,
|
|
||||||
comment_count: commentsCount,
|
|
||||||
is_loading_comments: isLoadingComments,
|
|
||||||
lastSeenCurrent,
|
|
||||||
} = useShallowSelect(selectNode);
|
|
||||||
|
|
||||||
useLoadNode(id);
|
|
||||||
// useOnNodeSeen(node);
|
|
||||||
|
|
||||||
return { node, comments, commentsCount, lastSeenCurrent, isLoading, isLoadingComments };
|
|
||||||
};
|
|
|
@ -1,30 +0,0 @@
|
||||||
import useSWR from 'swr';
|
|
||||||
import { ApiGetNodeResponse } from '~/redux/node/types';
|
|
||||||
import { API } from '~/constants/api';
|
|
||||||
import { useOnNodeSeen } from '~/hooks/node/useOnNodeSeen';
|
|
||||||
import { apiGetNode } from '~/redux/node/api';
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
import { INode } from '~/redux/types';
|
|
||||||
import { EMPTY_NODE } from '~/redux/node/constants';
|
|
||||||
|
|
||||||
export const useGetNode = (id: number) => {
|
|
||||||
const { data, isValidating, mutate } = useSWR<ApiGetNodeResponse>(API.NODE.GET_NODE(id), () =>
|
|
||||||
apiGetNode({ id })
|
|
||||||
);
|
|
||||||
|
|
||||||
const update = useCallback(
|
|
||||||
async (node?: Partial<INode>) => {
|
|
||||||
if (!data?.node) {
|
|
||||||
await mutate();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await mutate({ node: { ...data.node, ...node } }, true);
|
|
||||||
},
|
|
||||||
[data, mutate]
|
|
||||||
);
|
|
||||||
|
|
||||||
useOnNodeSeen(data?.node);
|
|
||||||
|
|
||||||
return { node: data?.node || EMPTY_NODE, isLoading: isValidating && !data, update };
|
|
||||||
};
|
|
|
@ -1,12 +1,35 @@
|
||||||
import { useEffect } from 'react';
|
import useSWR from 'swr';
|
||||||
import { nodeGotoNode } from '~/redux/node/actions';
|
import { ApiGetNodeResponse } from '~/redux/node/types';
|
||||||
import { useDispatch } from 'react-redux';
|
import { API } from '~/constants/api';
|
||||||
|
import { useOnNodeSeen } from '~/hooks/node/useOnNodeSeen';
|
||||||
|
import { apiGetNode } from '~/redux/node/api';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { INode } from '~/redux/types';
|
||||||
|
import { EMPTY_NODE } from '~/redux/node/constants';
|
||||||
|
|
||||||
// useLoadNode loads node on id change
|
export const useLoadNode = (id: number) => {
|
||||||
export const useLoadNode = (id: any) => {
|
const { data, isValidating, mutate } = useSWR<ApiGetNodeResponse>(API.NODE.GET_NODE(id), () =>
|
||||||
const dispatch = useDispatch();
|
apiGetNode({ id })
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
const update = useCallback(
|
||||||
dispatch(nodeGotoNode(parseInt(id, 10), undefined));
|
async (node?: Partial<INode>) => {
|
||||||
}, [dispatch, id]);
|
if (!data?.node) {
|
||||||
|
await mutate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await mutate({ node: { ...data.node, ...node } }, true);
|
||||||
|
},
|
||||||
|
[data, mutate]
|
||||||
|
);
|
||||||
|
|
||||||
|
useOnNodeSeen(data?.node);
|
||||||
|
|
||||||
|
return {
|
||||||
|
node: data?.node || EMPTY_NODE,
|
||||||
|
isLoading: isValidating && !data,
|
||||||
|
update,
|
||||||
|
lastSeen: data?.last_seen,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
import { useCallback } from 'react';
|
|
||||||
import { nodeLoadMoreComments, nodeLockComment } from '~/redux/node/actions';
|
|
||||||
import { IComment } from '~/redux/types';
|
|
||||||
import { useDispatch } from 'react-redux';
|
|
||||||
|
|
||||||
export const useNodeComments = (nodeId: number) => {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
const onLoadMoreComments = useCallback(() => dispatch(nodeLoadMoreComments()), [dispatch]);
|
|
||||||
|
|
||||||
const onDelete = useCallback(
|
|
||||||
(id: IComment['id'], locked: boolean) => dispatch(nodeLockComment(id, locked, nodeId)),
|
|
||||||
[dispatch, nodeId]
|
|
||||||
);
|
|
||||||
|
|
||||||
return { onLoadMoreComments, onDelete };
|
|
||||||
};
|
|
|
@ -2,11 +2,11 @@ import { useHistory } from 'react-router';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { ITag } from '~/redux/types';
|
import { ITag } from '~/redux/types';
|
||||||
import { URLS } from '~/constants/urls';
|
import { URLS } from '~/constants/urls';
|
||||||
import { useGetNode } from '~/hooks/node/useGetNode';
|
import { useLoadNode } from '~/hooks/node/useLoadNode';
|
||||||
import { apiDeleteNodeTag, apiPostNodeTags } from '~/redux/node/api';
|
import { apiDeleteNodeTag, apiPostNodeTags } from '~/redux/node/api';
|
||||||
|
|
||||||
export const useNodeTags = (id: number) => {
|
export const useNodeTags = (id: number) => {
|
||||||
const { update } = useGetNode(id);
|
const { update } = useLoadNode(id);
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useGetNode } from '~/hooks/node/useGetNode';
|
import { useLoadNode } from '~/hooks/node/useLoadNode';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { INode } from '~/redux/types';
|
import { INode } from '~/redux/types';
|
||||||
import { apiPostNode } from '~/redux/node/api';
|
import { apiPostNode } from '~/redux/node/api';
|
||||||
|
@ -11,7 +11,7 @@ import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
export const useUpdateNode = (id: number) => {
|
export const useUpdateNode = (id: number) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { update } = useGetNode(id);
|
const { update } = useLoadNode(id);
|
||||||
const flowNodes = useShallowSelect(selectFlowNodes);
|
const flowNodes = useShallowSelect(selectFlowNodes);
|
||||||
const labNodes = useShallowSelect(selectLabListNodes);
|
const labNodes = useShallowSelect(selectLabListNodes);
|
||||||
|
|
||||||
|
|
|
@ -1,37 +1,32 @@
|
||||||
import React, { useEffect, VFC } from 'react';
|
import React, { VFC } from 'react';
|
||||||
import { useDispatch } from 'react-redux';
|
|
||||||
import { useShallowSelect } from '~/hooks/data/useShallowSelect';
|
|
||||||
import { selectNode } from '~/redux/node/selectors';
|
|
||||||
import { BorisLayout } from '~/layouts/BorisLayout';
|
import { BorisLayout } from '~/layouts/BorisLayout';
|
||||||
import { nodeLoadNode } from '~/redux/node/actions';
|
|
||||||
import { CommentContextProvider } from '~/utils/context/CommentContextProvider';
|
import { CommentContextProvider } from '~/utils/context/CommentContextProvider';
|
||||||
import { useImageModal } from '~/hooks/navigation/useImageModal';
|
import { useImageModal } from '~/hooks/navigation/useImageModal';
|
||||||
import { useNodeComments } from '~/hooks/node/useNodeComments';
|
import { useNodeComments } from '~/hooks/comments/useNodeComments';
|
||||||
import { useBoris } from '~/hooks/boris/useBoris';
|
import { useBoris } from '~/hooks/boris/useBoris';
|
||||||
import { NodeContextProvider } from '~/utils/context/NodeContextProvider';
|
import { NodeContextProvider } from '~/utils/context/NodeContextProvider';
|
||||||
import { useGetNode } from '~/hooks/node/useGetNode';
|
import { useLoadNode } from '~/hooks/node/useLoadNode';
|
||||||
|
|
||||||
const BorisPage: VFC = () => {
|
const BorisPage: VFC = () => {
|
||||||
const dispatch = useDispatch();
|
const { node, isLoading, update } = useLoadNode(696);
|
||||||
const { node, isLoading, update } = useGetNode(696);
|
|
||||||
const {
|
|
||||||
comments,
|
|
||||||
comment_count: count,
|
|
||||||
is_loading_comments: isLoadingComments,
|
|
||||||
} = useShallowSelect(selectNode);
|
|
||||||
|
|
||||||
const onShowImageModal = useImageModal();
|
const onShowImageModal = useImageModal();
|
||||||
const { onLoadMoreComments, onDelete: onDeleteComment } = useNodeComments(696);
|
const {
|
||||||
|
onLoadMoreComments,
|
||||||
|
onDelete: onDeleteComment,
|
||||||
|
onEdit: onSaveComment,
|
||||||
|
comments,
|
||||||
|
hasMore,
|
||||||
|
isLoading: isLoadingComments,
|
||||||
|
} = useNodeComments(696);
|
||||||
const { title, setIsBetaTester, isTester, stats } = useBoris(comments);
|
const { title, setIsBetaTester, isTester, stats } = useBoris(comments);
|
||||||
useEffect(() => {
|
|
||||||
dispatch(nodeLoadNode(696, 'DESC'));
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NodeContextProvider node={node} isLoading={isLoading} update={update}>
|
<NodeContextProvider node={node} isLoading={isLoading} update={update}>
|
||||||
<CommentContextProvider
|
<CommentContextProvider
|
||||||
|
onSaveComment={onSaveComment}
|
||||||
comments={comments}
|
comments={comments}
|
||||||
count={count}
|
hasMore={hasMore}
|
||||||
isLoading={isLoadingComments}
|
isLoading={isLoadingComments}
|
||||||
onShowImageModal={onShowImageModal}
|
onShowImageModal={onShowImageModal}
|
||||||
onLoadMoreComments={onLoadMoreComments}
|
onLoadMoreComments={onLoadMoreComments}
|
||||||
|
|
|
@ -2,9 +2,8 @@ import React, { FC } from 'react';
|
||||||
import { NodeLayout } from '~/layouts/NodeLayout';
|
import { NodeLayout } from '~/layouts/NodeLayout';
|
||||||
import { RouteComponentProps } from 'react-router';
|
import { RouteComponentProps } from 'react-router';
|
||||||
import { useScrollToTop } from '~/hooks/dom/useScrollToTop';
|
import { useScrollToTop } from '~/hooks/dom/useScrollToTop';
|
||||||
import { useFullNode } from '~/hooks/node/useFullNode';
|
|
||||||
import { useImageModal } from '~/hooks/navigation/useImageModal';
|
import { useImageModal } from '~/hooks/navigation/useImageModal';
|
||||||
import { useNodeComments } from '~/hooks/node/useNodeComments';
|
import { useNodeComments } from '~/hooks/comments/useNodeComments';
|
||||||
import { useUser } from '~/hooks/user/userUser';
|
import { useUser } from '~/hooks/user/userUser';
|
||||||
import { useNodeTags } from '~/hooks/node/useNodeTags';
|
import { useNodeTags } from '~/hooks/node/useNodeTags';
|
||||||
import { NodeContextProvider } from '~/utils/context/NodeContextProvider';
|
import { NodeContextProvider } from '~/utils/context/NodeContextProvider';
|
||||||
|
@ -12,7 +11,7 @@ import { CommentContextProvider } from '~/utils/context/CommentContextProvider';
|
||||||
import { TagsContextProvider } from '~/utils/context/TagsContextProvider';
|
import { TagsContextProvider } from '~/utils/context/TagsContextProvider';
|
||||||
import { useNodePermissions } from '~/hooks/node/useNodePermissions';
|
import { useNodePermissions } from '~/hooks/node/useNodePermissions';
|
||||||
import { NodeRelatedProvider } from '~/utils/providers/NodeRelatedProvider';
|
import { NodeRelatedProvider } from '~/utils/providers/NodeRelatedProvider';
|
||||||
import { useGetNode } from '~/hooks/node/useGetNode';
|
import { useLoadNode } from '~/hooks/node/useLoadNode';
|
||||||
|
|
||||||
type Props = RouteComponentProps<{ id: string }> & {};
|
type Props = RouteComponentProps<{ id: string }> & {};
|
||||||
|
|
||||||
|
@ -21,11 +20,17 @@ const NodePage: FC<Props> = ({
|
||||||
params: { id },
|
params: { id },
|
||||||
},
|
},
|
||||||
}) => {
|
}) => {
|
||||||
const { node, isLoading, update } = useGetNode(parseInt(id, 10));
|
const { node, isLoading, update, lastSeen } = useLoadNode(parseInt(id, 10));
|
||||||
const { isLoadingComments, comments, commentsCount, lastSeenCurrent } = useFullNode(id);
|
|
||||||
|
|
||||||
const onShowImageModal = useImageModal();
|
const onShowImageModal = useImageModal();
|
||||||
const { onLoadMoreComments, onDelete: onDeleteComment } = useNodeComments(parseInt(id, 10));
|
const {
|
||||||
|
onLoadMoreComments,
|
||||||
|
onDelete: onDeleteComment,
|
||||||
|
onEdit: onSaveComment,
|
||||||
|
comments,
|
||||||
|
hasMore,
|
||||||
|
isLoading: isLoadingComments,
|
||||||
|
} = useNodeComments(parseInt(id, 10));
|
||||||
const { onDelete: onTagDelete, onChange: onTagsChange, onClick: onTagClick } = useNodeTags(
|
const { onDelete: onTagDelete, onChange: onTagsChange, onClick: onTagClick } = useNodeTags(
|
||||||
parseInt(id, 10)
|
parseInt(id, 10)
|
||||||
);
|
);
|
||||||
|
@ -43,9 +48,10 @@ const NodePage: FC<Props> = ({
|
||||||
<NodeContextProvider node={node} isLoading={isLoading} update={update}>
|
<NodeContextProvider node={node} isLoading={isLoading} update={update}>
|
||||||
<NodeRelatedProvider id={parseInt(id, 10)} tags={node.tags}>
|
<NodeRelatedProvider id={parseInt(id, 10)} tags={node.tags}>
|
||||||
<CommentContextProvider
|
<CommentContextProvider
|
||||||
|
onSaveComment={onSaveComment}
|
||||||
comments={comments}
|
comments={comments}
|
||||||
count={commentsCount}
|
hasMore={hasMore}
|
||||||
lastSeenCurrent={lastSeenCurrent}
|
lastSeenCurrent={lastSeen}
|
||||||
isLoading={isLoadingComments}
|
isLoading={isLoadingComments}
|
||||||
onShowImageModal={onShowImageModal}
|
onShowImageModal={onShowImageModal}
|
||||||
onLoadMoreComments={onLoadMoreComments}
|
onLoadMoreComments={onLoadMoreComments}
|
||||||
|
|
|
@ -1,67 +1,7 @@
|
||||||
import { IComment, IFile, INode, ITag, IValidationErrors } from '../types';
|
import { IFile } from '../types';
|
||||||
import { NODE_ACTIONS } from './constants';
|
import { NODE_ACTIONS } from './constants';
|
||||||
import { INodeState } from './reducer';
|
|
||||||
|
|
||||||
export const nodeSet = (node: Partial<INodeState>) => ({
|
|
||||||
node,
|
|
||||||
type: NODE_ACTIONS.SET,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const nodeGotoNode = (id: INode['id'], node_type: INode['type']) => ({
|
|
||||||
id,
|
|
||||||
node_type,
|
|
||||||
type: NODE_ACTIONS.GOTO_NODE,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const nodeLoadNode = (id: number, order?: 'ASC' | 'DESC') => ({
|
|
||||||
id,
|
|
||||||
order,
|
|
||||||
type: NODE_ACTIONS.LOAD_NODE,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const nodeSetLoading = (is_loading: INodeState['is_loading']) => ({
|
|
||||||
is_loading,
|
|
||||||
type: NODE_ACTIONS.SET_LOADING,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const nodeSetLoadingComments = (is_loading_comments: INodeState['is_loading_comments']) => ({
|
|
||||||
is_loading_comments,
|
|
||||||
type: NODE_ACTIONS.SET_LOADING_COMMENTS,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const nodePostLocalComment = (
|
|
||||||
nodeId: INode['id'],
|
|
||||||
comment: IComment,
|
|
||||||
callback: (e?: string) => void
|
|
||||||
) => ({
|
|
||||||
nodeId,
|
|
||||||
comment,
|
|
||||||
callback,
|
|
||||||
type: NODE_ACTIONS.POST_LOCAL_COMMENT,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const nodeSetSendingComment = (is_sending_comment: boolean) => ({
|
|
||||||
is_sending_comment,
|
|
||||||
type: NODE_ACTIONS.SET_SENDING_COMMENT,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const nodeSetComments = (comments: IComment[]) => ({
|
|
||||||
comments,
|
|
||||||
type: NODE_ACTIONS.SET_COMMENTS,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const nodeLockComment = (id: number, is_locked: boolean, nodeId: number) => ({
|
|
||||||
type: NODE_ACTIONS.LOCK_COMMENT,
|
|
||||||
nodeId,
|
|
||||||
id,
|
|
||||||
is_locked,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const nodeSetCoverImage = (current_cover_image?: IFile) => ({
|
export const nodeSetCoverImage = (current_cover_image?: IFile) => ({
|
||||||
type: NODE_ACTIONS.SET_COVER_IMAGE,
|
type: NODE_ACTIONS.SET_COVER_IMAGE,
|
||||||
current_cover_image,
|
current_cover_image,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const nodeLoadMoreComments = () => ({
|
|
||||||
type: NODE_ACTIONS.LOAD_MORE_COMMENTS,
|
|
||||||
});
|
|
||||||
|
|
|
@ -117,7 +117,7 @@ export const apiLockNode = ({ id, is_locked }: ApiLockNodeRequest) =>
|
||||||
.post<ApiLockNodeResult>(API.NODE.POST_LOCK(id), { is_locked })
|
.post<ApiLockNodeResult>(API.NODE.POST_LOCK(id), { is_locked })
|
||||||
.then(cleanResult);
|
.then(cleanResult);
|
||||||
|
|
||||||
export const apiLockComment = ({ id, is_locked, current }: ApiLockCommentRequest) =>
|
export const apiLockComment = ({ id, isLocked, nodeId }: ApiLockCommentRequest) =>
|
||||||
api
|
api
|
||||||
.post<ApiLockcommentResult>(API.NODE.LOCK_COMMENT(current, id), { is_locked })
|
.post<ApiLockcommentResult>(API.NODE.LOCK_COMMENT(nodeId, id), { is_locked: isLocked })
|
||||||
.then(cleanResult);
|
.then(cleanResult);
|
||||||
|
|
|
@ -25,21 +25,6 @@ import { LabAudio } from '~/components/lab/LabAudioBlock';
|
||||||
|
|
||||||
const prefix = 'NODE.';
|
const prefix = 'NODE.';
|
||||||
export const NODE_ACTIONS = {
|
export const NODE_ACTIONS = {
|
||||||
LOAD_NODE: `${prefix}LOAD_NODE`,
|
|
||||||
GOTO_NODE: `${prefix}GOTO_NODE`,
|
|
||||||
SET: `${prefix}SET`,
|
|
||||||
|
|
||||||
LOCK_COMMENT: `${prefix}LOCK_COMMENT`,
|
|
||||||
EDIT_COMMENT: `${prefix}EDIT_COMMENT`,
|
|
||||||
LOAD_MORE_COMMENTS: `${prefix}LOAD_MORE_COMMENTS`,
|
|
||||||
|
|
||||||
SET_LOADING: `${prefix}SET_LOADING`,
|
|
||||||
SET_LOADING_COMMENTS: `${prefix}SET_LOADING_COMMENTS`,
|
|
||||||
SET_SENDING_COMMENT: `${prefix}SET_SENDING_COMMENT`,
|
|
||||||
|
|
||||||
POST_LOCAL_COMMENT: `${prefix}POST_LOCAL_COMMENT`,
|
|
||||||
SET_COMMENTS: `${prefix}SET_COMMENTS`,
|
|
||||||
|
|
||||||
SET_COVER_IMAGE: `${prefix}SET_COVER_IMAGE`,
|
SET_COVER_IMAGE: `${prefix}SET_COVER_IMAGE`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,46 +1,13 @@
|
||||||
import { assocPath } from 'ramda';
|
import { assocPath } from 'ramda';
|
||||||
import { NODE_ACTIONS } from './constants';
|
import { NODE_ACTIONS } from './constants';
|
||||||
import {
|
import { nodeSetCoverImage } from './actions';
|
||||||
nodeSet,
|
|
||||||
nodeSetComments,
|
|
||||||
nodeSetCoverImage,
|
|
||||||
nodeSetLoading,
|
|
||||||
nodeSetLoadingComments,
|
|
||||||
nodeSetSendingComment,
|
|
||||||
} from './actions';
|
|
||||||
import { INodeState } from './reducer';
|
import { INodeState } from './reducer';
|
||||||
|
|
||||||
const setData = (state: INodeState, { node }: ReturnType<typeof nodeSet>) => ({
|
|
||||||
...state,
|
|
||||||
...node,
|
|
||||||
});
|
|
||||||
|
|
||||||
const setLoading = (state: INodeState, { is_loading }: ReturnType<typeof nodeSetLoading>) =>
|
|
||||||
assocPath(['is_loading'], is_loading, state);
|
|
||||||
|
|
||||||
const setLoadingComments = (
|
|
||||||
state: INodeState,
|
|
||||||
{ is_loading_comments }: ReturnType<typeof nodeSetLoadingComments>
|
|
||||||
) => assocPath(['is_loading_comments'], is_loading_comments, state);
|
|
||||||
|
|
||||||
const setSendingComment = (
|
|
||||||
state: INodeState,
|
|
||||||
{ is_sending_comment }: ReturnType<typeof nodeSetSendingComment>
|
|
||||||
) => assocPath(['is_sending_comment'], is_sending_comment, state);
|
|
||||||
|
|
||||||
const setComments = (state: INodeState, { comments }: ReturnType<typeof nodeSetComments>) =>
|
|
||||||
assocPath(['comments'], comments, state);
|
|
||||||
|
|
||||||
const setCoverImage = (
|
const setCoverImage = (
|
||||||
state: INodeState,
|
state: INodeState,
|
||||||
{ current_cover_image }: ReturnType<typeof nodeSetCoverImage>
|
{ current_cover_image }: ReturnType<typeof nodeSetCoverImage>
|
||||||
) => assocPath(['current_cover_image'], current_cover_image, state);
|
) => assocPath(['current_cover_image'], current_cover_image, state);
|
||||||
|
|
||||||
export const NODE_HANDLERS = {
|
export const NODE_HANDLERS = {
|
||||||
[NODE_ACTIONS.SET]: setData,
|
|
||||||
[NODE_ACTIONS.SET_LOADING]: setLoading,
|
|
||||||
[NODE_ACTIONS.SET_LOADING_COMMENTS]: setLoadingComments,
|
|
||||||
[NODE_ACTIONS.SET_SENDING_COMMENT]: setSendingComment,
|
|
||||||
[NODE_ACTIONS.SET_COMMENTS]: setComments,
|
|
||||||
[NODE_ACTIONS.SET_COVER_IMAGE]: setCoverImage,
|
[NODE_ACTIONS.SET_COVER_IMAGE]: setCoverImage,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,163 +0,0 @@
|
||||||
import { call, put, select, takeLatest, takeLeading } from 'redux-saga/effects';
|
|
||||||
|
|
||||||
import { COMMENTS_DISPLAY, EMPTY_NODE, NODE_ACTIONS } from './constants';
|
|
||||||
import {
|
|
||||||
nodeGotoNode,
|
|
||||||
nodeLoadNode,
|
|
||||||
nodeLockComment,
|
|
||||||
nodePostLocalComment,
|
|
||||||
nodeSet,
|
|
||||||
nodeSetComments,
|
|
||||||
nodeSetLoadingComments,
|
|
||||||
} from './actions';
|
|
||||||
import { apiGetNodeComments, apiLockComment, apiPostComment } from './api';
|
|
||||||
import { flowSetNodes } from '../flow/actions';
|
|
||||||
import { selectFlowNodes } from '../flow/selectors';
|
|
||||||
import { selectNode } from './selectors';
|
|
||||||
import { INode, Unwrap } from '../types';
|
|
||||||
import { showErrorToast } from '~/utils/errors/showToast';
|
|
||||||
|
|
||||||
export function* updateNodeEverywhere(node) {
|
|
||||||
const {
|
|
||||||
current: { id },
|
|
||||||
}: ReturnType<typeof selectNode> = yield select(selectNode);
|
|
||||||
|
|
||||||
const flow_nodes: ReturnType<typeof selectFlowNodes> = yield select(selectFlowNodes);
|
|
||||||
|
|
||||||
yield put(
|
|
||||||
flowSetNodes(
|
|
||||||
flow_nodes
|
|
||||||
.map(flow_node => (flow_node.id === node.id ? node : flow_node))
|
|
||||||
.filter(flow_node => !flow_node.deleted_at)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function* onNodeGoto({ id }: ReturnType<typeof nodeGotoNode>) {
|
|
||||||
if (!id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
yield put(nodeLoadNode(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
function* onNodeLoadMoreComments() {
|
|
||||||
try {
|
|
||||||
const {
|
|
||||||
current: { id },
|
|
||||||
comments,
|
|
||||||
}: ReturnType<typeof selectNode> = yield select(selectNode);
|
|
||||||
|
|
||||||
if (!id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data: Unwrap<typeof apiGetNodeComments> = yield call(apiGetNodeComments, {
|
|
||||||
id,
|
|
||||||
take: COMMENTS_DISPLAY,
|
|
||||||
skip: comments.length,
|
|
||||||
});
|
|
||||||
|
|
||||||
const current: ReturnType<typeof selectNode> = yield select(selectNode);
|
|
||||||
|
|
||||||
if (!data || current.current.id != id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
yield put(
|
|
||||||
nodeSet({
|
|
||||||
comments: [...comments, ...data.comments],
|
|
||||||
comment_count: data.comment_count,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} catch (error) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
function* nodeGetComments(id: INode['id']) {
|
|
||||||
try {
|
|
||||||
const { comments, comment_count }: Unwrap<typeof apiGetNodeComments> = yield call(
|
|
||||||
apiGetNodeComments,
|
|
||||||
{
|
|
||||||
id: id!,
|
|
||||||
take: COMMENTS_DISPLAY,
|
|
||||||
skip: 0,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
yield put(
|
|
||||||
nodeSet({
|
|
||||||
comments,
|
|
||||||
comment_count,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
function* onNodeLoad({ id }: ReturnType<typeof nodeLoadNode>) {
|
|
||||||
// Comments
|
|
||||||
try {
|
|
||||||
yield put(nodeSetLoadingComments(true));
|
|
||||||
yield call(nodeGetComments, id);
|
|
||||||
|
|
||||||
yield put(
|
|
||||||
nodeSet({
|
|
||||||
is_loading_comments: false,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
function* onPostComment({ nodeId, comment, callback }: ReturnType<typeof nodePostLocalComment>) {
|
|
||||||
try {
|
|
||||||
const data: Unwrap<typeof apiPostComment> = yield call(apiPostComment, {
|
|
||||||
data: comment,
|
|
||||||
id: nodeId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { comments }: ReturnType<typeof selectNode> = yield select(selectNode);
|
|
||||||
|
|
||||||
if (!comment.id) {
|
|
||||||
yield put(nodeSetComments([data.comment, ...comments]));
|
|
||||||
} else {
|
|
||||||
yield put(
|
|
||||||
nodeSet({
|
|
||||||
comments: comments.map(item => (item.id === comment.id ? data.comment : item)),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
callback();
|
|
||||||
} catch (error) {
|
|
||||||
return callback(error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function* onLockCommentSaga({ nodeId, id, is_locked }: ReturnType<typeof nodeLockComment>) {
|
|
||||||
const { comments }: ReturnType<typeof selectNode> = yield select(selectNode);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const data: Unwrap<typeof apiLockComment> = yield call(apiLockComment, {
|
|
||||||
current: nodeId,
|
|
||||||
id,
|
|
||||||
is_locked,
|
|
||||||
});
|
|
||||||
|
|
||||||
yield put(
|
|
||||||
nodeSetComments(
|
|
||||||
comments.map(comment =>
|
|
||||||
comment.id === id ? { ...comment, deleted_at: data.deleted_at || undefined } : comment
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
showErrorToast(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function* nodeSaga() {
|
|
||||||
yield takeLatest(NODE_ACTIONS.GOTO_NODE, onNodeGoto);
|
|
||||||
yield takeLatest(NODE_ACTIONS.LOAD_NODE, onNodeLoad);
|
|
||||||
yield takeLatest(NODE_ACTIONS.POST_LOCAL_COMMENT, onPostComment);
|
|
||||||
yield takeLatest(NODE_ACTIONS.LOCK_COMMENT, onLockCommentSaga);
|
|
||||||
yield takeLeading(NODE_ACTIONS.LOAD_MORE_COMMENTS, onNodeLoadMoreComments);
|
|
||||||
}
|
|
|
@ -79,8 +79,8 @@ export type ApiLockNodeResult = {
|
||||||
|
|
||||||
export type ApiLockCommentRequest = {
|
export type ApiLockCommentRequest = {
|
||||||
id: IComment['id'];
|
id: IComment['id'];
|
||||||
current: INode['id'];
|
nodeId: INode['id'];
|
||||||
is_locked: boolean;
|
isLocked: boolean;
|
||||||
};
|
};
|
||||||
export type ApiLockcommentResult = {
|
export type ApiLockcommentResult = {
|
||||||
deleted_at: string;
|
deleted_at: string;
|
||||||
|
|
|
@ -12,7 +12,6 @@ import authSaga from '~/redux/auth/sagas';
|
||||||
import { IAuthState } from '~/redux/auth/types';
|
import { IAuthState } from '~/redux/auth/types';
|
||||||
|
|
||||||
import node, { INodeState } from '~/redux/node/reducer';
|
import node, { INodeState } from '~/redux/node/reducer';
|
||||||
import nodeSaga from '~/redux/node/sagas';
|
|
||||||
|
|
||||||
import flow, { IFlowState } from '~/redux/flow/reducer';
|
import flow, { IFlowState } from '~/redux/flow/reducer';
|
||||||
import flowSaga from '~/redux/flow/sagas';
|
import flowSaga from '~/redux/flow/sagas';
|
||||||
|
@ -108,7 +107,6 @@ export function configureStore(): {
|
||||||
persistor: Persistor;
|
persistor: Persistor;
|
||||||
} {
|
} {
|
||||||
sagaMiddleware.run(authSaga);
|
sagaMiddleware.run(authSaga);
|
||||||
sagaMiddleware.run(nodeSaga);
|
|
||||||
sagaMiddleware.run(uploadSaga);
|
sagaMiddleware.run(uploadSaga);
|
||||||
sagaMiddleware.run(flowSaga);
|
sagaMiddleware.run(flowSaga);
|
||||||
sagaMiddleware.run(playerSaga);
|
sagaMiddleware.run(playerSaga);
|
||||||
|
|
|
@ -2,22 +2,23 @@ import { IComment, IFile } from '~/redux/types';
|
||||||
import React, { createContext, FC, useContext } from 'react';
|
import React, { createContext, FC, useContext } from 'react';
|
||||||
|
|
||||||
export interface CommentProviderProps {
|
export interface CommentProviderProps {
|
||||||
// user: IUser;
|
|
||||||
comments: IComment[];
|
comments: IComment[];
|
||||||
count: number;
|
hasMore: boolean;
|
||||||
lastSeenCurrent?: string;
|
lastSeenCurrent?: string;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
onShowImageModal: (images: IFile[], index: number) => void;
|
onShowImageModal: (images: IFile[], index: number) => void;
|
||||||
onLoadMoreComments: () => void;
|
onLoadMoreComments: () => void;
|
||||||
|
onSaveComment: (comment: IComment) => Promise<unknown>;
|
||||||
onDeleteComment: (id: IComment['id'], isLocked: boolean) => void;
|
onDeleteComment: (id: IComment['id'], isLocked: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CommentContext = createContext<CommentProviderProps>({
|
const CommentContext = createContext<CommentProviderProps>({
|
||||||
// user: EMPTY_USER,
|
// user: EMPTY_USER,
|
||||||
comments: [],
|
comments: [],
|
||||||
count: 0,
|
hasMore: false,
|
||||||
lastSeenCurrent: undefined,
|
lastSeenCurrent: undefined,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
onSaveComment: async () => {},
|
||||||
onShowImageModal: () => {},
|
onShowImageModal: () => {},
|
||||||
onLoadMoreComments: () => {},
|
onLoadMoreComments: () => {},
|
||||||
onDeleteComment: () => {},
|
onDeleteComment: () => {},
|
||||||
|
|
|
@ -1,9 +1,15 @@
|
||||||
|
const handle = (message: string) => console.warn(message);
|
||||||
|
|
||||||
export const showErrorToast = (error: unknown) => {
|
export const showErrorToast = (error: unknown) => {
|
||||||
|
if (typeof error === 'string') {
|
||||||
|
handle(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!(error instanceof Error)) {
|
if (!(error instanceof Error)) {
|
||||||
console.warn('catched strange exception', error);
|
console.warn('catched strange exception', error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: show toast or something
|
handle(error.message);
|
||||||
console.warn(error.message);
|
|
||||||
};
|
};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue