diff --git a/package.json b/package.json index bb79c035..03f37c0f 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "connected-react-router": "^6.5.2", "date-fns": "^2.4.1", "flexbin": "^0.2.0", + "formik": "^2.2.6", "insane": "^2.6.2", "marked": "^2.0.0", "node-sass": "4.14.1", @@ -23,7 +24,7 @@ "react": "^17.0.1", "react-dom": "^17.0.1", "react-popper": "^2.2.3", - "react-redux": "^6.0.1", + "react-redux": "^7.2.2", "react-router": "^5.1.2", "react-router-dom": "^5.1.2", "react-scripts": "3.4.4", @@ -36,7 +37,8 @@ "throttle-debounce": "^2.1.0", "typescript": "^4.0.5", "uuid4": "^1.1.4", - "web-vitals": "^0.2.4" + "web-vitals": "^0.2.4", + "yup": "^0.32.9" }, "scripts": { "start": "craco start", @@ -68,6 +70,7 @@ "@types/node": "^11.13.22", "@types/ramda": "^0.26.33", "@types/react-redux": "^7.1.11", + "@types/yup": "^0.29.11", "craco-alias": "^2.1.1", "craco-fast-refresh": "^1.0.2", "prettier": "^1.18.2" diff --git a/src/components/comment/Comment/index.tsx b/src/components/comment/Comment/index.tsx index 7608e702..6ee92c31 100644 --- a/src/components/comment/Comment/index.tsx +++ b/src/components/comment/Comment/index.tsx @@ -1,11 +1,8 @@ import React, { FC, HTMLAttributes, memo } from 'react'; import { CommentWrapper } from '~/components/containers/CommentWrapper'; -import { ICommentGroup } from '~/redux/types'; +import { IComment, ICommentGroup } from '~/redux/types'; import { CommentContent } from '~/components/comment/CommentContent'; import styles from './styles.module.scss'; -import { nodeEditComment, nodeLockComment } from '~/redux/node/actions'; -import { INodeState } from '~/redux/node/reducer'; -import { CommentForm } from '../CommentForm'; import { CommendDeleted } from '../../node/CommendDeleted'; import * as MODAL_ACTIONS from '~/redux/modal/actions'; @@ -13,25 +10,21 @@ type IProps = HTMLAttributes & { is_empty?: boolean; is_loading?: boolean; comment_group: ICommentGroup; - comment_data: INodeState['comment_data']; is_same?: boolean; can_edit?: boolean; - onDelete: typeof nodeLockComment; - onEdit: typeof nodeEditComment; + onDelete: (id: IComment['id'], isLocked: boolean) => void; modalShowPhotoswipe: typeof MODAL_ACTIONS.modalShowPhotoswipe; }; const Comment: FC = memo( ({ comment_group, - comment_data, is_empty, is_same, is_loading, className, can_edit, onDelete, - onEdit, modalShowPhotoswipe, ...props }) => { @@ -50,17 +43,12 @@ const Comment: FC = memo( return ; } - if (Object.prototype.hasOwnProperty.call(comment_data, comment.id)) { - return ; - } - return ( ); diff --git a/src/components/comment/CommentContent/index.tsx b/src/components/comment/CommentContent/index.tsx index a1a48d40..b215379e 100644 --- a/src/components/comment/CommentContent/index.tsx +++ b/src/components/comment/CommentContent/index.tsx @@ -1,112 +1,115 @@ -import React, { FC, useMemo, memo, createElement, useCallback, Fragment } from 'react'; +import React, { createElement, FC, Fragment, memo, useCallback, useMemo, useState } from 'react'; import { IComment, IFile } from '~/redux/types'; -import { path } from 'ramda'; -import { formatCommentText, getURL, getPrettyDate } from '~/utils/dom'; +import { append, assocPath, path } from 'ramda'; +import { formatCommentText, getPrettyDate, getURL } from '~/utils/dom'; import { Group } from '~/components/containers/Group'; import styles from './styles.module.scss'; import { UPLOAD_TYPES } from '~/redux/uploads/constants'; -import { assocPath } from 'ramda'; -import { append } from 'ramda'; import reduce from 'ramda/es/reduce'; import { AudioPlayer } from '~/components/media/AudioPlayer'; import classnames from 'classnames'; import { PRESETS } from '~/constants/urls'; import { COMMENT_BLOCK_RENDERERS } from '~/constants/comment'; -import { nodeLockComment, nodeEditComment } from '~/redux/node/actions'; import { CommentMenu } from '../CommentMenu'; import * as MODAL_ACTIONS from '~/redux/modal/actions'; +import { CommentForm } from '~/components/comment/CommentForm'; +import { useShallowSelect } from '~/utils/hooks/useShallowSelect'; +import { selectNode } from '~/redux/node/selectors'; interface IProps { comment: IComment; can_edit: boolean; - onDelete: typeof nodeLockComment; - onEdit: typeof nodeEditComment; + onDelete: (id: IComment['id'], isLocked: boolean) => void; modalShowPhotoswipe: typeof MODAL_ACTIONS.modalShowPhotoswipe; } -const CommentContent: FC = memo( - ({ comment, can_edit, onDelete, onEdit, modalShowPhotoswipe }) => { - const groupped = useMemo>( - () => - reduce( - (group, file) => assocPath([file.type], append(file, group[file.type]), group), - {}, - comment.files - ), - [comment] - ); +const CommentContent: FC = memo(({ comment, can_edit, onDelete, modalShowPhotoswipe }) => { + const [isEditing, setIsEditing] = useState(false); + const { current } = useShallowSelect(selectNode); - const onLockClick = useCallback(() => { - onDelete(comment.id, !comment.deleted_at); - }, [comment, onDelete]); + const startEditing = useCallback(() => setIsEditing(true), [setIsEditing]); + const stopEditing = useCallback(() => setIsEditing(false), [setIsEditing]); - const onEditClick = useCallback(() => { - onEdit(comment.id); - }, [comment, onEdit]); + const groupped = useMemo>( + () => + reduce( + (group, file) => assocPath([file.type], append(file, group[file.type]), group), + {}, + comment.files + ), + [comment] + ); - const menu = useMemo( - () => can_edit && , - [can_edit, comment, onEditClick, onLockClick] - ); + const onLockClick = useCallback(() => { + onDelete(comment.id, !comment.deleted_at); + }, [comment, onDelete]); - const blocks = useMemo( - () => - !!comment.text.trim() - ? formatCommentText(path(['user', 'username'], comment), comment.text) - : [], - [comment.text] - ); + const menu = useMemo( + () => can_edit && , + [can_edit, startEditing, onLockClick] + ); - return ( -
- {comment.text && ( - - {menu} + const blocks = useMemo( + () => + !!comment.text.trim() + ? formatCommentText(path(['user', 'username'], comment), comment.text) + : [], + [comment] + ); - - {blocks.map( - (block, key) => - COMMENT_BLOCK_RENDERERS[block.type] && - createElement(COMMENT_BLOCK_RENDERERS[block.type], { block, key }) - )} - + if (isEditing) { + return ; + } -
{getPrettyDate(comment.created_at)}
+ return ( +
+ {comment.text && ( + + {menu} + + + {blocks.map( + (block, key) => + COMMENT_BLOCK_RENDERERS[block.type] && + createElement(COMMENT_BLOCK_RENDERERS[block.type], { block, key }) + )} - )} - {groupped.image && groupped.image.length > 0 && ( -
- {menu} +
{getPrettyDate(comment.created_at)}
+ + )} -
- {groupped.image.map((file, index) => ( -
modalShowPhotoswipe(groupped.image, index)}> - {file.name} -
- ))} -
+ {groupped.image && groupped.image.length > 0 && ( +
+ {menu} -
{getPrettyDate(comment.created_at)}
-
- )} - - {groupped.audio && groupped.audio.length > 0 && ( - - {groupped.audio.map(file => ( -
- {menu} - - - -
{getPrettyDate(comment.created_at)}
+
+ {groupped.image.map((file, index) => ( +
modalShowPhotoswipe(groupped.image, index)}> + {file.name}
))} - - )} -
- ); - } -); +
+ +
{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/components/comment/CommentForm/index.tsx b/src/components/comment/CommentForm/index.tsx index 22613f08..8b0a1c8c 100644 --- a/src/components/comment/CommentForm/index.tsx +++ b/src/components/comment/CommentForm/index.tsx @@ -1,244 +1,97 @@ -import React, { - FC, - KeyboardEventHandler, - memo, - useCallback, - useEffect, - useMemo, - useState, -} from 'react'; -import { Textarea } from '~/components/input/Textarea'; -import styles from './styles.module.scss'; -import { Filler } from '~/components/containers/Filler'; +import React, { FC, useCallback, useState } from 'react'; +import { useCommentFormFormik } from '~/utils/hooks/useCommentFormFormik'; +import { FormikProvider } from 'formik'; +import { LocalCommentFormTextarea } from '~/components/comment/LocalCommentFormTextarea'; import { Button } from '~/components/input/Button'; -import assocPath from 'ramda/es/assocPath'; -import { IComment, IFileWithUUID, InputHandler } from '~/redux/types'; -import { connect } from 'react-redux'; -import * as NODE_ACTIONS from '~/redux/node/actions'; -import { selectNode } from '~/redux/node/selectors'; -import { LoaderCircle } from '~/components/input/LoaderCircle'; -import { Group } from '~/components/containers/Group'; -import { UPLOAD_SUBJECTS, UPLOAD_TARGETS, UPLOAD_TYPES } from '~/redux/uploads/constants'; -import uuid from 'uuid4'; -import * as UPLOAD_ACTIONS from '~/redux/uploads/actions'; -import { selectUploads } from '~/redux/uploads/selectors'; -import { IState } from '~/redux/store'; -import { getFileType } from '~/utils/uploader'; -import { useRandomPhrase } from '~/constants/phrases'; -import { ERROR_LITERAL } from '~/constants/errors'; -import { CommentFormAttaches } from '~/components/comment/CommentFormAttaches'; +import { FileUploaderProvider, useFileUploader } from '~/utils/hooks/fileUploader'; +import { UPLOAD_SUBJECTS, UPLOAD_TARGETS } from '~/redux/uploads/constants'; import { CommentFormAttachButtons } from '~/components/comment/CommentFormAttachButtons'; -import { CommentFormDropzone } from '~/components/comment/CommentFormDropzone'; import { CommentFormFormatButtons } from '~/components/comment/CommentFormFormatButtons'; +import { CommentFormAttaches } from '~/components/comment/CommentFormAttaches'; +import { LoaderCircle } from '~/components/input/LoaderCircle'; +import { IComment, INode } from '~/redux/types'; +import { EMPTY_COMMENT } from '~/redux/node/constants'; +import { CommentFormDropzone } from '~/components/comment/CommentFormDropzone'; +import styles from './styles.module.scss'; +import { ERROR_LITERAL } from '~/constants/errors'; +import { Group } from '~/components/containers/Group'; -const mapStateToProps = (state: IState) => ({ - node: selectNode(state), - uploads: selectUploads(state), -}); +interface IProps { + comment?: IComment; + nodeId: INode['id']; + onCancelEdit?: () => void; +} -const mapDispatchToProps = { - nodePostComment: NODE_ACTIONS.nodePostComment, - nodeCancelCommentEdit: NODE_ACTIONS.nodeCancelCommentEdit, - nodeSetCommentData: NODE_ACTIONS.nodeSetCommentData, - uploadUploadFiles: UPLOAD_ACTIONS.uploadUploadFiles, +const CommentForm: FC = ({ comment, nodeId, 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 isLoading = formik.isSubmitting || uploader.isUploading; + const isEditing = !!comment?.id; + + const clearError = useCallback(() => { + if (formik.status) { + formik.setStatus(''); + } + + if (formik.errors.text) { + formik.setErrors({ + ...formik.errors, + text: '', + }); + } + }, [formik]); + + const error = formik.status || formik.errors.text; + + return ( + +
+ + +
+ + + {!!error && ( +
+ {ERROR_LITERAL[error] || error} +
+ )} +
+ + + + + + + + {isLoading && } + + {isEditing && ( + + )} + + + +
+
+
+
+ ); }; -type IProps = ReturnType & - typeof mapDispatchToProps & { - id: number; - is_before?: boolean; - }; - -const CommentFormUnconnected: FC = memo( - ({ - node: { comment_data, is_sending_comment }, - uploads: { statuses, files }, - id, - is_before = false, - nodePostComment, - nodeSetCommentData, - uploadUploadFiles, - nodeCancelCommentEdit, - }) => { - const [textarea, setTextarea] = useState(); - const comment = useMemo(() => comment_data[id], [comment_data, id]); - - const onUpload = useCallback( - (files: File[]) => { - const items: IFileWithUUID[] = files.map( - (file: File): IFileWithUUID => ({ - file, - temp_id: uuid(), - subject: UPLOAD_SUBJECTS.COMMENT, - target: UPLOAD_TARGETS.COMMENTS, - type: getFileType(file), - }) - ); - - const temps = items.map(file => file.temp_id); - - nodeSetCommentData(id, assocPath(['temp_ids'], [...comment.temp_ids, ...temps], comment)); - uploadUploadFiles(items); - }, - [uploadUploadFiles, comment, id, nodeSetCommentData] - ); - - const onInput = useCallback( - text => { - nodeSetCommentData(id, assocPath(['text'], text, comment)); - }, - [nodeSetCommentData, comment, id] - ); - - useEffect(() => { - const temp_ids = (comment && comment.temp_ids) || []; - const added_files = temp_ids - .map(temp_uuid => statuses[temp_uuid] && statuses[temp_uuid].uuid) - .map(el => !!el && files[el]) - .filter(el => !!el && !comment.files.some(file => file && file.id === el.id)); - - const filtered_temps = temp_ids.filter( - temp_id => - statuses[temp_id] && - (!statuses[temp_id].uuid || !added_files.some(file => file.id === statuses[temp_id].uuid)) - ); - - if (added_files.length) { - nodeSetCommentData(id, { - ...comment, - temp_ids: filtered_temps, - files: [...comment.files, ...added_files], - }); - } - }, [statuses, files]); - - const isUploadingNow = useMemo(() => comment.temp_ids.length > 0, [comment.temp_ids]); - - const onSubmit = useCallback( - event => { - if (event) event.preventDefault(); - if (isUploadingNow || is_sending_comment) return; - - nodePostComment(id, is_before); - }, - [nodePostComment, id, is_before, isUploadingNow, is_sending_comment] - ); - - const onKeyDown = useCallback>( - ({ ctrlKey, key }) => { - if (!!ctrlKey && key === 'Enter') onSubmit(null); - }, - [onSubmit] - ); - - const images = useMemo( - () => comment.files.filter(file => file && file.type === UPLOAD_TYPES.IMAGE), - [comment.files] - ); - - const locked_images = useMemo( - () => - comment.temp_ids - .filter(temp => statuses[temp] && statuses[temp].type === UPLOAD_TYPES.IMAGE) - .map(temp_id => statuses[temp_id]), - [statuses, comment.temp_ids] - ); - - const audios = useMemo( - () => comment.files.filter(file => file && file.type === UPLOAD_TYPES.AUDIO), - [comment.files] - ); - - const locked_audios = useMemo( - () => - comment.temp_ids - .filter(temp => statuses[temp] && statuses[temp].type === UPLOAD_TYPES.AUDIO) - .map(temp_id => statuses[temp_id]), - [statuses, comment.temp_ids] - ); - - const onCancelEdit = useCallback(() => { - nodeCancelCommentEdit(id); - }, [nodeCancelCommentEdit, comment.id]); - - const placeholder = useRandomPhrase('SIMPLE'); - - const clearError = useCallback(() => nodeSetCommentData(id, { error: '' }), [ - id, - nodeSetCommentData, - ]); - - useEffect(() => { - if (comment.error) clearError(); - }, [comment.files, comment.text]); - - const setData = useCallback( - (data: Partial) => { - nodeSetCommentData(id, data); - }, - [nodeSetCommentData, id] - ); - - return ( - -
-
-