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

refactor comment components

This commit is contained in:
Fedor Katurov 2023-11-19 18:07:28 +06:00
parent 60da84aad9
commit eea7095e65
42 changed files with 89 additions and 76 deletions

View file

@ -1,106 +0,0 @@
import React, { FC, HTMLAttributes, memo } from 'react';
import classNames from 'classnames';
import { parseISO } from 'date-fns';
import { CommentContent } from '~/components/comment/CommentContent';
import { CommentDistance } from '~/components/comment/CommentDistance';
import { CommentWrapper } from '~/components/containers/CommentWrapper';
import { NEW_COMMENT_CLASSNAME } from '~/constants/comment';
import { IComment, ICommentGroup, IFile } from '~/types';
import { CommendDeleted } from '../../node/CommendDeleted';
import styles from './styles.module.scss';
type Props = HTMLAttributes<HTMLDivElement> & {
nodeId: number;
isEmpty?: boolean;
isLoading?: boolean;
group: ICommentGroup;
isSame?: boolean;
canEdit?: boolean;
canLike?: boolean;
highlighted?: boolean;
saveComment: (data: IComment) => Promise<IComment | undefined>;
onDelete: (id: IComment['id'], isLocked: boolean) => void;
onLike: (id: IComment['id'], isLiked: boolean) => void;
onShowImageModal: (images: IFile[], index: number) => void;
};
const Comment: FC<Props> = memo(
({
group,
nodeId,
isEmpty,
isSame,
isLoading,
className,
highlighted,
canEdit,
canLike,
onDelete,
onLike,
onShowImageModal,
saveComment,
...props
}) => {
return (
<CommentWrapper
className={classNames(styles.container, className, {
[NEW_COMMENT_CLASSNAME]: group.hasNew,
[styles.highlighted]: highlighted,
})}
isEmpty={isEmpty}
isLoading={isLoading}
user={group.user}
isNew={group.hasNew && !isSame}
{...props}
>
<div className={styles.wrap}>
{group.comments.map((comment, index) => {
if (comment.deleted_at) {
return (
<CommendDeleted
id={comment.id}
onDelete={onDelete}
key={comment.id}
/>
);
}
const prefix = Math.abs(group.distancesInDays[index]) > 14 && (
<CommentDistance
firstDate={
comment?.created_at ? parseISO(comment.created_at) : undefined
}
secondDate={
index > 0 && group.comments[index - 1]?.created_at
? parseISO(group.comments[index - 1].created_at!)
: undefined
}
/>
);
return (
<CommentContent
prefix={prefix}
saveComment={saveComment}
nodeId={nodeId}
comment={comment}
canEdit={!!canEdit}
canLike={!!canLike}
onLike={() => onLike(comment.id, !comment.liked)}
onDelete={(val: boolean) => onDelete(comment.id, val)}
onShowImageModal={onShowImageModal}
key={comment.id}
/>
);
})}
</div>
</CommentWrapper>
);
},
);
export { Comment };

View file

