mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-24 20:36:40 +07:00
99: fixed comments for SWR node
This commit is contained in:
parent
a1dfcc6048
commit
6964324ffb
13 changed files with 149 additions and 268 deletions
|
@ -8,8 +8,9 @@ import classNames from 'classnames';
|
||||||
import { NEW_COMMENT_CLASSNAME } from '~/constants/comment';
|
import { NEW_COMMENT_CLASSNAME } from '~/constants/comment';
|
||||||
|
|
||||||
type IProps = HTMLAttributes<HTMLDivElement> & {
|
type IProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
is_empty?: boolean;
|
nodeId: number;
|
||||||
is_loading?: boolean;
|
isEmpty?: boolean;
|
||||||
|
isLoading?: boolean;
|
||||||
group: ICommentGroup;
|
group: ICommentGroup;
|
||||||
isSame?: boolean;
|
isSame?: boolean;
|
||||||
canEdit?: boolean;
|
canEdit?: boolean;
|
||||||
|
@ -20,9 +21,10 @@ type IProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
const Comment: FC<IProps> = memo(
|
const Comment: FC<IProps> = memo(
|
||||||
({
|
({
|
||||||
group,
|
group,
|
||||||
is_empty,
|
nodeId,
|
||||||
|
isEmpty,
|
||||||
isSame,
|
isSame,
|
||||||
is_loading,
|
isLoading,
|
||||||
className,
|
className,
|
||||||
canEdit,
|
canEdit,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
@ -34,8 +36,8 @@ const Comment: FC<IProps> = memo(
|
||||||
className={classNames(className, {
|
className={classNames(className, {
|
||||||
[NEW_COMMENT_CLASSNAME]: group.hasNew,
|
[NEW_COMMENT_CLASSNAME]: group.hasNew,
|
||||||
})}
|
})}
|
||||||
isEmpty={is_empty}
|
isEmpty={isEmpty}
|
||||||
isLoading={is_loading}
|
isLoading={isLoading}
|
||||||
user={group.user}
|
user={group.user}
|
||||||
isNew={group.hasNew && !isSame}
|
isNew={group.hasNew && !isSame}
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -48,9 +50,10 @@ const Comment: FC<IProps> = memo(
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommentContent
|
<CommentContent
|
||||||
|
nodeId={nodeId}
|
||||||
comment={comment}
|
comment={comment}
|
||||||
key={comment.id}
|
key={comment.id}
|
||||||
can_edit={!!canEdit}
|
canEdit={!!canEdit}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
onShowImageModal={onShowImageModal}
|
onShowImageModal={onShowImageModal}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -13,106 +13,108 @@ import { PRESETS } from '~/constants/urls';
|
||||||
import { COMMENT_BLOCK_RENDERERS } from '~/constants/comment';
|
import { COMMENT_BLOCK_RENDERERS } from '~/constants/comment';
|
||||||
import { CommentMenu } from '../CommentMenu';
|
import { CommentMenu } from '../CommentMenu';
|
||||||
import { CommentForm } from '~/components/comment/CommentForm';
|
import { CommentForm } from '~/components/comment/CommentForm';
|
||||||
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
|
||||||
import { selectNode } from '~/redux/node/selectors';
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
|
nodeId: number;
|
||||||
comment: IComment;
|
comment: IComment;
|
||||||
can_edit: boolean;
|
canEdit: boolean;
|
||||||
onDelete: (id: IComment['id'], isLocked: boolean) => void;
|
onDelete: (id: IComment['id'], isLocked: boolean) => void;
|
||||||
onShowImageModal: (images: IFile[], index: number) => void;
|
onShowImageModal: (images: IFile[], index: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CommentContent: FC<IProps> = memo(({ comment, can_edit, onDelete, onShowImageModal }) => {
|
const CommentContent: FC<IProps> = memo(
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
({ comment, canEdit, nodeId, onDelete, onShowImageModal }) => {
|
||||||
const { current } = useShallowSelect(selectNode);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
|
||||||
const startEditing = useCallback(() => setIsEditing(true), [setIsEditing]);
|
const startEditing = useCallback(() => setIsEditing(true), [setIsEditing]);
|
||||||
const stopEditing = useCallback(() => setIsEditing(false), [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(
|
||||||
(group, file) =>
|
(group, file) =>
|
||||||
file.type ? assocPath([file.type], append(file, group[file.type]), group) : group,
|
file.type ? assocPath([file.type], append(file, group[file.type]), group) : group,
|
||||||
{},
|
{},
|
||||||
comment.files
|
comment.files
|
||||||
),
|
),
|
||||||
[comment]
|
[comment]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onLockClick = useCallback(() => {
|
const onLockClick = useCallback(() => {
|
||||||
onDelete(comment.id, !comment.deleted_at);
|
onDelete(comment.id, !comment.deleted_at);
|
||||||
}, [comment, onDelete]);
|
}, [comment, onDelete]);
|
||||||
|
|
||||||
const menu = useMemo(
|
const menu = useMemo(
|
||||||
() => can_edit && <CommentMenu onDelete={onLockClick} onEdit={startEditing} />,
|
() => canEdit && <CommentMenu onDelete={onLockClick} onEdit={startEditing} />,
|
||||||
[can_edit, startEditing, onLockClick]
|
[canEdit, startEditing, onLockClick]
|
||||||
);
|
);
|
||||||
|
|
||||||
const blocks = useMemo(
|
const blocks = useMemo(
|
||||||
() =>
|
() =>
|
||||||
!!comment.text.trim()
|
!!comment.text.trim()
|
||||||
? formatCommentText(path(['user', 'username'], comment), comment.text)
|
? formatCommentText(path(['user', 'username'], comment), comment.text)
|
||||||
: [],
|
: [],
|
||||||
[comment]
|
[comment]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
return <CommentForm nodeId={current.id} comment={comment} onCancelEdit={stopEditing} />;
|
return <CommentForm nodeId={nodeId} comment={comment} onCancelEdit={stopEditing} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrap}>
|
<div className={styles.wrap}>
|
||||||
{comment.text && (
|
{comment.text && (
|
||||||
<Group className={classnames(styles.block, styles.block_text)}>
|
<Group className={classnames(styles.block, styles.block_text)}>
|
||||||
{menu}
|
{menu}
|
||||||
|
|
||||||
<Group className={styles.renderers}>
|
<Group className={styles.renderers}>
|
||||||
{blocks.map(
|
{blocks.map(
|
||||||
(block, key) =>
|
(block, key) =>
|
||||||
COMMENT_BLOCK_RENDERERS[block.type] &&
|
COMMENT_BLOCK_RENDERERS[block.type] &&
|
||||||
createElement(COMMENT_BLOCK_RENDERERS[block.type], { block, key })
|
createElement(COMMENT_BLOCK_RENDERERS[block.type], { block, key })
|
||||||
)}
|
)}
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<div className={styles.date}>{getPrettyDate(comment.created_at)}</div>
|
||||||
</Group>
|
</Group>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className={styles.date}>{getPrettyDate(comment.created_at)}</div>
|
{groupped.image && groupped.image.length > 0 && (
|
||||||
</Group>
|
<div className={classnames(styles.block, styles.block_image)}>
|
||||||
)}
|
{menu}
|
||||||
|
|
||||||
{groupped.image && groupped.image.length > 0 && (
|
<div
|
||||||
<div className={classnames(styles.block, styles.block_image)}>
|
className={classNames(styles.images, {
|
||||||
{menu}
|
[styles.multiple]: groupped.image.length > 1,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{groupped.image.map((file, index) => (
|
||||||
|
<div key={file.id} onClick={() => onShowImageModal(groupped.image, index)}>
|
||||||
|
<img src={getURL(file, PRESETS['600'])} alt={file.name} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div className={styles.date}>{getPrettyDate(comment.created_at)}</div>
|
||||||
className={classNames(styles.images, { [styles.multiple]: groupped.image.length > 1 })}
|
</div>
|
||||||
>
|
)}
|
||||||
{groupped.image.map((file, index) => (
|
|
||||||
<div key={file.id} onClick={() => onShowImageModal(groupped.image, index)}>
|
{groupped.audio && groupped.audio.length > 0 && (
|
||||||
<img src={getURL(file, PRESETS['600'])} alt={file.name} />
|
<Fragment>
|
||||||
|
{groupped.audio.map(file => (
|
||||||
|
<div className={classnames(styles.block, styles.block_audio)} key={file.id}>
|
||||||
|
{menu}
|
||||||
|
|
||||||
|
<AudioPlayer file={file} />
|
||||||
|
|
||||||
|
<div className={styles.date}>{getPrettyDate(comment.created_at)}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</Fragment>
|
||||||
|
)}
|
||||||
<div className={styles.date}>{getPrettyDate(comment.created_at)}</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
)}
|
}
|
||||||
|
);
|
||||||
{groupped.audio && groupped.audio.length > 0 && (
|
|
||||||
<Fragment>
|
|
||||||
{groupped.audio.map(file => (
|
|
||||||
<div className={classnames(styles.block, styles.block_audio)} key={file.id}>
|
|
||||||
{menu}
|
|
||||||
|
|
||||||
<AudioPlayer file={file} />
|
|
||||||
|
|
||||||
<div className={styles.date}>{getPrettyDate(comment.created_at)}</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</Fragment>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export { CommentContent };
|
export { CommentContent };
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { useGrouppedComments } from '~/utils/hooks/node/useGrouppedComments';
|
||||||
import { useCommentContext } from '~/utils/context/CommentContextProvider';
|
import { useCommentContext } from '~/utils/context/CommentContextProvider';
|
||||||
import { Comment } from '~/components/comment/Comment';
|
import { Comment } from '~/components/comment/Comment';
|
||||||
import { useUserContext } from '~/utils/context/UserContextProvider';
|
import { useUserContext } from '~/utils/context/UserContextProvider';
|
||||||
|
import { useNodeContext } from '~/utils/context/NodeContextProvider';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
order: 'ASC' | 'DESC';
|
order: 'ASC' | 'DESC';
|
||||||
|
@ -16,6 +17,8 @@ interface IProps {
|
||||||
|
|
||||||
const NodeComments: FC<IProps> = memo(({ order }) => {
|
const NodeComments: FC<IProps> = memo(({ order }) => {
|
||||||
const user = useUserContext();
|
const user = useUserContext();
|
||||||
|
const { node } = useNodeContext();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
comments,
|
comments,
|
||||||
count,
|
count,
|
||||||
|
@ -41,12 +44,17 @@ const NodeComments: FC<IProps> = memo(({ order }) => {
|
||||||
[left, onLoadMoreComments]
|
[left, onLoadMoreComments]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!node?.id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrap}>
|
<div className={styles.wrap}>
|
||||||
{order === 'DESC' && more}
|
{order === 'DESC' && more}
|
||||||
|
|
||||||
{groupped.map(group => (
|
{groupped.map(group => (
|
||||||
<Comment
|
<Comment
|
||||||
|
nodeId={node.id!}
|
||||||
key={group.ids.join()}
|
key={group.ids.join()}
|
||||||
group={group}
|
group={group}
|
||||||
canEdit={canEditComment(group, user)}
|
canEdit={canEditComment(group, user)}
|
||||||
|
|
|
@ -47,7 +47,7 @@ export const nodePostLocalComment = (
|
||||||
nodeId,
|
nodeId,
|
||||||
comment,
|
comment,
|
||||||
callback,
|
callback,
|
||||||
type: NODE_ACTIONS.POST_COMMENT,
|
type: NODE_ACTIONS.POST_LOCAL_COMMENT,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const nodeSetSendingComment = (is_sending_comment: boolean) => ({
|
export const nodeSetSendingComment = (is_sending_comment: boolean) => ({
|
||||||
|
@ -60,34 +60,6 @@ export const nodeSetComments = (comments: IComment[]) => ({
|
||||||
type: NODE_ACTIONS.SET_COMMENTS,
|
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']) => ({
|
export const nodeLike = (id: INode['id']) => ({
|
||||||
type: NODE_ACTIONS.LIKE,
|
type: NODE_ACTIONS.LIKE,
|
||||||
id,
|
id,
|
||||||
|
@ -104,17 +76,13 @@ export const nodeLock = (id: INode['id'], is_locked: boolean) => ({
|
||||||
is_locked,
|
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,
|
type: NODE_ACTIONS.LOCK_COMMENT,
|
||||||
|
nodeId,
|
||||||
id,
|
id,
|
||||||
is_locked,
|
is_locked,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const nodeEditComment = (id: IComment['id']) => ({
|
|
||||||
type: NODE_ACTIONS.EDIT_COMMENT,
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const nodeSetEditor = (editor: INode) => ({
|
export const nodeSetEditor = (editor: INode) => ({
|
||||||
type: NODE_ACTIONS.SET_EDITOR,
|
type: NODE_ACTIONS.SET_EDITOR,
|
||||||
editor,
|
editor,
|
||||||
|
|
|
@ -29,13 +29,11 @@ export const NODE_ACTIONS = {
|
||||||
GOTO_NODE: `${prefix}GOTO_NODE`,
|
GOTO_NODE: `${prefix}GOTO_NODE`,
|
||||||
SET: `${prefix}SET`,
|
SET: `${prefix}SET`,
|
||||||
|
|
||||||
EDIT: `${prefix}EDIT`,
|
|
||||||
LIKE: `${prefix}LIKE`,
|
LIKE: `${prefix}LIKE`,
|
||||||
STAR: `${prefix}STAR`,
|
STAR: `${prefix}STAR`,
|
||||||
LOCK: `${prefix}LOCK`,
|
LOCK: `${prefix}LOCK`,
|
||||||
LOCK_COMMENT: `${prefix}LOCK_COMMENT`,
|
LOCK_COMMENT: `${prefix}LOCK_COMMENT`,
|
||||||
EDIT_COMMENT: `${prefix}EDIT_COMMENT`,
|
EDIT_COMMENT: `${prefix}EDIT_COMMENT`,
|
||||||
CREATE: `${prefix}CREATE`,
|
|
||||||
LOAD_MORE_COMMENTS: `${prefix}LOAD_MORE_COMMENTS`,
|
LOAD_MORE_COMMENTS: `${prefix}LOAD_MORE_COMMENTS`,
|
||||||
|
|
||||||
SET_SAVE_ERRORS: `${prefix}SET_SAVE_ERRORS`,
|
SET_SAVE_ERRORS: `${prefix}SET_SAVE_ERRORS`,
|
||||||
|
@ -45,13 +43,10 @@ export const NODE_ACTIONS = {
|
||||||
SET_CURRENT: `${prefix}SET_CURRENT`,
|
SET_CURRENT: `${prefix}SET_CURRENT`,
|
||||||
SET_EDITOR: `${prefix}SET_EDITOR`,
|
SET_EDITOR: `${prefix}SET_EDITOR`,
|
||||||
|
|
||||||
POST_COMMENT: `${prefix}POST_LOCAL_COMMENT`,
|
POST_LOCAL_COMMENT: `${prefix}POST_LOCAL_COMMENT`,
|
||||||
SET_COMMENTS: `${prefix}SET_COMMENTS`,
|
SET_COMMENTS: `${prefix}SET_COMMENTS`,
|
||||||
SET_RELATED: `${prefix}SET_RELATED`,
|
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`,
|
SET_COVER_IMAGE: `${prefix}SET_COVER_IMAGE`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,6 @@ import {
|
||||||
nodeSetLoadingComments,
|
nodeSetLoadingComments,
|
||||||
nodeSetSaveErrors,
|
nodeSetSaveErrors,
|
||||||
nodeSetSendingComment,
|
nodeSetSendingComment,
|
||||||
nodeSetTags,
|
|
||||||
} from './actions';
|
} from './actions';
|
||||||
import { INodeState } from './reducer';
|
import { INodeState } from './reducer';
|
||||||
|
|
||||||
|
@ -41,9 +40,6 @@ const setSendingComment = (
|
||||||
const setComments = (state: INodeState, { comments }: ReturnType<typeof nodeSetComments>) =>
|
const setComments = (state: INodeState, { comments }: ReturnType<typeof nodeSetComments>) =>
|
||||||
assocPath(['comments'], comments, state);
|
assocPath(['comments'], comments, state);
|
||||||
|
|
||||||
const setTags = (state: INodeState, { tags }: ReturnType<typeof nodeSetTags>) =>
|
|
||||||
assocPath(['current', 'tags'], tags, state);
|
|
||||||
|
|
||||||
const setEditor = (state: INodeState, { editor }: ReturnType<typeof nodeSetEditor>) =>
|
const setEditor = (state: INodeState, { editor }: ReturnType<typeof nodeSetEditor>) =>
|
||||||
assocPath(['editor'], editor, state);
|
assocPath(['editor'], editor, state);
|
||||||
|
|
||||||
|
@ -60,7 +56,6 @@ export const NODE_HANDLERS = {
|
||||||
[NODE_ACTIONS.SET_CURRENT]: setCurrent,
|
[NODE_ACTIONS.SET_CURRENT]: setCurrent,
|
||||||
[NODE_ACTIONS.SET_SENDING_COMMENT]: setSendingComment,
|
[NODE_ACTIONS.SET_SENDING_COMMENT]: setSendingComment,
|
||||||
[NODE_ACTIONS.SET_COMMENTS]: setComments,
|
[NODE_ACTIONS.SET_COMMENTS]: setComments,
|
||||||
[NODE_ACTIONS.SET_TAGS]: setTags,
|
|
||||||
[NODE_ACTIONS.SET_EDITOR]: setEditor,
|
[NODE_ACTIONS.SET_EDITOR]: setEditor,
|
||||||
[NODE_ACTIONS.SET_COVER_IMAGE]: setCoverImage,
|
[NODE_ACTIONS.SET_COVER_IMAGE]: setCoverImage,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,11 +1,7 @@
|
||||||
import { call, put, select, takeLatest, takeLeading } from 'redux-saga/effects';
|
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 { COMMENTS_DISPLAY, EMPTY_NODE, NODE_ACTIONS, NODE_EDITOR_DATA } from './constants';
|
||||||
import {
|
import {
|
||||||
nodeCreate,
|
|
||||||
nodeDeleteTag,
|
|
||||||
nodeEdit,
|
|
||||||
nodeGotoNode,
|
nodeGotoNode,
|
||||||
nodeLike,
|
nodeLike,
|
||||||
nodeLoadNode,
|
nodeLoadNode,
|
||||||
|
@ -15,15 +11,9 @@ import {
|
||||||
nodeSet,
|
nodeSet,
|
||||||
nodeSetComments,
|
nodeSetComments,
|
||||||
nodeSetCurrent,
|
nodeSetCurrent,
|
||||||
nodeSetEditor,
|
|
||||||
nodeSetLoading,
|
|
||||||
nodeSetLoadingComments,
|
nodeSetLoadingComments,
|
||||||
nodeSetTags,
|
|
||||||
nodeUpdateTags,
|
|
||||||
} from './actions';
|
} from './actions';
|
||||||
import {
|
import {
|
||||||
apiDeleteNodeTag,
|
|
||||||
apiGetNode,
|
|
||||||
apiGetNodeComments,
|
apiGetNodeComments,
|
||||||
apiLockComment,
|
apiLockComment,
|
||||||
apiLockNode,
|
apiLockNode,
|
||||||
|
@ -44,6 +34,7 @@ import { has } from 'ramda';
|
||||||
import { selectLabListNodes } from '~/redux/lab/selectors';
|
import { selectLabListNodes } from '~/redux/lab/selectors';
|
||||||
import { labSetList } from '~/redux/lab/actions';
|
import { labSetList } from '~/redux/lab/actions';
|
||||||
import { apiPostNode } from '~/redux/node/api';
|
import { apiPostNode } from '~/redux/node/api';
|
||||||
|
import { showErrorToast } from '~/utils/errors/showToast';
|
||||||
|
|
||||||
export function* updateNodeEverywhere(node) {
|
export function* updateNodeEverywhere(node) {
|
||||||
const {
|
const {
|
||||||
|
@ -127,22 +118,9 @@ function* nodeGetComments(id: INode['id']) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function* onNodeLoad({ id }: ReturnType<typeof nodeLoadNode>) {
|
function* onNodeLoad({ id }: ReturnType<typeof nodeLoadNode>) {
|
||||||
// Get node body
|
|
||||||
try {
|
|
||||||
yield put(nodeSetLoading(true));
|
|
||||||
yield put(nodeSetLoadingComments(true));
|
|
||||||
|
|
||||||
const { node, last_seen }: Unwrap<typeof apiGetNode> = 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
|
// Comments
|
||||||
try {
|
try {
|
||||||
|
yield put(nodeSetLoadingComments(true));
|
||||||
yield call(nodeGetComments, id);
|
yield call(nodeGetComments, id);
|
||||||
|
|
||||||
yield put(
|
yield put(
|
||||||
|
@ -160,82 +138,24 @@ function* onPostComment({ nodeId, comment, callback }: ReturnType<typeof nodePos
|
||||||
id: nodeId,
|
id: nodeId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { current }: ReturnType<typeof selectNode> = yield select(selectNode);
|
const { comments }: ReturnType<typeof selectNode> = yield select(selectNode);
|
||||||
|
|
||||||
if (current?.id === nodeId) {
|
if (!comment.id) {
|
||||||
const { comments }: ReturnType<typeof selectNode> = yield select(selectNode);
|
yield put(nodeSetComments([data.comment, ...comments]));
|
||||||
|
} else {
|
||||||
if (!comment.id) {
|
yield put(
|
||||||
yield put(nodeSetComments([data.comment, ...comments]));
|
nodeSet({
|
||||||
} else {
|
comments: comments.map(item => (item.id === comment.id ? data.comment : item)),
|
||||||
yield put(
|
})
|
||||||
nodeSet({
|
);
|
||||||
comments: comments.map(item => (item.id === comment.id ? data.comment : item)),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
callback();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
callback();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return callback(error.message);
|
return callback(error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function* onUpdateTags({ id, tags }: ReturnType<typeof nodeUpdateTags>) {
|
|
||||||
try {
|
|
||||||
const { node }: Unwrap<typeof apiPostNodeTags> = yield call(apiPostNodeTags, { id, tags });
|
|
||||||
const { current }: ReturnType<typeof selectNode> = yield select(selectNode);
|
|
||||||
if (!node || !node.id || node.id !== current.id) return;
|
|
||||||
yield put(nodeSetTags(node.tags));
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
function* onDeleteTag({ id, tagId }: ReturnType<typeof nodeDeleteTag>) {
|
|
||||||
try {
|
|
||||||
const { tags }: Unwrap<typeof apiDeleteNodeTag> = yield call(apiDeleteNodeTag, { id, tagId });
|
|
||||||
yield put(nodeSetTags(tags));
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
function* onCreateSaga({ node_type: type, isLab }: ReturnType<typeof nodeCreate>) {
|
|
||||||
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<typeof nodeEdit>) {
|
|
||||||
try {
|
|
||||||
if (!id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
yield put(modalShowDialog(DIALOGS.LOADING));
|
|
||||||
|
|
||||||
const { node }: Unwrap<typeof apiGetNode> = 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<typeof nodeLike>) {
|
function* onLikeSaga({ id }: ReturnType<typeof nodeLike>) {
|
||||||
const { current }: ReturnType<typeof selectNode> = yield select(selectNode);
|
const { current }: ReturnType<typeof selectNode> = yield select(selectNode);
|
||||||
|
|
||||||
|
@ -293,22 +213,12 @@ function* onLockSaga({ id, is_locked }: ReturnType<typeof nodeLock>) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function* onLockCommentSaga({ id, is_locked }: ReturnType<typeof nodeLockComment>) {
|
function* onLockCommentSaga({ nodeId, id, is_locked }: ReturnType<typeof nodeLockComment>) {
|
||||||
const { current, comments }: ReturnType<typeof selectNode> = yield select(selectNode);
|
const { comments }: ReturnType<typeof selectNode> = yield select(selectNode);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
yield put(
|
|
||||||
nodeSetComments(
|
|
||||||
comments.map(comment =>
|
|
||||||
comment.id === id
|
|
||||||
? { ...comment, deleted_at: is_locked ? new Date().toISOString() : undefined }
|
|
||||||
: comment
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const data: Unwrap<typeof apiLockComment> = yield call(apiLockComment, {
|
const data: Unwrap<typeof apiLockComment> = yield call(apiLockComment, {
|
||||||
current: current.id,
|
current: nodeId,
|
||||||
id,
|
id,
|
||||||
is_locked,
|
is_locked,
|
||||||
});
|
});
|
||||||
|
@ -320,25 +230,15 @@ function* onLockCommentSaga({ id, is_locked }: ReturnType<typeof nodeLockComment
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
} catch {
|
} catch (e) {
|
||||||
yield put(
|
showErrorToast(e);
|
||||||
nodeSetComments(
|
|
||||||
comments.map(comment =>
|
|
||||||
comment.id === id ? { ...comment, deleted_at: current.deleted_at } : comment
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function* nodeSaga() {
|
export default function* nodeSaga() {
|
||||||
yield takeLatest(NODE_ACTIONS.GOTO_NODE, onNodeGoto);
|
yield takeLatest(NODE_ACTIONS.GOTO_NODE, onNodeGoto);
|
||||||
yield takeLatest(NODE_ACTIONS.LOAD_NODE, onNodeLoad);
|
yield takeLatest(NODE_ACTIONS.LOAD_NODE, onNodeLoad);
|
||||||
yield takeLatest(NODE_ACTIONS.POST_COMMENT, onPostComment);
|
yield takeLatest(NODE_ACTIONS.POST_LOCAL_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.LIKE, onLikeSaga);
|
yield takeLatest(NODE_ACTIONS.LIKE, onLikeSaga);
|
||||||
yield takeLatest(NODE_ACTIONS.STAR, onStarSaga);
|
yield takeLatest(NODE_ACTIONS.STAR, onStarSaga);
|
||||||
yield takeLatest(NODE_ACTIONS.LOCK, onLockSaga);
|
yield takeLatest(NODE_ACTIONS.LOCK, onLockSaga);
|
||||||
|
|
9
src/utils/errors/showToast.ts
Normal file
9
src/utils/errors/showToast.ts
Normal file
|
@ -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);
|
||||||
|
};
|
|
@ -13,7 +13,7 @@ export const useFullNode = (id: string) => {
|
||||||
lastSeenCurrent,
|
lastSeenCurrent,
|
||||||
} = useShallowSelect(selectNode);
|
} = useShallowSelect(selectNode);
|
||||||
|
|
||||||
// useLoadNode(id);
|
useLoadNode(id);
|
||||||
// useOnNodeSeen(node);
|
// useOnNodeSeen(node);
|
||||||
|
|
||||||
return { node, comments, commentsCount, lastSeenCurrent, isLoading, isLoadingComments };
|
return { node, comments, commentsCount, lastSeenCurrent, isLoading, isLoadingComments };
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { nodeGotoNode, nodeSetCurrent } from '~/redux/node/actions';
|
import { nodeGotoNode } from '~/redux/node/actions';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { EMPTY_NODE } from '~/redux/node/constants';
|
|
||||||
|
|
||||||
// useLoadNode loads node on id change
|
// useLoadNode loads node on id change
|
||||||
export const useLoadNode = (id: any) => {
|
export const useLoadNode = (id: any) => {
|
||||||
|
@ -9,9 +8,5 @@ export const useLoadNode = (id: any) => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(nodeGotoNode(parseInt(id, 10), undefined));
|
dispatch(nodeGotoNode(parseInt(id, 10), undefined));
|
||||||
|
|
||||||
return () => {
|
|
||||||
dispatch(nodeSetCurrent(EMPTY_NODE));
|
|
||||||
};
|
|
||||||
}, [dispatch, id]);
|
}, [dispatch, id]);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,12 +1,21 @@
|
||||||
import { INode } from '~/redux/types';
|
import { INode } from '~/redux/types';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useDispatch } from 'react-redux';
|
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) => {
|
export const useNodeActions = (node: INode) => {
|
||||||
const dispatch = useDispatch();
|
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 onLike = useCallback(() => dispatch(nodeLike(node.id)), [dispatch, node]);
|
||||||
const onStar = useCallback(() => dispatch(nodeStar(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]);
|
const onLock = useCallback(() => dispatch(nodeLock(node.id, !node.deleted_at)), [dispatch, node]);
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { nodeLoadMoreComments, nodeLockComment } from '~/redux/node/actions';
|
import { nodeLoadMoreComments, nodeLockComment } from '~/redux/node/actions';
|
||||||
import { IComment, INode } from '~/redux/types';
|
import { IComment } from '~/redux/types';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
export const useNodeComments = (id: INode['id']) => {
|
export const useNodeComments = (nodeId: number) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const onLoadMoreComments = useCallback(() => dispatch(nodeLoadMoreComments()), [dispatch]);
|
const onLoadMoreComments = useCallback(() => dispatch(nodeLoadMoreComments()), [dispatch]);
|
||||||
|
|
||||||
const onDelete = useCallback(
|
const onDelete = useCallback(
|
||||||
(id: IComment['id'], locked: boolean) => dispatch(nodeLockComment(id, locked)),
|
(id: IComment['id'], locked: boolean) => dispatch(nodeLockComment(id, locked, nodeId)),
|
||||||
[dispatch]
|
[dispatch, nodeId]
|
||||||
);
|
);
|
||||||
|
|
||||||
return { onLoadMoreComments, onDelete };
|
return { onLoadMoreComments, onDelete };
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import { useDispatch } from 'react-redux';
|
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { nodeDeleteTag, nodeUpdateTags } from '~/redux/node/actions';
|
|
||||||
import { ITag } from '~/redux/types';
|
import { ITag } from '~/redux/types';
|
||||||
import { URLS } from '~/constants/urls';
|
import { URLS } from '~/constants/urls';
|
||||||
import { useGetNode } from '~/utils/hooks/data/useGetNode';
|
import { useGetNode } from '~/utils/hooks/data/useGetNode';
|
||||||
|
@ -9,7 +7,6 @@ import { apiDeleteNodeTag, apiPostNodeTags } from '~/redux/node/api';
|
||||||
|
|
||||||
export const useNodeTags = (id: number) => {
|
export const useNodeTags = (id: number) => {
|
||||||
const { update } = useGetNode(id);
|
const { update } = useGetNode(id);
|
||||||
const dispatch = useDispatch();
|
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue