diff --git a/projects/packages/jetpack-mu-wpcom/changelog/allow-multiple-verbum-instances-on-one-page b/projects/packages/jetpack-mu-wpcom/changelog/allow-multiple-verbum-instances-on-one-page new file mode 100644 index 0000000000000..06949a5bdb74d --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/changelog/allow-multiple-verbum-instances-on-one-page @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +To support adding a comment form inside a query loop diff --git a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/class-verbum-comments.php b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/class-verbum-comments.php index c4902aed64a22..ec54e40df5fe5 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/class-verbum-comments.php +++ b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/class-verbum-comments.php @@ -96,7 +96,7 @@ public function verbum_render_element() { $color_scheme = 'transparent'; } - $verbum = '
' . $this->hidden_fields(); + $verbum = '
' . $this->hidden_fields(); // If the blog requires login, Verbum need to be wrapped in a
to work. // Verbum is given `mustLogIn` to handle the login flow. @@ -535,8 +535,12 @@ public function add_verbum_meta_data( $comment_id ) { * Get the hidden fields for the comment form. */ public function hidden_fields() { + // Ironically, get_queried_post_id doesn't work inside query loop. + // See: https://github.com/Automattic/wp-calypso/issues/98136 + $queried_post = get_post(); + $queried_post_id = $queried_post ? $queried_post->ID : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended - $post_id = isset( $_GET['postid'] ) ? intval( $_GET['postid'] ) : get_queried_object_id(); + $post_id = isset( $_GET['postid'] ) ? intval( $_GET['postid'] ) : $queried_post_id; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $is_current_user_subscribed = isset( $_GET['is_current_user_subscribed'] ) ? intval( $_GET['is_current_user_subscribed'] ) : 0; $nonce = wp_create_nonce( 'highlander_comment' ); diff --git a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/EmailForm/email-form-cookie-consent.tsx b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/EmailForm/email-form-cookie-consent.tsx index 751f5daa6438e..d9f609e84c078 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/EmailForm/email-form-cookie-consent.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/EmailForm/email-form-cookie-consent.tsx @@ -1,12 +1,18 @@ +import { useContext, useCallback } from 'preact/hooks'; import { translate } from '../../i18n'; -import { shouldStoreEmailData } from '../../state'; +import { VerbumSignals } from '../../state'; import { ToggleControl } from '../ToggleControl'; -const handleChange = ( e: boolean ) => { - shouldStoreEmailData.value = e; -}; - export const EmailFormCookieConsent = () => { + const { shouldStoreEmailData } = useContext( VerbumSignals ); + + const handleChange = useCallback( + ( e: boolean ) => { + shouldStoreEmailData.value = e; + }, + [ shouldStoreEmailData ] + ); + const label = (

