Skip to content

Commit

Permalink
feat(ArticleDetail): add CommentFormBetaDialog
Browse files Browse the repository at this point in the history
  • Loading branch information
wlliaml committed Feb 23, 2024
1 parent 42c49e0 commit c99b720
Show file tree
Hide file tree
Showing 6 changed files with 333 additions and 21 deletions.
46 changes: 39 additions & 7 deletions src/components/CommentBeta/FooterActions/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -152,13 +154,43 @@ const BaseFooterActions = ({
<section className={styles.left}>
{hasUpvote && <UpvoteButton {...buttonProps} />}
{hasReply && (
<ReplyButton
type={type}
{...buttonProps}
{...replyButtonProps}
{...replyCustomButtonProps}
onClick={toggleShowForm}
/>
<>
<Media at="sm">
<CommentFormBetaDialog
articleId={article?.id}
type={'article'}
replyToId={comment.id}
parentId={comment.parentComment?.id || comment.id}
submitCallback={submitCallback}
// closeCallback={() => setShowForm(false)}
isInCommentDetail={isInCommentDetail}
defaultContent={`${makeMentionElement(
comment.author.id,
comment.author.userName || '',
comment.author.displayName || ''
)} `}
>
{({ openDialog }) => (
<ReplyButton
type={type}
{...buttonProps}
{...replyButtonProps}
{...replyCustomButtonProps}
onClick={openDialog}
/>
)}
</CommentFormBetaDialog>
</Media>
<Media greaterThan="sm">
<ReplyButton
type={type}
{...buttonProps}
{...replyButtonProps}
{...replyCustomButtonProps}
onClick={toggleShowForm}
/>
</Media>
</>
)}
</section>
</footer>
Expand Down
207 changes: 207 additions & 0 deletions src/components/Dialogs/CommentFormBetaDialog/CommentForm/index.tsx
Original file line number Diff line number Diff line change
@@ -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: () => <Spinner />,
})

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<CommentFormProps> = ({
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<CommentDraftQuery>(COMMENT_DRAFT, {
variables: { id: commentDraftId },
})

const [putComment] = useMutation<PutCommentBetaMutation>(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<HTMLFormElement>) => {
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 (
<>
<Dialog.Header
title=""
closeDialog={closeDialog}
rightBtn={
<Dialog.TextButton
type="submit"
form={formId}
disabled={isSubmitting || !isValid}
text={<Translate zh_hant="送出" zh_hans="送出" en="Send" />}
loading={isSubmitting}
/>
}
/>

<Dialog.Content noMaxHeight>
{context && <section className={styles.context}>{context}</section>}

<form className={styles.form} id={formId} onSubmit={handleSubmit}>
<CommentEditor content={content} update={onUpdate} />
</form>
</Dialog.Content>

<Dialog.Footer
smUpBtns={
<>
<Dialog.TextButton
text={<FormattedMessage defaultMessage="Cancel" id="47FYwb" />}
color="greyDarker"
onClick={closeDialog}
/>
<Dialog.TextButton
type="submit"
form={formId}
disabled={isSubmitting || !isValid}
text={<Translate zh_hant="送出" zh_hans="送出" en="Send" />}
loading={isSubmitting}
/>
</>
}
/>
</>
)
}

export default CommentForm
Original file line number Diff line number Diff line change
@@ -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;
}
51 changes: 51 additions & 0 deletions src/components/Dialogs/CommentFormBetaDialog/index.tsx
Original file line number Diff line number Diff line change
@@ -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<CommentFormProps, 'closeDialog'>

const BaseCommentFormBetaDialog = ({
children,
...props
}: CommentFormBetaDialogProps) => {
const { show, openDialog, closeDialog } = useDialogSwitch(true)
const ref: React.RefObject<HTMLDivElement> | 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 (
<div ref={ref}>
{children && children({ openDialog })}

<Dialog
isOpen={show}
onDismiss={closeDialog}
onRest={focusEditor}
testId={TEST_ID.DIALOG_COMMENT_FORM}
>
<CommentForm {...props} closeDialog={closeDialog} />
</Dialog>
</div>
)
}

export const CommentFormBetaDialog = (props: CommentFormBetaDialogProps) => (
<Dialog.Lazy mounted={<BaseCommentFormBetaDialog {...props} />}>
{({ openDialog }) => <>{props.children({ openDialog })}</>}
</Dialog.Lazy>
)
1 change: 1 addition & 0 deletions src/components/Dialogs/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Loading

0 comments on commit c99b720

Please sign in to comment.