diff --git a/src/components/comment/Comment/index.tsx b/src/components/comment/Comment/index.tsx index 4f2ee99b..5f256ffa 100644 --- a/src/components/comment/Comment/index.tsx +++ b/src/components/comment/Comment/index.tsx @@ -14,6 +14,7 @@ type IProps = HTMLAttributes & { group: ICommentGroup; isSame?: boolean; canEdit?: boolean; + saveComment: (data: IComment) => Promise; onDelete: (id: IComment['id'], isLocked: boolean) => void; onShowImageModal: (images: IFile[], index: number) => void; }; @@ -29,6 +30,7 @@ const Comment: FC = memo( canEdit, onDelete, onShowImageModal, + saveComment, ...props }) => { return ( @@ -50,6 +52,7 @@ const Comment: FC = memo( return ( Promise; onDelete: (id: IComment['id'], isLocked: boolean) => void; onShowImageModal: (images: IFile[], index: number) => void; } const CommentContent: FC = memo( - ({ comment, canEdit, nodeId, onDelete, onShowImageModal }) => { + ({ comment, canEdit, nodeId, saveComment, onDelete, onShowImageModal }) => { const [isEditing, setIsEditing] = useState(false); const startEditing = useCallback(() => setIsEditing(true), [setIsEditing]); @@ -58,7 +59,14 @@ const CommentContent: FC = memo( ); if (isEditing) { - return ; + return ( + + ); } return ( diff --git a/src/components/comment/CommentForm/index.tsx b/src/components/comment/CommentForm/index.tsx index 235d3707..2fee8eed 100644 --- a/src/components/comment/CommentForm/index.tsx +++ b/src/components/comment/CommentForm/index.tsx @@ -20,17 +20,24 @@ import { Filler } from '~/components/containers/Filler'; interface IProps { comment?: IComment; nodeId: INode['id']; + saveComment: (data: IComment) => Promise; onCancelEdit?: () => void; } -const CommentForm: FC = ({ comment, nodeId, onCancelEdit }) => { +const CommentForm: FC = ({ comment, nodeId, saveComment, onCancelEdit }) => { const [textarea, setTextarea] = useState(); const uploader = useFileUploader( UPLOAD_SUBJECTS.COMMENT, UPLOAD_TARGETS.COMMENTS, 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 isEditing = !!comment?.id; diff --git a/src/components/node/NodeCommentForm/index.tsx b/src/components/node/NodeCommentForm/index.tsx index 2635346c..fbfb0065 100644 --- a/src/components/node/NodeCommentForm/index.tsx +++ b/src/components/node/NodeCommentForm/index.tsx @@ -1,27 +1,21 @@ import React, { FC } from 'react'; import { CommentWrapper } from '~/components/containers/CommentWrapper'; -import { connect } from 'react-redux'; -import { selectAuthUser } from '~/redux/auth/selectors'; 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 => ({ - user: selectAuthUser(state), -}); +interface NodeCommentFormProps { + user: IUser; + nodeId?: number; + saveComment: (comment: IComment) => Promise; +} -type IProps = ReturnType & { - isBefore?: boolean; - nodeId: INode['id']; -}; - -const NodeCommentFormUnconnected: FC = ({ user, isBefore, nodeId }) => { +const NodeCommentForm: FC = ({ user, nodeId, saveComment }) => { return ( - + ); }; -const NodeCommentForm = connect(mapStateToProps)(NodeCommentFormUnconnected); - export { NodeCommentForm }; diff --git a/src/constants/api.ts b/src/constants/api.ts index 7b61e9b7..9b4d57ec 100644 --- a/src/constants/api.ts +++ b/src/constants/api.ts @@ -27,7 +27,7 @@ export const API = { GET_DIFF: '/flow/diff', 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`, UPDATE_TAGS: (id: INode['id']) => `/node/${id}/tags`, DELETE_TAG: (id: INode['id'], tagId: ITag['ID']) => `/node/${id}/tags/${tagId}`, diff --git a/src/containers/boris/BorisComments/index.tsx b/src/containers/boris/BorisComments/index.tsx index 04b68402..5f3615fe 100644 --- a/src/containers/boris/BorisComments/index.tsx +++ b/src/containers/boris/BorisComments/index.tsx @@ -16,24 +16,28 @@ const BorisComments: FC = () => { const { isLoading, comments, + onSaveComment, onLoadMoreComments, onDeleteComment, onShowImageModal, - count, + hasMore, } = useCommentContext(); const { node } = useNodeContext(); return ( <> - {user.is_user && } + {user.is_user && ( + + )} {isLoading ? ( ) : ( { history.replace(backUrl); }, [backUrl, history]); - const { node, isLoading } = useGetNode(parseInt(id, 10)); + const { node, isLoading } = useLoadNode(parseInt(id, 10)); const updateNode = useUpdateNode(parseInt(id, 10)); const onSubmit = useCallback( diff --git a/src/containers/node/NodeBottomBlock/index.tsx b/src/containers/node/NodeBottomBlock/index.tsx index 32e67c47..84d9514d 100644 --- a/src/containers/node/NodeBottomBlock/index.tsx +++ b/src/containers/node/NodeBottomBlock/index.tsx @@ -21,9 +21,9 @@ interface IProps { } const NodeBottomBlock: FC = ({ commentsOrder }) => { - const { is_user: isUser } = useUserContext(); + const user = useUserContext(); const { node, isLoading } = useNodeContext(); - const { comments, isLoading: isLoadingComments } = useCommentContext(); + const { comments, isLoading: isLoadingComments, onSaveComment } = useCommentContext(); const { related, isLoading: isLoadingRelated } = useNodeRelatedContext(); const { inline } = useNodeBlocks(node, isLoading); @@ -44,7 +44,9 @@ const NodeBottomBlock: FC = ({ commentsOrder }) => { )} - {isUser && !isLoading && } + {user.is_user && !isLoading && ( + + )}
diff --git a/src/containers/node/NodeComments/index.tsx b/src/containers/node/NodeComments/index.tsx index 0410039b..3c18b284 100644 --- a/src/containers/node/NodeComments/index.tsx +++ b/src/containers/node/NodeComments/index.tsx @@ -21,27 +21,24 @@ const NodeComments: FC = memo(({ order }) => { const { comments, - count, + hasMore, lastSeenCurrent, onLoadMoreComments, onDeleteComment, onShowImageModal, + onSaveComment, } = useCommentContext(); - const left = useMemo(() => Math.max(0, count - comments.length), [comments, count]); - const groupped: ICommentGroup[] = useGrouppedComments(comments, order, lastSeenCurrent); const more = useMemo( () => - left > 0 && ( + hasMore && (
- Показать ещё{' '} - {plural(Math.min(left, COMMENTS_DISPLAY), 'комментарий', 'комментария', 'комментариев')} - {left > COMMENTS_DISPLAY ? ` из ${left} оставшихся` : ''} + Показать ещё комментарии
), - [left, onLoadMoreComments] + [hasMore, onLoadMoreComments] ); if (!node?.id) { @@ -61,6 +58,7 @@ const NodeComments: FC = memo(({ order }) => { onDelete={onDeleteComment} onShowImageModal={onShowImageModal} isSame={group.user.id === user.id} + saveComment={onSaveComment} /> ))} diff --git a/src/containers/profile/ProfileLayout/index.tsx b/src/containers/profile/ProfileLayout/index.tsx index f66bece2..e6207038 100644 --- a/src/containers/profile/ProfileLayout/index.tsx +++ b/src/containers/profile/ProfileLayout/index.tsx @@ -41,7 +41,7 @@ const ProfileLayoutUnconnected: FC = ({ history, nodeSetCoverImage }) =>
- + console.log()} />
diff --git a/src/hooks/comments/useCommentFormFormik.ts b/src/hooks/comments/useCommentFormFormik.ts index 7e814fcc..84e792f4 100644 --- a/src/hooks/comments/useCommentFormFormik.ts +++ b/src/hooks/comments/useCommentFormFormik.ts @@ -3,21 +3,23 @@ import { useCallback, useEffect, useRef } from 'react'; import { FormikHelpers, useFormik, useFormikContext } from 'formik'; import { array, object, string } from 'yup'; import { FileUploader } from '~/hooks/data/useFileUploader'; -import { useDispatch } from 'react-redux'; -import { nodePostLocalComment } from '~/redux/node/actions'; +import { showErrorToast } from '~/utils/errors/showToast'; +import { hasPath, path } from 'ramda'; const validationSchema = object().shape({ text: string(), files: array(), }); -const onSuccess = ({ resetForm, setStatus, setSubmitting }: FormikHelpers) => ( - e?: string +const onSuccess = ({ resetForm, setSubmitting, setErrors }: FormikHelpers) => ( + error?: unknown ) => { setSubmitting(false); - if (e) { - setStatus(e); + if (hasPath(['response', 'data', 'error'], error)) { + const message = path(['response', 'data', 'error'], error) as string; + setErrors({ text: message }); + showErrorToast(error); return; } @@ -30,26 +32,23 @@ export const useCommentFormFormik = ( values: IComment, nodeId: INode['id'], uploader: FileUploader, + sendData: (data: IComment) => Promise, stopEditing?: () => void ) => { - const dispatch = useDispatch(); const { current: initialValues } = useRef(values); const onSubmit = useCallback( - (values: IComment, helpers: FormikHelpers) => { - helpers.setSubmitting(true); - dispatch( - nodePostLocalComment( - nodeId, - { - ...values, - files: uploader.files, - }, - onSuccess(helpers) - ) - ); + async (values: IComment, helpers: FormikHelpers) => { + try { + helpers.setSubmitting(true); + await sendData(values); + onSuccess(helpers)(); + } catch (error) { + console.log('error', error); + onSuccess(helpers)(error); + } }, - [dispatch, nodeId, uploader.files] + [sendData] ); const onReset = useCallback(() => { diff --git a/src/hooks/comments/useGetComments.ts b/src/hooks/comments/useGetComments.ts new file mode 100644 index 00000000..832eec0d --- /dev/null +++ b/src/hooks/comments/useGetComments.ts @@ -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 = (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 }; +}; diff --git a/src/hooks/comments/useNodeComments.ts b/src/hooks/comments/useNodeComments.ts new file mode 100644 index 00000000..4976b864 --- /dev/null +++ b/src/hooks/comments/useNodeComments.ts @@ -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 }; +}; diff --git a/src/hooks/node/useFullNode.ts b/src/hooks/node/useFullNode.ts deleted file mode 100644 index 4ee20fe6..00000000 --- a/src/hooks/node/useFullNode.ts +++ /dev/null @@ -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 }; -}; diff --git a/src/hooks/node/useGetNode.ts b/src/hooks/node/useGetNode.ts deleted file mode 100644 index de1d35e5..00000000 --- a/src/hooks/node/useGetNode.ts +++ /dev/null @@ -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(API.NODE.GET_NODE(id), () => - apiGetNode({ id }) - ); - - const update = useCallback( - async (node?: Partial) => { - 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 }; -}; diff --git a/src/hooks/node/useLoadNode.ts b/src/hooks/node/useLoadNode.ts index 8fc2f6c8..f27f2774 100644 --- a/src/hooks/node/useLoadNode.ts +++ b/src/hooks/node/useLoadNode.ts @@ -1,12 +1,35 @@ -import { useEffect } from 'react'; -import { nodeGotoNode } from '~/redux/node/actions'; -import { useDispatch } from 'react-redux'; +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'; -// useLoadNode loads node on id change -export const useLoadNode = (id: any) => { - const dispatch = useDispatch(); +export const useLoadNode = (id: number) => { + const { data, isValidating, mutate } = useSWR(API.NODE.GET_NODE(id), () => + apiGetNode({ id }) + ); - useEffect(() => { - dispatch(nodeGotoNode(parseInt(id, 10), undefined)); - }, [dispatch, id]); + const update = useCallback( + async (node?: Partial) => { + 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, + }; }; diff --git a/src/hooks/node/useNodeComments.ts b/src/hooks/node/useNodeComments.ts deleted file mode 100644 index 5b477e62..00000000 --- a/src/hooks/node/useNodeComments.ts +++ /dev/null @@ -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 }; -}; diff --git a/src/hooks/node/useNodeTags.ts b/src/hooks/node/useNodeTags.ts index 0838978e..a604480b 100644 --- a/src/hooks/node/useNodeTags.ts +++ b/src/hooks/node/useNodeTags.ts @@ -2,11 +2,11 @@ import { useHistory } from 'react-router'; import { useCallback } from 'react'; import { ITag } from '~/redux/types'; import { URLS } from '~/constants/urls'; -import { useGetNode } from '~/hooks/node/useGetNode'; +import { useLoadNode } from '~/hooks/node/useLoadNode'; import { apiDeleteNodeTag, apiPostNodeTags } from '~/redux/node/api'; export const useNodeTags = (id: number) => { - const { update } = useGetNode(id); + const { update } = useLoadNode(id); const history = useHistory(); const onChange = useCallback( diff --git a/src/hooks/node/useUpdateNode.ts b/src/hooks/node/useUpdateNode.ts index 5d8bd35a..22fb7c61 100644 --- a/src/hooks/node/useUpdateNode.ts +++ b/src/hooks/node/useUpdateNode.ts @@ -1,4 +1,4 @@ -import { useGetNode } from '~/hooks/node/useGetNode'; +import { useLoadNode } from '~/hooks/node/useLoadNode'; import { useCallback } from 'react'; import { INode } from '~/redux/types'; import { apiPostNode } from '~/redux/node/api'; @@ -11,7 +11,7 @@ import { useDispatch } from 'react-redux'; export const useUpdateNode = (id: number) => { const dispatch = useDispatch(); - const { update } = useGetNode(id); + const { update } = useLoadNode(id); const flowNodes = useShallowSelect(selectFlowNodes); const labNodes = useShallowSelect(selectLabListNodes); diff --git a/src/pages/boris.tsx b/src/pages/boris.tsx index b4d017fd..fac0f490 100644 --- a/src/pages/boris.tsx +++ b/src/pages/boris.tsx @@ -1,37 +1,32 @@ -import React, { useEffect, VFC } from 'react'; -import { useDispatch } from 'react-redux'; -import { useShallowSelect } from '~/hooks/data/useShallowSelect'; -import { selectNode } from '~/redux/node/selectors'; +import React, { VFC } from 'react'; import { BorisLayout } from '~/layouts/BorisLayout'; -import { nodeLoadNode } from '~/redux/node/actions'; import { CommentContextProvider } from '~/utils/context/CommentContextProvider'; 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 { NodeContextProvider } from '~/utils/context/NodeContextProvider'; -import { useGetNode } from '~/hooks/node/useGetNode'; +import { useLoadNode } from '~/hooks/node/useLoadNode'; const BorisPage: VFC = () => { - const dispatch = useDispatch(); - const { node, isLoading, update } = useGetNode(696); - const { - comments, - comment_count: count, - is_loading_comments: isLoadingComments, - } = useShallowSelect(selectNode); + const { node, isLoading, update } = useLoadNode(696); 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); - useEffect(() => { - dispatch(nodeLoadNode(696, 'DESC')); - }, [dispatch]); return ( & {}; @@ -21,11 +20,17 @@ const NodePage: FC = ({ params: { id }, }, }) => { - const { node, isLoading, update } = useGetNode(parseInt(id, 10)); - const { isLoadingComments, comments, commentsCount, lastSeenCurrent } = useFullNode(id); + const { node, isLoading, update, lastSeen } = useLoadNode(parseInt(id, 10)); 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( parseInt(id, 10) ); @@ -43,9 +48,10 @@ const NodePage: FC = ({ ) => ({ - 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) => ({ type: NODE_ACTIONS.SET_COVER_IMAGE, current_cover_image, }); - -export const nodeLoadMoreComments = () => ({ - type: NODE_ACTIONS.LOAD_MORE_COMMENTS, -}); diff --git a/src/redux/node/api.ts b/src/redux/node/api.ts index c2bb458c..eee36f4c 100644 --- a/src/redux/node/api.ts +++ b/src/redux/node/api.ts @@ -117,7 +117,7 @@ export const apiLockNode = ({ id, is_locked }: ApiLockNodeRequest) => .post(API.NODE.POST_LOCK(id), { is_locked }) .then(cleanResult); -export const apiLockComment = ({ id, is_locked, current }: ApiLockCommentRequest) => +export const apiLockComment = ({ id, isLocked, nodeId }: ApiLockCommentRequest) => api - .post(API.NODE.LOCK_COMMENT(current, id), { is_locked }) + .post(API.NODE.LOCK_COMMENT(nodeId, id), { is_locked: isLocked }) .then(cleanResult); diff --git a/src/redux/node/constants.ts b/src/redux/node/constants.ts index 0fa9204c..8497c003 100644 --- a/src/redux/node/constants.ts +++ b/src/redux/node/constants.ts @@ -25,21 +25,6 @@ import { LabAudio } from '~/components/lab/LabAudioBlock'; const prefix = 'NODE.'; 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`, }; diff --git a/src/redux/node/handlers.ts b/src/redux/node/handlers.ts index fddfcc9e..d9915f39 100644 --- a/src/redux/node/handlers.ts +++ b/src/redux/node/handlers.ts @@ -1,46 +1,13 @@ import { assocPath } from 'ramda'; import { NODE_ACTIONS } from './constants'; -import { - nodeSet, - nodeSetComments, - nodeSetCoverImage, - nodeSetLoading, - nodeSetLoadingComments, - nodeSetSendingComment, -} from './actions'; +import { nodeSetCoverImage } from './actions'; import { INodeState } from './reducer'; -const setData = (state: INodeState, { node }: ReturnType) => ({ - ...state, - ...node, -}); - -const setLoading = (state: INodeState, { is_loading }: ReturnType) => - assocPath(['is_loading'], is_loading, state); - -const setLoadingComments = ( - state: INodeState, - { is_loading_comments }: ReturnType -) => assocPath(['is_loading_comments'], is_loading_comments, state); - -const setSendingComment = ( - state: INodeState, - { is_sending_comment }: ReturnType -) => assocPath(['is_sending_comment'], is_sending_comment, state); - -const setComments = (state: INodeState, { comments }: ReturnType) => - assocPath(['comments'], comments, state); - const setCoverImage = ( state: INodeState, { current_cover_image }: ReturnType ) => assocPath(['current_cover_image'], current_cover_image, state); 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, }; diff --git a/src/redux/node/sagas.ts b/src/redux/node/sagas.ts deleted file mode 100644 index e3182306..00000000 --- a/src/redux/node/sagas.ts +++ /dev/null @@ -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 = yield select(selectNode); - - const flow_nodes: ReturnType = 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) { - if (!id) { - return; - } - - yield put(nodeLoadNode(id)); -} - -function* onNodeLoadMoreComments() { - try { - const { - current: { id }, - comments, - }: ReturnType = yield select(selectNode); - - if (!id) { - return; - } - - const data: Unwrap = yield call(apiGetNodeComments, { - id, - take: COMMENTS_DISPLAY, - skip: comments.length, - }); - - const current: ReturnType = 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 = yield call( - apiGetNodeComments, - { - id: id!, - take: COMMENTS_DISPLAY, - skip: 0, - } - ); - - yield put( - nodeSet({ - comments, - comment_count, - }) - ); - } catch {} -} - -function* onNodeLoad({ id }: ReturnType) { - // Comments - try { - yield put(nodeSetLoadingComments(true)); - yield call(nodeGetComments, id); - - yield put( - nodeSet({ - is_loading_comments: false, - }) - ); - } catch {} -} - -function* onPostComment({ nodeId, comment, callback }: ReturnType) { - try { - const data: Unwrap = yield call(apiPostComment, { - data: comment, - id: nodeId, - }); - - const { comments }: ReturnType = 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) { - const { comments }: ReturnType = yield select(selectNode); - - try { - const data: Unwrap = 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); -} diff --git a/src/redux/node/types.ts b/src/redux/node/types.ts index 0f657755..bcbc0b6d 100644 --- a/src/redux/node/types.ts +++ b/src/redux/node/types.ts @@ -79,8 +79,8 @@ export type ApiLockNodeResult = { export type ApiLockCommentRequest = { id: IComment['id']; - current: INode['id']; - is_locked: boolean; + nodeId: INode['id']; + isLocked: boolean; }; export type ApiLockcommentResult = { deleted_at: string; diff --git a/src/redux/store.ts b/src/redux/store.ts index 6b19ebe9..d92e664b 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -12,7 +12,6 @@ import authSaga from '~/redux/auth/sagas'; import { IAuthState } from '~/redux/auth/types'; import node, { INodeState } from '~/redux/node/reducer'; -import nodeSaga from '~/redux/node/sagas'; import flow, { IFlowState } from '~/redux/flow/reducer'; import flowSaga from '~/redux/flow/sagas'; @@ -108,7 +107,6 @@ export function configureStore(): { persistor: Persistor; } { sagaMiddleware.run(authSaga); - sagaMiddleware.run(nodeSaga); sagaMiddleware.run(uploadSaga); sagaMiddleware.run(flowSaga); sagaMiddleware.run(playerSaga); diff --git a/src/utils/context/CommentContextProvider.tsx b/src/utils/context/CommentContextProvider.tsx index b8e8617b..b9bb3f6b 100644 --- a/src/utils/context/CommentContextProvider.tsx +++ b/src/utils/context/CommentContextProvider.tsx @@ -2,22 +2,23 @@ import { IComment, IFile } from '~/redux/types'; import React, { createContext, FC, useContext } from 'react'; export interface CommentProviderProps { - // user: IUser; comments: IComment[]; - count: number; + hasMore: boolean; lastSeenCurrent?: string; isLoading: boolean; onShowImageModal: (images: IFile[], index: number) => void; onLoadMoreComments: () => void; + onSaveComment: (comment: IComment) => Promise; onDeleteComment: (id: IComment['id'], isLocked: boolean) => void; } const CommentContext = createContext({ // user: EMPTY_USER, comments: [], - count: 0, + hasMore: false, lastSeenCurrent: undefined, isLoading: false, + onSaveComment: async () => {}, onShowImageModal: () => {}, onLoadMoreComments: () => {}, onDeleteComment: () => {}, diff --git a/src/utils/errors/showToast.ts b/src/utils/errors/showToast.ts index ce357a56..2b2cd335 100644 --- a/src/utils/errors/showToast.ts +++ b/src/utils/errors/showToast.ts @@ -1,9 +1,15 @@ +const handle = (message: string) => console.warn(message); + export const showErrorToast = (error: unknown) => { + if (typeof error === 'string') { + handle(error); + return; + } + if (!(error instanceof Error)) { console.warn('catched strange exception', error); return; } - // TODO: show toast or something - console.warn(error.message); + handle(error.message); };