From e4088070b42f912ba70b7f7537fe1377ce8da533 Mon Sep 17 00:00:00 2001 From: Addison Stavlo Date: Tue, 24 Feb 2026 10:45:36 -0500 Subject: [PATCH 1/6] add previous send awareness to newsletter panel --- .../memberships/subscribers-affirmation.js | 376 +++++++++++++++--- .../store/membership-products/actions.js | 10 +- .../store/membership-products/reducer.js | 14 +- .../store/membership-products/resolvers.js | 81 ++-- .../store/membership-products/selectors.js | 9 +- .../membership-products/test/actions-test.js | 12 - .../membership-products/test/reducer-test.js | 15 - .../test/selectors-test.js | 9 - 8 files changed, 393 insertions(+), 133 deletions(-) diff --git a/projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js b/projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js index 9ced31e5f14d..7a1333a976ce 100644 --- a/projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js +++ b/projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js @@ -3,7 +3,7 @@ import { isComingSoon } from '@automattic/jetpack-shared-extension-utils'; import { Animate } from '@wordpress/components'; import { useSelect } from '@wordpress/data'; import { store as editorStore } from '@wordpress/editor'; -import { createInterpolateElement } from '@wordpress/element'; +import { createInterpolateElement, useRef, useEffect } from '@wordpress/element'; import { sprintf, __, _n } from '@wordpress/i18n'; import paywallBlockMetadata from '../../blocks/paywall/block.json'; import { @@ -164,36 +164,193 @@ export const getCopyForSubscribers = ( { ); }; +const SENDING_IN_PROGRESS_WINDOW_MS = 15 * 60 * 1000; + +/** + * Format sent date from Unix timestamp or MySQL timestamp string. + * + * @param {number|null} emailSentAt - Unix timestamp from email_notification meta + * @param {string|null} statsTimestamp - MySQL timestamp from stats_on_send + * @return {string} Formatted date string + */ +function formatSentDate( emailSentAt, statsTimestamp ) { + let date = null; + if ( emailSentAt ) { + date = new Date( emailSentAt * 1000 ); + } else if ( statsTimestamp ) { + date = new Date( statsTimestamp ); + } + if ( ! date || isNaN( date.getTime() ) ) { + return ''; + } + return date.toLocaleDateString( undefined, { + year: 'numeric', + month: 'long', + day: 'numeric', + } ); +} + +/** + * Get category names from stats post_categories (term IDs) using newsletter categories list. + * + * @param {Array} postCategories - Term IDs from stats_on_send + * @param {Array} newsletterCategories - Site newsletter categories with id, name + * @return {string} Formatted category list (e.g. "Category A and Category B" or "All content") + */ +function getCategoryNamesFromStats( postCategories, newsletterCategories ) { + if ( ! postCategories?.length ) { + return ''; + } + const categoryNames = postCategories + .map( termId => { + const cat = newsletterCategories.find( c => c.id === termId ); + return cat ? cat.name : null; + } ) + .filter( Boolean ); + const hasUnknown = postCategories.some( + termId => ! newsletterCategories.some( c => c.id === termId ) + ); + if ( hasUnknown ) { + categoryNames.push( __( 'All content', 'jetpack' ) ); + } + const formatted = categoryNames.map( n => `${ n }` ); + if ( formatted.length === 1 ) return formatted[ 0 ]; + if ( formatted.length === 2 ) { + return sprintf( + /* translators: %1$s: first category, %2$s: second category */ + __( '%1$s and %2$s', 'jetpack' ), + formatted[ 0 ], + formatted[ 1 ] + ); + } + const allButLast = formatted.slice( 0, -1 ).join( `${ __( ',', 'jetpack' ) } ` ); + return sprintf( + /* translators: %1$s: comma-separated categories, %2$s: last category */ + __( '%1$s, and %2$s', 'jetpack' ), + allButLast, + formatted[ formatted.length - 1 ] + ); +} + +/** + * Get access level label for "was emailed to X" copy from stats access_level string. + * + * @param {string} accessLevel - e.g. 'everybody', 'subscribers', 'paid_subscribers' + * @return {string} Access level label for display (e.g. "all subscribers", "paid subscribers"). + */ +function getAccessLevelLabelFromStats( accessLevel ) { + if ( ! accessLevel ) return __( 'all subscribers', 'jetpack' ); + const key = accessLevel.startsWith( 'paid_subscribers' ) ? 'paid_subscribers' : accessLevel; + switch ( key ) { + case 'everybody': + return __( 'all subscribers', 'jetpack' ); + case 'subscribers': + return __( 'all subscribers', 'jetpack' ); + case 'paid_subscribers': + return __( 'paid subscribers', 'jetpack' ); + default: + return __( 'all subscribers', 'jetpack' ); + } +} + +/** + * Build "was sent" or "is being sent" copy for access + categories. + * + * @param {object} opts + * @param {string} opts.accessLabel - "all subscribers" or "paid subscribers" + * @param {string} opts.categoryNames - Formatted category list (or empty) + * @param {boolean} opts.pastTense - "was emailed" vs "is being emailed" + * @param {string} opts.dateStr - For past tense only + * @return {string} Formatted sentence for "was sent" or "is being sent" copy. + */ +function getSentCopyLine( { accessLabel, categoryNames, pastTense, dateStr } ) { + if ( categoryNames ) { + if ( pastTense ) { + return sprintf( + /* translators: %1$s: access (e.g. "all subscribers"), %2$s: category list, %3$s: date */ + __( + 'This post was emailed to %1$s subscribers of %2$s on %3$s. View delivery details on your email stats page.', + 'jetpack' + ), + accessLabel, + categoryNames, + dateStr + ); + } + return sprintf( + /* translators: %1$s: access, %2$s: category list */ + __( + 'This post is being emailed to %1$s subscribers of %2$s. Delivery details can be seen on your email stats page shortly.', + 'jetpack' + ), + accessLabel, + categoryNames + ); + } + if ( pastTense ) { + return sprintf( + /* translators: %1$s: access, %2$s: date */ + __( + 'This post was emailed to %1$s subscribers on %2$s. View delivery details on your email stats page.', + 'jetpack' + ), + accessLabel, + dateStr + ); + } + return sprintf( + /* translators: %s: access level */ + __( + 'This post is being emailed to %s subscribers. Delivery details can be seen on your email stats page shortly.', + 'jetpack' + ), + accessLabel + ); +} + /* * Determines copy to show in pre/post-publish panels to confirm number and type of subscribers receiving the post as email. */ function SubscribersAffirmation( { accessLevel, prePublish = false } ) { + const wasPublishedOnLoad = useRef( undefined ); + const transitionedToPublishInSession = useRef( false ); + const postHasPaywallBlock = useSelect( select => select( 'core/block-editor' ) .getBlocks() .some( block => block.name === paywallBlockMetadata.name ) ); - const { isScheduledPost, isPostOlderThanADay, postCategories, postId, postMeta } = useSelect( + const { isScheduledPost, postCategories, postId, postMeta, publishDate, status } = useSelect( select => { const { isCurrentPostScheduled, getEditedPostAttribute, getCurrentPost } = select( editorStore ); - const status = getCurrentPost()?.status; - const publishTime = new Date( getCurrentPost()?.date ); - const time24HoursAgo = new Date( Date.now() - 24 * 60 * 60 * 1000 ); + const post = getCurrentPost(); + const statusVal = post?.status; + const dateVal = post?.date; + const publishTime = dateVal ? new Date( dateVal ) : null; return { isScheduledPost: isCurrentPostScheduled(), - isPostOlderThanADay: status === 'publish' && publishTime < time24HoursAgo, postCategories: getEditedPostAttribute( 'categories' ), - postId: getCurrentPost()?.id, + postId: post?.id, postMeta: getEditedPostAttribute( 'meta' ), + publishDate: publishTime, + status: statusVal, }; } ); + useEffect( () => { + if ( status === 'publish' ) { + if ( wasPublishedOnLoad.current === undefined ) { + wasPublishedOnLoad.current = true; + } + transitionedToPublishInSession.current = true; + } + }, [ status ] ); + const isSendEmailEnabled = () => { - // Meta value is negated, "don't send", but toggle is truthy when enabled "send" return ! postMeta?.[ META_NAME_FOR_POST_DONT_EMAIL_TO_SUBS ]; }; @@ -205,35 +362,47 @@ function SubscribersAffirmation( { accessLevel, prePublish = false } ) { newsletterCategoriesEnabled, newsletterCategorySubscriberCount, paidSubscribersCount, - totalEmailsSentCount, // To display stats from Jetpack. It is necessary to provide the accurate count for historical posts. - } = useSelect( select => { - const { - getNewsletterCategories, - getNewsletterCategoriesEnabled, - getNewsletterCategoriesSubscriptionsCount, - getSubscriberCounts, - getTotalEmailsSentCount, - hasFinishedResolution, - } = select( membershipProductsStore ); - - // Free and paid subscriber counts - const { emailSubscribers, paidSubscribers } = getSubscriberCounts(); - - return { - hasFinishedLoading: [ - // getNewsletterCategoriesEnabled state is set by getNewsletterCategories so no need to check for it here. - hasFinishedResolution( 'getSubscriberCounts' ), - hasFinishedResolution( 'getNewsletterCategories' ), - hasFinishedResolution( 'getNewsletterCategoriesSubscriptionsCount' ), - ].every( Boolean ), - emailSubscribersCount: emailSubscribers, - newsletterCategories: getNewsletterCategories(), - newsletterCategoriesEnabled: getNewsletterCategoriesEnabled(), - newsletterCategorySubscriberCount: getNewsletterCategoriesSubscriptionsCount(), - paidSubscribersCount: paidSubscribers, - totalEmailsSentCount: isPostOlderThanADay ? getTotalEmailsSentCount( blogId, postId ) : null, - }; - } ); + postEmailSentState, + } = useSelect( + select => { + const { + getNewsletterCategories, + getNewsletterCategoriesEnabled, + getNewsletterCategoriesSubscriptionsCount, + getPostEmailSentState, + getSubscriberCounts, + hasFinishedResolution, + } = select( membershipProductsStore ); + + const { emailSubscribers, paidSubscribers } = getSubscriberCounts(); + + // Trigger fetch when post is published so we have email_sent_at / stats_on_send + if ( status === 'publish' && postId ) { + getPostEmailSentState( postId ); + } + + const postEmailResolved = + status !== 'publish' || + ! postId || + hasFinishedResolution( 'getPostEmailSentState', [ postId ] ); + + return { + hasFinishedLoading: [ + hasFinishedResolution( 'getSubscriberCounts' ), + hasFinishedResolution( 'getNewsletterCategories' ), + hasFinishedResolution( 'getNewsletterCategoriesSubscriptionsCount' ), + postEmailResolved, + ].every( Boolean ), + emailSubscribersCount: emailSubscribers, + newsletterCategories: getNewsletterCategories(), + newsletterCategoriesEnabled: getNewsletterCategoriesEnabled(), + newsletterCategorySubscriberCount: getNewsletterCategoriesSubscriptionsCount(), + paidSubscribersCount: paidSubscribers, + postEmailSentState: status === 'publish' && postId ? getPostEmailSentState( postId ) : null, + }; + }, + [ status, postId ] + ); if ( ! hasFinishedLoading ) { return ( @@ -248,38 +417,154 @@ function SubscribersAffirmation( { accessLevel, prePublish = false } ) { } const isPaidPost = accessLevel === accessOptions.paid_subscribers.key; - - // Show all copy in future tense const futureTense = prePublish || isScheduledPost; + const emailSentAt = postEmailSentState?.email_sent_at ?? null; + const statsOnSend = postEmailSentState?.stats_on_send ?? null; + + const isAlreadySent = emailSentAt != null; + const isSendingInProgress = + status === 'publish' && + isSendEmailEnabled() && + emailSentAt == null && + ( transitionedToPublishInSession.current || + ( wasPublishedOnLoad.current && + publishDate && + publishDate.getTime() >= Date.now() - SENDING_IN_PROGRESS_WINDOW_MS ) ); + const isPublishedNotSent = + status === 'publish' && isSendEmailEnabled() && emailSentAt == null && ! isSendingInProgress; + const reachForAccessLevel = getReachForAccessLevelKey( { accessLevel, - subscribers: isPostOlderThanADay ? totalEmailsSentCount : emailSubscribersCount, + subscribers: emailSubscribersCount, paidSubscribers: paidSubscribersCount, postHasPaywallBlock, } ); let text; + let append = ''; if ( ! isSendEmailEnabled() ) { - text = __( 'Not sent via email.', 'jetpack' ); + // "Post only" but already emailed: show "was sent" copy, not "Not sent via email" + if ( isAlreadySent && statsOnSend ) { + const dateStr = formatSentDate( emailSentAt, statsOnSend.timestamp ); + const accessLabel = getAccessLevelLabelFromStats( statsOnSend.access_level ); + const categoryNames = getCategoryNamesFromStats( + statsOnSend.post_categories, + newsletterCategories + ); + text = getSentCopyLine( { + accessLabel, + categoryNames, + pastTense: true, + dateStr, + } ); + } else if ( isAlreadySent ) { + text = sprintf( + /* translators: %s: formatted date */ + __( + 'This post was emailed on %s. View delivery details on your email stats page.', + 'jetpack' + ), + formatSentDate( emailSentAt, null ) + ); + } else { + text = __( 'Not sent via email.', 'jetpack' ); + } } else if ( isComingSoon() ) { text = __( 'Your site is in Coming Soon mode. Emails are sent only when your site is public.', 'jetpack' ); + } else if ( isAlreadySent ) { + const dateStr = formatSentDate( emailSentAt, statsOnSend?.timestamp ?? null ); + if ( statsOnSend ) { + const accessLabel = getAccessLevelLabelFromStats( statsOnSend.access_level ); + const categoryNames = getCategoryNamesFromStats( + statsOnSend.post_categories, + newsletterCategories + ); + text = getSentCopyLine( { + accessLabel, + categoryNames, + pastTense: true, + dateStr, + } ); + } else { + text = sprintf( + /* translators: %s: formatted date */ + __( + 'This post was emailed on %s. View delivery details on your email stats page.', + 'jetpack' + ), + dateStr + ); + } + if ( transitionedToPublishInSession.current ) { + append = __( 'Updating or republishing does not send a new email.', 'jetpack' ); + } + const statsAccess = statsOnSend?.access_level; + const statsCats = statsOnSend?.post_categories ?? []; + const accessMatches = + ! statsAccess || statsAccess === accessLevel || statsAccess.startsWith( accessLevel ); + const categoriesMatch = + ! statsOnSend?.has_newsletter_categories || + ( Array.isArray( postCategories ) && + statsCats.length === postCategories.length && + statsCats.every( ( id, i ) => postCategories[ i ] === id ) ); + if ( ! accessMatches || ! categoriesMatch ) { + append = append + ? append + ' ' + __( 'Changing access settings does not resend the email.', 'jetpack' ) + : __( 'Changing access settings does not resend the email.', 'jetpack' ); + } + } else if ( isSendingInProgress ) { + const accessLabel = getAccessLevelLabelFromStats( accessLevel ); + const categoryNames = + newsletterCategoriesEnabled && newsletterCategories?.length && postCategories?.length + ? getFormattedCategories( postCategories, newsletterCategories ) + : ''; + text = getSentCopyLine( { + accessLabel, + categoryNames, + pastTense: false, + dateStr: '', + } ); + } else if ( isPublishedNotSent ) { + text = __( + "This post was published without sending an email. To send, move the post to draft, enable 'Post and email,' and republish.", + 'jetpack' + ); + } else if ( futureTense ) { + // Pre-send: "will be sent" with subscriber counts + if ( newsletterCategoriesEnabled && newsletterCategories.length > 0 && ! isPaidPost ) { + text = getCopyForCategorySubscribers( { + futureTense: true, + isPaidPost, + newsletterCategories, + postCategories, + reachCount: newsletterCategorySubscriberCount, + } ); + } else { + text = getCopyForSubscribers( { + futureTense: true, + isPaidPost, + postHasPaywallBlock, + reachCount: reachForAccessLevel, + } ); + } } else if ( newsletterCategoriesEnabled && newsletterCategories.length > 0 && ! isPaidPost ) { - // Get newsletter category copy & count separately, unless post is paid + // Published, not sent, not sending in progress (fallback) — category subscribers text = getCopyForCategorySubscribers( { - futureTense, + futureTense: false, isPaidPost, newsletterCategories, postCategories, - reachCount: isPostOlderThanADay ? totalEmailsSentCount : newsletterCategorySubscriberCount, + reachCount: newsletterCategorySubscriberCount, } ); } else { + // Published, not sent, not sending in progress (fallback) — all/paid subscribers text = getCopyForSubscribers( { - futureTense, + futureTense: false, isPaidPost, postHasPaywallBlock, reachCount: reachForAccessLevel, @@ -292,6 +577,7 @@ function SubscribersAffirmation( { accessLevel, prePublish = false } ) { strong: , link: , } ) } + { append ? <> { createInterpolateElement( append, { strong: } ) } : null }

); } diff --git a/projects/plugins/jetpack/extensions/store/membership-products/actions.js b/projects/plugins/jetpack/extensions/store/membership-products/actions.js index 69074677ac4e..75bdc8018c59 100644 --- a/projects/plugins/jetpack/extensions/store/membership-products/actions.js +++ b/projects/plugins/jetpack/extensions/store/membership-products/actions.js @@ -110,11 +110,6 @@ export const setSubscriberCounts = subscriberCounts => ( { subscriberCounts, } ); -export const setTotalEmailsSentCount = totalEmailsSentCount => ( { - type: 'SET_TOTAL_EMAILS_SENT_COUNT', - totalEmailsSentCount, -} ); - export const setNewsletterCategories = newsletterCategories => ( { type: 'SET_NEWSLETTER_CATEGORIES', newsletterCategories, @@ -125,3 +120,8 @@ export const setNewsletterCategoriesSubscriptionsCount = type: 'SET_NEWSLETTER_CATEGORIES_SUBSCRIPTIONS_COUNT', newsletterCategoriesSubscriptionsCount, } ); + +export const setPostEmailSentState = ( { email_sent_at, stats_on_send } ) => ( { + type: 'SET_POST_EMAIL_SENT_STATE', + payload: { email_sent_at, stats_on_send }, +} ); diff --git a/projects/plugins/jetpack/extensions/store/membership-products/reducer.js b/projects/plugins/jetpack/extensions/store/membership-products/reducer.js index e85fbf9fc528..47f6d2ed0d96 100644 --- a/projects/plugins/jetpack/extensions/store/membership-products/reducer.js +++ b/projects/plugins/jetpack/extensions/store/membership-products/reducer.js @@ -16,6 +16,10 @@ export const DEFAULT_STATE = { enabled: false, categories: [], }, + postEmailSentState: { + email_sent_at: null, + stats_on_send: null, + }, }; export default function reducer( state = DEFAULT_STATE, action ) { @@ -38,11 +42,6 @@ export default function reducer( state = DEFAULT_STATE, action ) { ...state, subscriberCounts: action.subscriberCounts, }; - case 'SET_TOTAL_EMAILS_SENT_COUNT': - return { - ...state, - totalEmailsSentCount: action.totalEmailsSentCount, - }; case 'SET_NEWSLETTER_CATEGORIES': return { ...state, @@ -53,6 +52,11 @@ export default function reducer( state = DEFAULT_STATE, action ) { ...state, newsletterCategoriesSubscriptionsCount: action.newsletterCategoriesSubscriptionsCount, }; + case 'SET_POST_EMAIL_SENT_STATE': + return { + ...state, + postEmailSentState: action.payload, + }; } return state; } diff --git a/projects/plugins/jetpack/extensions/store/membership-products/resolvers.js b/projects/plugins/jetpack/extensions/store/membership-products/resolvers.js index d943c52a1d8f..a7e557d8b8aa 100644 --- a/projects/plugins/jetpack/extensions/store/membership-products/resolvers.js +++ b/projects/plugins/jetpack/extensions/store/membership-products/resolvers.js @@ -1,4 +1,3 @@ -import { isSimpleSite } from '@automattic/jetpack-script-data'; import apiFetch from '@wordpress/api-fetch'; import { store as editorStore } from '@wordpress/editor'; import { addQueryArgs, getQueryArg } from '@wordpress/url'; @@ -16,19 +15,19 @@ import { setSubscriberCounts, setNewsletterCategories, setNewsletterCategoriesSubscriptionsCount, - setTotalEmailsSentCount, + setPostEmailSentState, } from './actions'; import { API_STATE_CONNECTED, API_STATE_NOTCONNECTED } from './constants'; import { onError } from './utils'; const EXECUTION_KEY = 'membership-products-resolver-getProducts'; const SUBSCRIBER_COUNT_EXECUTION_KEY = 'membership-products-resolver-getSubscriberCounts'; -const TOTAL_EMAILS_SENT_COUNT_EXECUTION_KEY = - 'membership-products-resolver-getTotalEmailsSentCount'; const GET_NEWSLETTER_CATEGORIES_EXECUTION_KEY = 'membership-products-resolver-getNewsletterCategories'; const GET_NEWSLETTER_CATEGORIES_SUBSCRIPTIONS_COUNT_EXECUTION_KEY = 'membership-products-resolver-getNewsletterCategoriesSubscriptionsCount'; +const GET_POST_EMAIL_SENT_STATE_EXECUTION_KEY = + 'membership-products-resolver-getPostEmailSentState'; let hydratedFromAPI = false; const fetchMemberships = async () => { @@ -101,14 +100,9 @@ const fetchSubscriberCounts = async () => { return response; }; -const fetchTotalEmailsSentCount = async ( blogId, postId ) => { - if ( ! blogId || ! postId ) { - return; - } - - const baseUrl = isSimpleSite() ? '/rest/v1.1/sites' : '/jetpack/v4/stats-app/sites'; +const fetchNewsletterCategories = async () => { const response = await apiFetch( { - path: baseUrl + `/${ blogId }/stats/opens/emails/${ postId }/rate`, + path: '/wpcom/v2/newsletter-categories', } ); if ( ! response || typeof response !== 'object' ) { @@ -131,9 +125,10 @@ const fetchTotalEmailsSentCount = async ( blogId, postId ) => { return response; }; -const fetchNewsletterCategories = async () => { +export const fetchNewsletterCategoriesSubscriptionsCount = async termIds => { const response = await apiFetch( { - path: '/wpcom/v2/newsletter-categories', + path: `/wpcom/v2/newsletter-categories/count?term_ids=${ termIds.join( ',' ) }`, + method: 'GET', } ); if ( ! response || typeof response !== 'object' ) { @@ -156,9 +151,13 @@ const fetchNewsletterCategories = async () => { return response; }; -export const fetchNewsletterCategoriesSubscriptionsCount = async termIds => { +const fetchPostEmailSentState = async postId => { + if ( ! postId ) { + return { email_sent_at: null, stats_on_send: null }; + } + const response = await apiFetch( { - path: `/wpcom/v2/newsletter-categories/count?term_ids=${ termIds.join( ',' ) }`, + path: `/wpcom/v2/newsletter-email-sent-status?post_id=${ postId }`, method: 'GET', } ); @@ -166,14 +165,6 @@ export const fetchNewsletterCategoriesSubscriptionsCount = async termIds => { throw new Error( 'Unexpected API response' ); } - /** - * WP_Error returns a list of errors with custom names: - * `errors: { foo: [ 'message' ], bar: [ 'message' ] }` - * Since we don't know their names, to get the message, we transform the object - * into an array, and just pick the first message of the first error. - * - * @see https://developer.wordpress.org/reference/classes/wp_error/ - */ const wpError = response?.errors && Object.values( response.errors )?.[ 0 ]?.[ 0 ]; if ( wpError ) { throw new Error( wpError ); @@ -288,23 +279,6 @@ export const getSubscriberCounts = } }; -export const getTotalEmailsSentCount = - ( blogId, postId ) => - async ( { dispatch, registry } ) => { - await executionLock.blockExecution( TOTAL_EMAILS_SENT_COUNT_EXECUTION_KEY ); - - const lock = executionLock.acquire( TOTAL_EMAILS_SENT_COUNT_EXECUTION_KEY ); - try { - const response = await fetchTotalEmailsSentCount( blogId, postId ); - dispatch( setTotalEmailsSentCount( response?.total_sends ) ); - } catch ( error ) { - dispatch( setApiState( API_STATE_NOTCONNECTED ) ); - onError( error.message, registry ); - } finally { - executionLock.release( lock ); - } - }; - export const getNewsletterCategories = () => async ( { dispatch, registry } ) => { @@ -349,3 +323,30 @@ export const getNewsletterCategoriesSubscriptionsCount = executionLock.release( lock ); } }; + +export const getPostEmailSentState = + postId => + async ( { dispatch, registry } ) => { + if ( ! postId ) { + dispatch( setPostEmailSentState( { email_sent_at: null, stats_on_send: null } ) ); + return; + } + + await executionLock.blockExecution( GET_POST_EMAIL_SENT_STATE_EXECUTION_KEY ); + + const lock = executionLock.acquire( GET_POST_EMAIL_SENT_STATE_EXECUTION_KEY ); + try { + const response = await fetchPostEmailSentState( postId ); + dispatch( + setPostEmailSentState( { + email_sent_at: response.email_sent_at ?? null, + stats_on_send: response.stats_on_send ?? null, + } ) + ); + } catch ( error ) { + dispatch( setApiState( API_STATE_NOTCONNECTED ) ); + onError( error.message, registry ); + } finally { + executionLock.release( lock ); + } + }; diff --git a/projects/plugins/jetpack/extensions/store/membership-products/selectors.js b/projects/plugins/jetpack/extensions/store/membership-products/selectors.js index 6207c7803a0c..125d3cd27f1a 100644 --- a/projects/plugins/jetpack/extensions/store/membership-products/selectors.js +++ b/projects/plugins/jetpack/extensions/store/membership-products/selectors.js @@ -30,8 +30,6 @@ export const isInvalidProduct = ( state, productId ) => export const getSubscriberCounts = state => state.subscriberCounts; -export const getTotalEmailsSentCount = state => state.totalEmailsSentCount; - export const getNewsletterCategories = state => state.newsletterCategories.categories; export const getNewsletterCategoriesEnabled = state => state.newsletterCategories.enabled; @@ -39,6 +37,13 @@ export const getNewsletterCategoriesEnabled = state => state.newsletterCategorie export const getNewsletterCategoriesSubscriptionsCount = state => state.newsletterCategoriesSubscriptionsCount; +export const getPostEmailSentState = ( state, postId ) => { + if ( ! postId ) { + return { email_sent_at: null, stats_on_send: null }; + } + return state.postEmailSentState || { email_sent_at: null, stats_on_send: null }; +}; + export const hasInvalidProducts = ( state, selectedProductIds ) => { return ( !! selectedProductIds && diff --git a/projects/plugins/jetpack/extensions/store/membership-products/test/actions-test.js b/projects/plugins/jetpack/extensions/store/membership-products/test/actions-test.js index 665e3e531dcb..3096d91d11dd 100644 --- a/projects/plugins/jetpack/extensions/store/membership-products/test/actions-test.js +++ b/projects/plugins/jetpack/extensions/store/membership-products/test/actions-test.js @@ -11,7 +11,6 @@ import { setConnectedAccountDefaultCurrency, setNewsletterCategories, setNewsletterCategoriesSubscriptionsCount, - setTotalEmailsSentCount, } from '../actions'; import * as utils from '../utils'; @@ -331,15 +330,4 @@ describe( 'Membership Products Actions', () => { // Then expect( result ).toStrictEqual( anyValidNewsletterCategoriesSubscriptionsCountWithType ); } ); - - test( 'Set total emails sent count works as expected', () => { - const anyValidTotalEmailsSentCountWithType = { - type: 'SET_TOTAL_EMAILS_SENT_COUNT', - totalEmailsSentCount: ANY_VALID_DATA, - }; - - const result = setTotalEmailsSentCount( ANY_VALID_DATA ); - - expect( result ).toStrictEqual( anyValidTotalEmailsSentCountWithType ); - } ); } ); diff --git a/projects/plugins/jetpack/extensions/store/membership-products/test/reducer-test.js b/projects/plugins/jetpack/extensions/store/membership-products/test/reducer-test.js index 3eab2ac47b10..4769f6eeecb1 100644 --- a/projects/plugins/jetpack/extensions/store/membership-products/test/reducer-test.js +++ b/projects/plugins/jetpack/extensions/store/membership-products/test/reducer-test.js @@ -148,19 +148,4 @@ describe( 'Membership products reducer testing', () => { newsletterCategoriesSubscriptionsCount: anyNewsletterCategoriesSubscriptionsCount, } ); } ); - - test( 'SET_TOTAL_EMAILS_SENT_COUNT action adds the total emails sent count to the returned state.', () => { - const anyTotalEmailsSentCount = 10; - const anySetTotalEmailsSentCountAction = { - type: 'SET_TOTAL_EMAILS_SENT_COUNT', - totalEmailsSentCount: anyTotalEmailsSentCount, - }; - - const returnedState = reducer( DEFAULT_STATE, anySetTotalEmailsSentCountAction ); - - expect( returnedState ).toStrictEqual( { - ...DEFAULT_STATE, - totalEmailsSentCount: anyTotalEmailsSentCount, - } ); - } ); } ); diff --git a/projects/plugins/jetpack/extensions/store/membership-products/test/selectors-test.js b/projects/plugins/jetpack/extensions/store/membership-products/test/selectors-test.js index 070eef4a65b2..3844e55f4a95 100644 --- a/projects/plugins/jetpack/extensions/store/membership-products/test/selectors-test.js +++ b/projects/plugins/jetpack/extensions/store/membership-products/test/selectors-test.js @@ -4,7 +4,6 @@ import { getNewsletterTierProducts, getNewsletterCategoriesSubscriptionsCount, getProducts, - getTotalEmailsSentCount, } from '../selectors'; describe( 'Membership Products Selectors', () => { @@ -57,12 +56,4 @@ describe( 'Membership Products Selectors', () => { state.newsletterCategoriesSubscriptionsCount ); } ); - - test( 'getTotalEmailsSentCount works as expected', () => { - const state = { - totalEmailsSentCount: 100, - }; - - expect( getTotalEmailsSentCount( state ) ).toStrictEqual( state.totalEmailsSentCount ); - } ); } ); From cb477dc9bdebe6f7545e210f783a7b8c42ae9d1c Mon Sep 17 00:00:00 2001 From: Addison Stavlo Date: Tue, 24 Feb 2026 10:47:28 -0500 Subject: [PATCH 2/6] Add changelog entries. --- .../changelog/update-newsletter-editor-panel-sent-awareness | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 projects/plugins/jetpack/changelog/update-newsletter-editor-panel-sent-awareness diff --git a/projects/plugins/jetpack/changelog/update-newsletter-editor-panel-sent-awareness b/projects/plugins/jetpack/changelog/update-newsletter-editor-panel-sent-awareness new file mode 100644 index 000000000000..5af6b0e5fbc7 --- /dev/null +++ b/projects/plugins/jetpack/changelog/update-newsletter-editor-panel-sent-awareness @@ -0,0 +1,4 @@ +Significance: minor +Type: other + +Newsletter editor panel: update to reflect previous email sends and update copies. From 176b570f1a4a17bc65a37fba73b40c8e6d76bf28 Mon Sep 17 00:00:00 2001 From: Addison Stavlo Date: Wed, 25 Feb 2026 09:16:07 -0500 Subject: [PATCH 3/6] add stats page links, fix state bugs --- .../memberships/subscribers-affirmation.js | 21 +++++++++++-------- .../store/membership-products/resolvers.js | 1 - 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js b/projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js index 7a1333a976ce..349fad0cfd93 100644 --- a/projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js +++ b/projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js @@ -269,7 +269,7 @@ function getSentCopyLine( { accessLabel, categoryNames, pastTense, dateStr } ) { return sprintf( /* translators: %1$s: access (e.g. "all subscribers"), %2$s: category list, %3$s: date */ __( - 'This post was emailed to %1$s subscribers of %2$s on %3$s. View delivery details on your email stats page.', + 'This post was emailed to %1$s of %2$s on %3$s. View delivery details on your email stats page.', 'jetpack' ), accessLabel, @@ -280,7 +280,7 @@ function getSentCopyLine( { accessLabel, categoryNames, pastTense, dateStr } ) { return sprintf( /* translators: %1$s: access, %2$s: category list */ __( - 'This post is being emailed to %1$s subscribers of %2$s. Delivery details can be seen on your email stats page shortly.', + 'This post is being emailed to %1$s of %2$s. Delivery details can be seen on your email stats page shortly.', 'jetpack' ), accessLabel, @@ -291,7 +291,7 @@ function getSentCopyLine( { accessLabel, categoryNames, pastTense, dateStr } ) { return sprintf( /* translators: %1$s: access, %2$s: date */ __( - 'This post was emailed to %1$s subscribers on %2$s. View delivery details on your email stats page.', + 'This post was emailed to %1$s on %2$s. View delivery details on your email stats page.', 'jetpack' ), accessLabel, @@ -301,7 +301,7 @@ function getSentCopyLine( { accessLabel, categoryNames, pastTense, dateStr } ) { return sprintf( /* translators: %s: access level */ __( - 'This post is being emailed to %s subscribers. Delivery details can be seen on your email stats page shortly.', + 'This post is being emailed to %s. Delivery details can be seen on your email stats page shortly.', 'jetpack' ), accessLabel @@ -314,6 +314,7 @@ function getSentCopyLine( { accessLabel, categoryNames, pastTense, dateStr } ) { function SubscribersAffirmation( { accessLevel, prePublish = false } ) { const wasPublishedOnLoad = useRef( undefined ); const transitionedToPublishInSession = useRef( false ); + const prevStatusRef = useRef( null ); const postHasPaywallBlock = useSelect( select => select( 'core/block-editor' ) @@ -346,8 +347,11 @@ function SubscribersAffirmation( { accessLevel, prePublish = false } ) { if ( wasPublishedOnLoad.current === undefined ) { wasPublishedOnLoad.current = true; } - transitionedToPublishInSession.current = true; + if ( prevStatusRef.current !== null && prevStatusRef.current !== 'publish' ) { + transitionedToPublishInSession.current = true; + } } + prevStatusRef.current = status; }, [ status ] ); const isSendEmailEnabled = () => { @@ -383,8 +387,7 @@ function SubscribersAffirmation( { accessLevel, prePublish = false } ) { const postEmailResolved = status !== 'publish' || - ! postId || - hasFinishedResolution( 'getPostEmailSentState', [ postId ] ); + ( !! postId && hasFinishedResolution( 'getPostEmailSentState', [ postId ] ) ); return { hasFinishedLoading: [ @@ -463,7 +466,7 @@ function SubscribersAffirmation( { accessLevel, prePublish = false } ) { text = sprintf( /* translators: %s: formatted date */ __( - 'This post was emailed on %s. View delivery details on your email stats page.', + 'This post was emailed on %s. View delivery details on your email stats page.', 'jetpack' ), formatSentDate( emailSentAt, null ) @@ -494,7 +497,7 @@ function SubscribersAffirmation( { accessLevel, prePublish = false } ) { text = sprintf( /* translators: %s: formatted date */ __( - 'This post was emailed on %s. View delivery details on your email stats page.', + 'This post was emailed on %s. View delivery details on your email stats page.', 'jetpack' ), dateStr diff --git a/projects/plugins/jetpack/extensions/store/membership-products/resolvers.js b/projects/plugins/jetpack/extensions/store/membership-products/resolvers.js index a7e557d8b8aa..6a111d1ed26a 100644 --- a/projects/plugins/jetpack/extensions/store/membership-products/resolvers.js +++ b/projects/plugins/jetpack/extensions/store/membership-products/resolvers.js @@ -328,7 +328,6 @@ export const getPostEmailSentState = postId => async ( { dispatch, registry } ) => { if ( ! postId ) { - dispatch( setPostEmailSentState( { email_sent_at: null, stats_on_send: null } ) ); return; } From 59a0df61a29c82d795aad8b8dc2707e87ef0c24d Mon Sep 17 00:00:00 2001 From: Addison Stavlo Date: Wed, 25 Feb 2026 10:19:00 -0500 Subject: [PATCH 4/6] use redux store to persist info and fix more bugs --- .../memberships/subscribers-affirmation.js | 63 ++++++++++++++----- .../store/membership-products/actions.js | 10 +++ .../store/membership-products/reducer.js | 18 ++++++ .../store/membership-products/selectors.js | 10 +++ 4 files changed, 84 insertions(+), 17 deletions(-) diff --git a/projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js b/projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js index 349fad0cfd93..74aa619ac087 100644 --- a/projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js +++ b/projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js @@ -1,7 +1,7 @@ import { getAdminUrl } from '@automattic/jetpack-script-data'; import { isComingSoon } from '@automattic/jetpack-shared-extension-utils'; import { Animate } from '@wordpress/components'; -import { useSelect } from '@wordpress/data'; +import { useDispatch, useSelect } from '@wordpress/data'; import { store as editorStore } from '@wordpress/editor'; import { createInterpolateElement, useRef, useEffect } from '@wordpress/element'; import { sprintf, __, _n } from '@wordpress/i18n'; @@ -12,6 +12,10 @@ import { } from '../../shared/memberships/constants'; import { getReachForAccessLevelKey } from '../../shared/memberships/settings'; import { store as membershipProductsStore } from '../../store/membership-products'; +import { + setPublishedWithEmailEnabledInSession, + setRepublishedAlreadySentPostInSession, +} from '../../store/membership-products/actions'; /** * Get the formatted list of categories for a post. @@ -342,23 +346,12 @@ function SubscribersAffirmation( { accessLevel, prePublish = false } ) { } ); - useEffect( () => { - if ( status === 'publish' ) { - if ( wasPublishedOnLoad.current === undefined ) { - wasPublishedOnLoad.current = true; - } - if ( prevStatusRef.current !== null && prevStatusRef.current !== 'publish' ) { - transitionedToPublishInSession.current = true; - } - } - prevStatusRef.current = status; - }, [ status ] ); - const isSendEmailEnabled = () => { return ! postMeta?.[ META_NAME_FOR_POST_DONT_EMAIL_TO_SUBS ]; }; const blogId = window.Jetpack_Editor_Initial_State?.wpcomBlogId; + const dispatch = useDispatch( membershipProductsStore ); const { emailSubscribersCount, hasFinishedLoading, @@ -367,6 +360,8 @@ function SubscribersAffirmation( { accessLevel, prePublish = false } ) { newsletterCategorySubscriberCount, paidSubscribersCount, postEmailSentState, + republishedAlreadySentInSession, + publishedWithEmailEnabledInSession, } = useSelect( select => { const { @@ -374,14 +369,16 @@ function SubscribersAffirmation( { accessLevel, prePublish = false } ) { getNewsletterCategoriesEnabled, getNewsletterCategoriesSubscriptionsCount, getPostEmailSentState, + getPublishedWithEmailEnabledInSession, + getRepublishedAlreadySentPostInSession, getSubscriberCounts, hasFinishedResolution, } = select( membershipProductsStore ); const { emailSubscribers, paidSubscribers } = getSubscriberCounts(); - // Trigger fetch when post is published so we have email_sent_at / stats_on_send - if ( status === 'publish' && postId ) { + // Trigger fetch when we have a postId so we have email_sent_at / stats_on_send (including for draft) + if ( postId ) { getPostEmailSentState( postId ); } @@ -401,12 +398,38 @@ function SubscribersAffirmation( { accessLevel, prePublish = false } ) { newsletterCategoriesEnabled: getNewsletterCategoriesEnabled(), newsletterCategorySubscriberCount: getNewsletterCategoriesSubscriptionsCount(), paidSubscribersCount: paidSubscribers, - postEmailSentState: status === 'publish' && postId ? getPostEmailSentState( postId ) : null, + postEmailSentState: postId ? getPostEmailSentState( postId ) : null, + republishedAlreadySentInSession: postId + ? getRepublishedAlreadySentPostInSession( postId ) + : false, + publishedWithEmailEnabledInSession: postId + ? getPublishedWithEmailEnabledInSession( postId ) + : false, }; }, [ status, postId ] ); + useEffect( () => { + if ( status === 'publish' ) { + if ( wasPublishedOnLoad.current === undefined ) { + wasPublishedOnLoad.current = true; + } + if ( prevStatusRef.current !== null && prevStatusRef.current !== 'publish' ) { + transitionedToPublishInSession.current = true; + if ( postId ) { + if ( postEmailSentState?.email_sent_at != null ) { + dispatch( setRepublishedAlreadySentPostInSession( postId ) ); + } + if ( ! postMeta?.[ META_NAME_FOR_POST_DONT_EMAIL_TO_SUBS ] ) { + dispatch( setPublishedWithEmailEnabledInSession( postId ) ); + } + } + } + } + prevStatusRef.current = status; + }, [ status, postId, postEmailSentState?.email_sent_at, dispatch, postMeta ] ); + if ( ! hasFinishedLoading ) { return ( @@ -431,6 +454,7 @@ function SubscribersAffirmation( { accessLevel, prePublish = false } ) { isSendEmailEnabled() && emailSentAt == null && ( transitionedToPublishInSession.current || + publishedWithEmailEnabledInSession || ( wasPublishedOnLoad.current && publishDate && publishDate.getTime() >= Date.now() - SENDING_IN_PROGRESS_WINDOW_MS ) ); @@ -471,6 +495,11 @@ function SubscribersAffirmation( { accessLevel, prePublish = false } ) { ), formatSentDate( emailSentAt, null ) ); + } else if ( status === 'publish' ) { + text = __( + "This post was published without sending an email. To send, move the post to draft, enable 'Post and email,' and republish.", + 'jetpack' + ); } else { text = __( 'Not sent via email.', 'jetpack' ); } @@ -503,7 +532,7 @@ function SubscribersAffirmation( { accessLevel, prePublish = false } ) { dateStr ); } - if ( transitionedToPublishInSession.current ) { + if ( transitionedToPublishInSession.current || republishedAlreadySentInSession ) { append = __( 'Updating or republishing does not send a new email.', 'jetpack' ); } const statsAccess = statsOnSend?.access_level; diff --git a/projects/plugins/jetpack/extensions/store/membership-products/actions.js b/projects/plugins/jetpack/extensions/store/membership-products/actions.js index 75bdc8018c59..b163d4b41991 100644 --- a/projects/plugins/jetpack/extensions/store/membership-products/actions.js +++ b/projects/plugins/jetpack/extensions/store/membership-products/actions.js @@ -125,3 +125,13 @@ export const setPostEmailSentState = ( { email_sent_at, stats_on_send } ) => ( { type: 'SET_POST_EMAIL_SENT_STATE', payload: { email_sent_at, stats_on_send }, } ); + +export const setRepublishedAlreadySentPostInSession = postId => ( { + type: 'SET_REPUBLISHED_ALREADY_SENT_POST_IN_SESSION', + postId, +} ); + +export const setPublishedWithEmailEnabledInSession = postId => ( { + type: 'SET_PUBLISHED_WITH_EMAIL_ENABLED_IN_SESSION', + postId, +} ); diff --git a/projects/plugins/jetpack/extensions/store/membership-products/reducer.js b/projects/plugins/jetpack/extensions/store/membership-products/reducer.js index 47f6d2ed0d96..8cb56fbd1b4c 100644 --- a/projects/plugins/jetpack/extensions/store/membership-products/reducer.js +++ b/projects/plugins/jetpack/extensions/store/membership-products/reducer.js @@ -20,6 +20,8 @@ export const DEFAULT_STATE = { email_sent_at: null, stats_on_send: null, }, + republishedAlreadySentPostInSession: {}, + publishedWithEmailEnabledInSession: {}, }; export default function reducer( state = DEFAULT_STATE, action ) { @@ -57,6 +59,22 @@ export default function reducer( state = DEFAULT_STATE, action ) { ...state, postEmailSentState: action.payload, }; + case 'SET_REPUBLISHED_ALREADY_SENT_POST_IN_SESSION': + return { + ...state, + republishedAlreadySentPostInSession: { + ...state.republishedAlreadySentPostInSession, + [ action.postId ]: true, + }, + }; + case 'SET_PUBLISHED_WITH_EMAIL_ENABLED_IN_SESSION': + return { + ...state, + publishedWithEmailEnabledInSession: { + ...state.publishedWithEmailEnabledInSession, + [ action.postId ]: true, + }, + }; } return state; } diff --git a/projects/plugins/jetpack/extensions/store/membership-products/selectors.js b/projects/plugins/jetpack/extensions/store/membership-products/selectors.js index 125d3cd27f1a..6f401b32590f 100644 --- a/projects/plugins/jetpack/extensions/store/membership-products/selectors.js +++ b/projects/plugins/jetpack/extensions/store/membership-products/selectors.js @@ -44,6 +44,16 @@ export const getPostEmailSentState = ( state, postId ) => { return state.postEmailSentState || { email_sent_at: null, stats_on_send: null }; }; +export const getRepublishedAlreadySentPostInSession = ( state, postId ) => + !! ( + state.republishedAlreadySentPostInSession && state.republishedAlreadySentPostInSession[ postId ] + ); + +export const getPublishedWithEmailEnabledInSession = ( state, postId ) => + !! ( + state.publishedWithEmailEnabledInSession && state.publishedWithEmailEnabledInSession[ postId ] + ); + export const hasInvalidProducts = ( state, selectedProductIds ) => { return ( !! selectedProductIds && From 35797c531920033351ae69678c2731be8d3c4cd9 Mon Sep 17 00:00:00 2001 From: Addison Stavlo Date: Wed, 25 Feb 2026 10:49:01 -0500 Subject: [PATCH 5/6] dummy arg to avoid bad minifcation of translated strings... --- .../extensions/shared/memberships/subscribers-affirmation.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js b/projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js index 74aa619ac087..dfe12f70f50e 100644 --- a/projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js +++ b/projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js @@ -498,7 +498,8 @@ function SubscribersAffirmation( { accessLevel, prePublish = false } ) { } else if ( status === 'publish' ) { text = __( "This post was published without sending an email. To send, move the post to draft, enable 'Post and email,' and republish.", - 'jetpack' + 'jetpack', + 0 // dummy arg to avoid bad minification - https://github.com/Automattic/i18n-check-webpack-plugin?tab=readme-ov-file#conditional-function-call-compaction ); } else { text = __( 'Not sent via email.', 'jetpack' ); From d3bc57b8242be08a705db82f9678d9b1fc64224e Mon Sep 17 00:00:00 2001 From: Addison Stavlo Date: Wed, 25 Feb 2026 15:10:54 -0500 Subject: [PATCH 6/6] use useLayoutEffect to resolve wrong state flicker --- .../memberships/subscribers-affirmation.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js b/projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js index dfe12f70f50e..7f6a9e07d319 100644 --- a/projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js +++ b/projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js @@ -3,7 +3,7 @@ import { isComingSoon } from '@automattic/jetpack-shared-extension-utils'; import { Animate } from '@wordpress/components'; import { useDispatch, useSelect } from '@wordpress/data'; import { store as editorStore } from '@wordpress/editor'; -import { createInterpolateElement, useRef, useEffect } from '@wordpress/element'; +import { createInterpolateElement, useRef, useLayoutEffect } from '@wordpress/element'; import { sprintf, __, _n } from '@wordpress/i18n'; import paywallBlockMetadata from '../../blocks/paywall/block.json'; import { @@ -410,7 +410,7 @@ function SubscribersAffirmation( { accessLevel, prePublish = false } ) { [ status, postId ] ); - useEffect( () => { + useLayoutEffect( () => { if ( status === 'publish' ) { if ( wasPublishedOnLoad.current === undefined ) { wasPublishedOnLoad.current = true; @@ -430,6 +430,17 @@ function SubscribersAffirmation( { accessLevel, prePublish = false } ) { prevStatusRef.current = status; }, [ status, postId, postEmailSentState?.email_sent_at, dispatch, postMeta ] ); + useLayoutEffect( () => { + if ( + postId && + status === 'publish' && + transitionedToPublishInSession.current && + postEmailSentState?.email_sent_at != null + ) { + dispatch( setRepublishedAlreadySentPostInSession( postId ) ); + } + }, [ postId, status, postEmailSentState?.email_sent_at, dispatch ] ); + if ( ! hasFinishedLoading ) { return ( @@ -457,7 +468,8 @@ function SubscribersAffirmation( { accessLevel, prePublish = false } ) { publishedWithEmailEnabledInSession || ( wasPublishedOnLoad.current && publishDate && - publishDate.getTime() >= Date.now() - SENDING_IN_PROGRESS_WINDOW_MS ) ); + publishDate.getTime() >= Date.now() - SENDING_IN_PROGRESS_WINDOW_MS ) || + ( publishDate && publishDate.getTime() >= Date.now() - SENDING_IN_PROGRESS_WINDOW_MS ) ); const isPublishedNotSent = status === 'publish' && isSendEmailEnabled() && emailSentAt == null && ! isSendingInProgress;