From 59d544c5f4e217a685481eb38adf08f12f1c14f5 Mon Sep 17 00:00:00 2001 From: Fedor Katurov Date: Fri, 29 Nov 2019 12:21:11 +0700 Subject: [PATCH] comment locking initial --- src/components/node/Comment/index.tsx | 14 +++-- src/components/node/CommentContent/index.tsx | 28 ++++++++-- .../node/CommentContent/styles.scss | 55 +++++++++++++++++++ src/components/node/NodeComments/index.tsx | 13 ++++- src/containers/node/BorisLayout/index.tsx | 52 ++++++++---------- src/containers/node/NodeLayout/index.tsx | 7 ++- src/redux/node/actions.ts | 6 ++ src/redux/node/constants.ts | 1 + src/redux/types.ts | 1 + src/utils/node.ts | 6 +- 10 files changed, 139 insertions(+), 44 deletions(-) diff --git a/src/components/node/Comment/index.tsx b/src/components/node/Comment/index.tsx index 52298ba0..2c2e5d42 100644 --- a/src/components/node/Comment/index.tsx +++ b/src/components/node/Comment/index.tsx @@ -1,7 +1,6 @@ import React, { FC, HTMLAttributes, memo } from 'react'; import { CommentWrapper } from '~/components/containers/CommentWrapper'; -import { ICommentGroup } from '~/redux/types'; -import { getURL } from '~/utils/dom'; +import { ICommentGroup, IComment } from '~/redux/types'; import { CommentContent } from '~/components/node/CommentContent'; import * as styles from './styles.scss'; @@ -10,10 +9,12 @@ type IProps = HTMLAttributes & { is_loading?: boolean; comment_group?: ICommentGroup; is_same?: boolean; + can_edit?: boolean; + onDelete: (id: IComment['id'], is_deteted: boolean) => void; }; const Comment: FC = memo( - ({ comment_group, is_empty, is_same, is_loading, className, ...props }) => { + ({ comment_group, is_empty, is_same, is_loading, className, can_edit, onDelete, ...props }) => { return ( = memo( >
{comment_group.comments.map(comment => ( - + ))}
diff --git a/src/components/node/CommentContent/index.tsx b/src/components/node/CommentContent/index.tsx index 749cbbd0..ae521b6f 100644 --- a/src/components/node/CommentContent/index.tsx +++ b/src/components/node/CommentContent/index.tsx @@ -1,4 +1,4 @@ -import React, { FC, useMemo, memo, createElement } from 'react'; +import React, { FC, useMemo, memo, createElement, useCallback } from 'react'; import { IComment, IFile } from '~/redux/types'; import path from 'ramda/es/path'; import { formatCommentText, getURL, getPrettyDate } from '~/utils/dom'; @@ -12,12 +12,15 @@ import { AudioPlayer } from '~/components/media/AudioPlayer'; import classnames from 'classnames'; import { PRESETS } from '~/constants/urls'; import { COMMENT_BLOCK_RENDERERS } from '~/constants/comment'; +import { Icon } from '~/components/input/Icon'; interface IProps { comment: IComment; + can_edit: boolean; + onDelete: (id: IComment['id'], is_deteted: boolean) => void; } -const CommentContent: FC = memo(({ comment }) => { +const CommentContent: FC = memo(({ comment, can_edit, onDelete }) => { const groupped = useMemo>( () => reduce( @@ -28,10 +31,27 @@ const CommentContent: FC = memo(({ comment }) => { [comment] ); + const onLockClick = useCallback(() => { + onDelete(comment.id, !comment.deleted_at); + }, [comment, onDelete]); + + const lock = useMemo( + () => + can_edit ? ( +
+
+ +
+
+ ) : null, + [can_edit, comment] + ); + return ( - <> +
{comment.text && ( + {lock} {formatCommentText(path(['user', 'username'], comment), comment.text).map( (block, key) => COMMENT_BLOCK_RENDERERS[block.type] && @@ -67,7 +87,7 @@ const CommentContent: FC = memo(({ comment }) => { ))} )} - +
); }); diff --git a/src/components/node/CommentContent/styles.scss b/src/components/node/CommentContent/styles.scss index c6c10807..942b743c 100644 --- a/src/components/node/CommentContent/styles.scss +++ b/src/components/node/CommentContent/styles.scss @@ -1,5 +1,51 @@ @import 'flexbin/flexbin.scss'; +.wrap { + position: relative; +} + +.lock { + position: absolute; + right: 0; + top: 0; + width: 32px; + height: 32px; + border-radius: $radius; + transform: translate(10px, 0); + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + pointer-events: none; + touch-action: none; + transition: opacity 0.25s, transform 0.25s; + cursor: pointer; + background: $red; + + & > div { + width: 20px; + height: 20px; + border-radius: 100%; + + display: flex; + align-items: center; + justify-content: center; + } + + svg { + width: 16px; + height: 16px; + } + + @include tablet { + right: 0; + border-radius: 0 0 0 $radius; + opacity: 1; + transform: translate(0, 0); + background: transparentize($red, $amount: 0.5); + } +} + .block { @include outer_shadow(); min-height: $comment_height; @@ -20,6 +66,15 @@ &:last-child { border-bottom-right-radius: $radius; } + + &:hover { + .lock { + opacity: 1; + pointer-events: all; + touch-action: initial; + transform: translate(0, 0); + } + } } .block_audio { diff --git a/src/components/node/NodeComments/index.tsx b/src/components/node/NodeComments/index.tsx index c038a405..f9491597 100644 --- a/src/components/node/NodeComments/index.tsx +++ b/src/components/node/NodeComments/index.tsx @@ -5,12 +5,16 @@ import { Filler } from '~/components/containers/Filler'; import * as styles from './styles.scss'; import { ICommentGroup, IComment } from '~/redux/types'; import { groupCommentsByUser } from '~/utils/fn'; +import { IUser } from '~/redux/auth/types'; +import { canEditComment } from '~/utils/node'; interface IProps { comments?: IComment[]; + user: IUser; + onDelete: (id: IComment['id'], is_deteted: boolean) => void; } -const NodeComments: FC = memo(({ comments }) => { +const NodeComments: FC = memo(({ comments, user, onDelete }) => { const groupped: ICommentGroup[] = useMemo(() => comments.reduce(groupCommentsByUser, []), [ comments, ]); @@ -18,7 +22,12 @@ const NodeComments: FC = memo(({ comments }) => { return (
{groupped.map(group => ( - + ))} diff --git a/src/containers/node/BorisLayout/index.tsx b/src/containers/node/BorisLayout/index.tsx index ec006ec6..9e45985b 100644 --- a/src/containers/node/BorisLayout/index.tsx +++ b/src/containers/node/BorisLayout/index.tsx @@ -1,29 +1,25 @@ -import React, { FC, useEffect } from "react"; -import { RouteComponentProps } from "react-router"; -import * as NODE_ACTIONS from "~/redux/node/actions"; -import { selectNode } from "~/redux/node/selectors"; -import { selectUser } from "~/redux/auth/selectors"; -import { connect } from "react-redux"; -import { NodeComments } from "~/components/node/NodeComments"; -import styles from "./styles.scss"; -import { CommentForm } from "~/components/node/CommentForm"; -import { Group } from "~/components/containers/Group"; -import boris from "~/sprites/boris_robot.svg"; -import { NodeNoComments } from "~/components/node/NodeNoComments"; -import { getRandomPhrase } from "~/constants/phrases"; +import React, { FC, useEffect } from 'react'; +import { RouteComponentProps } from 'react-router'; +import * as NODE_ACTIONS from '~/redux/node/actions'; +import { selectNode } from '~/redux/node/selectors'; +import { selectUser } from '~/redux/auth/selectors'; +import { connect } from 'react-redux'; +import { NodeComments } from '~/components/node/NodeComments'; +import styles from './styles.scss'; +import { CommentForm } from '~/components/node/CommentForm'; +import { Group } from '~/components/containers/Group'; +import boris from '~/sprites/boris_robot.svg'; +import { NodeNoComments } from '~/components/node/NodeNoComments'; +import { getRandomPhrase } from '~/constants/phrases'; const mapStateToProps = state => ({ node: selectNode(state), - user: selectUser(state) + user: selectUser(state), }); const mapDispatchToProps = { nodeLoadNode: NODE_ACTIONS.nodeLoadNode, - nodeUpdateTags: NODE_ACTIONS.nodeUpdateTags, - nodeSetCoverImage: NODE_ACTIONS.nodeSetCoverImage, - nodeEdit: NODE_ACTIONS.nodeEdit, - nodeLike: NODE_ACTIONS.nodeLike, - nodeStar: NODE_ACTIONS.nodeStar + nodeLockComment: NODE_ACTIONS.nodeLockComment, }; type IProps = ReturnType & @@ -33,21 +29,17 @@ type IProps = ReturnType & const id = 696; const BorisLayoutUnconnected: FC = ({ - node: { - is_loading, - is_loading_comments, - comments = [], - current: node, - related - }, + node: { is_loading, is_loading_comments, comments = [] }, + user, user: { is_user }, - nodeLoadNode + nodeLoadNode, + nodeLockComment, }) => { - const title = getRandomPhrase("BORIS_TITLE"); + const title = getRandomPhrase('BORIS_TITLE'); useEffect(() => { if (is_loading) return; - nodeLoadNode(id, "DESC"); + nodeLoadNode(id, 'DESC'); }, [nodeLoadNode, id]); return ( @@ -90,7 +82,7 @@ const BorisLayoutUnconnected: FC = ({ {is_loading_comments && !comments.length ? ( ) : ( - + )}
diff --git a/src/containers/node/NodeLayout/index.tsx b/src/containers/node/NodeLayout/index.tsx index e0e95ebe..a8243a72 100644 --- a/src/containers/node/NodeLayout/index.tsx +++ b/src/containers/node/NodeLayout/index.tsx @@ -20,6 +20,7 @@ import { selectUser } from '~/redux/auth/selectors'; import pick from 'ramda/es/pick'; import { NodeRelatedPlaceholder } from '~/components/node/NodeRelated/placeholder'; import { NodeDeletedBadge } from '~/components/node/NodeDeletedBadge'; +import { IComment } from '~/redux/types'; const mapStateToProps = state => ({ node: selectNode(state), @@ -34,6 +35,7 @@ const mapDispatchToProps = { nodeLike: NODE_ACTIONS.nodeLike, nodeStar: NODE_ACTIONS.nodeStar, nodeLock: NODE_ACTIONS.nodeLock, + nodeLockComment: NODE_ACTIONS.nodeLockComment, }; type IProps = ReturnType & @@ -55,9 +57,8 @@ const NodeLayoutUnconnected: FC = memo( nodeStar, nodeLock, nodeSetCoverImage, + nodeLockComment, }) => { - // const is_loading = true; - const [layout, setLayout] = useState({}); const updateLayout = useCallback(() => setLayout({}), []); @@ -130,7 +131,7 @@ const NodeLayoutUnconnected: FC = memo( {is_loading || is_loading_comments || (!comments.length && !inline_block) ? ( ) : ( - + )} {is_user && !is_loading && } diff --git a/src/redux/node/actions.ts b/src/redux/node/actions.ts index 1cff016f..b4c54a8b 100644 --- a/src/redux/node/actions.ts +++ b/src/redux/node/actions.ts @@ -102,6 +102,12 @@ export const nodeLock = (id: INode['id'], is_locked: boolean) => ({ is_locked, }); +export const nodeLockComment = (id: IComment['id'], is_locked: boolean) => ({ + type: NODE_ACTIONS.LOCK_COMMENT, + id, + is_locked, +}); + 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 6e858db0..db71f55b 100644 --- a/src/redux/node/constants.ts +++ b/src/redux/node/constants.ts @@ -24,6 +24,7 @@ export const NODE_ACTIONS = { LIKE: `${prefix}LIKE`, STAR: `${prefix}STAR`, LOCK: `${prefix}LOCK`, + LOCK_COMMENT: `${prefix}LOCK_COMMENT`, CREATE: `${prefix}CREATE`, SET_SAVE_ERRORS: `${prefix}SET_SAVE_ERRORS`, diff --git a/src/redux/types.ts b/src/redux/types.ts index 894a12f2..ae0a4ccc 100644 --- a/src/redux/types.ts +++ b/src/redux/types.ts @@ -146,6 +146,7 @@ export interface IComment { created_at?: string; update_at?: string; + deleted_at?: string; } export type IMessage = Omit & { diff --git a/src/utils/node.ts b/src/utils/node.ts index 53868c87..286b3fe2 100644 --- a/src/utils/node.ts +++ b/src/utils/node.ts @@ -1,5 +1,5 @@ import { USER_ROLES } from '~/redux/auth/constants'; -import { INode } from '~/redux/types'; +import { INode, IComment, ICommentGroup } from '~/redux/types'; import { IUser } from '~/redux/auth/types'; import path from 'ramda/es/path'; import { NODE_TYPES } from '~/redux/node/constants'; @@ -8,6 +8,10 @@ export const canEditNode = (node: Partial, user: Partial): boolean path(['role'], user) === USER_ROLES.ADMIN || (path(['user', 'id'], node) && path(['user', 'id'], node) === path(['id'], user)); +export const canEditComment = (comment: Partial, user: Partial): boolean => + path(['role'], user) === USER_ROLES.ADMIN || + (path(['user', 'id'], comment) && path(['user', 'id'], comment) === path(['id'], user)); + export const canLikeNode = (node: Partial, user: Partial): boolean => path(['role'], user) && path(['role'], user) !== USER_ROLES.GUEST;