Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected spelling of 'commnet' to 'comment' in the filename 'add-feedback-commnet-support'.

Copilot uses AI. Check for mistakes.
Type: added

Forms: add feedback comments
Original file line number Diff line number Diff line change
Expand Up @@ -237,29 +237,34 @@ protected function __construct() {

add_filter( 'use_block_editor_for_post_type', array( $this, 'use_block_editor_for_post_type' ), 10, 2 );

// Restrict feedback comments to logged-in users only
add_filter( 'comments_open', array( $this, 'restrict_feedback_comments_to_logged_in' ), 10, 2 );

// custom post type we'll use to keep copies of the feedback items
register_post_type(
'feedback',
array(
'labels' => array(
'labels' => array(
'name' => __( 'Form Responses', 'jetpack-forms' ),
'singular_name' => __( 'Form Responses', 'jetpack-forms' ),
'search_items' => __( 'Search Responses', 'jetpack-forms' ),
'not_found' => __( 'No responses found', 'jetpack-forms' ),
'not_found_in_trash' => __( 'No responses found', 'jetpack-forms' ),
),
'menu_icon' => 'dashicons-feedback',
'menu_icon' => 'dashicons-feedback',
// when the legacy menu item is retired, we don't want to show the default post type listing
'show_ui' => false,
'show_in_menu' => false,
'show_in_admin_bar' => false,
'public' => false,
'rewrite' => false,
'query_var' => false,
'capability_type' => 'page',
'show_in_rest' => true,
'rest_controller_class' => '\Automattic\Jetpack\Forms\ContactForm\Contact_Form_Endpoint',
'capabilities' => array(
'show_ui' => false,
'show_in_menu' => false,
'show_in_admin_bar' => false,
'public' => false,
'rewrite' => false,
'query_var' => false,
'capability_type' => 'page',
'show_in_rest' => true,
'rest_controller_class' => '\Automattic\Jetpack\Forms\ContactForm\Contact_Form_Endpoint',
'supports' => array( 'comments' ),
'default_comment_status' => 'open',
'capabilities' => array(
'create_posts' => 'do_not_allow',
'publish_posts' => 'publish_pages',
'edit_posts' => 'edit_pages',
Expand All @@ -271,7 +276,7 @@ protected function __construct() {
'delete_post' => 'delete_page',
'read_post' => 'read_page',
),
'map_meta_cap' => true,
'map_meta_cap' => true,
)
);
add_filter( 'wp_untrash_post_status', array( $this, 'untrash_feedback_status_handler' ), 10, 3 );
Expand Down Expand Up @@ -3455,6 +3460,29 @@ public function use_block_editor_for_post_type( $can_edit, $post_type ) {
return 'feedback' === $post_type ? false : $can_edit;
}

/**
* Restrict comments on feedback posts to logged-in users only.
* Hooks into comment permissions to enforce authentication requirement.
*
* For feedback posts, we override the comment_status field (which we use
* for read/unread tracking) and always allow comments for logged-in users.
*
* @param bool $open Whether comments are open.
* @param int $post_id Post ID.
* @return bool Whether comments are open for this post.
*/
public function restrict_feedback_comments_to_logged_in( $open, $post_id ) {
$post = get_post( $post_id );

if ( ! $post || 'feedback' !== $post->post_type ) {
return $open;
}

// For feedback posts, comments are always open for logged-in users,
// regardless of comment_status (which we use for read/unread tracking)
return is_user_logged_in();
}

/**
* Kludge method: reverses the output of a standard print_r( $array ).
* Sort of what unserialize does to a serialized object.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
/**
* External dependencies
*/
import apiFetch from '@wordpress/api-fetch';
import { Button, TextareaControl, DropdownMenu, Spinner } from '@wordpress/components';
import { store as coreStore } from '@wordpress/core-data';
import { useSelect, useDispatch } from '@wordpress/data';
import { dateI18n, getSettings as getDateSettings } from '@wordpress/date';
import { useState, useEffect, useCallback } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import { moreVertical, trash } from '@wordpress/icons';
import { store as noticesStore } from '@wordpress/notices';
/**
* Internal dependencies
*/
import type { FeedbackComment } from '../../../types';
import './style.scss';

export type FeedbackCommentsProps = {
postId: number;
};

/**
* Component for displaying and adding comments to feedback posts.
* Uses WordPress core comments REST API (wp/v2/comments).
*
* @param {FeedbackCommentsProps} props - Component props
* @return {JSX.Element} The feedback comments component
*/
const FeedbackComments = ( { postId }: FeedbackCommentsProps ): JSX.Element => {
const [ comments, setComments ] = useState< FeedbackComment[] >( [] );
const [ isLoadingComments, setIsLoadingComments ] = useState( true );
const [ newComment, setNewComment ] = useState( '' );
const [ isSubmitting, setIsSubmitting ] = useState( false );
const [ isDeleting, setIsDeleting ] = useState( false );
const [ error, setError ] = useState< string | null >( null );
const { createSuccessNotice, createErrorNotice } = useDispatch( noticesStore );

// Get current user data
const currentUser = useSelect( select => {
return select( coreStore ).getCurrentUser();
}, [] );

const loadComments = useCallback( async () => {
setIsLoadingComments( true );
setError( null );

try {
const fetchedComments = await apiFetch< FeedbackComment[] >( {
path: `/wp/v2/comments?post=${ postId }&per_page=100&order=asc`,
} );
setComments( fetchedComments || [] );
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch ( err ) {
setError( __( 'Failed to load comments.', 'jetpack-forms' ) );
createErrorNotice( __( 'Failed to load comments.', 'jetpack-forms' ) );
} finally {
setIsLoadingComments( false );
}
}, [ postId, createErrorNotice ] );

// Load comments on mount and when postId changes
useEffect( () => {
loadComments();
}, [ postId, createErrorNotice, loadComments, setIsLoadingComments, setError, setComments ] );
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The useEffect dependency array includes state setters (setIsLoadingComments, setError, setComments) which are stable and don't need to be listed. Additionally, createErrorNotice is already a dependency of loadComments, making it redundant here. The dependency array should only include postId and loadComments.

Suggested change
}, [ postId, createErrorNotice, loadComments, setIsLoadingComments, setError, setComments ] );
}, [ postId, loadComments ] );

Copilot uses AI. Check for mistakes.

const handleSubmit = useCallback( async () => {
if ( ! newComment.trim() ) {
return;
}

setIsSubmitting( true );
setError( null );

try {
const createdComment = await apiFetch< FeedbackComment >( {
path: '/wp/v2/comments',
method: 'POST',
data: {
post: postId,
content: newComment,
},
} );

setComments( [ ...comments, createdComment ] );
setNewComment( '' );
createSuccessNotice( __( 'Note added successfully.', 'jetpack-forms' ) );
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch ( err ) {
setError( __( 'Failed to save the note. Please try again.', 'jetpack-forms' ) );
createErrorNotice( __( 'Failed to save the note.', 'jetpack-forms' ) );
} finally {
setIsSubmitting( false );
}
}, [ newComment, postId, comments, createSuccessNotice, createErrorNotice ] );

const handleDelete = useCallback(
async ( commentId: number ) => {
if ( comments.length === 0 ) {
return;
}
setIsDeleting( true );
try {
await apiFetch( {
path: `/wp/v2/comments/${ commentId }`,
method: 'DELETE',
} );

setComments( comments.filter( c => c.id !== commentId ) );
createSuccessNotice( __( 'Note deleted.', 'jetpack-forms' ) );
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch ( err ) {
setError( __( 'Failed to delete the note. Please try again.', 'jetpack-forms' ) );
createErrorNotice( __( 'Failed to save the note.', 'jetpack-forms' ) );
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error message 'Failed to save the note.' is misleading in the delete handler. Should be 'Failed to delete the note.' to accurately describe the delete operation.

Suggested change
createErrorNotice( __( 'Failed to save the note.', 'jetpack-forms' ) );
createErrorNotice( __( 'Failed to delete the note.', 'jetpack-forms' ) );

Copilot uses AI. Check for mistakes.
} finally {
setIsDeleting( false );
}
},
[ comments, createSuccessNotice, createErrorNotice ]
);

const formatCommentDate = ( dateString: string ) => {
return sprintf(
/* Translators: %1$s is the date, %2$s is the time. */
__( '%1$s at %2$s', 'jetpack-forms' ),
dateI18n( getDateSettings().formats.date, dateString ),
dateI18n( getDateSettings().formats.time, dateString )
);
};

return (
<div className="jp-forms__feedback-comments">
<h3 className="jp-forms__feedback-comments-heading">
{ __( 'Notes', 'jetpack-forms' ) }

{ isLoadingComments && (
<span className="jp-forms__feedback-loading">
<Spinner size={ 12 } />
</span>
) }
</h3>

<div className="jp-forms__feedback-comments-content">
{ ! isLoadingComments && comments.length > 0 && (
<div className="jp-forms__feedback-comments-list">
{ comments.map( comment => (
<div key={ comment.id } className="jp-forms__feedback-comment">
<div className="jp-forms__feedback-comment-meta">
<strong className="jp-forms__feedback-comment-author">
{ comment.author_name }
</strong>
<span className="jp-forms__feedback-comment-date">
{ formatCommentDate( comment.date ) }
</span>
<DropdownMenu
icon={ moreVertical }
label={ __( 'Note options', 'jetpack-forms' ) }
controls={ [
{
title: __( 'Delete', 'jetpack-forms' ),
icon: trash,
onClick: () => handleDelete( comment.id ),
isDisabled: isDeleting,
},
] }
/>
</div>
<div
className="jp-forms__feedback-comment-content"
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={ { __html: comment.content.rendered } }
/>
</div>
) ) }
</div>
) }
</div>

{ /* Add comment form */ }
<div className="jp-forms__feedback-comments-form">
{ error && <div className="jp-forms__feedback-comments-error">{ error }</div> }
<div className="jp-forms__feedback-comments-form-wrapper">
<div className="jp-forms__feedback-comments-form-input">
<TextareaControl
hideLabelFromVision
label={ __( 'Leave a note', 'jetpack-forms' ) }
value={ newComment }
onChange={ setNewComment }
rows={ 1 }
disabled={ isSubmitting }
placeholder={ __( 'Write a quick note…', 'jetpack-forms' ) }
/>
</div>
<div className="jp-forms__feedback-comments-user-info">
{ currentUser && (
<div className="jp-forms__feedback-comments-form-avatar">
<img src={ currentUser.avatar_urls?.[ '48' ] || '' } alt={ currentUser.name } />
<strong>{ currentUser.name }</strong>
</div>
) }
<div className="jp-forms__feedback-comments-form-button">
<Button
variant="primary"
onClick={ handleSubmit }
disabled={ isSubmitting || ! newComment.trim() }
isBusy={ isSubmitting }
>
{ __( 'Post', 'jetpack-forms' ) }
</Button>
</div>
</div>
</div>
</div>
</div>
);
};

export default FeedbackComments;
Loading
Loading