diff --git a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/EmailForm/index.tsx b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/EmailForm/index.tsx index aec84e0b61af7..abd71618aec23 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/EmailForm/index.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/EmailForm/index.tsx @@ -1,9 +1,9 @@ -import { signal, effect, batch, computed } from '@preact/signals'; +import { effect, batch, useSignal, useComputed } from '@preact/signals'; import clsx from 'clsx'; -import { useState, useEffect } from 'preact/hooks'; +import { useState, useEffect, useContext } from 'preact/hooks'; import { translate } from '../../i18n'; import { Name, Website, Email } from '../../images'; -import { mailLoginData, isMailFormInvalid, shouldStoreEmailData } from '../../state'; +import { VerbumSignals } from '../../state'; import { getUserInfoCookie, isAuthRequired } from '../../utils'; import { NewCommentEmail } from '../new-comment-email'; import { NewPostsEmail } from '../new-posts-email'; @@ -15,32 +15,34 @@ interface EmailFormProps { shouldShowEmailForm: boolean; } -const isValidEmail = signal( true ); -const isEmailTouched = signal( false ); -const isNameTouched = signal( false ); -const isValidAuthor = signal( true ); -const userEmail = computed( () => mailLoginData.value.email || '' ); -const userName = computed( () => mailLoginData.value.author || '' ); -const userUrl = computed( () => mailLoginData.value.url || '' ); +export const EmailForm = ( { shouldShowEmailForm }: EmailFormProps ) => { + const { mailLoginData, isMailFormInvalid, shouldStoreEmailData } = useContext( VerbumSignals ); -const validateFormData = () => { - const emailRegex = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i; - batch( () => { - isValidEmail.value = - Boolean( userEmail.value ) && Boolean( emailRegex.test( userEmail.value ) ); - isValidAuthor.value = Boolean( userName.value.length > 0 ); - } ); -}; + const isValidEmail = useSignal( true ); + const isEmailTouched = useSignal( false ); + const isNameTouched = useSignal( false ); + const isValidAuthor = useSignal( true ); + const userEmail = useComputed( () => mailLoginData.value.email || '' ); + const userName = useComputed( () => mailLoginData.value.author || '' ); + const userUrl = useComputed( () => mailLoginData.value.url || '' ); -const setFormData = ( event: ChangeEvent< HTMLInputElement > ) => { - mailLoginData.value = { - ...mailLoginData.peek(), - [ event.currentTarget.name ]: event.currentTarget.value, + const validateFormData = () => { + const emailRegex = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i; + batch( () => { + isValidEmail.value = + Boolean( userEmail.value ) && Boolean( emailRegex.test( userEmail.value ) ); + isValidAuthor.value = Boolean( userName.value.length > 0 ); + } ); + }; + + const setFormData = ( event: ChangeEvent< HTMLInputElement > ) => { + mailLoginData.value = { + ...mailLoginData.peek(), + [ event.currentTarget.name ]: event.currentTarget.value, + }; + validateFormData(); }; - validateFormData(); -}; -export const EmailForm = ( { shouldShowEmailForm }: EmailFormProps ) => { const { subscribeToComment, subscribeToBlog } = VerbumComments; const [ emailNewComment, setEmailNewComment ] = useState( false ); const [ emailNewPosts, setEmailNewPosts ] = useState( false ); diff --git a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/EmailForm/style.scss b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/EmailForm/style.scss index 401bb63df6273..2982263226350 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/EmailForm/style.scss +++ b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/EmailForm/style.scss @@ -1,4 +1,4 @@ -#comment-form__verbum .verbum-subscriptions .verbum-form +.comment-form__verbum .verbum-subscriptions .verbum-form { .verbum-form__content { // protect the button from style leaks from the site; reset all. diff --git a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/SimpleSubscribeModal/index.tsx b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/SimpleSubscribeModal/index.tsx index def4226e3f437..f4fad5d7b06c3 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/SimpleSubscribeModal/index.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/SimpleSubscribeModal/index.tsx @@ -1,7 +1,7 @@ import clsx from 'clsx'; -import { useEffect, useState, useRef } from 'preact/hooks'; +import { useEffect, useState, useRef, useContext } from 'preact/hooks'; import { translate } from '../../i18n'; -import { userInfo, userLoggedIn, commentUrl, subscribeModalStatus } from '../../state'; +import { VerbumSignals } from '../../state'; import { SimpleSubscribeModalProps } from '../../types'; import { getSubscriptionModalViewCount, @@ -13,6 +13,7 @@ import { SimpleSubscribeModalLoggedOut } from './logged-out'; import './style.scss'; export const SimpleSubscribeModal = ( { closeModalHandler, email }: SimpleSubscribeModalProps ) => { + const { userInfo, userLoggedIn, commentUrl, subscribeModalStatus } = useContext( VerbumSignals ); const [ subscribeState, setSubscribeState ] = useState< 'SUBSCRIBING' | 'LOADING' | 'SUBSCRIBED' >(); @@ -51,6 +52,13 @@ export const SimpleSubscribeModal = ( { closeModalHandler, email }: SimpleSubscr // eslint-disable-next-line react-hooks/exhaustive-deps }, [] ); + // This is used to track how many times the modal was shown to the user. + useEffect( () => { + const userId = userInfo.value?.uid || 0; + const currentViewCount = getSubscriptionModalViewCount( userId ); + setSubscriptionModalViewCount( currentViewCount + 1, userId ); + }, [ userInfo ] ); + if ( ! commentUrl.value ) { // When not showing the modal, we check for modal conditions to show it. // This is done to avoid subscriptionApi calls for logged out users. @@ -69,14 +77,6 @@ export const SimpleSubscribeModal = ( { closeModalHandler, email }: SimpleSubscr return null; } - // This is used to track how many times the modal was shown to the user. - // eslint-disable-next-line react-hooks/rules-of-hooks - useEffect( () => { - const userId = userInfo.value?.uid || 0; - const currentViewCount = getSubscriptionModalViewCount( userId ); - setSubscriptionModalViewCount( currentViewCount + 1, userId ); - }, [] ); - if ( subscribeState === 'LOADING' ) { return (

diff --git a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/SimpleSubscribeModal/logged-in.tsx b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/SimpleSubscribeModal/logged-in.tsx index 5fb056021e793..cd71be6d08b41 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/SimpleSubscribeModal/logged-in.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/SimpleSubscribeModal/logged-in.tsx @@ -1,6 +1,7 @@ +import { useContext } from 'preact/hooks'; import useSubscriptionApi from '../../hooks/useSubscriptionApi'; import { translate } from '../../i18n'; -import { subscriptionSettings, userInfo, commentUrl, subscribeModalStatus } from '../../state'; +import { VerbumSignals } from '../../state'; import { SimpleSubscribeModalProps } from '../../types'; import { shouldShowSubscriptionModal } from '../../utils'; import SubscriptionModal from './subscription-modal'; @@ -8,6 +9,7 @@ import SubscriptionModal from './subscription-modal'; // This determines if the modal should be shown to the user. // It's called before the modal is rendered. export const SimpleSubscribeSetModalShowLoggedIn = () => { + const { subscriptionSettings, userInfo, subscribeModalStatus } = useContext( VerbumSignals ); const { email } = subscriptionSettings.value ?? { email: { send_posts: false, @@ -27,6 +29,7 @@ export const SimpleSubscribeModalLoggedIn = ( { closeModalHandler, }: SimpleSubscribeModalProps ) => { const { setEmailPostsSubscription } = useSubscriptionApi(); + const { userInfo, commentUrl } = useContext( VerbumSignals ); /** * Handle the subscribe button click. diff --git a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/SimpleSubscribeModal/logged-out.tsx b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/SimpleSubscribeModal/logged-out.tsx index d6f904fbe8143..180d73e83ef4a 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/SimpleSubscribeModal/logged-out.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/SimpleSubscribeModal/logged-out.tsx @@ -1,6 +1,6 @@ -import { useEffect, useState } from 'preact/hooks'; +import { useContext, useEffect, useState } from 'preact/hooks'; import { translate } from '../../i18n'; -import { commentUrl } from '../../state'; +import { VerbumSignals } from '../../state'; import { SimpleSubscribeModalProps } from '../../types'; import SubscriptionModal from './subscription-modal'; import type { ChangeEvent } from 'preact/compat'; @@ -16,6 +16,7 @@ export const SimpleSubscribeModalLoggedOut = ( { const [ userEmail, setUserEmail ] = useState( '' ); const [ iframeUrl, setIframeUrl ] = useState( '' ); const [ subscribeDisabled, setSubscribeDisabled ] = useState( false ); + const { commentUrl } = useContext( VerbumSignals ); // Only want this to run once, when email is set for the first time useEffect( () => { diff --git a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/comment-footer.tsx b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/comment-footer.tsx index 8a44653d2dc00..07377a48c1d0c 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/comment-footer.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/comment-footer.tsx @@ -1,12 +1,7 @@ import clsx from 'clsx'; +import { useContext } from 'preact/hooks'; import { translate } from '../i18n'; -import { - commentParent, - isReplyDisabled, - isSavingComment, - isTrayOpen, - userLoggedIn, -} from '../state'; +import { VerbumSignals } from '../state'; import { SettingsButton } from './settings-button'; interface CommentFooterProps { @@ -14,6 +9,8 @@ interface CommentFooterProps { } export const CommentFooter = ( { toggleTray }: CommentFooterProps ) => { + const { commentParent, isReplyDisabled, isSavingComment, isTrayOpen, userLoggedIn } = + useContext( VerbumSignals ); return (
) => { + const { commentParent, commentValue } = useContext( VerbumSignals ); const [ editorState, setEditorState ] = useState< 'LOADING' | 'LOADED' | 'ERROR' >( null ); const [ isGBEditorEnabled, setIsGBEditorEnabled ] = useState( false ); diff --git a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/editor-placeholder.tsx b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/editor-placeholder.tsx index 00753498edade..5d81e8f9173cd 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/editor-placeholder.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/editor-placeholder.tsx @@ -1,9 +1,11 @@ import clsx from 'clsx'; +import { useContext } from 'preact/hooks'; import { translate } from '../i18n'; -import { commentParent } from '../state'; +import { VerbumSignals } from '../state'; import { CustomLoadingSpinner } from './custom-loading-spinner'; export const EditorPlaceholder = ( { onClick, loading } ) => { + const { commentParent } = useContext( VerbumSignals ); return (
{ + const { isTrayOpen, subscriptionSettings, userInfo } = useContext( VerbumSignals ); const { setEmailPostsSubscription, setCommentSubscription, setNotificationSubscription } = useSubscriptionApi(); const { subscribeToComment, subscribeToBlog } = VerbumComments; diff --git a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/logged-out.tsx b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/logged-out.tsx index deb16040612d9..b2a2f41b8c650 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/logged-out.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/logged-out.tsx @@ -1,7 +1,8 @@ +import { Signal } from '@preact/signals'; import clsx from 'clsx'; -import { useEffect, useState } from 'preact/hooks'; +import { useContext, useEffect, useState } from 'preact/hooks'; import { translate } from '../i18n'; -import { commentParent } from '../state'; +import { VerbumSignals } from '../state'; import { serviceData } from '../utils'; import { EmailForm } from './EmailForm'; @@ -12,7 +13,7 @@ interface LoggedOutProps { loginWindow: Window | null; } -const getLoginCommentText = () => { +const getLoginCommentText = ( commentParent: Signal ) => { let defaultText = translate( 'Log in to leave a comment.' ); let optionalText = translate( 'Leave a comment. (log in optional)' ); let nameAndEmailRequired = translate( @@ -80,13 +81,17 @@ export const LoggedOut = ( { login, canWeAccessCookies, loginWindow }: LoggedOut setActiveService( service ); }; + const { commentParent } = useContext( VerbumSignals ); + return (
{ canWeAccessCookies && ( <> -
{ getLoginCommentText() }
+
+ { getLoginCommentText( commentParent ) } +
{ + const { userInfo } = useContext( VerbumSignals ); const subscriptionOptionsVisible = hasSubscriptionOptionsVisible(); const handleOnClick = ( event: MouseEvent ) => { diff --git a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/types.d.ts b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/types.d.ts index 5483f4cbf3797..6ab2338030982 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/types.d.ts +++ b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/types.d.ts @@ -6,6 +6,14 @@ type ScriptLoader = { declare global { const VerbumComments: VerbumCommentsType; + const verbumBlockEditor: { + attachGutenberg: ( + textarea: HTMLTextAreaElement, + content: ( embedUrl: string ) => void, + isRtl: boolean, + onEmbedContent: ( embedUrl: string ) => void + ) => void; + }; const vbeCacheBuster: string; const WP_Enqueue_Dynamic_Script: ScriptLoader; const wp: Record< string, unknown >; diff --git a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/hooks/useFormMutations.tsx b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/hooks/useFormMutations.tsx index 52fe81c655378..011efe1264bd6 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/hooks/useFormMutations.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/hooks/useFormMutations.tsx @@ -1,12 +1,15 @@ -import { useEffect } from 'preact/hooks'; -import { commentParent } from '../state'; +import { useEffect, useContext } from 'preact/hooks'; +import { VerbumSignals } from '../state'; /** * Hook to observe comment form changes and update state according to comment_parent changes. + * + * @param formElement - The form element to observe. */ -export default function useFormMutations() { +export default function useFormMutations( formElement: HTMLFormElement ) { + const { commentParent } = useContext( VerbumSignals ); + useEffect( () => { - const formElement = document.querySelector( '#commentform' ) as HTMLFormElement; const commentParentInput = formElement.querySelector( '#comment_parent' ); if ( ! formElement || ! commentParentInput ) { @@ -28,5 +31,5 @@ export default function useFormMutations() { return () => { mutationObserver.disconnect(); }; - }, [] ); + }, [ formElement, commentParent ] ); } diff --git a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/hooks/useSocialLogin.tsx b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/hooks/useSocialLogin.tsx index 1fd4a984ee340..421456bdf777c 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/hooks/useSocialLogin.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/hooks/useSocialLogin.tsx @@ -1,6 +1,6 @@ -import { useState, useEffect } from 'preact/hooks'; +import { useState, useEffect, useContext } from 'preact/hooks'; import wpcomRequest from 'wpcom-proxy-request'; -import { userInfo } from '../state'; +import { VerbumSignals } from '../state'; import { UserInfo } from '../types'; import { serviceData, setUserInfoCookie } from '../utils'; @@ -30,6 +30,7 @@ const addWordPressDomain = window.location.hostname.endsWith( '.wordpress.com' ) */ export default function useSocialLogin() { const [ loginWindowRef, setLoginWindowRef ] = useState< Window >(); + const { userInfo } = useContext( VerbumSignals ); useEffect( () => { wpcomRequest< UserInfo >( { @@ -42,6 +43,7 @@ export default function useSocialLogin() { .catch( () => { // User may not be logged in. } ); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [] ); if ( VerbumComments.isJetpackCommentsLoggedIn ) { diff --git a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/hooks/useSubscriptionApi.tsx b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/hooks/useSubscriptionApi.tsx index 0fd1b1660bdbb..3ff058c6a7088 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/hooks/useSubscriptionApi.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/hooks/useSubscriptionApi.tsx @@ -1,6 +1,6 @@ -import { useState, useEffect } from 'preact/hooks'; +import { useState, useEffect, useContext } from 'preact/hooks'; import wpcomRequest from 'wpcom-proxy-request'; -import { subscriptionSettings } from '../state'; +import { VerbumSignals } from '../state'; import { SubscriptionDetails, EmailPostsChange, EmailSubscriptionResponse } from '../types'; const getSubscriptionDetails = async () => { @@ -24,7 +24,8 @@ const getSubscriptionDetails = async () => { * * @return {object} Object containing functions to manage subscriptions. */ -export default function useSubscriptionApi(): object { +export default function useSubscriptionApi() { + const { subscriptionSettings } = useContext( VerbumSignals ); const { siteId } = VerbumComments; const [ subscriptionSettingsIsLoading, setSubscriptionSettingsIsLoading ] = useState( true ); @@ -61,6 +62,7 @@ export default function useSubscriptionApi(): object { .finally( () => { setSubscriptionSettingsIsLoading( false ); } ); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [] ); const setEmailPostsSubscription = async function ( change: EmailPostsChange ) { diff --git a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/index.tsx b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/index.tsx index b3d38e8e67db0..da832a89da10a 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/index.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/index.tsx @@ -1,7 +1,7 @@ import { effect } from '@preact/signals'; import clsx from 'clsx'; import { render } from 'preact'; -import { useState, useEffect, useRef, useCallback } from 'preact/hooks'; +import { useState, useEffect, useRef, useCallback, useContext } from 'preact/hooks'; import { SimpleSubscribeModal } from './components/SimpleSubscribeModal'; import { CommentFooter } from './components/comment-footer'; import { CommentInputField } from './components/comment-input-field'; @@ -11,31 +11,32 @@ import { LoggedOut } from './components/logged-out'; import useFormMutations from './hooks/useFormMutations'; import useSocialLogin from './hooks/useSocialLogin'; import { translate } from './i18n'; -import { - hasOpenedTrayOnce, - isEmptyComment, - isSavingComment, - isTrayOpen, - mailLoginData, - shouldStoreEmailData, - userInfo, - userLoggedIn, - commentUrl, - commentParent, - subscribeModalStatus, -} from './state'; +import { createSignals, VerbumSignals } from './state'; import { canWeAccessCookies, setUserInfoCookie, addWordPressDomain, hasSubscriptionOptionsVisible, } from './utils'; -import type { VerbumComments } from './types'; +import type { VerbumAppProps, VerbumComments } from './types'; import './style.scss'; -const Verbum = ( { siteId }: VerbumComments ) => { - const formRef = useRef< HTMLFormElement >( null ); +const Verbum = ( { siteId, parentForm }: VerbumAppProps ) => { + const { + hasOpenedTrayOnce, + isEmptyComment, + isSavingComment, + isTrayOpen, + mailLoginData, + shouldStoreEmailData, + userInfo, + userLoggedIn, + commentUrl, + commentParent, + subscribeModalStatus, + } = useContext( VerbumSignals ); + const [ showMessage, setShowMessage ] = useState( '' ); const [ isErrorMessage, setIsErrorMessage ] = useState( false ); @@ -43,7 +44,7 @@ const Verbum = ( { siteId }: VerbumComments ) => { const [ email, setEmail ] = useState( '' ); const [ ignoreSubscriptionModal, setIgnoreSubscriptionModal ] = useState( false ); const { login, loginWindowRef, logout } = useSocialLogin(); - useFormMutations(); + useFormMutations( parentForm ); const dispose = effect( () => { // The tray, when there is no sub options, is pretty minimal. @@ -59,16 +60,14 @@ const Verbum = ( { siteId }: VerbumComments ) => { }, [] ); useEffect( () => { - formRef.current = document.getElementById( 'commentform' ) as HTMLFormElement | null; - - if ( formRef.current ) { - formRef.current.addEventListener( 'submit', handleCommentSubmit ); + if ( parentForm ) { + parentForm.addEventListener( 'submit', handleCommentSubmit ); return () => { - formRef.current.removeEventListener( 'submit', handleCommentSubmit ); + parentForm.removeEventListener( 'submit', handleCommentSubmit ); }; } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [] ); + }, [ parentForm ] ); useEffect( () => { if ( ! isEmptyComment.value ) { @@ -117,8 +116,8 @@ const Verbum = ( { siteId }: VerbumComments ) => { event.preventDefault(); setShowMessage( '' ); - const formAction = formRef.current.getAttribute( 'action' ); - const formData = new FormData( formRef.current ); + const formAction = parentForm.getAttribute( 'action' ); + const formData = new FormData( parentForm ); // if formData email address is set, set the newUserEmail state if ( formData.get( 'email' ) ) { @@ -155,8 +154,8 @@ const Verbum = ( { siteId }: VerbumComments ) => { // If no error message and not redirect, we re-submit the form as usual instead of using fetch. setIgnoreSubscriptionModal( true ); isSavingComment.value = false; - const submitFormFunction = Object.getPrototypeOf( formRef.current ).submit; - submitFormFunction.call( formRef.current ); + const submitFormFunction = Object.getPrototypeOf( parentForm ).submit; + submitFormFunction.call( parentForm ); }; const handleCommentSubmit = async event => { @@ -246,4 +245,11 @@ const { siteId } = { ...VerbumComments, }; -render( , document.getElementById( 'comment-form__verbum' ) ); +document.querySelectorAll( '.comment-form__verbum' ).forEach( element => { + render( + + + , + element + ); +} ); diff --git a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/state.tsx b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/state.tsx index 1c5702ede9bad..75734e981be0a 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/state.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/state.tsx @@ -1,110 +1,139 @@ import { signal, computed } from '@preact/signals'; +import { createContext } from 'preact'; import { canWeAccessCookies, getUserInfoCookie, isAuthRequired, isEmptyEditor } from './utils'; import type { UserInfo, SubscriptionDetails } from './types'; import type { Signal } from '@preact/signals'; -/* - * In userInfo we store the user data for logged-in users. +/** + * Creates an instance of the app state. + * + * @return An object containing all the signals used in the app. */ -export const userInfo: Signal< UserInfo > = signal( getUserInfoCookie() ); - -/* - * Calculate if user is logged in. For self-hosted sites this check is based only on VerbumComments.isJetpackCommentsLoggedIn. - * Here we also check if cookies are accessible, userInfo is set and the service is different from 'guest' or 'jetpack'. - */ -export const userLoggedIn = computed( () => { - return ( - VerbumComments.isJetpackCommentsLoggedIn || - ( canWeAccessCookies() && - userInfo.value && - userInfo.value?.service !== 'guest' && - userInfo.value?.service !== 'jetpack' ) - ); -} ); - -/* - * Store user input: email, author and url from email form. - */ -export const mailLoginData = signal( { - email: '', - author: '', - url: '', -} ); - -/* - * Indicate whether the tray showing the subscription options is open. - */ -export const isTrayOpen = signal( false ); - -/* - * Indicate whether the subscription option tray has been opened once. - */ -export const hasOpenedTrayOnce = signal( false ); - -/* - * Store the value of the comment input field. - */ -export const commentValue = signal( '' ); - -/* - * Calculate if the comment value is empty. - */ -export const isEmptyComment = computed( () => { - return isEmptyEditor( commentValue.value ); -} ); - -/* - * Indicate whether we are saving the comment. - */ -export const isSavingComment = signal( false ); - -/* - * isMailFormInvalid is used to if the required email form data was not properly filled. - */ -export const isMailFormInvalid = signal( false ); - -/* - * isMailFormMissingInput is used to determine if the mail input is not set. - */ -const isMailFormMissingInput = computed( () => { - return ! mailLoginData.value.email || ! mailLoginData.value.author; -} ); - -/* - * Calculate if the reply button should be disabled. When we have no user data we check the shouldDisableReply value, - * otherwise we check if the comment is empty or saving. - */ -export const isReplyDisabled = computed( () => { - return ( - ( isAuthRequired() && - ! userLoggedIn.value && - ( isMailFormMissingInput.value || isMailFormInvalid.value ) ) || - isEmptyComment.value || - isSavingComment.value - ); -} ); - -/* - * commentUrl is used to store the url of the comment page. - * This is used to redirect the user to the comment page after the comment is saved. - */ -export const commentUrl = signal( '' ); - -/* - * Indicate whether we need to store the email data. If set we use this to store the user info cookie. - */ -export const shouldStoreEmailData = signal( false ); - -// -export const subscriptionSettings: Signal< SubscriptionDetails > = signal( undefined ); - -/* - * Store the comment parent which is updated by external scripts - */ -export const commentParent = signal( 0 ); - -/* - * Store the subscription modal status calculated for the user. - * Can be one of these values: 'showed', 'hidden_cookies_disabled', 'hidden_subscribe_not_enabled', 'hidden_views_limit' and 'hidden_already_subscribed'. - */ -export const subscribeModalStatus = signal( undefined ); +export function createSignals() { + /* + * In userInfo we store the user data for logged-in users. + */ + const userInfo: Signal< UserInfo > = signal( getUserInfoCookie() ); + + /* + * Calculate if user is logged in. For self-hosted sites this check is based only on VerbumComments.isJetpackCommentsLoggedIn. + * Here we also check if cookies are accessible, userInfo is set and the service is different from 'guest' or 'jetpack'. + */ + const userLoggedIn = computed( () => { + return ( + VerbumComments.isJetpackCommentsLoggedIn || + ( canWeAccessCookies() && + userInfo.value && + userInfo.value?.service !== 'guest' && + userInfo.value?.service !== 'jetpack' ) + ); + } ); + + /* + * Store user input: email, author and url from email form. + */ + const mailLoginData = signal( { + email: '', + author: '', + url: '', + } ); + + /* + * Indicate whether the tray showing the subscription options is open. + */ + const isTrayOpen = signal( false ); + + /* + * Indicate whether the subscription option tray has been opened once. + */ + const hasOpenedTrayOnce = signal( false ); + + /* + * Store the value of the comment input field. + */ + const commentValue = signal( '' ); + + /* + * Calculate if the comment value is empty. + */ + const isEmptyComment = computed( () => { + return isEmptyEditor( commentValue.value ); + } ); + + /* + * Indicate whether we are saving the comment. + */ + const isSavingComment = signal( false ); + + /* + * isMailFormInvalid is used to if the required email form data was not properly filled. + */ + const isMailFormInvalid = signal( false ); + + /* + * isMailFormMissingInput is used to determine if the mail input is not set. + */ + const isMailFormMissingInput = computed( () => { + return ! mailLoginData.value.email || ! mailLoginData.value.author; + } ); + + /* + * Calculate if the reply button should be disabled. When we have no user data we check the shouldDisableReply value, + * otherwise we check if the comment is empty or saving. + */ + const isReplyDisabled = computed( () => { + return ( + ( isAuthRequired() && + ! userLoggedIn.value && + ( isMailFormMissingInput.value || isMailFormInvalid.value ) ) || + isEmptyComment.value || + isSavingComment.value + ); + } ); + + /* + * commentUrl is used to store the url of the comment page. + * This is used to redirect the user to the comment page after the comment is saved. + */ + const commentUrl = signal( '' ); + + /* + * Indicate whether we need to store the email data. If set we use this to store the user info cookie. + */ + const shouldStoreEmailData = signal( false ); + + // + const subscriptionSettings: Signal< SubscriptionDetails > = signal( undefined ); + + /* + * Store the comment parent which is updated by external scripts + */ + const commentParent = signal( 0 ); + + /* + * Store the subscription modal status calculated for the user. + * Can be one of these values: 'showed', 'hidden_cookies_disabled', 'hidden_subscribe_not_enabled', 'hidden_views_limit' and 'hidden_already_subscribed'. + */ + const subscribeModalStatus = signal( undefined ); + + return { + userInfo, + userLoggedIn, + mailLoginData, + isTrayOpen, + hasOpenedTrayOnce, + commentValue, + isEmptyComment, + isSavingComment, + isMailFormInvalid, + isMailFormMissingInput, + isReplyDisabled, + commentUrl, + shouldStoreEmailData, + subscriptionSettings, + commentParent, + subscribeModalStatus, + } as const; +} + +export const VerbumSignals = createContext( createSignals() ); diff --git a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/style.scss b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/style.scss index 6e7e4229bd5ba..16d52d377945c 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/style.scss +++ b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/style.scss @@ -89,7 +89,7 @@ display: none; } - #comment-form__verbum { + .comment-form__verbum { @include color-schemes; &.dark { diff --git a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/types.tsx b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/types.tsx index 9ae4e3961b206..228bf852dc280 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/types.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/types.tsx @@ -36,6 +36,10 @@ export type EmailPostsChange = trackSource: 'verbum-subscription-modal' | 'verbum-toggle'; }; +export type VerbumAppProps = { + parentForm: HTMLFormElement; + siteId?: number; +}; export interface VerbumComments { loginPostMessage?: UserInfo; siteId?: number; @@ -67,6 +71,7 @@ export interface VerbumComments { * Contains the time we started loading Highlander. */ fullyLoadedTime: number; + vbeCacheBuster: string; } export type EmailSubscriptionResponse = { diff --git a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/utils.ts b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/utils.ts index 9a28d20fb0291..d39846036940c 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/utils.ts +++ b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/utils.ts @@ -1,6 +1,6 @@ import { translate } from './i18n'; import { Facebook, Mail, WordPress } from './images'; -import type { UserInfo, VerbumComments } from './types'; +import type { UserInfo } from './types'; export const serviceData = { wordpress: { diff --git a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/tests/simple/open_comments_for_everyone.test.ts b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/tests/simple/open_comments_for_everyone.test.ts index 6b14ec8b07582..0fa2029ed6817 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/tests/simple/open_comments_for_everyone.test.ts +++ b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/tests/simple/open_comments_for_everyone.test.ts @@ -21,7 +21,7 @@ test( 'Simple: open_comments_for_everyone - Anonymous', async ( { page } ) => { await page.getByPlaceholder( 'Write a comment...' ).click(); await page.getByPlaceholder( 'Write a comment...' ).pressSequentially( randomComment ); await expect( page.getByRole( 'button', { name: 'Comment' } ) ).toBeVisible(); - await expect( page.locator( '#comment-form__verbum' ) ).toContainText( + await expect( page.locator( '.comment-form__verbum' ) ).toContainText( 'Leave a comment. (log in optional)' ); await page.getByRole( 'button', { name: 'Comment' } ).click(); diff --git a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/tests/simple/user_must_be_registered_and_logged_in_to_comment.test.ts b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/tests/simple/user_must_be_registered_and_logged_in_to_comment.test.ts index 58b2a6111ee92..49268612d0e31 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/tests/simple/user_must_be_registered_and_logged_in_to_comment.test.ts +++ b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/tests/simple/user_must_be_registered_and_logged_in_to_comment.test.ts @@ -25,7 +25,7 @@ test( 'Simple: user_must_be_registered_and_logged_in_to_comment - Anonymous', as .locator( 'p[contenteditable="true"]' ) .pressSequentially( randomComment ); - await expect( page.locator( '#comment-form__verbum' ) ).toContainText( + await expect( page.locator( '.comment-form__verbum' ) ).toContainText( 'Log in to leave a comment.' ); // Reply button should be disabled before log in. @@ -41,7 +41,7 @@ test( 'Simple: user_must_be_registered_and_logged_in_to_comment - Anonymous', as await loginPopupPage.getByRole( 'button', { name: 'Log In' } ).click(); // - await expect( page.locator( '#comment-form__verbum' ) ).toContainText( + await expect( page.locator( '.comment-form__verbum' ) ).toContainText( `${ testingUser.username } - Logged in via WordPress.com` ); await page.getByRole( 'button', { name: 'Comment' } ).click();