@ -1,17 +0,0 @@
@import 'src/styles/variables';
@keyframes appear {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.wrap {
animation: appear 1s;
}
.highlighted {
box-shadow: $color_primary 0 0 0px 2px;
}

View file

@ -1,35 +0,0 @@
import React, { FC } from 'react';
import { Avatar } from '~/components/common/Avatar';
import { MenuButton } from '~/components/menu/MenuButton';
import { ProfileQuickInfo } from '~/containers/profile/ProfileQuickInfo';
import { IUser } from '~/types/auth';
import { path } from '~/utils/ramda';
interface Props {
user?: IUser;
className?: string;
}
const CommentAvatar: FC<Props> = ({ user, className }) => {
if (!user) {
return <Avatar className={className} />;
}
return (
<MenuButton
position="auto"
icon={
<Avatar
url={path(['photo', 'url'], user)}
username={user.username}
className={className}
/>
}
>
<ProfileQuickInfo user={user} />
</MenuButton>
);
};
export { CommentAvatar };

View file

@ -1,37 +0,0 @@
@import 'src/styles/variables.scss';
@keyframes appear {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.popper {
@include outer_shadow;
background-color: $content_bg_darker;
padding: $gap;
box-sizing: border-box;
touch-action: none;
pointer-events: none;
border-radius: $radius;
animation: appear forwards 250ms;
z-index: 100;
}
.username {
font: $font_18_semibold;
line-height: 1.25em;
}
.description {
@include clamp(2, 12px);
font: $font_12_regular;
opacity: 0.5;
margin-top: 3px;
line-height: 1.25em;
}

View file

@ -1,168 +0,0 @@
import {
createElement,
FC,
memo,
ReactNode,
useCallback,
useMemo,
useState,
} from 'react';
import classnames from 'classnames';
import { Group } from '~/components/containers/Group';
import { AudioPlayer } from '~/components/media/AudioPlayer';
import { COMMENT_BLOCK_RENDERERS } from '~/constants/comment';
import { UploadType } from '~/constants/uploads';
import { IComment, IFile } from '~/types';
import { formatCommentText, getPrettyDate } from '~/utils/dom';
import { append, assocPath, path, reduce } from '~/utils/ramda';
import { CommentEditingForm } from '../CommentEditingForm';
import { CommentImageGrid } from '../CommentImageGrid';
import { CommentLike } from '../CommentLike';
import { CommentMenu } from '../CommentMenu';
import styles from './styles.module.scss';
interface IProps {
prefix?: ReactNode;
nodeId: number;
comment: IComment;
canEdit: boolean;
canLike: boolean;
saveComment: (data: IComment) => Promise<IComment | undefined>;
onDelete: (isLocked: boolean) => void;
onLike: () => void;
onShowImageModal: (images: IFile[], index: number) => void;
}
const CommentContent: FC<IProps> = memo(
({
comment,
nodeId,
saveComment,
canEdit,
canLike,
onLike,
onDelete,
onShowImageModal,
prefix,
}) => {
const [isEditing, setIsEditing] = useState(false);
const startEditing = useCallback(() => setIsEditing(true), [setIsEditing]);
const stopEditing = useCallback(() => setIsEditing(false), [setIsEditing]);
const groupped = useMemo<Record<UploadType, IFile[]>>(
() =>
reduce(
(group, file) =>
file.type
? assocPath([file.type], append(file, group[file.type]), group)
: group,
{} as Record<UploadType, IFile[]>,
comment.files,
),
[comment],
);
const onLockClick = useCallback(() => {
onDelete(!comment.deleted_at);
}, [comment, onDelete]);
const onImageClick = useCallback(
(file: IFile) =>
onShowImageModal(groupped.image, groupped.image.indexOf(file)),
[onShowImageModal, groupped],
);
const menu = useMemo(
() =>
canEdit && (
<div className={styles.menu}>
<CommentMenu onDelete={onLockClick} onEdit={startEditing} />
</div>
),
[canEdit, startEditing, onLockClick],
);
const blocks = useMemo(
() =>
!!comment.text.trim()
? formatCommentText(path(['user', 'username'], comment), comment.text)
: [],
[comment],
);
if (isEditing) {
return (
<CommentEditingForm
saveComment={saveComment}
nodeId={nodeId}
comment={comment}
onCancelEdit={stopEditing}
/>
);
}
return (
<div className={styles.wrap}>
{!!prefix && <div className={styles.prefix}>{prefix}</div>}
<div className={styles.content}>
{menu}
<div>
{comment.text.trim() && (
<Group className={classnames(styles.block, styles.block_text)}>
<Group className={styles.renderers}>
{blocks.map(
(block, key) =>
COMMENT_BLOCK_RENDERERS[block.type] &&
createElement(COMMENT_BLOCK_RENDERERS[block.type], {
block,
key,
}),
)}
</Group>
</Group>
)}
{groupped.image && groupped.image.length > 0 && (
<div className={classnames(styles.block, styles.block_image)}>
<CommentImageGrid
files={groupped.image}
onClick={onImageClick}
/>
</div>
)}
{groupped.audio &&
groupped.audio.length > 0 &&
groupped.audio.map((file) => (
<div
className={classnames(styles.block, styles.block_audio)}
key={file.id}
>
<AudioPlayer file={file} />
</div>
))}
</div>
</div>
<div className={styles.date}>
{getPrettyDate(comment.created_at)}
<CommentLike
onLike={onLike}
count={comment.like_count}
active={canLike}
liked={comment.liked}
/>
</div>
</div>
);
},
);
export { CommentContent };

View file

@ -1,159 +0,0 @@
@import 'src/styles/variables';
@import '~flexbin/flexbin';
.wrap {
@include row_shadow;
position: relative;
}
.lock,
.edit {
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: $color_danger;
z-index: 2;
& > 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: $content_bg_danger;
}
}
.edit {
top: 28px;
}
.content {
width: 100%;
position: relative;
z-index: 1;
}
.block {
@include row_shadow;
min-height: $comment_height;
display: flex;
align-items: flex-start;
justify-content: flex-start;
position: relative;
padding-bottom: 10px;
box-sizing: border-box;
flex-direction: column;
&:first-child {
border-top-right-radius: $radius;
}
&:last-child {
border-bottom-right-radius: $radius;
}
&:hover {
.lock,
.edit {
opacity: 1;
pointer-events: all;
touch-action: initial;
transform: translate(0, 0);
}
}
}
.block_audio {
align-items: stretch;
justify-content: center;
padding-bottom: 0 !important;
}
.block_image {
padding: $gap / 2;
}
.block_text {
padding: $gap * 0.5 0;
}
.date {
position: absolute;
bottom: 1px; // should not cover block shadow
right: 0;
font: $font_12_medium;
color: $gray_75;
fill: $gray_75;
padding: 3px 5px 3px 8px;
z-index: 2;
background: $content_bg_light;
border-radius: 4px;
user-select: none;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
}
.audios {
& > div {
height: $comment_height;
border-radius: $radius;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
}
}
.renderers {
width: 100%;
margin: 0 !important;
}
.prefix {
@include row_shadow;
margin-top: 1px;
display: flex;
align-items: center;
justify-content: center;
}
.menu {
position: absolute;
right: 0;
top: 0;
width: 48px;
height: 48px;
z-index: 10;
outline: none;
}

View file

@ -1,34 +0,0 @@
import React, { FC, memo, useMemo } from 'react';
import { formatDistance } from 'date-fns';
import ru from 'date-fns/locale/ru';
import styles from './styles.module.scss';
interface CommentDistanceProps {
firstDate?: Date;
secondDate?: Date;
}
const CommentDistance: FC<CommentDistanceProps> = memo(
({ firstDate, secondDate }) => {
const distance = useMemo(() => {
if (!firstDate || !secondDate) {
return undefined;
}
return formatDistance(secondDate, firstDate, {
locale: ru,
addSuffix: false,
});
}, [firstDate, secondDate]);
if (!distance) {
return null;
}
return <div className={styles.bar}>прошло {distance}</div>;
},
);
export { CommentDistance };

View file

@ -1,10 +0,0 @@
@import 'src/styles/variables';
.bar {
font: $font_12_regular;
color: $gray_50;
user-select: none;
width: 100%;
text-align: center;
padding: 2px 0 4px;
}

View file

@ -1,42 +0,0 @@
import { FC } from 'react';
import { observer } from 'mobx-react-lite';
import { CommentForm } from '~/components/comment/CommentForm';
import { UploadDropzone } from '~/components/upload/UploadDropzone';
import { UploadSubject, UploadTarget } from '~/constants/uploads';
import { useUploader } from '~/hooks/data/useUploader';
import { IComment, INode } from '~/types';
import { UploaderContextProvider } from '~/utils/context/UploaderContextProvider';
interface CommentEditingFormProps {
comment: IComment;
nodeId: INode['id'];
saveComment: (data: IComment) => Promise<IComment | undefined>;
onCancelEdit?: () => void;
}
const CommentEditingForm: FC<CommentEditingFormProps> = observer(
({ saveComment, comment, onCancelEdit }) => {
const uploader = useUploader(
UploadSubject.Comment,
UploadTarget.Comments,
comment.files,
);
return (
<UploadDropzone onUpload={uploader.uploadFiles}>
<UploaderContextProvider value={uploader}>
<CommentForm
saveComment={saveComment}
comment={comment}
onCancelEdit={onCancelEdit}
allowUploads
/>
</UploaderContextProvider>
</UploadDropzone>
);
},
);
export { CommentEditingForm };

View file

@ -1,48 +0,0 @@
import React, { FC, memo, useMemo } from 'react';
import { Icon } from '~/components/input/Icon';
import { ICommentBlockProps } from '~/constants/comment';
import { useYoutubeMetadata } from '~/hooks/metadata/useYoutubeMetadata';
import { getYoutubeThumb } from '~/utils/dom';
import styles from './styles.module.scss';
type Props = ICommentBlockProps & {};
const CommentEmbedBlock: FC<Props> = memo(({ block }) => {
const id = useMemo(() => {
const match = block.content.match(
/https?:\/\/(?:www\.)?(?:youtube\.com|youtu\.be)\/(?:watch)?(?:\?v=)?([\w\-=]+)/
);
return (match && match[1]) || '';
}, [block.content]);
const url = useMemo(() => `https://youtube.com/watch?v=${id}`, [id]);
const preview = useMemo(() => getYoutubeThumb(block.content), [block.content]);
const metadata = useYoutubeMetadata(id);
const title = metadata?.metadata?.title || '';
return (
<div className={styles.embed}>
<a href={url} target="_blank" rel="noreferrer" />
<div className={styles.preview}>
<div style={{ backgroundImage: `url("${preview}")` }}>
<div className={styles.backdrop}>
<div className={styles.play}>
<Icon icon="play" size={32} />
</div>
<div className={styles.title}>{title}</div>
</div>
</div>
</div>
</div>
);
});
export { CommentEmbedBlock };

View file

@ -1,100 +0,0 @@
@import 'src/styles/variables';
.embed {
padding: 0 $gap;
height: $comment_height;
width: 100%;
box-sizing: border-box;
background: 50% 50% no-repeat;
background-size: cover;
position: relative;
display: flex;
align-items: center;
justify-content: flex-start;
margin: $gap * 0.25 0 !important;
&:last-child {
margin-bottom: 0 !important;
}
&:first-child {
margin-top: 0 !important;
}
a {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 5;
}
svg {
fill: white;
position: relative;
z-index: 6;
}
}
.backdrop {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: $content_bg_backdrop 50% 50%;
background-size: cover;
z-index: 15;
border-radius: $radius;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
font: $font_16_medium;
flex-direction: row;
@include outer_shadow();
}
.preview {
padding: 0 $gap * 0.5 0;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: stretch;
justify-content: stretch;
box-sizing: border-box;
z-index: 2;
& > div {
width: 100%;
border-radius: $radius;
position: relative;
background-position: 50% 50%;
background-size: cover;
}
}
.play {
flex: 0 0 $comment_height - $gap;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.title {
flex: 1;
text-align: left;
box-sizing: border-box;
font: $font_18_semibold;
padding: 0 $gap 0 $gap * 1.5;
text-transform: capitalize;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

View file

@ -1,49 +0,0 @@
import { FC } from 'react';
import classNames from 'classnames';
import { Hoverable } from '~/components/common/Hoverable';
import { Icon } from '~/components/input/Icon';
import { imagePresets } from '~/constants/urls';
import { IFile } from '~/types';
import { getURL } from '~/utils/dom';
import { getFileSrcSet } from '~/utils/srcset';
import styles from './styles.module.scss';
interface CommentImageGridProps {
files: IFile[];
onClick: (file: IFile) => void;
}
const singleSrcSet = '(max-width: 1024px) 40vw, 20vw';
const multipleSrcSet = '(max-width: 1024px) 50vw, 20vw';
const CommentImageGrid: FC<CommentImageGridProps> = ({ files, onClick }) => {
return (
<div
className={classNames(styles.images, {
[styles.multiple]: files.length > 1,
})}
>
{files.map((file) => (
<Hoverable
key={file.id}
onClick={() => onClick(file)}
className={styles.item}
icon={<Icon icon="zoom" size={30} />}
>
<img
srcSet={getFileSrcSet(file)}
src={getURL(file, imagePresets['300'])}
alt={file.name}
className={styles.image}
sizes={files.length > 1 ? singleSrcSet : multipleSrcSet}
/>
</Hoverable>
))}
</div>
);
};
export { CommentImageGrid };

View file

@ -1,37 +0,0 @@
@import 'src/styles/variables';
@import '~flexbin/flexbin';
.images {
cursor: pointer;
overflow: visible !important;
&.multiple {
// Desktop devices
@include flexbin(25vh, $flexbin-space);
// Tablet devices
@media (max-width: $flexbin-tablet-max) {
@include flexbin($flexbin-row-height-tablet, $flexbin-space-tablet);
}
// Phone devices
@media (max-width: $flexbin-phone-max) {
@include flexbin($flexbin-row-height-phone, $flexbin-space-phone);
}
}
}
.image {
max-height: 400px;
border-radius: $radius;
max-width: 100%;
.multiple & {
max-height: 250px;
max-inline-size: 250px;
}
}
.item {
border-radius: $radius;
}

View file

@ -1,44 +0,0 @@
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={18} />
</div>
{Boolean(count) && <span className={styles.count}>{count}</span>}
</div>
);
};
export { CommentLike };

View file

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

View file

@ -1,25 +0,0 @@
import React, { FC, useMemo } from 'react';
import { CornerMenu } from '~/components/common/CornerMenu';
interface IProps {
onEdit: () => void;
onDelete: () => void;
}
const CommentMenu: FC<IProps> = ({ onEdit, onDelete }) => {
const actions = useMemo(
() => [
{
title: 'Редактировать',
action: onEdit,
},
{ title: 'Удалить', action: onDelete },
],
[onEdit, onDelete]
);
return <CornerMenu actions={actions} />;
};
export { CommentMenu };

View file

@ -1,49 +0,0 @@
@import 'src/styles/variables';
.wrap {
cursor: pointer;
}
@keyframes appear {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.menu {
position: absolute;
right: 5px;
top: 5px;
display: flex;
flex-direction: column;
z-index: 6;
white-space: nowrap;
animation: appear 0.25s forwards;
}
.item {
user-select: none;
font: $font_12_semibold;
text-transform: uppercase;
padding: 8px $gap;
background: $content_bg;
cursor: pointer;
&:first-child {
border-top-left-radius: $radius;
border-top-right-radius: $radius;
}
&:last-child {
border-bottom-left-radius: $radius;
border-bottom-right-radius: $radius;
}
&:hover {
background: $color_primary;
}
}

View file

@ -1,26 +0,0 @@
import React, { FC, useMemo } from 'react';
import classNames from 'classnames';
import { ICommentBlockProps } from '~/constants/comment';
import markdown from '~/styles/common/markdown.module.scss';
import { formatText } from '~/utils/dom';
import styles from './styles.module.scss';
interface IProps extends ICommentBlockProps {}
const CommentTextBlock: FC<IProps> = ({ block }) => {
const content = useMemo(() => formatText(block.content), [block.content]);
return (
<div
className={classNames(styles.text, markdown.wrapper)}
dangerouslySetInnerHTML={{
__html: content,
}}
/>
);
};
export { CommentTextBlock };

View file

@ -1,39 +0,0 @@
@import 'src/styles/variables';
.text {
padding: 0 $gap;
font-weight: 300;
font: $font_16_medium;
line-height: 20px;
box-sizing: border-box;
position: relative;
color: #cccccc;
word-break: break-word;
width: 100%;
b {
font-weight: 900;
}
i {
font-style: italic;
}
hr {
height: 1px;
background: rgba(255, 255, 255, 0.1);
border: none;
}
:global(.green) {
color: $color_primary;
}
& > :last-child::after {
display: inline-block;
content: ' ';
height: 1em;
width: 120px;
flex: 0 0 120px;
}
}

View file

@ -1,36 +0,0 @@
import React, { forwardRef, KeyboardEventHandler, TextareaHTMLAttributes, useCallback } from 'react';
import { Textarea } from '~/components/input/Textarea';
import { useRandomPhrase } from '~/constants/phrases';
import { useCommentFormContext } from '~/hooks/comments/useCommentFormFormik';
interface IProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
isLoading?: boolean;
}
const LocalCommentFormTextarea = forwardRef<HTMLTextAreaElement, IProps>(({ ...rest }, ref) => {
const { values, handleChange, handleSubmit, isSubmitting } = useCommentFormContext();
const onKeyDown = useCallback<KeyboardEventHandler<HTMLTextAreaElement>>(
({ ctrlKey, key, metaKey }) => {
if ((ctrlKey || metaKey) && key === 'Enter') handleSubmit(undefined);
},
[handleSubmit]
);
const placeholder = useRandomPhrase('SIMPLE');
return (
<Textarea
{...rest}
ref={ref}
value={values.text}
handler={handleChange('text')}
onKeyDown={onKeyDown}
disabled={isSubmitting}
placeholder={placeholder}
/>
);
});
export { LocalCommentFormTextarea };

View file

@ -0,0 +1,44 @@
import React, {
forwardRef,
KeyboardEventHandler,
TextareaHTMLAttributes,
useCallback,
} from 'react';
import { Textarea } from '~/components/input/Textarea';
import { useRandomPhrase } from '~/constants/phrases';
import { useCommentFormContext } from '~/hooks/comments/useCommentFormFormik';
interface IProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
isLoading?: boolean;
}
const CommentFormTextarea = forwardRef<HTMLTextAreaElement, IProps>(
({ ...rest }, ref) => {
const { values, handleChange, handleSubmit, isSubmitting } =
useCommentFormContext();
const onKeyDown = useCallback<KeyboardEventHandler<HTMLTextAreaElement>>(
({ ctrlKey, key, metaKey }) => {
if ((ctrlKey || metaKey) && key === 'Enter') handleSubmit(undefined);
},
[handleSubmit],
);
const placeholder = useRandomPhrase('SIMPLE');
return (
<Textarea
{...rest}
ref={ref}
value={values.text}
handler={handleChange('text')}
onKeyDown={onKeyDown}
disabled={isSubmitting}
placeholder={placeholder}
/>
);
},
);
export { CommentFormTextarea };

