diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000..02b61d9 --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,9 @@ +# Comments and Replies + +This plugin lets visitors comment on and reply to Halo content, with optional image uploads. + +## Language + +**Comment or reply submission**: One attempt to send a visitor's comment or reply and obtain its outcome. It may include images. + +**Upload submission ticket**: A record linking a comment or reply submission to its temporary image uploads so the outcome and image ownership can be confirmed or recovered. diff --git a/packages/comment-widget/src/comment-form.ts b/packages/comment-widget/src/comment-form.ts index b5df1fc..57e8551 100644 --- a/packages/comment-widget/src/comment-form.ts +++ b/packages/comment-widget/src/comment-form.ts @@ -1,11 +1,9 @@ -import type { Comment, CommentRequest, User } from '@halo-dev/api-client'; +import type { CommentRequest, User } from '@halo-dev/api-client'; import { consume } from '@lit/context'; import { html, LitElement } from 'lit'; import { state } from 'lit/decorators.js'; import { createRef, type Ref, ref } from 'lit/directives/ref.js'; import './base-form'; -import { msg } from '@lit/localize'; -import { FetchError, type FetchResponse } from 'ofetch'; import type { BaseForm } from './base-form'; import { allowAnonymousCommentsContext, @@ -18,19 +16,7 @@ import { versionContext, } from './context'; import type { ToastManager } from './lit-toast'; -import type { ProblemDetail } from './types'; -import { - type CaptchaRequiredResponse, - getAltchaHeader, - getCaptchaCodeHeader, - getCaptchaMessage, - isRequireCaptcha, -} from './utils/captcha'; -import { - isPendingReview, - type SubmissionEvent, - submissionErrorMessage, -} from './utils/submission'; +import { type SubmissionEvent, submitCommentOrReply } from './utils/submission'; export class CommentForm extends LitElement { @consume({ context: baseUrlContext }) @@ -82,16 +68,9 @@ export class CommentForm extends LitElement { >`; } - async onSubmit(e: SubmissionEvent) { - e.preventDefault(); - - this.submitting = true; - + onSubmit(e: SubmissionEvent) { const data = e.detail; - const baseForm = this.baseFormRef.value; - const submittedDraft = baseForm?.getDraftSnapshot(); - - const { displayName, email, website, content, hidden } = data || {}; + const { content, hidden } = data || {}; const commentRequest: CommentRequest = { raw: content, @@ -107,80 +86,14 @@ export class CommentForm extends LitElement { }, }; - if (!this.currentUser && !this.allowAnonymousComments) { - this.toastManager?.warn(msg('Please login first')); - this.submitting = false; - return; - } - - if (!this.currentUser && this.allowAnonymousComments) { - if (!displayName || !email) { - this.toastManager?.warn( - msg('Please log in or complete the information first') - ); - this.submitting = false; - return; - } else { - commentRequest.owner = { - displayName: displayName, - email: email, - website: website, - }; - } - } - - try { - const newComment = await data.uploadSession.submit( - `${this.baseUrl}/apis/api.halo.run/v1alpha1/comments`, - commentRequest, - data.uploadIds, - { - ...getCaptchaCodeHeader(data.captchaCode ?? '', data.turnstileToken), - ...getAltchaHeader(data.altchaPayload), - }, - this.baseUrl - ); - - this.baseFormRef.value?.handleFetchCaptcha(); - - if (!isPendingReview(newComment)) { - this.toastManager?.success(msg('Comment submitted successfully')); - } else { - this.toastManager?.success( - msg('Comment submitted successfully, pending review') - ); - } - - baseForm?.resetForm(submittedDraft); - window.dispatchEvent(new CustomEvent('halo:comment:created')); - } catch (error) { - this.reportSubmissionError(error); - } finally { - this.baseFormRef.value?.resetVerification(); - this.submitting = false; - } - } - private reportSubmissionError(error: unknown) { - if (error instanceof FetchError) { - if ( - isRequireCaptcha( - error.response as FetchResponse - ) - ) { - const response = error.data as CaptchaRequiredResponse; - this.captcha = response.captcha ?? ''; - this.toastManager?.warn(getCaptchaMessage(response)); - return; - } - - const problemDetail = error.data as unknown as ProblemDetail; - this.toastManager?.error( - [problemDetail?.title, problemDetail?.detail].join(' - ') || - msg('Comment failed, please try again later') - ); - return; - } - this.toastManager?.error(submissionErrorMessage(error)); + return submitCommentOrReply(e, this, { + url: `${this.baseUrl}/apis/api.halo.run/v1alpha1/comments`, + request: commentRequest, + onSuccess: (baseForm, submittedDraft) => { + baseForm?.resetForm(submittedDraft); + window.dispatchEvent(new CustomEvent('halo:comment:created')); + }, + }); } } diff --git a/packages/comment-widget/src/reply-form.ts b/packages/comment-widget/src/reply-form.ts index cb0cf61..649775f 100644 --- a/packages/comment-widget/src/reply-form.ts +++ b/packages/comment-widget/src/reply-form.ts @@ -1,6 +1,5 @@ import type { CommentVo, - Reply, ReplyRequest, ReplyVo, User, @@ -10,8 +9,6 @@ import { html, LitElement } from 'lit'; import { property, state } from 'lit/decorators.js'; import { createRef, type Ref, ref } from 'lit/directives/ref.js'; import './base-form'; -import { msg } from '@lit/localize'; -import { FetchError, type FetchResponse } from 'ofetch'; import type { BaseForm } from './base-form'; import { allowAnonymousCommentsContext, @@ -20,19 +17,7 @@ import { toastContext, } from './context'; import type { ToastManager } from './lit-toast'; -import type { ProblemDetail } from './types'; -import { - type CaptchaRequiredResponse, - getAltchaHeader, - getCaptchaCodeHeader, - getCaptchaMessage, - isRequireCaptcha, -} from './utils/captcha'; -import { - isPendingReview, - type SubmissionEvent, - submissionErrorMessage, -} from './utils/submission'; +import { type SubmissionEvent, submitCommentOrReply } from './utils/submission'; export class ReplyForm extends LitElement { @consume({ context: baseUrlContext }) @@ -92,16 +77,9 @@ export class ReplyForm extends LitElement { >`; } - async onSubmit(e: SubmissionEvent) { - e.preventDefault(); - - this.submitting = true; - + onSubmit(e: SubmissionEvent) { const data = e.detail; - const baseForm = this.baseFormRef.value; - const submittedDraft = baseForm?.getDraftSnapshot(); - - const { displayName, email, website, content } = data || {}; + const { content } = data || {}; const replyRequest: ReplyRequest = { raw: content, @@ -114,88 +92,21 @@ export class ReplyForm extends LitElement { replyRequest.quoteReply = this.quoteReply.metadata.name; } - if (!this.currentUser && !this.allowAnonymousComments) { - this.toastManager?.warn(msg('Please login first')); - this.submitting = false; - return; - } - - if (!this.currentUser && this.allowAnonymousComments) { - if (!displayName || !email) { - this.toastManager?.warn( - msg('Please log in or complete the information first') + return submitCommentOrReply(e, this, { + url: `${this.baseUrl}/apis/api.halo.run/v1alpha1/comments/${this.comment?.metadata.name}/reply`, + request: replyRequest, + onSuccess: (baseForm, submittedDraft) => { + this.dispatchEvent( + new CustomEvent('reload', { + detail: { + resetForm: (form: BaseForm) => form.resetForm(submittedDraft), + }, + }) ); - this.submitting = false; - return; - } else { - replyRequest.owner = { - displayName: displayName, - email: email, - website: website, - }; - } - } - - try { - const newReply = await data.uploadSession.submit( - `${this.baseUrl}/apis/api.halo.run/v1alpha1/comments/${this.comment?.metadata.name}/reply`, - replyRequest, - data.uploadIds, - { - ...getCaptchaCodeHeader(data.captchaCode ?? '', data.turnstileToken), - ...getAltchaHeader(data.altchaPayload), - }, - this.baseUrl - ); - - this.baseFormRef.value?.handleFetchCaptcha(); - - if (!isPendingReview(newReply)) { - this.toastManager?.success(msg('Comment submitted successfully')); - } else { - this.toastManager?.success( - msg('Comment submitted successfully, pending review') - ); - } - - this.dispatchEvent( - new CustomEvent('reload', { - detail: { - resetForm: (form: BaseForm) => form.resetForm(submittedDraft), - }, - }) - ); - baseForm?.resetForm(submittedDraft); - window.dispatchEvent(new CustomEvent('halo:comment-reply:created')); - } catch (error) { - this.reportSubmissionError(error); - } finally { - this.baseFormRef.value?.resetVerification(); - this.submitting = false; - } - } - private reportSubmissionError(error: unknown) { - if (error instanceof FetchError) { - if ( - isRequireCaptcha( - error.response as FetchResponse - ) - ) { - const response = error.data as CaptchaRequiredResponse; - this.captcha = response.captcha ?? ''; - this.toastManager?.warn(getCaptchaMessage(response)); - return; - } - - const problemDetail = error.data as unknown as ProblemDetail; - this.toastManager?.error( - [problemDetail?.title, problemDetail?.detail].join(' - ') || - msg('Comment failed, please try again later') - ); - return; - } - - this.toastManager?.error(submissionErrorMessage(error)); + baseForm?.resetForm(submittedDraft); + window.dispatchEvent(new CustomEvent('halo:comment-reply:created')); + }, + }); } } diff --git a/packages/comment-widget/src/utils/submission.ts b/packages/comment-widget/src/utils/submission.ts index 3bdfc7b..403ad18 100644 --- a/packages/comment-widget/src/utils/submission.ts +++ b/packages/comment-widget/src/utils/submission.ts @@ -1,4 +1,23 @@ +import type { + Comment, + CommentRequest, + Reply, + ReplyRequest, + User, +} from '@halo-dev/api-client'; import { msg } from '@lit/localize'; +import type { Ref } from 'lit/directives/ref.js'; +import { FetchError, type FetchResponse } from 'ofetch'; +import type { BaseForm } from '../base-form'; +import type { ToastManager } from '../lit-toast'; +import type { ProblemDetail } from '../types'; +import { + type CaptchaRequiredResponse, + getAltchaHeader, + getCaptchaCodeHeader, + getCaptchaMessage, + isRequireCaptcha, +} from './captcha'; import type { UploadSession } from './upload-session'; /** Contract between BaseForm and the comment/reply request handlers. */ @@ -18,7 +37,80 @@ export interface SubmissionDetail { export type SubmissionEvent = CustomEvent; -export function isPendingReview( +type SubmissionForm = { + baseUrl: string; + baseFormRef: Ref; + currentUser: User | undefined; + allowAnonymousComments: boolean; + toastManager: ToastManager | undefined; + submitting: boolean; + captcha: string; +}; + +type DraftSnapshot = ReturnType | undefined; + +export async function submitCommentOrReply( + event: SubmissionEvent, + form: SubmissionForm, + target: { + url: string; + request: CommentRequest | ReplyRequest; + onSuccess: (baseForm: BaseForm | undefined, draft: DraftSnapshot) => void; + } +) { + event.preventDefault(); + form.submitting = true; + + const data = event.detail; + const baseForm = form.baseFormRef.value; + const submittedDraft = baseForm?.getDraftSnapshot(); + const { displayName, email, website } = data || {}; + + if (!form.currentUser && !form.allowAnonymousComments) { + form.toastManager?.warn(msg('Please login first')); + form.submitting = false; + return; + } + + if (!form.currentUser && form.allowAnonymousComments) { + if (!displayName || !email) { + form.toastManager?.warn( + msg('Please log in or complete the information first') + ); + form.submitting = false; + return; + } + target.request.owner = { displayName, email, website }; + } + + try { + const created = await data.uploadSession.submit( + target.url, + target.request, + data.uploadIds, + { + ...getCaptchaCodeHeader(data.captchaCode ?? '', data.turnstileToken), + ...getAltchaHeader(data.altchaPayload), + }, + form.baseUrl + ); + + form.baseFormRef.value?.handleFetchCaptcha(); + form.toastManager?.success( + isPendingReview(created) + ? msg('Comment submitted successfully, pending review') + : msg('Comment submitted successfully') + ); + target.onSuccess(baseForm, submittedDraft); + } catch (error) { + reportSubmissionError(form, error); + } finally { + form.baseFormRef.value?.resetVerification(); + form.submitting = false; + } +} + +function isPendingReview( resource: { spec: { approved?: boolean } } | undefined ) { if (!resource) { @@ -27,7 +119,28 @@ export function isPendingReview( return !resource.spec.approved; } -export function submissionErrorMessage(error: unknown) { +function reportSubmissionError(form: SubmissionForm, error: unknown) { + if (error instanceof FetchError) { + if ( + isRequireCaptcha(error.response as FetchResponse) + ) { + const response = error.data as CaptchaRequiredResponse; + form.captcha = response.captcha ?? ''; + form.toastManager?.warn(getCaptchaMessage(response)); + return; + } + + const problemDetail = error.data as unknown as ProblemDetail; + form.toastManager?.error( + [problemDetail?.title, problemDetail?.detail].join(' - ') || + msg('Comment failed, please try again later') + ); + return; + } + form.toastManager?.error(submissionErrorMessage(error)); +} + +function submissionErrorMessage(error: unknown) { if (error instanceof Error) { return error.message; }