mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-25 04:46:40 +07:00
#34 made local comment form uploads
This commit is contained in:
parent
f45e34f330
commit
051b199d5d
14 changed files with 422 additions and 189 deletions
|
@ -24,7 +24,7 @@
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.1",
|
||||||
"react-popper": "^2.2.3",
|
"react-popper": "^2.2.3",
|
||||||
"react-redux": "^6.0.1",
|
"react-redux": "^7.2.2",
|
||||||
"react-router": "^5.1.2",
|
"react-router": "^5.1.2",
|
||||||
"react-router-dom": "^5.1.2",
|
"react-router-dom": "^5.1.2",
|
||||||
"react-scripts": "3.4.4",
|
"react-scripts": "3.4.4",
|
||||||
|
|
|
@ -5,7 +5,6 @@ import { CommentContent } from '~/components/comment/CommentContent';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { nodeEditComment, nodeLockComment } from '~/redux/node/actions';
|
import { nodeEditComment, nodeLockComment } from '~/redux/node/actions';
|
||||||
import { INodeState } from '~/redux/node/reducer';
|
import { INodeState } from '~/redux/node/reducer';
|
||||||
import { CommentForm } from '../CommentForm';
|
|
||||||
import { CommendDeleted } from '../../node/CommendDeleted';
|
import { CommendDeleted } from '../../node/CommendDeleted';
|
||||||
import * as MODAL_ACTIONS from '~/redux/modal/actions';
|
import * as MODAL_ACTIONS from '~/redux/modal/actions';
|
||||||
|
|
||||||
|
@ -50,17 +49,12 @@ const Comment: FC<IProps> = memo(
|
||||||
return <CommendDeleted id={comment.id} onDelete={onDelete} key={comment.id} />;
|
return <CommendDeleted id={comment.id} onDelete={onDelete} key={comment.id} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.prototype.hasOwnProperty.call(comment_data, comment.id)) {
|
|
||||||
return <CommentForm id={comment.id} key={comment.id} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommentContent
|
<CommentContent
|
||||||
comment={comment}
|
comment={comment}
|
||||||
key={comment.id}
|
key={comment.id}
|
||||||
can_edit={!!can_edit}
|
can_edit={!!can_edit}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
onEdit={onEdit}
|
|
||||||
modalShowPhotoswipe={modalShowPhotoswipe}
|
modalShowPhotoswipe={modalShowPhotoswipe}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,31 +1,36 @@
|
||||||
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 { IComment, IFile } from '~/redux/types';
|
||||||
import { path } from 'ramda';
|
import { append, assocPath, path } from 'ramda';
|
||||||
import { formatCommentText, getURL, getPrettyDate } from '~/utils/dom';
|
import { formatCommentText, getPrettyDate, getURL } from '~/utils/dom';
|
||||||
import { Group } from '~/components/containers/Group';
|
import { Group } from '~/components/containers/Group';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { UPLOAD_TYPES } from '~/redux/uploads/constants';
|
import { UPLOAD_TYPES } from '~/redux/uploads/constants';
|
||||||
import { assocPath } from 'ramda';
|
|
||||||
import { append } from 'ramda';
|
|
||||||
import reduce from 'ramda/es/reduce';
|
import reduce from 'ramda/es/reduce';
|
||||||
import { AudioPlayer } from '~/components/media/AudioPlayer';
|
import { AudioPlayer } from '~/components/media/AudioPlayer';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { PRESETS } from '~/constants/urls';
|
import { PRESETS } from '~/constants/urls';
|
||||||
import { COMMENT_BLOCK_RENDERERS } from '~/constants/comment';
|
import { COMMENT_BLOCK_RENDERERS } from '~/constants/comment';
|
||||||
import { nodeLockComment, nodeEditComment } from '~/redux/node/actions';
|
import { nodeLockComment } from '~/redux/node/actions';
|
||||||
import { CommentMenu } from '../CommentMenu';
|
import { CommentMenu } from '../CommentMenu';
|
||||||
import * as MODAL_ACTIONS from '~/redux/modal/actions';
|
import * as MODAL_ACTIONS from '~/redux/modal/actions';
|
||||||
|
import { LocalCommentForm } from '~/components/comment/LocalCommentForm';
|
||||||
|
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
||||||
|
import { selectNode } from '~/redux/node/selectors';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
comment: IComment;
|
comment: IComment;
|
||||||
can_edit: boolean;
|
can_edit: boolean;
|
||||||
onDelete: typeof nodeLockComment;
|
onDelete: typeof nodeLockComment;
|
||||||
onEdit: typeof nodeEditComment;
|
|
||||||
modalShowPhotoswipe: typeof MODAL_ACTIONS.modalShowPhotoswipe;
|
modalShowPhotoswipe: typeof MODAL_ACTIONS.modalShowPhotoswipe;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CommentContent: FC<IProps> = memo(
|
const CommentContent: FC<IProps> = memo(({ comment, can_edit, onDelete, modalShowPhotoswipe }) => {
|
||||||
({ comment, can_edit, onDelete, onEdit, modalShowPhotoswipe }) => {
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const { current } = useShallowSelect(selectNode);
|
||||||
|
|
||||||
|
const startEditing = useCallback(() => setIsEditing(true), [setIsEditing]);
|
||||||
|
const stopEditing = useCallback(() => setIsEditing(false), [setIsEditing]);
|
||||||
|
|
||||||
const groupped = useMemo<Record<keyof typeof UPLOAD_TYPES, IFile[]>>(
|
const groupped = useMemo<Record<keyof typeof UPLOAD_TYPES, IFile[]>>(
|
||||||
() =>
|
() =>
|
||||||
reduce(
|
reduce(
|
||||||
|
@ -40,13 +45,9 @@ const CommentContent: FC<IProps> = memo(
|
||||||
onDelete(comment.id, !comment.deleted_at);
|
onDelete(comment.id, !comment.deleted_at);
|
||||||
}, [comment, onDelete]);
|
}, [comment, onDelete]);
|
||||||
|
|
||||||
const onEditClick = useCallback(() => {
|
|
||||||
onEdit(comment.id);
|
|
||||||
}, [comment, onEdit]);
|
|
||||||
|
|
||||||
const menu = useMemo(
|
const menu = useMemo(
|
||||||
() => can_edit && <CommentMenu onDelete={onLockClick} onEdit={onEditClick} />,
|
() => can_edit && <CommentMenu onDelete={onLockClick} onEdit={startEditing} />,
|
||||||
[can_edit, comment, onEditClick, onLockClick]
|
[can_edit, startEditing, onLockClick]
|
||||||
);
|
);
|
||||||
|
|
||||||
const blocks = useMemo(
|
const blocks = useMemo(
|
||||||
|
@ -54,9 +55,13 @@ const CommentContent: FC<IProps> = memo(
|
||||||
!!comment.text.trim()
|
!!comment.text.trim()
|
||||||
? formatCommentText(path(['user', 'username'], comment), comment.text)
|
? formatCommentText(path(['user', 'username'], comment), comment.text)
|
||||||
: [],
|
: [],
|
||||||
[comment.text]
|
[comment]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (isEditing) {
|
||||||
|
return <LocalCommentForm nodeId={current.id} comment={comment} onCancelEdit={stopEditing} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrap}>
|
<div className={styles.wrap}>
|
||||||
{comment.text && (
|
{comment.text && (
|
||||||
|
@ -106,7 +111,6 @@ const CommentContent: FC<IProps> = memo(
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export { CommentContent };
|
export { CommentContent };
|
||||||
|
|
|
@ -1,12 +1,4 @@
|
||||||
import React, {
|
import React, { FC, KeyboardEventHandler, memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
FC,
|
|
||||||
KeyboardEventHandler,
|
|
||||||
memo,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import { Textarea } from '~/components/input/Textarea';
|
import { Textarea } from '~/components/input/Textarea';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { Filler } from '~/components/containers/Filler';
|
import { Filler } from '~/components/containers/Filler';
|
||||||
|
@ -30,7 +22,6 @@ import { CommentFormAttaches } from '~/components/comment/CommentFormAttaches';
|
||||||
import { CommentFormAttachButtons } from '~/components/comment/CommentFormAttachButtons';
|
import { CommentFormAttachButtons } from '~/components/comment/CommentFormAttachButtons';
|
||||||
import { CommentFormDropzone } from '~/components/comment/CommentFormDropzone';
|
import { CommentFormDropzone } from '~/components/comment/CommentFormDropzone';
|
||||||
import { CommentFormFormatButtons } from '~/components/comment/CommentFormFormatButtons';
|
import { CommentFormFormatButtons } from '~/components/comment/CommentFormFormatButtons';
|
||||||
import { LocalCommentForm } from '~/components/comment/LocalCommentForm';
|
|
||||||
|
|
||||||
const mapStateToProps = (state: IState) => ({
|
const mapStateToProps = (state: IState) => ({
|
||||||
node: selectNode(state),
|
node: selectNode(state),
|
||||||
|
@ -237,8 +228,6 @@ const CommentFormUnconnected: FC<IProps> = memo(
|
||||||
</Group>
|
</Group>
|
||||||
</form>
|
</form>
|
||||||
</CommentFormDropzone>
|
</CommentFormDropzone>
|
||||||
|
|
||||||
<LocalCommentForm />
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +1,64 @@
|
||||||
import React, { FC, useState } from 'react';
|
import React, { FC, useState } from 'react';
|
||||||
import { CommentFormValues, useCommentFormFormik } from '~/utils/hooks/useCommentFormFormik';
|
import { useCommentFormFormik } from '~/utils/hooks/useCommentFormFormik';
|
||||||
import { FormikProvider } from 'formik';
|
import { FormikProvider } from 'formik';
|
||||||
import { LocalCommentFormTextarea } from '~/components/comment/LocalCommentFormTextarea';
|
import { LocalCommentFormTextarea } from '~/components/comment/LocalCommentFormTextarea';
|
||||||
import { Button } from '~/components/input/Button';
|
import { Button } from '~/components/input/Button';
|
||||||
|
import { FileUploaderProvider, useFileUploader } from '~/utils/hooks/fileUploader';
|
||||||
|
import { UPLOAD_SUBJECTS, UPLOAD_TARGETS } from '~/redux/uploads/constants';
|
||||||
|
import { CommentFormAttachButtons } from '~/components/comment/CommentFormAttachButtons';
|
||||||
|
import { CommentFormFormatButtons } from '~/components/comment/CommentFormFormatButtons';
|
||||||
|
import { LocalCommentFormAttaches } from '~/components/comment/LocalCommentFormAttaches';
|
||||||
|
import { LoaderCircle } from '~/components/input/LoaderCircle';
|
||||||
|
import { IComment, INode } from '~/redux/types';
|
||||||
|
import { EMPTY_COMMENT } from '~/redux/node/constants';
|
||||||
|
|
||||||
interface IProps {}
|
interface IProps {
|
||||||
|
comment?: IComment;
|
||||||
|
nodeId: INode['id'];
|
||||||
|
isBefore?: boolean;
|
||||||
|
onCancelEdit?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
const initialValues: CommentFormValues = {
|
const LocalCommentForm: FC<IProps> = ({ comment, nodeId, isBefore, onCancelEdit }) => {
|
||||||
text: '',
|
|
||||||
images: [],
|
|
||||||
songs: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const LocalCommentForm: FC<IProps> = () => {
|
|
||||||
const [textarea, setTextarea] = useState<HTMLTextAreaElement>();
|
const [textarea, setTextarea] = useState<HTMLTextAreaElement>();
|
||||||
const { formik } = useCommentFormFormik(initialValues);
|
const uploader = useFileUploader(UPLOAD_SUBJECTS.COMMENT, UPLOAD_TARGETS.COMMENTS);
|
||||||
|
const formik = useCommentFormFormik(
|
||||||
|
comment || EMPTY_COMMENT,
|
||||||
|
nodeId,
|
||||||
|
uploader,
|
||||||
|
onCancelEdit,
|
||||||
|
isBefore
|
||||||
|
);
|
||||||
|
const isLoading = formik.isSubmitting || uploader.isUploading;
|
||||||
|
const isEditing = !!comment?.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={formik.handleSubmit}>
|
<form onSubmit={formik.handleSubmit}>
|
||||||
<FormikProvider value={formik}>
|
<FormikProvider value={formik}>
|
||||||
|
<FileUploaderProvider value={uploader}>
|
||||||
<LocalCommentFormTextarea setRef={setTextarea} />
|
<LocalCommentFormTextarea setRef={setTextarea} />
|
||||||
{formik.isSubmitting && <div>LOADING</div>}
|
|
||||||
{!!formik.status && <div>error: {formik.status}</div>}
|
<CommentFormAttachButtons onUpload={uploader.uploadFiles} />
|
||||||
<Button size="small" disabled={formik.isSubmitting}>
|
<CommentFormFormatButtons element={textarea} handler={formik.handleChange('text')} />
|
||||||
SEND
|
<LocalCommentFormAttaches />
|
||||||
|
|
||||||
|
{isLoading && <LoaderCircle size={20} />}
|
||||||
|
|
||||||
|
{isEditing && (
|
||||||
|
<Button size="small" color="link" type="button" onClick={onCancelEdit}>
|
||||||
|
Отмена
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
color="gray"
|
||||||
|
iconRight={!isEditing ? 'enter' : 'check'}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{!isEditing ? 'Сказать' : 'Сохранить'}
|
||||||
|
</Button>
|
||||||
|
</FileUploaderProvider>
|
||||||
</FormikProvider>
|
</FormikProvider>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|
116
src/components/comment/LocalCommentFormAttaches/index.tsx
Normal file
116
src/components/comment/LocalCommentFormAttaches/index.tsx
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
import React, { FC, useCallback, useMemo } from 'react';
|
||||||
|
import styles from '~/components/comment/CommentForm/styles.module.scss';
|
||||||
|
import { SortableImageGrid } from '~/components/editors/SortableImageGrid';
|
||||||
|
import { SortableAudioGrid } from '~/components/editors/SortableAudioGrid';
|
||||||
|
import { IFile } from '~/redux/types';
|
||||||
|
import { SortEnd } from 'react-sortable-hoc';
|
||||||
|
import { moveArrItem } from '~/utils/fn';
|
||||||
|
import { useDropZone } from '~/utils/hooks';
|
||||||
|
import { COMMENT_FILE_TYPES, UPLOAD_TYPES } from '~/redux/uploads/constants';
|
||||||
|
import { useFileUploaderContext } from '~/utils/hooks/fileUploader';
|
||||||
|
|
||||||
|
const LocalCommentFormAttaches: FC = () => {
|
||||||
|
const { files, pending, setFiles, uploadFiles } = useFileUploaderContext();
|
||||||
|
|
||||||
|
const images = useMemo(() => files.filter(file => file && file.type === UPLOAD_TYPES.IMAGE), [
|
||||||
|
files,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const pendingImages = useMemo(() => pending.filter(item => item.type === UPLOAD_TYPES.IMAGE), [
|
||||||
|
pending,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const audios = useMemo(() => files.filter(file => file && file.type === UPLOAD_TYPES.AUDIO), [
|
||||||
|
files,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const pendingAudios = useMemo(() => pending.filter(item => item.type === UPLOAD_TYPES.AUDIO), [
|
||||||
|
pending,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const onDrop = useDropZone(uploadFiles, COMMENT_FILE_TYPES);
|
||||||
|
|
||||||
|
const hasImageAttaches = images.length > 0 || pendingImages.length > 0;
|
||||||
|
const hasAudioAttaches = audios.length > 0 || pendingAudios.length > 0;
|
||||||
|
const hasAttaches = hasImageAttaches || hasAudioAttaches;
|
||||||
|
|
||||||
|
const onImageMove = useCallback(
|
||||||
|
({ oldIndex, newIndex }: SortEnd) => {
|
||||||
|
setFiles([
|
||||||
|
...audios,
|
||||||
|
...(moveArrItem(
|
||||||
|
oldIndex,
|
||||||
|
newIndex,
|
||||||
|
images.filter(file => !!file)
|
||||||
|
) as IFile[]),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
[images, audios, setFiles]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onAudioMove = useCallback(
|
||||||
|
({ oldIndex, newIndex }: SortEnd) => {
|
||||||
|
setFiles([
|
||||||
|
...images,
|
||||||
|
...(moveArrItem(
|
||||||
|
oldIndex,
|
||||||
|
newIndex,
|
||||||
|
audios.filter(file => !!file)
|
||||||
|
) as IFile[]),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
[images, audios, setFiles]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onFileDelete = useCallback(
|
||||||
|
(fileId: IFile['id']) => {
|
||||||
|
setFiles(files.filter(file => file.id !== fileId));
|
||||||
|
},
|
||||||
|
[setFiles, files]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onAudioTitleChange = useCallback(
|
||||||
|
(fileId: IFile['id'], title: IFile['metadata']['title']) => {
|
||||||
|
setFiles(
|
||||||
|
files.map(file =>
|
||||||
|
file.id === fileId ? { ...file, metadata: { ...file.metadata, title } } : file
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[files, setFiles]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
hasAttaches && (
|
||||||
|
<div className={styles.attaches} onDropCapture={onDrop}>
|
||||||
|
{hasImageAttaches && (
|
||||||
|
<SortableImageGrid
|
||||||
|
onDelete={onFileDelete}
|
||||||
|
onSortEnd={onImageMove}
|
||||||
|
axis="xy"
|
||||||
|
items={images}
|
||||||
|
locked={pendingImages}
|
||||||
|
pressDelay={50}
|
||||||
|
helperClass={styles.helper}
|
||||||
|
size={120}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasAudioAttaches && (
|
||||||
|
<SortableAudioGrid
|
||||||
|
items={audios}
|
||||||
|
onDelete={onFileDelete}
|
||||||
|
onTitleChange={onAudioTitleChange}
|
||||||
|
onSortEnd={onAudioMove}
|
||||||
|
axis="y"
|
||||||
|
locked={pendingAudios}
|
||||||
|
pressDelay={50}
|
||||||
|
helperClass={styles.helper}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { LocalCommentFormAttaches };
|
|
@ -6,7 +6,7 @@ import React, {
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import autosize from 'autosize';
|
import autosize from 'autosize';
|
||||||
|
@ -59,14 +59,21 @@ const Textarea = memo<IProps>(
|
||||||
const onFocus = useCallback(() => setFocused(true), [setFocused]);
|
const onFocus = useCallback(() => setFocused(true), [setFocused]);
|
||||||
const onBlur = useCallback(() => setFocused(false), [setFocused]);
|
const onBlur = useCallback(() => setFocused(false), [setFocused]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const target = ref?.current;
|
||||||
|
if (!target) return;
|
||||||
|
|
||||||
|
autosize(target);
|
||||||
|
setRef(target);
|
||||||
|
|
||||||
|
return () => autosize.destroy(target);
|
||||||
|
}, [ref, setRef]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ref.current) return;
|
if (!ref.current) return;
|
||||||
|
|
||||||
autosize(ref.current);
|
autosize.update(ref.current);
|
||||||
setRef(ref.current);
|
}, [value]);
|
||||||
|
|
||||||
return () => autosize.destroy(ref.current);
|
|
||||||
}, [ref.current]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -89,7 +96,7 @@ const Textarea = memo<IProps>(
|
||||||
onFocus={onFocus}
|
onFocus={onFocus}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
style={{
|
style={{
|
||||||
maxHeight: maxRows * 20,
|
// maxHeight: maxRows * 20,
|
||||||
minHeight: minRows * 20,
|
minHeight: minRows * 20,
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
@ -1,32 +1,25 @@
|
||||||
import React, { FC, useCallback, KeyboardEventHandler, useEffect, useMemo } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { Textarea } from '~/components/input/Textarea';
|
|
||||||
import { CommentWrapper } from '~/components/containers/CommentWrapper';
|
import { CommentWrapper } from '~/components/containers/CommentWrapper';
|
||||||
import styles from './styles.module.scss';
|
|
||||||
import { Filler } from '~/components/containers/Filler';
|
|
||||||
import { Button } from '~/components/input/Button';
|
|
||||||
import { assocPath } from 'ramda';
|
|
||||||
import { InputHandler, IFileWithUUID, IFile } from '~/redux/types';
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import * as NODE_ACTIONS from '~/redux/node/actions';
|
import { selectAuthUser } from '~/redux/auth/selectors';
|
||||||
import { selectNode } from '~/redux/node/selectors';
|
|
||||||
import * as UPLOAD_ACTIONS from '~/redux/uploads/actions';
|
|
||||||
import { selectUploads } from '~/redux/uploads/selectors';
|
|
||||||
import { IState } from '~/redux/store';
|
|
||||||
import { selectUser, selectAuthUser } from '~/redux/auth/selectors';
|
|
||||||
import { CommentForm } from '../../comment/CommentForm';
|
import { CommentForm } from '../../comment/CommentForm';
|
||||||
|
import { LocalCommentForm } from '~/components/comment/LocalCommentForm';
|
||||||
|
import { INode } from '~/redux/types';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
user: selectAuthUser(state),
|
user: selectAuthUser(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
type IProps = ReturnType<typeof mapStateToProps> & {
|
type IProps = ReturnType<typeof mapStateToProps> & {
|
||||||
is_before?: boolean;
|
isBefore?: boolean;
|
||||||
|
nodeId: INode['id'];
|
||||||
};
|
};
|
||||||
|
|
||||||
const NodeCommentFormUnconnected: FC<IProps> = ({ user, is_before }) => {
|
const NodeCommentFormUnconnected: FC<IProps> = ({ user, isBefore, nodeId }) => {
|
||||||
return (
|
return (
|
||||||
<CommentWrapper user={user}>
|
<CommentWrapper user={user}>
|
||||||
<CommentForm id={0} is_before={is_before} />
|
<CommentForm id={0} is_before={isBefore} />
|
||||||
|
<LocalCommentForm isBefore={isBefore} nodeId={nodeId} />
|
||||||
</CommentWrapper>
|
</CommentWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -45,7 +45,7 @@ type IProps = ReturnType<typeof mapStateToProps> &
|
||||||
const id = 696;
|
const id = 696;
|
||||||
|
|
||||||
const BorisLayoutUnconnected: FC<IProps> = ({
|
const BorisLayoutUnconnected: FC<IProps> = ({
|
||||||
node: { is_loading, is_loading_comments, comments = [], comment_data, comment_count },
|
node: { is_loading, is_loading_comments, comments = [], comment_data, comment_count, id },
|
||||||
user,
|
user,
|
||||||
user: { is_user, last_seen_boris },
|
user: { is_user, last_seen_boris },
|
||||||
nodeLoadNode,
|
nodeLoadNode,
|
||||||
|
@ -92,7 +92,7 @@ const BorisLayoutUnconnected: FC<IProps> = ({
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<Card className={styles.content}>
|
<Card className={styles.content}>
|
||||||
<Group className={styles.grid}>
|
<Group className={styles.grid}>
|
||||||
{is_user && <NodeCommentForm is_before />}
|
{is_user && <NodeCommentForm isBefore nodeId={id} />}
|
||||||
|
|
||||||
{is_loading_comments ? (
|
{is_loading_comments ? (
|
||||||
<NodeNoComments is_loading count={7} />
|
<NodeNoComments is_loading count={7} />
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { NodeNoComments } from '~/components/node/NodeNoComments';
|
||||||
import { NodeRelated } from '~/components/node/NodeRelated';
|
import { NodeRelated } from '~/components/node/NodeRelated';
|
||||||
import { NodeComments } from '~/components/node/NodeComments';
|
import { NodeComments } from '~/components/node/NodeComments';
|
||||||
import { NodeTags } from '~/components/node/NodeTags';
|
import { NodeTags } from '~/components/node/NodeTags';
|
||||||
import { INodeComponentProps, NODE_COMPONENTS, NODE_HEADS, NODE_INLINES, } from '~/redux/node/constants';
|
import { INodeComponentProps, NODE_COMPONENTS, NODE_HEADS, NODE_INLINES } from '~/redux/node/constants';
|
||||||
import { selectUser } from '~/redux/auth/selectors';
|
import { selectUser } from '~/redux/auth/selectors';
|
||||||
import { pick } from 'ramda';
|
import { pick } from 'ramda';
|
||||||
import { NodeRelatedPlaceholder } from '~/components/node/NodeRelated/placeholder';
|
import { NodeRelatedPlaceholder } from '~/components/node/NodeRelated/placeholder';
|
||||||
|
@ -30,6 +30,7 @@ import { selectModal } from '~/redux/modal/selectors';
|
||||||
import { SidebarRouter } from '~/containers/main/SidebarRouter';
|
import { SidebarRouter } from '~/containers/main/SidebarRouter';
|
||||||
import { ITag } from '~/redux/types';
|
import { ITag } from '~/redux/types';
|
||||||
import { URLS } from '~/constants/urls';
|
import { URLS } from '~/constants/urls';
|
||||||
|
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
||||||
|
|
||||||
const mapStateToProps = (state: IState) => ({
|
const mapStateToProps = (state: IState) => ({
|
||||||
node: selectNode(state),
|
node: selectNode(state),
|
||||||
|
@ -60,15 +61,6 @@ const NodeLayoutUnconnected: FC<IProps> = memo(
|
||||||
match: {
|
match: {
|
||||||
params: { id },
|
params: { id },
|
||||||
},
|
},
|
||||||
node: {
|
|
||||||
is_loading,
|
|
||||||
is_loading_comments,
|
|
||||||
comments = [],
|
|
||||||
current: node,
|
|
||||||
related,
|
|
||||||
comment_data,
|
|
||||||
comment_count,
|
|
||||||
},
|
|
||||||
modal: { is_shown: is_modal_shown },
|
modal: { is_shown: is_modal_shown },
|
||||||
user,
|
user,
|
||||||
user: { is_user },
|
user: { is_user },
|
||||||
|
@ -86,7 +78,15 @@ const NodeLayoutUnconnected: FC<IProps> = memo(
|
||||||
}) => {
|
}) => {
|
||||||
const [layout, setLayout] = useState({});
|
const [layout, setLayout] = useState({});
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
const {
|
||||||
|
is_loading,
|
||||||
|
is_loading_comments,
|
||||||
|
comments = [],
|
||||||
|
current: node,
|
||||||
|
related,
|
||||||
|
comment_data,
|
||||||
|
comment_count,
|
||||||
|
} = useShallowSelect(selectNode);
|
||||||
const updateLayout = useCallback(() => setLayout({}), []);
|
const updateLayout = useCallback(() => setLayout({}), []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -193,7 +193,7 @@ const NodeLayoutUnconnected: FC<IProps> = memo(
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{is_user && !is_loading && <NodeCommentForm />}
|
{is_user && !is_loading && <NodeCommentForm nodeId={node.id} />}
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<div className={styles.panel}>
|
<div className={styles.panel}>
|
||||||
|
|
|
@ -230,6 +230,7 @@
|
||||||
padding: 12px 0;
|
padding: 12px 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
max-height: 60vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status,
|
.status,
|
||||||
|
|
77
src/utils/hooks/fileUploader.tsx
Normal file
77
src/utils/hooks/fileUploader.tsx
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
import React, { createContext, FC, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { IFile, IFileWithUUID } from '~/redux/types';
|
||||||
|
import { UPLOAD_SUBJECTS, UPLOAD_TARGETS } from '~/redux/uploads/constants';
|
||||||
|
import { getFileType } from '~/utils/uploader';
|
||||||
|
import uuid from 'uuid4';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { uploadUploadFiles } from '~/redux/uploads/actions';
|
||||||
|
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
||||||
|
import { selectUploads } from '~/redux/uploads/selectors';
|
||||||
|
|
||||||
|
export const useFileUploader = (
|
||||||
|
subject: typeof UPLOAD_SUBJECTS[keyof typeof UPLOAD_SUBJECTS],
|
||||||
|
target: typeof UPLOAD_TARGETS[keyof typeof UPLOAD_TARGETS]
|
||||||
|
) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { files: uploadedFiles, statuses } = useShallowSelect(selectUploads);
|
||||||
|
|
||||||
|
const [files, setFiles] = useState<IFile[]>([]);
|
||||||
|
const [pendingIDs, setPendingIDs] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const uploadFiles = useCallback(
|
||||||
|
(files: File[]) => {
|
||||||
|
const items: IFileWithUUID[] = files.map(
|
||||||
|
(file: File): IFileWithUUID => ({
|
||||||
|
file,
|
||||||
|
temp_id: uuid(),
|
||||||
|
subject,
|
||||||
|
target,
|
||||||
|
type: getFileType(file),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const temps = items.map(file => file.temp_id);
|
||||||
|
|
||||||
|
setPendingIDs([...pendingIDs, ...temps]);
|
||||||
|
dispatch(uploadUploadFiles(items));
|
||||||
|
},
|
||||||
|
[pendingIDs, setPendingIDs, dispatch, subject, target]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const added = pendingIDs
|
||||||
|
.map(temp_uuid => statuses[temp_uuid] && statuses[temp_uuid].uuid)
|
||||||
|
.map(el => !!el && uploadedFiles[el])
|
||||||
|
.filter(el => !!el && !files.some(file => file && file.id === el.id));
|
||||||
|
|
||||||
|
const newPending = pendingIDs.filter(
|
||||||
|
temp_id =>
|
||||||
|
statuses[temp_id] &&
|
||||||
|
(!statuses[temp_id].uuid || !added.some(file => file.id === statuses[temp_id].uuid))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (added.length) {
|
||||||
|
setPendingIDs(newPending);
|
||||||
|
setFiles([...files, ...added]);
|
||||||
|
}
|
||||||
|
}, [statuses, files, pendingIDs, uploadedFiles]);
|
||||||
|
|
||||||
|
const pending = useMemo(() => pendingIDs.map(id => statuses[id]).filter(el => !!el), [
|
||||||
|
statuses,
|
||||||
|
pendingIDs,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const isLoading = pending.length > 0;
|
||||||
|
|
||||||
|
return { uploadFiles, pending, files, setFiles, isUploading: isLoading };
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FileUploader = ReturnType<typeof useFileUploader>;
|
||||||
|
const FileUploaderContext = createContext<FileUploader>(null);
|
||||||
|
|
||||||
|
export const FileUploaderProvider: FC<{ value: FileUploader; children }> = ({
|
||||||
|
value,
|
||||||
|
children,
|
||||||
|
}) => <FileUploaderContext.Provider value={value}>{children}</FileUploaderContext.Provider>;
|
||||||
|
|
||||||
|
export const useFileUploaderContext = () => useContext(FileUploaderContext);
|
|
@ -1,34 +1,27 @@
|
||||||
import { IComment, IFile } from '~/redux/types';
|
import { IComment, INode } from '~/redux/types';
|
||||||
import { useCallback, useRef } from 'react';
|
import { useCallback, 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 '~/utils/hooks/fileUploader';
|
||||||
export interface CommentFormValues {
|
|
||||||
text: string;
|
|
||||||
images: IFile[];
|
|
||||||
songs: IFile[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const validationSchema = object().shape({
|
const validationSchema = object().shape({
|
||||||
text: string(),
|
text: string(),
|
||||||
images: array(),
|
files: array(),
|
||||||
songs: array(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const submitComment = async (
|
const submitComment = (
|
||||||
id: IComment['id'],
|
id: INode['id'],
|
||||||
values: CommentFormValues,
|
values: IComment,
|
||||||
callback: (e: string) => void
|
isBefore: boolean,
|
||||||
|
callback: (e?: string) => void
|
||||||
) => {
|
) => {
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
console.log('Submitting', id, values);
|
||||||
callback('wrong');
|
new Promise(resolve => setTimeout(resolve, 500)).then(() => callback());
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useCommentFormFormik = (values: CommentFormValues) => {
|
const onSuccess = ({ resetForm, setStatus, setSubmitting }: FormikHelpers<IComment>) => (
|
||||||
const { current: initialValues } = useRef(values);
|
e: string
|
||||||
|
) => {
|
||||||
const onSuccess = useCallback(
|
|
||||||
({ resetForm, setStatus, setSubmitting }: FormikHelpers<CommentFormValues>) => (e: string) => {
|
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
|
|
||||||
if (e) {
|
if (e) {
|
||||||
|
@ -39,26 +32,46 @@ export const useCommentFormFormik = (values: CommentFormValues) => {
|
||||||
if (resetForm) {
|
if (resetForm) {
|
||||||
resetForm();
|
resetForm();
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
[]
|
|
||||||
);
|
export const useCommentFormFormik = (
|
||||||
|
values: IComment,
|
||||||
|
nodeId: INode['id'],
|
||||||
|
uploader: FileUploader,
|
||||||
|
stopEditing?: () => void,
|
||||||
|
isBefore: boolean = false
|
||||||
|
) => {
|
||||||
|
const { current: initialValues } = useRef(values);
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(
|
||||||
(values: CommentFormValues, helpers: FormikHelpers<CommentFormValues>) => {
|
(values: IComment, helpers: FormikHelpers<IComment>) => {
|
||||||
helpers.setSubmitting(true);
|
helpers.setSubmitting(true);
|
||||||
submitComment(0, values, onSuccess(helpers));
|
submitComment(
|
||||||
|
nodeId,
|
||||||
|
{
|
||||||
|
...values,
|
||||||
|
files: uploader.files,
|
||||||
},
|
},
|
||||||
[values, onSuccess]
|
isBefore,
|
||||||
|
onSuccess(helpers)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[isBefore, nodeId, uploader.files]
|
||||||
);
|
);
|
||||||
|
|
||||||
const formik = useFormik({
|
const onReset = useCallback(() => {
|
||||||
|
uploader.setFiles([]);
|
||||||
|
|
||||||
|
if (stopEditing) stopEditing();
|
||||||
|
}, [uploader, stopEditing]);
|
||||||
|
|
||||||
|
return useFormik({
|
||||||
initialValues,
|
initialValues,
|
||||||
validationSchema,
|
validationSchema,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
initialStatus: '',
|
initialStatus: '',
|
||||||
|
onReset,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { formik };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useCommentFormContext = () => useFormikContext<CommentFormValues>();
|
export const useCommentFormContext = () => useFormikContext<IComment>();
|
||||||
|
|
5
src/utils/hooks/useShallowSelect.ts
Normal file
5
src/utils/hooks/useShallowSelect.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import { shallowEqual, useSelector } from 'react-redux';
|
||||||
|
import { IState } from '~/redux/store';
|
||||||
|
|
||||||
|
export const useShallowSelect = <T extends (state: IState) => any>(selector: T): ReturnType<T> =>
|
||||||
|
useSelector(selector, shallowEqual);
|
Loading…
Add table
Add a link
Reference in a new issue