View file

@ -3,10 +3,6 @@ import { FC, useCallback, useState } from 'react';
import { FormikProvider } from 'formik';
import { observer } from 'mobx-react-lite';
import { CommentFormAttachButtons } from '~/components/comment/CommentFormAttachButtons';
import { CommentFormAttaches } from '~/components/comment/CommentFormAttaches';
import { CommentFormFormatButtons } from '~/components/comment/CommentFormFormatButtons';
import { LocalCommentFormTextarea } from '~/components/comment/LocalCommentFormTextarea';
import { Filler } from '~/components/containers/Filler';
import { Button } from '~/components/input/Button';
import { ERROR_LITERAL } from '~/constants/errors';
@ -14,10 +10,12 @@ import { EMPTY_COMMENT } from '~/constants/node';
import { useCommentFormFormik } from '~/hooks/comments/useCommentFormFormik';
import { useInputPasteUpload } from '~/hooks/dom/useInputPasteUpload';
import { IComment } from '~/types';
import {
useUploaderContext,
} from '~/utils/context/UploaderContextProvider';
import { useUploaderContext } from '~/utils/context/UploaderContextProvider';
import { CommentFormAttachButtons } from './components/CommentFormAttachButtons';
import { CommentFormAttaches } from './components/CommentFormAttaches';
import { CommentFormFormatButtons } from './components/CommentFormFormatButtons';
import { CommentFormTextarea } from './components/CommentFormTextarea';
import styles from './styles.module.scss';
interface IProps {
@ -63,7 +61,7 @@ const CommentForm: FC<IProps> = observer(
<form onSubmit={formik.handleSubmit} className={styles.wrap}>
<FormikProvider value={formik}>
<div className={styles.input}>
<LocalCommentFormTextarea onPaste={onPaste} ref={setTextArea} />
<CommentFormTextarea onPaste={onPaste} ref={setTextArea} />
{!!error && (
<div className={styles.error} onClick={clearError}>

View file

@ -1,8 +1,9 @@
import React, { useCallback, useState, VFC } from 'react';
import styles from '~/components/comment/CommentMenu/styles.module.scss';
import { MenuDots } from '~/components/common/MenuDots';
import styles from './styles.module.scss';
interface MenuAction {
title: string;
action: () => void;
@ -18,7 +19,12 @@ const CornerMenu: VFC<CornerMenuProps> = ({ actions }) => {
const onBlur = useCallback(() => setIsMenuOpened(false), [setIsMenuOpened]);
return (
<div className={styles.wrap} onFocus={onFocus} onBlur={onBlur} tabIndex={-1}>
<div
className={styles.wrap}
onFocus={onFocus}
onBlur={onBlur}
tabIndex={-1}
>
<MenuDots onClick={onFocus} />
{is_menu_opened && (

View file

@ -1,50 +0,0 @@
import React, { FC } from 'react';
import classNames from 'classnames';
import { CommentAvatar } from '~/components/comment/CommentAvatar';
import { IUser } from '~/types/auth';
import { path } from '~/utils/ramda';
import { DivProps } from '~/utils/types';
import styles from './styles.module.scss';
type IProps = DivProps & {
user?: IUser;
isEmpty?: boolean;
isLoading?: boolean;
isForm?: boolean;
isNew?: boolean;
};
const CommentWrapper: FC<IProps> = ({
user,
className,
isEmpty,
isLoading,
isForm,
children,
isNew,
...props
}) => (
<div
className={classNames(styles.wrap, className, {
[styles.is_empty]: isEmpty,
[styles.is_loading]: isLoading,
[styles.is_new]: isNew,
})}
{...props}
>
<div className={styles.thumb}>
<CommentAvatar
user={user}
className={classNames(styles.thumb_image, { [styles.pointer]: user })}
/>
<div className={styles.thumb_user}>~{path(['username'], user)}</div>
</div>
<div className={styles.text}>{children}</div>
</div>
);
export { CommentWrapper };

View file

@ -1,124 +0,0 @@
@import 'src/styles/variables';
@keyframes highlight {
0% {
opacity: 0.75;
}
25% {
opacity: 0.5;
}
50% {
opacity: 0.75;
}
75% {
opacity: 0;
}
100% {
opacity: 0;
}
}
.wrap {
@include outer_shadow;
background: $content_bg_light;
min-height: $comment_height;
display: flex;
position: relative;
min-width: 0;
border-radius: $radius;
&.is_empty {
opacity: 0.5;
}
&.is_same {
margin: 0 !important;
border-radius: 0;
}
&.is_new::after {
content: ' ';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
animation: highlight 1s 0.5s forwards;
border-radius: $radius;
pointer-events: none;
touch-action: none;
}
@include tablet {
flex-direction: column;
}
}
.text {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
align-items: stretch;
justify-content: center;
@include tablet {
:global(.comment-author) {
display: none !important;
}
}
}
.thumb {
flex: 0 0 $comment_height;
border-radius: $panel_radius 0 0 $panel_radius;
background-color: $content_bg;
display: flex;
flex-direction: column;
box-sizing: border-box;
@include tablet {
flex-direction: row;
flex: 0 0 40px;
padding: 8px;
box-shadow: inset rgba(255, 255, 255, 0.05) 1px 1px,
inset rgba(0, 0, 0, 0.1) -1px -1px;
border-radius: $panel_radius $panel_radius 0 0;
}
}
div.thumb_image {
height: $comment_height;
background: $gray_90 no-repeat 50% 50%;
border-radius: $panel_radius 0 0 $panel_radius;
background-size: cover;
flex: 0 0 $comment_height;
will-change: transform;
cursor: default;
@include tablet {
height: 32px;
width: 32px;
flex: 0 0 32px;
border-radius: $panel_radius;
}
}
.pointer {
cursor: pointer;
}
.thumb_user {
display: none;
flex: 1;
align-items: center;
justify-content: flex-start;
box-sizing: border-box;
padding: 0 $gap;
font: $font_14_medium;
@include tablet {
display: flex;
}
}