1
0
Fork 0
mirror of https://github.com/muerwre/vault-frontend.git synced 2025-04-24 20:36:40 +07:00

let users like comments

This commit is contained in:
Fedor Katurov 2023-11-01 20:56:47 +06:00
parent 822f51f5de
commit bd802ede10
22 changed files with 332 additions and 154 deletions

View file

@ -12,7 +12,7 @@
"@tippyjs/react": "^4.2.6", "@tippyjs/react": "^4.2.6",
"@v9v/ts-react-telegram-login": "^1.1.1", "@v9v/ts-react-telegram-login": "^1.1.1",
"autosize": "^4.0.2", "autosize": "^4.0.2",
"axios": "^0.21.2", "axios": "^0.21.4",
"body-scroll-lock": "^2.6.4", "body-scroll-lock": "^2.6.4",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"color2k": "^1.2.4", "color2k": "^1.2.4",

View file

@ -1,4 +1,4 @@
import axios, { AxiosRequestConfig } from 'axios'; import axios, { AxiosRequestConfig, CancelToken } from 'axios';
import { API } from '~/constants/api'; import { API } from '~/constants/api';
import { COMMENTS_DISPLAY } from '~/constants/node'; import { COMMENTS_DISPLAY } from '~/constants/node';
@ -10,6 +10,7 @@ import {
ApiGetNodeRelatedResult, ApiGetNodeRelatedResult,
ApiGetNodeRequest, ApiGetNodeRequest,
ApiGetNodeResponse, ApiGetNodeResponse,
ApiLikeCommentRequest,
ApiLockCommentRequest, ApiLockCommentRequest,
ApiLockcommentResult, ApiLockcommentResult,
ApiLockNodeRequest, ApiLockNodeRequest,
@ -97,6 +98,20 @@ export const apiGetNodeWithCancel = ({ id }: ApiGetNodeRequest) => {
export const apiPostComment = ({ id, data }: ApiPostCommentRequest) => export const apiPostComment = ({ id, data }: ApiPostCommentRequest) =>
api.post<ApiPostCommentResult>(API.NODES.COMMENT(id), data).then(cleanResult); api.post<ApiPostCommentResult>(API.NODES.COMMENT(id), data).then(cleanResult);
export const apiLikeComment = (
nodeId: number,
commentId: number,
data: ApiLikeCommentRequest,
options?: { cancelToken?: CancelToken },
) =>
api
.post<ApiPostCommentResult>(
API.NODES.COMMENT_LIKES(nodeId, commentId),
data,
{ cancelToken: options?.cancelToken },
)
.then(cleanResult);
export const apiGetNodeComments = ({ export const apiGetNodeComments = ({
id, id,
take = COMMENTS_DISPLAY, take = COMMENTS_DISPLAY,

View file

@ -13,20 +13,22 @@ import { CommendDeleted } from '../../node/CommendDeleted';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
type IProps = HTMLAttributes<HTMLDivElement> & { type Props = HTMLAttributes<HTMLDivElement> & {
nodeId: number; nodeId: number;
isEmpty?: boolean; isEmpty?: boolean;
isLoading?: boolean; isLoading?: boolean;
group: ICommentGroup; group: ICommentGroup;
isSame?: boolean; isSame?: boolean;
canEdit?: boolean; canEdit?: boolean;
canLike?: boolean;
highlighted?: boolean; highlighted?: boolean;
saveComment: (data: IComment) => Promise<IComment | undefined>; saveComment: (data: IComment) => Promise<IComment | undefined>;
onDelete: (id: IComment['id'], isLocked: boolean) => void; onDelete: (id: IComment['id'], isLocked: boolean) => void;
onLike: (id: IComment['id'], isLiked: boolean) => void;
onShowImageModal: (images: IFile[], index: number) => void; onShowImageModal: (images: IFile[], index: number) => void;
}; };
const Comment: FC<IProps> = memo( const Comment: FC<Props> = memo(
({ ({
group, group,
nodeId, nodeId,
@ -36,7 +38,9 @@ const Comment: FC<IProps> = memo(
className, className,
highlighted, highlighted,
canEdit, canEdit,
canLike,
onDelete, onDelete,
onLike,
onShowImageModal, onShowImageModal,
saveComment, saveComment,
...props ...props
@ -84,10 +88,12 @@ const Comment: FC<IProps> = memo(
saveComment={saveComment} saveComment={saveComment}
nodeId={nodeId} nodeId={nodeId}
comment={comment} comment={comment}
key={comment.id}
canEdit={!!canEdit} canEdit={!!canEdit}
onDelete={onDelete} canLike={!!canLike}
onLike={() => onLike(comment.id, !comment.liked)}
onDelete={(val: boolean) => onDelete(comment.id, val)}
onShowImageModal={onShowImageModal} onShowImageModal={onShowImageModal}
key={comment.id}
/> />
); );
})} })}

View file

@ -1,7 +1,6 @@
import React, { import {
createElement, createElement,
FC, FC,
Fragment,
memo, memo,
ReactNode, ReactNode,
useCallback, useCallback,
@ -11,7 +10,6 @@ import React, {
import classnames from 'classnames'; import classnames from 'classnames';
import { Authorized } from '~/components/containers/Authorized';
import { Group } from '~/components/containers/Group'; import { Group } from '~/components/containers/Group';
import { AudioPlayer } from '~/components/media/AudioPlayer'; import { AudioPlayer } from '~/components/media/AudioPlayer';
import { COMMENT_BLOCK_RENDERERS } from '~/constants/comment'; import { COMMENT_BLOCK_RENDERERS } from '~/constants/comment';
@ -22,6 +20,7 @@ import { append, assocPath, path, reduce } from '~/utils/ramda';
import { CommentEditingForm } from '../CommentEditingForm'; import { CommentEditingForm } from '../CommentEditingForm';
import { CommentImageGrid } from '../CommentImageGrid'; import { CommentImageGrid } from '../CommentImageGrid';
import { CommentLike } from '../CommentLike';
import { CommentMenu } from '../CommentMenu'; import { CommentMenu } from '../CommentMenu';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
@ -31,17 +30,21 @@ interface IProps {
nodeId: number; nodeId: number;
comment: IComment; comment: IComment;
canEdit: boolean; canEdit: boolean;
canLike: boolean;
saveComment: (data: IComment) => Promise<IComment | undefined>; saveComment: (data: IComment) => Promise<IComment | undefined>;
onDelete: (id: IComment['id'], isLocked: boolean) => void; onDelete: (isLocked: boolean) => void;
onLike: () => void;
onShowImageModal: (images: IFile[], index: number) => void; onShowImageModal: (images: IFile[], index: number) => void;
} }
const CommentContent: FC<IProps> = memo( const CommentContent: FC<IProps> = memo(
({ ({
comment, comment,
canEdit,
nodeId, nodeId,
saveComment, saveComment,
canEdit,
canLike,
onLike,
onDelete, onDelete,
onShowImageModal, onShowImageModal,
prefix, prefix,
@ -65,7 +68,7 @@ const CommentContent: FC<IProps> = memo(
); );
const onLockClick = useCallback(() => { const onLockClick = useCallback(() => {
onDelete(comment.id, !comment.deleted_at); onDelete(!comment.deleted_at);
}, [comment, onDelete]); }, [comment, onDelete]);
const onImageClick = useCallback( const onImageClick = useCallback(
@ -75,13 +78,10 @@ const CommentContent: FC<IProps> = memo(
); );
const menu = useMemo( const menu = useMemo(
() => ( () =>
<div> canEdit && (
{canEdit && ( <div className={styles.menu}>
<Authorized>
<CommentMenu onDelete={onLockClick} onEdit={startEditing} /> <CommentMenu onDelete={onLockClick} onEdit={startEditing} />
</Authorized>
)}
</div> </div>
), ),
[canEdit, startEditing, onLockClick], [canEdit, startEditing, onLockClick],
@ -110,10 +110,12 @@ const CommentContent: FC<IProps> = memo(
<div className={styles.wrap}> <div className={styles.wrap}>
{!!prefix && <div className={styles.prefix}>{prefix}</div>} {!!prefix && <div className={styles.prefix}>{prefix}</div>}
{comment.text.trim() && ( <div className={styles.content}>
<Group className={classnames(styles.block, styles.block_text)}>
{menu} {menu}
<div>
{comment.text.trim() && (
<Group className={classnames(styles.block, styles.block_text)}>
<Group className={styles.renderers}> <Group className={styles.renderers}>
{blocks.map( {blocks.map(
(block, key) => (block, key) =>
@ -124,44 +126,41 @@ const CommentContent: FC<IProps> = memo(
}), }),
)} )}
</Group> </Group>
<div className={styles.date}>
{getPrettyDate(comment.created_at)}
</div>
</Group> </Group>
)} )}
{groupped.image && groupped.image.length > 0 && ( {groupped.image && groupped.image.length > 0 && (
<div className={classnames(styles.block, styles.block_image)}> <div className={classnames(styles.block, styles.block_image)}>
{menu} <CommentImageGrid
files={groupped.image}
<CommentImageGrid files={groupped.image} onClick={onImageClick} /> onClick={onImageClick}
/>
<div className={styles.date}>
{getPrettyDate(comment.created_at)}
</div>
</div> </div>
)} )}
{groupped.audio && groupped.audio.length > 0 && ( {groupped.audio &&
<Fragment> groupped.audio.length > 0 &&
{groupped.audio.map((file) => ( groupped.audio.map((file) => (
<div <div
className={classnames(styles.block, styles.block_audio)} className={classnames(styles.block, styles.block_audio)}
key={file.id} key={file.id}
> >
{menu}
<AudioPlayer file={file} /> <AudioPlayer file={file} />
</div>
))}
</div>
</div>
<div className={styles.date}> <div className={styles.date}>
{getPrettyDate(comment.created_at)} {getPrettyDate(comment.created_at)}
<CommentLike
onLike={onLike}
count={comment.like_count}
active={canLike}
liked={comment.liked}
/>
</div> </div>
</div> </div>
))}
</Fragment>
)}
</div>
); );
}, },
); );

View file

@ -54,7 +54,14 @@
top: 28px; top: 28px;
} }
.content {
width: 100%;
position: relative;
}
.block { .block {
@include row_shadow;
min-height: $comment_height; min-height: $comment_height;
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
@ -90,13 +97,7 @@
} }
.block_image { .block_image {
padding-bottom: 0 !important; padding: $gap / 2;
.date {
background: $content_bg;
border-radius: $radius 0 $radius 0;
color: $gray_25;
}
} }
.block_text { .block_text {
@ -105,16 +106,20 @@
.date { .date {
position: absolute; position: absolute;
bottom: 1px; bottom: 1px; // should not cover block shadow
right: 0; right: 0;
font: $font_12_regular; font: $font_12_medium;
color: $gray_75; color: $gray_75;
padding: 0 6px 2px; fill: $gray_75;
padding: 3px 6px;
z-index: 2; z-index: 2;
background: $content_bg_light; background: $content_bg_light;
border-radius: 4px; border-radius: 4px;
pointer-events: none; user-select: none;
touch-action: none; display: flex;
align-items: center;
justify-content: center;
gap: 4px;
} }
.audios { .audios {
@ -141,3 +146,13 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.menu {
position: absolute;
right: 0;
top: 0;
width: 48px;
height: 48px;
z-index: 10;
outline: none;
}

View file

@ -0,0 +1,44 @@
import React, { FC } from 'react';
import classnames from 'classnames';
import { Icon } from '~/components/input/Icon';
import styles from './styles.module.scss';
interface CommentLikeProps {
className?: string;
count?: number;
active?: boolean;
liked?: boolean;
onLike?: () => void;
}
const CommentLike: FC<CommentLikeProps> = ({
className,
count,
active,
liked,
onLike,
}) => {
if (!active && !count) {
return null;
}
return (
<div
onClick={active ? onLike : undefined}
className={classnames(styles.likes, className, {
[styles.liked]: active && liked,
[styles.active]: active,
})}
>
<div className={styles.icon}>
<Icon icon={count ? 'heart_full' : 'heart'} size={14} />
</div>
{Boolean(count) && <span className={styles.count}>{count}</span>}
</div>
);
};
export { CommentLike };

View file

@ -0,0 +1,34 @@
@import 'src/styles/variables';
.likes {
display: flex;
align-items: center;
justify-content: center;
gap: 2px;
position: relative;
&.active {
cursor: pointer;
&::before {
content: ' ';
position: absolute;
inset: -10px -10px -8px -16px;
}
&:not(.liked):hover .icon {
@include pulse;
}
}
&.liked {
color: $color_like;
fill: currentColor;
}
}
.icon {
position: relative;
width: 14px;
height: 14px;
}

View file

@ -1,13 +1,6 @@
@import 'src/styles/variables'; @import 'src/styles/variables';
.wrap { .wrap {
position: absolute;
right: -3px;
top: -3px;
width: 48px;
height: 48px;
z-index: 10;
outline: none;
cursor: pointer; cursor: pointer;
} }

View file

@ -25,7 +25,7 @@ const NodeLikeButton: FC<NodeLikeButtonProps> = ({
[styles.is_liked]: active, [styles.is_liked]: active,
})} })}
> >
{active ? ( {count ? (
<Icon icon="heart_full" size={24} onClick={onClick} /> <Icon icon="heart_full" size={24} onClick={onClick} />
) : ( ) : (
<Icon icon="heart" size={24} onClick={onClick} /> <Icon icon="heart" size={24} onClick={onClick} />

View file

@ -1,31 +1,5 @@
@import 'src/styles/variables'; @import 'src/styles/variables';
@keyframes pulse {
0% {
transform: scale(1);
}
45% {
transform: scale(1);
}
60% {
transform: scale(1.4);
}
75% {
transform: scale(1);
}
90% {
transform: scale(1.4);
}
100% {
transform: scale(1);
}
}
.like { .like {
transition: fill, stroke 0.25s; transition: fill, stroke 0.25s;
will-change: transform; will-change: transform;
@ -46,8 +20,9 @@
} }
&:hover { &:hover {
@include pulse;
fill: $color_like; fill: $color_like;
animation: pulse 0.75s infinite;
.count { .count {
opacity: 0; opacity: 0;

View file

@ -41,6 +41,8 @@ export const API = {
`/nodes/${id}/tags/${tagId}`, `/nodes/${id}/tags/${tagId}`,
COMMENT: (id: INode['id'] | string) => `/nodes/${id}/comments`, COMMENT: (id: INode['id'] | string) => `/nodes/${id}/comments`,
COMMENT_LIKES: (id: INode['id'] | string, cid: number) =>
`/nodes/${id}/comments/${cid}/likes`,
LOCK_COMMENT: (id: INode['id'], comment_id: IComment['id']) => LOCK_COMMENT: (id: INode['id'], comment_id: IComment['id']) =>
`/nodes/${id}/comments/${comment_id}`, `/nodes/${id}/comments/${comment_id}`,
}, },

View file

@ -1,4 +1,6 @@
import React, { FC, memo, useMemo } from 'react'; import React, { FC, useMemo } from 'react';
import { observer } from 'mobx-react-lite';
import { Comment } from '~/components/comment/Comment'; import { Comment } from '~/components/comment/Comment';
import { LoadMoreButton } from '~/components/input/LoadMoreButton'; import { LoadMoreButton } from '~/components/input/LoadMoreButton';
@ -8,7 +10,7 @@ import { ICommentGroup } from '~/types';
import { useCommentContext } from '~/utils/context/CommentContextProvider'; import { useCommentContext } from '~/utils/context/CommentContextProvider';
import { useNodeContext } from '~/utils/context/NodeContextProvider'; import { useNodeContext } from '~/utils/context/NodeContextProvider';
import { useUserContext } from '~/utils/context/UserContextProvider'; import { useUserContext } from '~/utils/context/UserContextProvider';
import { canEditComment } from '~/utils/node'; import { canEditComment, canLikeComment } from '~/utils/node';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
@ -16,7 +18,7 @@ interface IProps {
order: 'ASC' | 'DESC'; order: 'ASC' | 'DESC';
} }
const NodeComments: FC<IProps> = memo(({ order }) => { const NodeComments: FC<IProps> = observer(({ order }) => {
const user = useUserContext(); const user = useUserContext();
const { node } = useNodeContext(); const { node } = useNodeContext();
@ -26,6 +28,7 @@ const NodeComments: FC<IProps> = memo(({ order }) => {
isLoading, isLoading,
isLoadingMore, isLoadingMore,
lastSeenCurrent, lastSeenCurrent,
onLike,
onLoadMoreComments, onLoadMoreComments,
onDeleteComment, onDeleteComment,
onShowImageModal, onShowImageModal,
@ -68,6 +71,8 @@ const NodeComments: FC<IProps> = memo(({ order }) => {
highlighted={ highlighted={
node.id === BORIS_NODE_ID && group.user.id === ANNOUNCE_USER_ID node.id === BORIS_NODE_ID && group.user.id === ANNOUNCE_USER_ID
} }
onLike={onLike}
canLike={canLikeComment(group, user)}
canEdit={canEditComment(group, user)} canEdit={canEditComment(group, user)}
onDelete={onDeleteComment} onDelete={onDeleteComment}
onShowImageModal={onShowImageModal} onShowImageModal={onShowImageModal}

View file

@ -1,11 +1,33 @@
import { useCallback } from 'react'; import { useCallback, useRef } from 'react';
import { apiLockComment, apiPostComment } from '~/api/node'; import { CancelTokenSource } from 'axios';
import axios from 'axios';
import { apiLikeComment, apiLockComment, apiPostComment } from '~/api/node';
import { useGetComments } from '~/hooks/comments/useGetComments'; import { useGetComments } from '~/hooks/comments/useGetComments';
import { IComment } from '~/types'; import { IComment } from '~/types';
import { showErrorToast } from '~/utils/errors/showToast'; import { showErrorToast } from '~/utils/errors/showToast';
const updateComment =
(id: number, data: Partial<IComment>) => (pages?: IComment[][]) =>
pages?.map((comments) =>
comments.map((comment) =>
comment.id === id ? { ...comment, ...data } : comment,
),
);
const transformComment =
(id: number, cb: (val: IComment) => IComment) => (pages?: IComment[][]) =>
pages?.map((comments) =>
comments.map((comment) => (comment.id === id ? cb(comment) : comment)),
);
const insertComment = (data: IComment) => (pages?: IComment[][]) =>
pages?.map((list, index) => (index === 0 ? [data, ...list] : list));
export const useNodeComments = (nodeId: number, fallbackData?: IComment[]) => { export const useNodeComments = (nodeId: number, fallbackData?: IComment[]) => {
const likeAbortController = useRef<CancelTokenSource>();
const { const {
comments, comments,
isLoading, isLoading,
@ -17,7 +39,7 @@ export const useNodeComments = (nodeId: number, fallbackData?: IComment[]) => {
} = useGetComments(nodeId, fallbackData); } = useGetComments(nodeId, fallbackData);
const onDelete = useCallback( const onDelete = useCallback(
async (id: IComment['id'], isLocked: boolean) => { async (id: number, isLocked: boolean) => {
try { try {
const { deleted_at } = await apiLockComment({ id, nodeId, isLocked }); const { deleted_at } = await apiLockComment({ id, nodeId, isLocked });
@ -25,15 +47,7 @@ export const useNodeComments = (nodeId: number, fallbackData?: IComment[]) => {
return; return;
} }
await mutate( await mutate(updateComment(id, { deleted_at }), false);
(prev) =>
prev?.map((list) =>
list.map((comment) =>
comment.id === id ? { ...comment, deleted_at } : comment,
),
),
false,
);
} catch (error) { } catch (error) {
showErrorToast(error); showErrorToast(error);
} }
@ -51,34 +65,57 @@ export const useNodeComments = (nodeId: number, fallbackData?: IComment[]) => {
// Comment was created // Comment was created
if (!comment.id) { if (!comment.id) {
await mutate( await mutate(insertComment(result.comment), false);
data.map((list, index) => } else {
index === 0 ? [result.comment, ...list] : list, await mutate(updateComment(comment.id, result.comment), false);
),
false,
);
return result.comment;
} }
await mutate(
(prev) =>
prev?.map((list) =>
list.map((it) =>
it.id === result.comment.id ? { ...it, ...result.comment } : it,
),
),
false,
);
return result.comment; return result.comment;
}, },
[data, mutate, nodeId], [data, mutate, nodeId],
); );
const sendLikeRequest = useCallback(
async (id: number, liked: boolean) => {
if (likeAbortController.current) {
likeAbortController.current.cancel();
}
likeAbortController.current = axios.CancelToken.source();
await apiLikeComment(
nodeId,
id,
{ liked },
{ cancelToken: likeAbortController.current?.token },
);
likeAbortController.current = undefined;
},
[nodeId],
);
const onLike = useCallback(
async (id: number, liked: boolean) => {
const increment = liked ? 1 : -1;
await mutate(
transformComment(id, (val) => ({
...val,
liked,
like_count: (val.like_count ?? 0) + increment,
})),
false,
);
sendLikeRequest(id, liked).catch(showErrorToast);
},
[mutate, sendLikeRequest],
);
return { return {
onLoadMoreComments, onLoadMoreComments,
onDelete, onDelete,
onLike,
comments, comments,
hasMore, hasMore,
isLoading, isLoading,

View file

@ -22,6 +22,7 @@ const BorisPage: VFC = observer(() => {
onLoadMoreComments, onLoadMoreComments,
onDelete: onDeleteComment, onDelete: onDeleteComment,
onEdit: onSaveComment, onEdit: onSaveComment,
onLike: onLikeComment,
comments, comments,
hasMore, hasMore,
isLoading: isLoadingComments, isLoading: isLoadingComments,
@ -36,6 +37,7 @@ const BorisPage: VFC = observer(() => {
onSaveComment={onSaveComment} onSaveComment={onSaveComment}
comments={comments} comments={comments}
hasMore={hasMore} hasMore={hasMore}
onLike={onLikeComment}
isLoading={isLoadingComments} isLoading={isLoadingComments}
isLoadingMore={isLoadingMore} isLoadingMore={isLoadingMore}
onShowImageModal={onShowImageModal} onShowImageModal={onShowImageModal}

View file

@ -91,7 +91,7 @@ export const getStaticProps = async (
revalidate: 7 * 86400, // every week revalidate: 7 * 86400, // every week
}; };
} catch (error) { } catch (error) {
console.warn('[NEXT] can\'t generate node: ', error); console.warn("[NEXT] can't generate node: ", error);
return { return {
notFound: true, notFound: true,
}; };
@ -112,6 +112,7 @@ const NodePage: FC<Props> = observer((props) => {
const { const {
onLoadMoreComments, onLoadMoreComments,
onLike: onLikeComment,
onDelete: onDeleteComment, onDelete: onDeleteComment,
onEdit: onSaveComment, onEdit: onSaveComment,
comments, comments,
@ -141,6 +142,7 @@ const NodePage: FC<Props> = observer((props) => {
> >
<NodeRelatedProvider id={parseInt(id, 10)} tags={node.tags}> <NodeRelatedProvider id={parseInt(id, 10)} tags={node.tags}>
<CommentContextProvider <CommentContextProvider
onLike={onLikeComment}
onSaveComment={onSaveComment} onSaveComment={onSaveComment}
comments={comments} comments={comments}
hasMore={hasMore} hasMore={hasMore}

View file

@ -0,0 +1,29 @@
@keyframes pulse_animation {
0% {
transform: scale(1);
}
45% {
transform: scale(1);
}
60% {
transform: scale(1.4);
}
75% {
transform: scale(1);
}
90% {
transform: scale(1.4);
}
100% {
transform: scale(1);
}
}
@mixin pulse {
animation: pulse_animation 0.75s infinite;
}

View file

@ -2,6 +2,7 @@
@import 'inputs'; @import 'inputs';
@import 'fonts'; @import 'fonts';
@import 'mixins'; @import 'mixins';
@import 'animations';
$header_height: 64px; $header_height: 64px;
$cell: 250px; $cell: 250px;

View file

@ -122,6 +122,8 @@ export interface IComment {
text: string; text: string;
files: IFile[]; files: IFile[];
user?: IUser; user?: IUser;
like_count?: number;
liked?: boolean;
created_at?: string; created_at?: string;
update_at?: string; update_at?: string;

View file

@ -47,6 +47,10 @@ export type ApiPostCommentRequest = {
id: INode['id']; id: INode['id'];
data: IComment; data: IComment;
}; };
export type ApiLikeCommentRequest = {
liked: boolean;
};
export type ApiPostCommentResult = { export type ApiPostCommentResult = {
comment: IComment; comment: IComment;
}; };

View file

@ -12,6 +12,7 @@ export interface CommentProviderProps {
onLoadMoreComments: () => void; onLoadMoreComments: () => void;
onSaveComment: (comment: IComment) => Promise<IComment | undefined>; onSaveComment: (comment: IComment) => Promise<IComment | undefined>;
onDeleteComment: (id: IComment['id'], isLocked: boolean) => void; onDeleteComment: (id: IComment['id'], isLocked: boolean) => void;
onLike: (id: number, liked: boolean) => void;
} }
const CommentContext = createContext<CommentProviderProps>({ const CommentContext = createContext<CommentProviderProps>({
@ -20,6 +21,7 @@ const CommentContext = createContext<CommentProviderProps>({
lastSeenCurrent: null, lastSeenCurrent: null,
isLoading: false, isLoading: false,
isLoadingMore: false, isLoadingMore: false,
onLike: () => {},
onSaveComment: async () => undefined, onSaveComment: async () => undefined,
onShowImageModal: () => {}, onShowImageModal: () => {},
onLoadMoreComments: () => {}, onLoadMoreComments: () => {},

View file

@ -18,6 +18,17 @@ export const canEditComment = (
path(['role'], user) === Role.Admin || path(['role'], user) === Role.Admin ||
path(['user', 'id'], comment) === path(['id'], user); path(['user', 'id'], comment) === path(['id'], user);
export const canLikeComment = (
comment?: Partial<ICommentGroup>,
user?: Partial<IUser>,
): boolean =>
Boolean(
user?.role &&
user?.id &&
user?.role !== Role.Guest &&
user.id !== comment?.user?.id,
);
export const canLikeNode = ( export const canLikeNode = (
node?: Partial<INode>, node?: Partial<INode>,
user?: Partial<IUser>, user?: Partial<IUser>,

View file

@ -710,7 +710,7 @@ autosize@^4.0.2:
resolved "https://registry.yarnpkg.com/autosize/-/autosize-4.0.4.tgz#924f13853a466b633b9309330833936d8bccce03" resolved "https://registry.yarnpkg.com/autosize/-/autosize-4.0.4.tgz#924f13853a466b633b9309330833936d8bccce03"
integrity sha512-5yxLQ22O0fCRGoxGfeLSNt3J8LB1v+umtpMnPW6XjkTWXKoN0AmXAIhelJcDtFT/Y/wYWmfE+oqU10Q0b8FhaQ== integrity sha512-5yxLQ22O0fCRGoxGfeLSNt3J8LB1v+umtpMnPW6XjkTWXKoN0AmXAIhelJcDtFT/Y/wYWmfE+oqU10Q0b8FhaQ==
axios@^0.21.2: axios@^0.21.100:
version "0.21.4" version "0.21.4"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575"
integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==
@ -1484,9 +1484,9 @@ flexbin@^0.2.0:
integrity sha1-ASYwbT1ZX8t9/LhxSbnJWZ/49Ok= integrity sha1-ASYwbT1ZX8t9/LhxSbnJWZ/49Ok=
follow-redirects@^1.14.0: follow-redirects@^1.14.0:
version "1.14.8" version "1.15.3"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a"
integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA== integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==
formik@^2.2.6: formik@^2.2.6:
version "2.2.9" version "2.2.9"