diff --git a/projects/packages/forms/changelog/add-forms-unread-state-optimistic-updates2 b/projects/packages/forms/changelog/add-forms-unread-state-optimistic-updates2
new file mode 100644
index 0000000000000..113f19289110b
--- /dev/null
+++ b/projects/packages/forms/changelog/add-forms-unread-state-optimistic-updates2
@@ -0,0 +1,4 @@
+Significance: minor
+Type: changed
+
+Forms: update read/unread counts optimistically in the sidebar
diff --git a/projects/packages/forms/src/contact-form/class-contact-form-plugin.php b/projects/packages/forms/src/contact-form/class-contact-form-plugin.php
index b7d5a82660900..2eab2b571acde 100644
--- a/projects/packages/forms/src/contact-form/class-contact-form-plugin.php
+++ b/projects/packages/forms/src/contact-form/class-contact-form-plugin.php
@@ -1415,9 +1415,7 @@ public function unread_count() {
$unread = self::get_unread_count();
if ( isset( $submenu['jetpack'] ) && is_array( $submenu['jetpack'] ) && ! empty( $submenu['jetpack'] ) ) {
- $inline_style = ( $unread > 0 ) ? '' : 'style="display: none;"';
-
- $forms_unread_count_tag = " " . number_format_i18n( $unread ) . '';
+ $forms_unread_count_tag = " " . number_format_i18n( $unread ) . '';
$jetpack_badge_count = $unread;
// Main menu entries
diff --git a/projects/packages/forms/src/dashboard/inbox/dataviews/actions.js b/projects/packages/forms/src/dashboard/inbox/dataviews/actions.js
index 9cef329450c47..2baf911a1867e 100644
--- a/projects/packages/forms/src/dashboard/inbox/dataviews/actions.js
+++ b/projects/packages/forms/src/dashboard/inbox/dataviews/actions.js
@@ -7,7 +7,7 @@ import { store as noticesStore } from '@wordpress/notices';
import { notSpam, spam } from '../../icons';
import { store as dashboardStore } from '../../store';
import InboxResponse from '../response';
-import { updateMenuCounter } from '../utils';
+import { updateMenuCounter, updateMenuCounterOptimistically } from '../utils';
export const BULK_ACTIONS = {
markAsSpam: 'mark_as_spam',
@@ -340,8 +340,9 @@ export const markAsReadAction = {
const { editEntityRecord } = registry.dispatch( coreStore );
const { getEntityRecord } = registry.select( coreStore );
const { createSuccessNotice, createErrorNotice } = registry.dispatch( noticesStore );
+
const promises = await Promise.allSettled(
- items.map( async ( { id } ) => {
+ items.map( async ( { id, status } ) => {
// Get current entity from store
const currentEntity = getEntityRecord( 'postType', 'feedback', id );
@@ -350,6 +351,11 @@ export const markAsReadAction = {
editEntityRecord( 'postType', 'feedback', id, {
is_unread: false,
} );
+
+ // Immediately update menu counters optimistically to avoid delays, but only for inbox
+ if ( status === 'publish' ) {
+ updateMenuCounterOptimistically( -1 );
+ }
}
// Update on server
@@ -359,7 +365,7 @@ export const markAsReadAction = {
data: { is_unread: false },
} )
.then( ( { count } ) => {
- // Update the unread count in the menu.
+ // Update menu counter with accurate count from server.
updateMenuCounter( count );
} )
.catch( () => {
@@ -368,6 +374,11 @@ export const markAsReadAction = {
editEntityRecord( 'postType', 'feedback', id, {
is_unread: true,
} );
+
+ // Revert the optimistic change in the sidebar.
+ if ( status === 'publish' ) {
+ updateMenuCounterOptimistically( 1 );
+ }
}
throw new Error( 'Failed to mark as read' );
} );
@@ -419,7 +430,7 @@ export const markAsUnreadAction = {
const { getEntityRecord } = registry.select( coreStore );
const { createSuccessNotice, createErrorNotice } = registry.dispatch( noticesStore );
const promises = await Promise.allSettled(
- items.map( async ( { id } ) => {
+ items.map( async ( { id, status } ) => {
// Get current entity from store
const currentEntity = getEntityRecord( 'postType', 'feedback', id );
@@ -428,6 +439,11 @@ export const markAsUnreadAction = {
editEntityRecord( 'postType', 'feedback', id, {
is_unread: true,
} );
+
+ // Immediately update menu counters optimistically to avoid delays, but only for inbox
+ if ( status === 'publish' ) {
+ updateMenuCounterOptimistically( 1 );
+ }
}
// Update on server
@@ -437,7 +453,7 @@ export const markAsUnreadAction = {
data: { is_unread: true },
} )
.then( ( { count } ) => {
- // Update the unread count in the menu.
+ // Update menu counter with accurate count from server.
updateMenuCounter( count );
} )
.catch( () => {
@@ -446,6 +462,11 @@ export const markAsUnreadAction = {
editEntityRecord( 'postType', 'feedback', id, {
is_unread: false,
} );
+
+ // Revert the optimistic change in the sidebar.
+ if ( status === 'publish' ) {
+ updateMenuCounterOptimistically( -1 );
+ }
}
throw new Error( 'Failed to mark as unread' );
} );
diff --git a/projects/packages/forms/src/dashboard/inbox/response.js b/projects/packages/forms/src/dashboard/inbox/response.js
index 06dcd61965c0b..964e90232f627 100644
--- a/projects/packages/forms/src/dashboard/inbox/response.js
+++ b/projects/packages/forms/src/dashboard/inbox/response.js
@@ -37,7 +37,7 @@ import {
markAsReadAction,
markAsUnreadAction,
} from './dataviews/actions';
-import { getPath, updateMenuCounter } from './utils';
+import { getPath, updateMenuCounter, updateMenuCounterOptimistically } from './utils';
const getDisplayName = response => {
const { author_name, author_email, author_url, ip } = response;
@@ -550,6 +550,11 @@ const InboxResponse = ( {
is_unread: false,
} );
+ // Immediately update menu counters optimistically to avoid delays
+ if ( response.status === 'publish' ) {
+ updateMenuCounterOptimistically( -1 );
+ }
+
// Then update on server
apiFetch( {
path: `/wp/v2/feedback/${ response.id }/read`,
@@ -557,12 +562,19 @@ const InboxResponse = ( {
data: { is_unread: false },
} )
.then( ( { count } ) => {
+ // Update menu counter with accurate count from server
updateMenuCounter( count );
} )
.catch( () => {
+ // Revert the change in the store
editEntityRecord( 'postType', 'feedback', response.id, {
is_unread: true,
} );
+
+ // Revert the change in the sidebar
+ if ( response.status === 'publish' ) {
+ updateMenuCounterOptimistically( 1 );
+ }
} );
}, [ response, editEntityRecord, hasMarkedSelfAsRead ] );
diff --git a/projects/packages/forms/src/dashboard/inbox/utils.js b/projects/packages/forms/src/dashboard/inbox/utils.js
index add9f7c8a2e03..1dd7ebd1094c1 100644
--- a/projects/packages/forms/src/dashboard/inbox/utils.js
+++ b/projects/packages/forms/src/dashboard/inbox/utils.js
@@ -1,3 +1,5 @@
+import { formatNumber } from '@automattic/number-formatters';
+
// Function to get the URL of the page or post where the form was submitted.
export const getPath = item => {
try {
@@ -9,20 +11,57 @@ export const getPath = item => {
};
/**
- * Update the unread count in the admin menu.
+ * Update `count-0` style CSS class in the unread menu badge with new count like `count-1`.
+ *
+ * @param {HTMLElement} element - Counter badge element
+ * @param {number} count - Count to use in new CSS class
+ */
+function updateBadge( element, count ) {
+ const oldClass = [ ...element.classList ].find( c => c.startsWith( 'count-' ) );
+ if ( oldClass ) {
+ element.classList.replace( oldClass, `count-${ count }` );
+ } else {
+ element.classList.add( `count-${ count }` );
+ }
+
+ element.ariaHidden = count > 0 ? 'false' : 'true';
+ element.textContent = formatNumber( count );
+}
+
+/**
+ * Update the unread count in the admin menu to specific count.
*
* @param {number} count - The new unread count.
*/
export const updateMenuCounter = count => {
- // iterate over all elements with the class 'jp-feedback-unread-counter' and update their text content
+ // Iterate over all badges with the class 'jp-feedback-unread-counter' and update their count
document.querySelectorAll( '.jp-feedback-unread-counter' ).forEach( item => {
+ // Jetpack menu item has combined count and forms unread counter
if ( item.dataset.unreadDiff ) {
- const newCount = parseInt( item.dataset.unreadDiff, 10 ) + count;
- item.textContent = newCount > 0 ? newCount : '';
- item.style.display = newCount > 0 ? '' : 'none';
+ const unreadDiff = parseInt( item.dataset.unreadDiff, 10 ) + count;
+ updateBadge( item, unreadDiff );
} else {
- item.textContent = count > 0 ? count : '';
- item.style.display = count > 0 ? '' : 'none';
+ updateBadge( item, count );
+ }
+ } );
+};
+
+/**
+ * Update the unread count in the admin menu by addition or substraction, not by knowing the actual count.
+ *
+ * @param {number} count - By how much we should add or substract from the current sidebar menu count; either positive or negative integer.
+ */
+export const updateMenuCounterOptimistically = count => {
+ // Iterate over all badges with the class 'jp-feedback-unread-counter' and update their count
+ document.querySelectorAll( '.jp-feedback-unread-counter' ).forEach( item => {
+ let optimisticCount = 0;
+ if ( item.textContent !== '' ) {
+ // Ensure large formatted numbers like "1,000" are converted to integers properly
+ optimisticCount = parseInt( item.textContent.replace( /\D/g, '' ), 10 ) + count;
+ }
+
+ if ( optimisticCount >= 0 ) {
+ updateBadge( item, optimisticCount );
}
} );
};