From c99b7206b0ce87c26c56d2801ad2e23bbf00dbbd Mon Sep 17 00:00:00 2001 From: bluecloud <96812901+pitb2022@users.noreply.github.com> Date: Fri, 23 Feb 2024 13:20:24 +0800 Subject: [PATCH] feat(ArticleDetail): add CommentFormBetaDialog --- .../CommentBeta/FooterActions/index.tsx | 46 +++- .../CommentForm/index.tsx | 207 ++++++++++++++++++ .../CommentForm/styles.module.css | 13 ++ .../Dialogs/CommentFormBetaDialog/index.tsx | 51 +++++ src/components/Dialogs/index.tsx | 1 + .../Toolbar/FixedToolbar/index.tsx | 36 +-- 6 files changed, 333 insertions(+), 21 deletions(-) create mode 100644 src/components/Dialogs/CommentFormBetaDialog/CommentForm/index.tsx create mode 100644 src/components/Dialogs/CommentFormBetaDialog/CommentForm/styles.module.css create mode 100644 src/components/Dialogs/CommentFormBetaDialog/index.tsx diff --git a/src/components/CommentBeta/FooterActions/index.tsx b/src/components/CommentBeta/FooterActions/index.tsx index 89d62a8cff..93fee90cbf 100644 --- a/src/components/CommentBeta/FooterActions/index.tsx +++ b/src/components/CommentBeta/FooterActions/index.tsx @@ -6,8 +6,10 @@ import { ERROR_CODES, ERROR_MESSAGES } from '~/common/enums' import { makeMentionElement, translate } from '~/common/utils' import { CommentFormBeta, + CommentFormBetaDialog, CommentFormType, LanguageContext, + Media, Spacer, toast, ViewerContext, @@ -152,13 +154,43 @@ const BaseFooterActions = ({
{hasUpvote && } {hasReply && ( - + <> + + setShowForm(false)} + isInCommentDetail={isInCommentDetail} + defaultContent={`${makeMentionElement( + comment.author.id, + comment.author.userName || '', + comment.author.displayName || '' + )} `} + > + {({ openDialog }) => ( + + )} + + + + + + )}
diff --git a/src/components/Dialogs/CommentFormBetaDialog/CommentForm/index.tsx b/src/components/Dialogs/CommentFormBetaDialog/CommentForm/index.tsx new file mode 100644 index 0000000000..19389c76a1 --- /dev/null +++ b/src/components/Dialogs/CommentFormBetaDialog/CommentForm/index.tsx @@ -0,0 +1,207 @@ +import { useQuery } from '@apollo/react-hooks' +import dynamic from 'next/dynamic' +import { useState } from 'react' +import { FormattedMessage } from 'react-intl' + +import { MAX_ARTICLE_COMMENT_LENGTH } from '~/common/enums' +import { dom, stripHtml } from '~/common/utils' +import { + CommentFormType, + Dialog, + Spinner, + Translate, + useMutation, +} from '~/components' +import PUT_COMMENT_BETA from '~/components/GQL/mutations/putCommentBeta' +import COMMENT_DRAFT from '~/components/GQL/queries/commentDraft' +import { + updateArticleComments, + updateCommentDetail, +} from '~/components/GQL/updates' +import { CommentDraftQuery, PutCommentBetaMutation } from '~/gql/graphql' + +import styles from './styles.module.css' + +const CommentEditor = dynamic(() => import('~/components/Editor/Comment'), { + ssr: false, + loading: () => , +}) + +export interface CommentFormProps { + commentId?: string + replyToId?: string + parentId?: string + circleId?: string + articleId?: string + type: CommentFormType + + isInCommentDetail?: boolean + + defaultContent?: string | null + submitCallback?: () => void + closeDialog: () => void + title?: React.ReactNode + context?: React.ReactNode +} + +const CommentForm: React.FC = ({ + commentId, + replyToId, + parentId, + articleId, + circleId, + type, + + isInCommentDetail, + defaultContent, + submitCallback, + closeDialog, + title, + context, + + ...props +}) => { + // retrieve comment draft + const commentDraftId = `${articleId || circleId}:${commentId || 0}:${ + parentId || 0 + }:${replyToId || 0}` + const formId = `comment-form-${commentDraftId}` + + const { data, client } = useQuery(COMMENT_DRAFT, { + variables: { id: commentDraftId }, + }) + + const [putComment] = useMutation(PUT_COMMENT_BETA) + const [isSubmitting, setSubmitting] = useState(false) + const [content, setContent] = useState( + data?.commentDraft.content || defaultContent || '' + ) + const contentCount = stripHtml(content).trim().length + + const isValid = contentCount > 0 && contentCount <= MAX_ARTICLE_COMMENT_LENGTH + + const handleSubmit = async (event: React.FormEvent) => { + const mentions = dom.getAttributes('data-id', content) + const input = { + id: commentId, + comment: { + content, + replyTo: replyToId, + articleId, + circleId, + parentId, + type, + mentions, + }, + } + + event.preventDefault() + setSubmitting(true) + + try { + await putComment({ + variables: { input }, + update: (cache, mutationResult) => { + if (!!parentId && !isInCommentDetail) { + updateArticleComments({ + cache, + articleId: articleId || '', + commentId: parentId, + type: 'addSecondaryComment', + comment: mutationResult.data?.putComment, + }) + } else if (!!parentId && isInCommentDetail) { + updateCommentDetail({ + cache, + commentId: parentId || '', + type: 'add', + comment: mutationResult.data?.putComment, + }) + } else { + updateArticleComments({ + cache, + articleId: articleId || '', + type: 'add', + comment: mutationResult.data?.putComment, + }) + } + }, + }) + + setContent('') + + // clear draft + client.writeData({ + id: `CommentDraft:${commentDraftId}`, + data: { content: '' }, + }) + + setSubmitting(false) + + if (submitCallback) { + submitCallback() + } + + closeDialog() + } catch (e) { + setSubmitting(false) + console.error(e) + } + } + + const onUpdate = ({ content: newContent }: { content: string }) => { + setContent(newContent) + + client.writeData({ + id: `CommentDraft:${commentDraftId}`, + data: { content: newContent }, + }) + } + + return ( + <> + } + loading={isSubmitting} + /> + } + /> + + + {context &&
{context}
} + +
+ + +
+ + + } + color="greyDarker" + onClick={closeDialog} + /> + } + loading={isSubmitting} + /> + + } + /> + + ) +} + +export default CommentForm diff --git a/src/components/Dialogs/CommentFormBetaDialog/CommentForm/styles.module.css b/src/components/Dialogs/CommentFormBetaDialog/CommentForm/styles.module.css new file mode 100644 index 0000000000..2920cd43ed --- /dev/null +++ b/src/components/Dialogs/CommentFormBetaDialog/CommentForm/styles.module.css @@ -0,0 +1,13 @@ +.context { + flex-grow: 0; + flex-shrink: 0; + margin-bottom: var(--spacing-base); +} + +.form { + @mixin scrollbar-thin; + + position: relative; + flex-grow: 1; + overflow-y: auto; +} diff --git a/src/components/Dialogs/CommentFormBetaDialog/index.tsx b/src/components/Dialogs/CommentFormBetaDialog/index.tsx new file mode 100644 index 0000000000..dc07549f53 --- /dev/null +++ b/src/components/Dialogs/CommentFormBetaDialog/index.tsx @@ -0,0 +1,51 @@ +import { useRef } from 'react' + +import { TEST_ID } from '~/common/enums' +import { Dialog, useDialogSwitch } from '~/components' + +import CommentForm, { CommentFormProps } from './CommentForm' + +export type CommentFormBetaDialogProps = { + children: ({ openDialog }: { openDialog: () => void }) => React.ReactNode +} & Omit + +const BaseCommentFormBetaDialog = ({ + children, + ...props +}: CommentFormBetaDialogProps) => { + const { show, openDialog, closeDialog } = useDialogSwitch(true) + const ref: React.RefObject | null = useRef(null) + + // FIXME: editor can't be focused with dialog on Android devices + const focusEditor = () => { + if (!show) { + return + } + + const $editor = ref.current?.querySelector('.ProseMirror') as HTMLElement + if ($editor) { + $editor.focus() + } + } + + return ( +
+ {children && children({ openDialog })} + + + + +
+ ) +} + +export const CommentFormBetaDialog = (props: CommentFormBetaDialogProps) => ( + }> + {({ openDialog }) => <>{props.children({ openDialog })}} + +) diff --git a/src/components/Dialogs/index.tsx b/src/components/Dialogs/index.tsx index 1119299e4d..d958bedb9e 100644 --- a/src/components/Dialogs/index.tsx +++ b/src/components/Dialogs/index.tsx @@ -15,6 +15,7 @@ export * from './SetUserNameDialog' // Article export * from './AppreciatorsDialog' export * from './BindEmailHintDialog' +export * from './CommentFormBetaDialog' export * from './CommentFormDialog' export * from './FingerprintDialog' export * from './MigrationDialog' diff --git a/src/views/ArticleDetail/Toolbar/FixedToolbar/index.tsx b/src/views/ArticleDetail/Toolbar/FixedToolbar/index.tsx index 413aafb285..1913f9e218 100644 --- a/src/views/ArticleDetail/Toolbar/FixedToolbar/index.tsx +++ b/src/views/ArticleDetail/Toolbar/FixedToolbar/index.tsx @@ -3,7 +3,12 @@ import { FormattedMessage } from 'react-intl' import { TEST_ID, TOOLBAR_FIXEDTOOLBAR_ID } from '~/common/enums' import { toLocale, toPath } from '~/common/utils' -import { BookmarkButton, ButtonProps, ReCaptchaProvider } from '~/components' +import { + BookmarkButton, + ButtonProps, + CommentFormBetaDialog, + ReCaptchaProvider, +} from '~/components' import DropdownActions, { DropdownActionsControls, } from '~/components/ArticleDigest/DropdownActions' @@ -14,7 +19,6 @@ import { } from '~/gql/graphql' import AppreciationButton from '../../AppreciationButton' -import { CommentsDialog } from '../../Comments/CommentsDialog' import CommentButton from '../CommentButton' import DonationButton from '../DonationButton' import styles from './styles.module.css' @@ -27,6 +31,7 @@ export type FixedToolbarProps = { privateFetched: boolean lock: boolean showCommentToolbar: boolean + openCommentsDialog?: () => void } & DropdownActionsControls const fragments = { @@ -70,6 +75,7 @@ const FixedToolbar = ({ privateFetched, lock, showCommentToolbar, + openCommentsDialog, ...props }: FixedToolbarProps) => { const path = toPath({ page: 'articleDetail', article }) @@ -97,19 +103,19 @@ const FixedToolbar = ({ } return ( - - {({ openDialog: openCommentsDialog }) => ( -
+
+ + {({ openDialog: openFormBetaDialog }) => (
{showCommentToolbar && (
-
- )} - + )} + +
) }