From 6964324ffba6ab8055af2172e8852b826b9cb6fa Mon Sep 17 00:00:00 2001 From: Fedor Katurov Date: Sun, 2 Jan 2022 17:07:00 +0700 Subject: [PATCH] 99: fixed comments for SWR node --- src/components/comment/Comment/index.tsx | 17 +- .../comment/CommentContent/index.tsx | 162 +++++++++--------- src/containers/node/NodeComments/index.tsx | 8 + src/redux/node/actions.ts | 38 +--- src/redux/node/constants.ts | 7 +- src/redux/node/handlers.ts | 5 - src/redux/node/sagas.ts | 138 ++------------- src/utils/errors/showToast.ts | 9 + src/utils/hooks/node/useFullNode.ts | 2 +- src/utils/hooks/node/useLoadNode.ts | 7 +- src/utils/hooks/node/useNodeActions.ts | 13 +- src/utils/hooks/node/useNodeComments.ts | 8 +- src/utils/hooks/node/useNodeTags.ts | 3 - 13 files changed, 149 insertions(+), 268 deletions(-) create mode 100644 src/utils/errors/showToast.ts diff --git a/src/components/comment/Comment/index.tsx b/src/components/comment/Comment/index.tsx index 50d9cd82..4f2ee99b 100644 --- a/src/components/comment/Comment/index.tsx +++ b/src/components/comment/Comment/index.tsx @@ -8,8 +8,9 @@ import classNames from 'classnames'; import { NEW_COMMENT_CLASSNAME } from '~/constants/comment'; type IProps = HTMLAttributes & { - is_empty?: boolean; - is_loading?: boolean; + nodeId: number; + isEmpty?: boolean; + isLoading?: boolean; group: ICommentGroup; isSame?: boolean; canEdit?: boolean; @@ -20,9 +21,10 @@ type IProps = HTMLAttributes & { const Comment: FC = memo( ({ group, - is_empty, + nodeId, + isEmpty, isSame, - is_loading, + isLoading, className, canEdit, onDelete, @@ -34,8 +36,8 @@ const Comment: FC = memo( className={classNames(className, { [NEW_COMMENT_CLASSNAME]: group.hasNew, })} - isEmpty={is_empty} - isLoading={is_loading} + isEmpty={isEmpty} + isLoading={isLoading} user={group.user} isNew={group.hasNew && !isSame} {...props} @@ -48,9 +50,10 @@ const Comment: FC = memo( return ( diff --git a/src/components/comment/CommentContent/index.tsx b/src/components/comment/CommentContent/index.tsx index bc280fbc..e8b79667 100644 --- a/src/components/comment/CommentContent/index.tsx +++ b/src/components/comment/CommentContent/index.tsx @@ -13,106 +13,108 @@ import { PRESETS } from '~/constants/urls'; import { COMMENT_BLOCK_RENDERERS } from '~/constants/comment'; import { CommentMenu } from '../CommentMenu'; import { CommentForm } from '~/components/comment/CommentForm'; -import { useShallowSelect } from '~/utils/hooks/useShallowSelect'; -import { selectNode } from '~/redux/node/selectors'; interface IProps { + nodeId: number; comment: IComment; - can_edit: boolean; + canEdit: boolean; onDelete: (id: IComment['id'], isLocked: boolean) => void; onShowImageModal: (images: IFile[], index: number) => void; } -const CommentContent: FC = memo(({ comment, can_edit, onDelete, onShowImageModal }) => { - const [isEditing, setIsEditing] = useState(false); - const { current } = useShallowSelect(selectNode); +const CommentContent: FC = memo( + ({ comment, canEdit, nodeId, onDelete, onShowImageModal }) => { + const [isEditing, setIsEditing] = useState(false); - const startEditing = useCallback(() => setIsEditing(true), [setIsEditing]); - const stopEditing = useCallback(() => setIsEditing(false), [setIsEditing]); + const startEditing = useCallback(() => setIsEditing(true), [setIsEditing]); + const stopEditing = useCallback(() => setIsEditing(false), [setIsEditing]); - const groupped = useMemo>( - () => - reduce( - (group, file) => - file.type ? assocPath([file.type], append(file, group[file.type]), group) : group, - {}, - comment.files - ), - [comment] - ); + const groupped = useMemo>( + () => + reduce( + (group, file) => + file.type ? assocPath([file.type], append(file, group[file.type]), group) : group, + {}, + comment.files + ), + [comment] + ); - const onLockClick = useCallback(() => { - onDelete(comment.id, !comment.deleted_at); - }, [comment, onDelete]); + const onLockClick = useCallback(() => { + onDelete(comment.id, !comment.deleted_at); + }, [comment, onDelete]); - const menu = useMemo( - () => can_edit && , - [can_edit, startEditing, onLockClick] - ); + const menu = useMemo( + () => canEdit && , + [canEdit, startEditing, onLockClick] + ); - const blocks = useMemo( - () => - !!comment.text.trim() - ? formatCommentText(path(['user', 'username'], comment), comment.text) - : [], - [comment] - ); + const blocks = useMemo( + () => + !!comment.text.trim() + ? formatCommentText(path(['user', 'username'], comment), comment.text) + : [], + [comment] + ); - if (isEditing) { - return ; - } + if (isEditing) { + return ; + } - return ( -
- {comment.text && ( - - {menu} + return ( +
+ {comment.text && ( + + {menu} - - {blocks.map( - (block, key) => - COMMENT_BLOCK_RENDERERS[block.type] && - createElement(COMMENT_BLOCK_RENDERERS[block.type], { block, key }) - )} + + {blocks.map( + (block, key) => + COMMENT_BLOCK_RENDERERS[block.type] && + createElement(COMMENT_BLOCK_RENDERERS[block.type], { block, key }) + )} + + +
{getPrettyDate(comment.created_at)}
+ )} -
{getPrettyDate(comment.created_at)}
-
- )} + {groupped.image && groupped.image.length > 0 && ( +
+ {menu} - {groupped.image && groupped.image.length > 0 && ( -
- {menu} +
1, + })} + > + {groupped.image.map((file, index) => ( +
onShowImageModal(groupped.image, index)}> + {file.name} +
+ ))} +
-
1 })} - > - {groupped.image.map((file, index) => ( -
onShowImageModal(groupped.image, index)}> - {file.name} +
{getPrettyDate(comment.created_at)}
+
+ )} + + {groupped.audio && groupped.audio.length > 0 && ( + + {groupped.audio.map(file => ( +
+ {menu} + + + +
{getPrettyDate(comment.created_at)}
))} -
- -
{getPrettyDate(comment.created_at)}
-
- )} - - {groupped.audio && groupped.audio.length > 0 && ( - - {groupped.audio.map(file => ( -
- {menu} - - - -
{getPrettyDate(comment.created_at)}
-
- ))} -
- )} -
- ); -}); + + )} +
+ ); + } +); export { CommentContent }; diff --git a/src/containers/node/NodeComments/index.tsx b/src/containers/node/NodeComments/index.tsx index 4cedbe28..0d129c4a 100644 --- a/src/containers/node/NodeComments/index.tsx +++ b/src/containers/node/NodeComments/index.tsx @@ -9,6 +9,7 @@ import { useGrouppedComments } from '~/utils/hooks/node/useGrouppedComments'; import { useCommentContext } from '~/utils/context/CommentContextProvider'; import { Comment } from '~/components/comment/Comment'; import { useUserContext } from '~/utils/context/UserContextProvider'; +import { useNodeContext } from '~/utils/context/NodeContextProvider'; interface IProps { order: 'ASC' | 'DESC'; @@ -16,6 +17,8 @@ interface IProps { const NodeComments: FC = memo(({ order }) => { const user = useUserContext(); + const { node } = useNodeContext(); + const { comments, count, @@ -41,12 +44,17 @@ const NodeComments: FC = memo(({ order }) => { [left, onLoadMoreComments] ); + if (!node?.id) { + return null; + } + return (
{order === 'DESC' && more} {groupped.map(group => ( ({ @@ -60,34 +60,6 @@ export const nodeSetComments = (comments: IComment[]) => ({ type: NODE_ACTIONS.SET_COMMENTS, }); -export const nodeUpdateTags = (id: INode['id'], tags: string[]) => ({ - type: NODE_ACTIONS.UPDATE_TAGS, - id, - tags, -}); - -export const nodeDeleteTag = (id: INode['id'], tagId: ITag['ID']) => ({ - type: NODE_ACTIONS.DELETE_TAG, - id: id!, - tagId, -}); - -export const nodeSetTags = (tags: ITag[]) => ({ - type: NODE_ACTIONS.SET_TAGS, - tags, -}); - -export const nodeCreate = (node_type: INode['type'], isLab?: boolean) => ({ - type: NODE_ACTIONS.CREATE, - node_type, - isLab, -}); - -export const nodeEdit = (id: INode['id']) => ({ - type: NODE_ACTIONS.EDIT, - id, -}); - export const nodeLike = (id: INode['id']) => ({ type: NODE_ACTIONS.LIKE, id, @@ -104,17 +76,13 @@ export const nodeLock = (id: INode['id'], is_locked: boolean) => ({ is_locked, }); -export const nodeLockComment = (id: IComment['id'], is_locked: boolean) => ({ +export const nodeLockComment = (id: number, is_locked: boolean, nodeId: number) => ({ type: NODE_ACTIONS.LOCK_COMMENT, + nodeId, id, is_locked, }); -export const nodeEditComment = (id: IComment['id']) => ({ - type: NODE_ACTIONS.EDIT_COMMENT, - id, -}); - export const nodeSetEditor = (editor: INode) => ({ type: NODE_ACTIONS.SET_EDITOR, editor, diff --git a/src/redux/node/constants.ts b/src/redux/node/constants.ts index 5fdd5868..314d7ad1 100644 --- a/src/redux/node/constants.ts +++ b/src/redux/node/constants.ts @@ -29,13 +29,11 @@ export const NODE_ACTIONS = { GOTO_NODE: `${prefix}GOTO_NODE`, SET: `${prefix}SET`, - EDIT: `${prefix}EDIT`, LIKE: `${prefix}LIKE`, STAR: `${prefix}STAR`, LOCK: `${prefix}LOCK`, LOCK_COMMENT: `${prefix}LOCK_COMMENT`, EDIT_COMMENT: `${prefix}EDIT_COMMENT`, - CREATE: `${prefix}CREATE`, LOAD_MORE_COMMENTS: `${prefix}LOAD_MORE_COMMENTS`, SET_SAVE_ERRORS: `${prefix}SET_SAVE_ERRORS`, @@ -45,13 +43,10 @@ export const NODE_ACTIONS = { SET_CURRENT: `${prefix}SET_CURRENT`, SET_EDITOR: `${prefix}SET_EDITOR`, - POST_COMMENT: `${prefix}POST_LOCAL_COMMENT`, + POST_LOCAL_COMMENT: `${prefix}POST_LOCAL_COMMENT`, SET_COMMENTS: `${prefix}SET_COMMENTS`, SET_RELATED: `${prefix}SET_RELATED`, - UPDATE_TAGS: `${prefix}UPDATE_TAGS`, - DELETE_TAG: `${prefix}DELETE_TAG`, - SET_TAGS: `${prefix}SET_TAGS`, SET_COVER_IMAGE: `${prefix}SET_COVER_IMAGE`, }; diff --git a/src/redux/node/handlers.ts b/src/redux/node/handlers.ts index 2bdd2da3..87b6d9dd 100644 --- a/src/redux/node/handlers.ts +++ b/src/redux/node/handlers.ts @@ -10,7 +10,6 @@ import { nodeSetLoadingComments, nodeSetSaveErrors, nodeSetSendingComment, - nodeSetTags, } from './actions'; import { INodeState } from './reducer'; @@ -41,9 +40,6 @@ const setSendingComment = ( const setComments = (state: INodeState, { comments }: ReturnType) => assocPath(['comments'], comments, state); -const setTags = (state: INodeState, { tags }: ReturnType) => - assocPath(['current', 'tags'], tags, state); - const setEditor = (state: INodeState, { editor }: ReturnType) => assocPath(['editor'], editor, state); @@ -60,7 +56,6 @@ export const NODE_HANDLERS = { [NODE_ACTIONS.SET_CURRENT]: setCurrent, [NODE_ACTIONS.SET_SENDING_COMMENT]: setSendingComment, [NODE_ACTIONS.SET_COMMENTS]: setComments, - [NODE_ACTIONS.SET_TAGS]: setTags, [NODE_ACTIONS.SET_EDITOR]: setEditor, [NODE_ACTIONS.SET_COVER_IMAGE]: setCoverImage, }; diff --git a/src/redux/node/sagas.ts b/src/redux/node/sagas.ts index 7cc8cae2..df713aa0 100644 --- a/src/redux/node/sagas.ts +++ b/src/redux/node/sagas.ts @@ -1,11 +1,7 @@ import { call, put, select, takeLatest, takeLeading } from 'redux-saga/effects'; -import { push } from 'connected-react-router'; import { COMMENTS_DISPLAY, EMPTY_NODE, NODE_ACTIONS, NODE_EDITOR_DATA } from './constants'; import { - nodeCreate, - nodeDeleteTag, - nodeEdit, nodeGotoNode, nodeLike, nodeLoadNode, @@ -15,15 +11,9 @@ import { nodeSet, nodeSetComments, nodeSetCurrent, - nodeSetEditor, - nodeSetLoading, nodeSetLoadingComments, - nodeSetTags, - nodeUpdateTags, } from './actions'; import { - apiDeleteNodeTag, - apiGetNode, apiGetNodeComments, apiLockComment, apiLockNode, @@ -44,6 +34,7 @@ import { has } from 'ramda'; import { selectLabListNodes } from '~/redux/lab/selectors'; import { labSetList } from '~/redux/lab/actions'; import { apiPostNode } from '~/redux/node/api'; +import { showErrorToast } from '~/utils/errors/showToast'; export function* updateNodeEverywhere(node) { const { @@ -127,22 +118,9 @@ function* nodeGetComments(id: INode['id']) { } function* onNodeLoad({ id }: ReturnType) { - // Get node body - try { - yield put(nodeSetLoading(true)); - yield put(nodeSetLoadingComments(true)); - - const { node, last_seen }: Unwrap = yield call(apiGetNode, { id }); - - yield put(nodeSet({ current: node, lastSeenCurrent: last_seen })); - yield put(nodeSetLoading(false)); - } catch (error) { - yield put(push(URLS.ERRORS.NOT_FOUND)); - yield put(nodeSetLoading(false)); - } - // Comments try { + yield put(nodeSetLoadingComments(true)); yield call(nodeGetComments, id); yield put( @@ -160,82 +138,24 @@ function* onPostComment({ nodeId, comment, callback }: ReturnType = yield select(selectNode); + const { comments }: ReturnType = yield select(selectNode); - if (current?.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(); + 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* onUpdateTags({ id, tags }: ReturnType) { - try { - const { node }: Unwrap = yield call(apiPostNodeTags, { id, tags }); - const { current }: ReturnType = yield select(selectNode); - if (!node || !node.id || node.id !== current.id) return; - yield put(nodeSetTags(node.tags)); - } catch {} -} - -function* onDeleteTag({ id, tagId }: ReturnType) { - try { - const { tags }: Unwrap = yield call(apiDeleteNodeTag, { id, tagId }); - yield put(nodeSetTags(tags)); - } catch {} -} - -function* onCreateSaga({ node_type: type, isLab }: ReturnType) { - if (!type || !has(type, NODE_EDITOR_DIALOGS)) return; - - yield put( - nodeSetEditor({ - ...EMPTY_NODE, - ...(NODE_EDITOR_DATA[type] || {}), - type, - is_promoted: !isLab, - }) - ); - - yield put(modalShowDialog(NODE_EDITOR_DIALOGS[type])); -} - -function* onEditSaga({ id }: ReturnType) { - try { - if (!id) { - return; - } - - yield put(modalShowDialog(DIALOGS.LOADING)); - - const { node }: Unwrap = yield call(apiGetNode, { id }); - - if (!node.type || !has(node.type, NODE_EDITOR_DIALOGS)) return; - - if (!NODE_EDITOR_DIALOGS[node?.type]) { - throw new Error('Unknown node type'); - } - - yield put(nodeSetEditor(node)); - yield put(modalShowDialog(NODE_EDITOR_DIALOGS[node.type])); - } catch (error) { - yield put(modalSetShown(false)); - } -} - function* onLikeSaga({ id }: ReturnType) { const { current }: ReturnType = yield select(selectNode); @@ -293,22 +213,12 @@ function* onLockSaga({ id, is_locked }: ReturnType) { } } -function* onLockCommentSaga({ id, is_locked }: ReturnType) { - const { current, comments }: ReturnType = yield select(selectNode); +function* onLockCommentSaga({ nodeId, id, is_locked }: ReturnType) { + const { comments }: ReturnType = yield select(selectNode); try { - yield put( - nodeSetComments( - comments.map(comment => - comment.id === id - ? { ...comment, deleted_at: is_locked ? new Date().toISOString() : undefined } - : comment - ) - ) - ); - const data: Unwrap = yield call(apiLockComment, { - current: current.id, + current: nodeId, id, is_locked, }); @@ -320,25 +230,15 @@ function* onLockCommentSaga({ id, is_locked }: ReturnType - comment.id === id ? { ...comment, deleted_at: current.deleted_at } : 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_COMMENT, onPostComment); - yield takeLatest(NODE_ACTIONS.UPDATE_TAGS, onUpdateTags); - yield takeLatest(NODE_ACTIONS.DELETE_TAG, onDeleteTag); - yield takeLatest(NODE_ACTIONS.CREATE, onCreateSaga); - yield takeLatest(NODE_ACTIONS.EDIT, onEditSaga); + yield takeLatest(NODE_ACTIONS.POST_LOCAL_COMMENT, onPostComment); yield takeLatest(NODE_ACTIONS.LIKE, onLikeSaga); yield takeLatest(NODE_ACTIONS.STAR, onStarSaga); yield takeLatest(NODE_ACTIONS.LOCK, onLockSaga); diff --git a/src/utils/errors/showToast.ts b/src/utils/errors/showToast.ts new file mode 100644 index 00000000..ce357a56 --- /dev/null +++ b/src/utils/errors/showToast.ts @@ -0,0 +1,9 @@ +export const showErrorToast = (error: unknown) => { + if (!(error instanceof Error)) { + console.warn('catched strange exception', error); + return; + } + + // TODO: show toast or something + console.warn(error.message); +}; diff --git a/src/utils/hooks/node/useFullNode.ts b/src/utils/hooks/node/useFullNode.ts index 2f5e91dd..8ace61f7 100644 --- a/src/utils/hooks/node/useFullNode.ts +++ b/src/utils/hooks/node/useFullNode.ts @@ -13,7 +13,7 @@ export const useFullNode = (id: string) => { lastSeenCurrent, } = useShallowSelect(selectNode); - // useLoadNode(id); + useLoadNode(id); // useOnNodeSeen(node); return { node, comments, commentsCount, lastSeenCurrent, isLoading, isLoadingComments }; diff --git a/src/utils/hooks/node/useLoadNode.ts b/src/utils/hooks/node/useLoadNode.ts index 03eee3d0..8fc2f6c8 100644 --- a/src/utils/hooks/node/useLoadNode.ts +++ b/src/utils/hooks/node/useLoadNode.ts @@ -1,7 +1,6 @@ import { useEffect } from 'react'; -import { nodeGotoNode, nodeSetCurrent } from '~/redux/node/actions'; +import { nodeGotoNode } from '~/redux/node/actions'; import { useDispatch } from 'react-redux'; -import { EMPTY_NODE } from '~/redux/node/constants'; // useLoadNode loads node on id change export const useLoadNode = (id: any) => { @@ -9,9 +8,5 @@ export const useLoadNode = (id: any) => { useEffect(() => { dispatch(nodeGotoNode(parseInt(id, 10), undefined)); - - return () => { - dispatch(nodeSetCurrent(EMPTY_NODE)); - }; }, [dispatch, id]); }; diff --git a/src/utils/hooks/node/useNodeActions.ts b/src/utils/hooks/node/useNodeActions.ts index 389ed94a..04be1d72 100644 --- a/src/utils/hooks/node/useNodeActions.ts +++ b/src/utils/hooks/node/useNodeActions.ts @@ -1,12 +1,21 @@ import { INode } from '~/redux/types'; import { useCallback } from 'react'; import { useDispatch } from 'react-redux'; -import { nodeEdit, nodeLike, nodeLock, nodeStar } from '~/redux/node/actions'; +import { nodeLike, nodeLock, nodeStar } from '~/redux/node/actions'; +import { modalShowDialog } from '~/redux/modal/actions'; +import { NODE_EDITOR_DIALOGS } from '~/constants/dialogs'; export const useNodeActions = (node: INode) => { const dispatch = useDispatch(); - const onEdit = useCallback(() => dispatch(nodeEdit(node.id)), [dispatch, node]); + const onEdit = useCallback(() => { + if (!node.type) { + return; + } + + dispatch(modalShowDialog(NODE_EDITOR_DIALOGS[node.type])); + }, [dispatch, node]); + const onLike = useCallback(() => dispatch(nodeLike(node.id)), [dispatch, node]); const onStar = useCallback(() => dispatch(nodeStar(node.id)), [dispatch, node]); const onLock = useCallback(() => dispatch(nodeLock(node.id, !node.deleted_at)), [dispatch, node]); diff --git a/src/utils/hooks/node/useNodeComments.ts b/src/utils/hooks/node/useNodeComments.ts index 97511475..5b477e62 100644 --- a/src/utils/hooks/node/useNodeComments.ts +++ b/src/utils/hooks/node/useNodeComments.ts @@ -1,16 +1,16 @@ import { useCallback } from 'react'; import { nodeLoadMoreComments, nodeLockComment } from '~/redux/node/actions'; -import { IComment, INode } from '~/redux/types'; +import { IComment } from '~/redux/types'; import { useDispatch } from 'react-redux'; -export const useNodeComments = (id: INode['id']) => { +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)), - [dispatch] + (id: IComment['id'], locked: boolean) => dispatch(nodeLockComment(id, locked, nodeId)), + [dispatch, nodeId] ); return { onLoadMoreComments, onDelete }; diff --git a/src/utils/hooks/node/useNodeTags.ts b/src/utils/hooks/node/useNodeTags.ts index 00cfe620..ff175f15 100644 --- a/src/utils/hooks/node/useNodeTags.ts +++ b/src/utils/hooks/node/useNodeTags.ts @@ -1,7 +1,5 @@ -import { useDispatch } from 'react-redux'; import { useHistory } from 'react-router'; import { useCallback } from 'react'; -import { nodeDeleteTag, nodeUpdateTags } from '~/redux/node/actions'; import { ITag } from '~/redux/types'; import { URLS } from '~/constants/urls'; import { useGetNode } from '~/utils/hooks/data/useGetNode'; @@ -9,7 +7,6 @@ import { apiDeleteNodeTag, apiPostNodeTags } from '~/redux/node/api'; export const useNodeTags = (id: number) => { const { update } = useGetNode(id); - const dispatch = useDispatch(); const history = useHistory(); const onChange = useCallback(