Repository: Automattic/wp-calypso
Pull request #103485: [Help center]  Restore unread status
Author: login: AllTerrainDeveloper
Created: 2025-05-16T10:05:36Z | Merged: 2025-05-22T10:13:53Z

<!--
Link a related Github/Linear issue to this PR. If the PR does not immediately resolve the issue,
for example, it requires a separate deployment to production, avoid
using the "fixes" keyword. Consider using "part of" instead.
-->

Part of https://github.com/Automattic/wp-calypso/pull/102667

## Proposed Changes

The[ Pull Request](https://github.com/Automattic/wp-calypso/pull/102667) enabling history for Odie AI Conversations mistakenly removed the unread badges and count due to a lack of knowledge on my side. This set of changes is bringing back that minimal set of changes, with improved namings, and a bit less convoluted development.

## Why are these changes being made?
<!--
It's easy to see what a PR does but much harder to find out why it was made,
particularly when researching old changes in history. Record an explanation of
the motivation behind this change and how it will help.
-->

Happiness Engineers were asking for this feature to be added, and it's indeed a useful indicator for our users. The badge for unread messages is being added as well as the blueish fill color for unread conversations.

## Testing Instructions

<!--
Add as many details as possible to help others reproduce the issue and test the fix.
"Before / After" screenshots can also be very helpful when the change is visual.
-->

1. Apply this branch locally, it's a pretty much needed step. Don't use Calypso live link.
2. Modify the files: `help-center-smooch.tsx` and `use-authenticate-zendesk-messaging.ts` (37d87-pb/  37d88-pb/). Those changes will enable the Zendesk production endpoints (not staging)
3. Now, use your ZD account. For that, you will need to give yourself permissions (if you are a developer) through Mission Control -> Tools -> Happiness -> Zendesk Account Manager. Give you permissions for WPCOM, and click on "Just today"
4. Now run Calypso locally and start a Odie AI conversation, escalate the conversation, identify in the conversation as an a11n and kindly ask not to handle this request. Close the Help Center now.
5. Once in Zendesk, handle your own conversation, reply to you a few times.
6. Open now Help Center in WordPress (Calypso local instance)
7. Assert the following:
7.1 The number of unread messages match in the main help center window.
7.2 After clicking on History, the conversation is highlighted in blue
7.3 After clicking on the highlighted conversation, the blueish background is removed.

**Screenshots**
|Step 7.1|Step 7.2 |Step 7.3|
|---|---|---|
|![image](https://github.com/user-attachments/assets/583bae21-cbe2-44ca-a152-3d708a1d3c8c)|![image](https://github.com/user-attachments/assets/dfc6dda8-34c8-473a-9af5-26f189d0b128)|![image](https://github.com/user-attachments/assets/6baac29f-face-4a06-a3c8-eeff06376599)

## Pre-merge Checklist

<!--
Complete applicable items on this checklist **before** merging into trunk. Inapplicable items can be left unchecked.

Both the PR author and reviewer are responsible for ensuring the checklist is completed.
-->

- [ ] Has the general commit checklist been followed? (PCYsg-hS-p2)
- [ ] [Have you written new tests](https://wpcalypso.wordpress.com/devdocs/docs/testing/index.md) for your changes?
- [ ] Have you tested the feature in Simple (P9HQHe-k8-p2), Atomic (P9HQHe-jW-p2), and self-hosted Jetpack sites (PCYsg-g6b-p2)?
- [ ] Have you checked for TypeScript, React or other console errors?
- [ ] Have you used memoizing on expensive computations? More info in [Memoizing with create-selector](https://github.com/Automattic/wp-calypso/blob/trunk/packages/state-utils/src/create-selector/README.md) and [Using memoizing selectors](https://react-redux.js.org/api/hooks#using-memoizing-selectors) and [Our Approach to Data](https://github.com/Automattic/wp-calypso/blob/trunk/docs/our-approach-to-data.md)
- [ ] Have we added the "[Status] String Freeze" label as soon as any new strings were ready for translation (p4TIVU-5Jq-p2)?
    - [ ] For UI changes, have we tested the change in various languages (for example, ES, PT, FR, or DE)? The length of text and words vary significantly between languages. 
- [ ] For changes affecting Jetpack: Have we added the "[Status] Needs Privacy Updates" label if this pull request changes what data or activity we track or use (p4TIVU-aUh-p2)?


Diff:
diff --git a/packages/data-stores/src/help-center/actions.ts b/packages/data-stores/src/help-center/actions.ts
index de4432d52d6e..d763eaa2e931 100644
--- a/packages/data-stores/src/help-center/actions.ts
+++ b/packages/data-stores/src/help-center/actions.ts
@@ -27,6 +27,12 @@ export const setOdieChatId = ( odieChatId: number | undefined ) =>
 		odieChatId,
 	} ) as const;
 
+export const setLastMessageReceivedAt = ( lastMessageReceivedAt: number ) =>
+	( {
+		type: 'HELP_CENTER_SET_LAST_MESSAGE_RECEIVED_AT',
+		lastMessageReceivedAt,
+	} ) as const;
+
 export const setNavigateToRoute = ( route?: string ) =>
 	( {
 		type: 'HELP_CENTER_SET_NAVIGATE_TO_ROUTE',
@@ -250,5 +256,6 @@ export type HelpCenterAction =
 			| typeof setAllowPremiumSupport
 			| typeof setHelpCenterOptions
 			| typeof setOdieChatId
+			| typeof setLastMessageReceivedAt
 	  >
 	| GeneratorReturnType< typeof setShowHelpCenter >;
diff --git a/packages/data-stores/src/help-center/reducer.ts b/packages/data-stores/src/help-center/reducer.ts
index 87294ba39ec4..70f941c30a24 100644
--- a/packages/data-stores/src/help-center/reducer.ts
+++ b/packages/data-stores/src/help-center/reducer.ts
@@ -54,6 +54,15 @@ const isMinimized: Reducer< boolean, HelpCenterAction > = ( state = false, actio
 	return state;
 };
 
+const lastMessageReceivedAt: Reducer< number | undefined, HelpCenterAction > = (
+	state,
+	action
+) => {
+	return action.type === 'HELP_CENTER_SET_LAST_MESSAGE_RECEIVED_AT'
+		? action.lastMessageReceivedAt
+		: state;
+};
+
 const isChatLoaded: Reducer< boolean, HelpCenterAction > = ( state = false, action ) => {
 	switch ( action.type ) {
 		case 'HELP_CENTER_SET_IS_CHAT_LOADED':
@@ -194,6 +203,7 @@ const reducer = combineReducers( {
 	unreadCount,
 	navigateToRoute,
 	odieChatId,
+	lastMessageReceivedAt,
 	odieInitialPromptText,
 	odieBotNameSlug,
 	allowPremiumSupport,
diff --git a/packages/data-stores/src/help-center/selectors.ts b/packages/data-stores/src/help-center/selectors.ts
index 58b92c760b3d..a7ffca9bc339 100644
--- a/packages/data-stores/src/help-center/selectors.ts
+++ b/packages/data-stores/src/help-center/selectors.ts
@@ -20,4 +20,5 @@ export const getCurrentSupportInteraction = ( state: State ) => state.currentSup
 export const getAllowPremiumSupport = ( state: State ) => state.allowPremiumSupport;
 export const getHelpCenterOptions = ( state: State ) => state.helpCenterOptions;
 export const getOdieChatId = ( state: State ) => state.odieChatId;
+export const getLastMessageReceivedAt = ( state: State ) => state.lastMessageReceivedAt;
 export const getContextTerm = ( state: State ) => state.contextTerm;
diff --git a/packages/help-center/src/components/help-center-chat-history.tsx b/packages/help-center/src/components/help-center-chat-history.tsx
index 78eadb22321b..b4061d0e5cd0 100644
--- a/packages/help-center/src/components/help-center-chat-history.tsx
+++ b/packages/help-center/src/components/help-center-chat-history.tsx
@@ -1,5 +1,6 @@
 /* eslint-disable no-restricted-imports */
 import { HelpCenterSelect } from '@automattic/data-stores';
+import { calculateUnread } from '@automattic/odie-client/src/data/use-get-unread-conversations';
 import { Card, CardHeader, CardBody, Spinner } from '@wordpress/components';
 import { useSelect } from '@wordpress/data';
 import { useState } from '@wordpress/element';
@@ -13,9 +14,12 @@ import { useGetHistoryChats } from '../hooks/use-get-history-chats';
 import { HELP_CENTER_STORE } from '../stores';
 import { HelpCenterSupportChatMessage } from './help-center-support-chat-message';
 import { getLastMessage } from './utils';
-import type { Conversations, SupportInteraction } from '@automattic/odie-client';
-
 import './help-center-chat-history.scss';
+import type {
+	Conversations,
+	SupportInteraction,
+	ZendeskConversation,
+} from '@automattic/odie-client';
 
 // temporarily we want to show a simplified version of the chat history
 // this bool controls it.
@@ -36,7 +40,7 @@ const Conversations = ( {
 } ) => {
 	const { __ } = useI18n();
 
-	if ( isLoadingInteractions ) {
+	if ( isLoadingInteractions && ! conversations.length ) {
 		return (
 			<div className="help-center-chat-history__no-results">
 				<Spinner />
@@ -55,6 +59,9 @@ const Conversations = ( {
 	return (
 		<>
 			{ conversations.map( ( conversation ) => {
+				const { numberOfUnreadMessages } = calculateUnread( [
+					conversation as ZendeskConversation,
+				] );
 				const lastMessage = getLastMessage( { conversation } );
 
 				if ( ! lastMessage ) {
@@ -67,6 +74,7 @@ const Conversations = ( {
 						key={ conversation.id }
 						message={ lastMessage }
 						conversation={ conversation }
+						numberOfUnreadMessages={ numberOfUnreadMessages }
 					/>
 				);
 			} ) }
diff --git a/packages/help-center/src/components/help-center-recent-conversations.tsx b/packages/help-center/src/components/help-center-recent-conversations.tsx
index 3251808220a9..e6fd5f2a7b31 100644
--- a/packages/help-center/src/components/help-center-recent-conversations.tsx
+++ b/packages/help-center/src/components/help-center-recent-conversations.tsx
@@ -1,3 +1,5 @@
+import { calculateUnread } from '@automattic/odie-client/src/data/use-get-unread-conversations';
+import { ZendeskConversation } from '@automattic/odie-client/src/types';
 import { useI18n } from '@wordpress/react-i18n';
 import React from 'react';
 import { useGetHistoryChats } from '../hooks';
@@ -7,21 +9,27 @@ import { getLastMessage } from './utils';
 import './help-center-recent-conversations.scss';
 
 const HelpCenterRecentConversations: React.FC = () => {
-	const { isLoadingInteractions, recentConversations } = useGetHistoryChats();
+	const { recentConversations } = useGetHistoryChats();
 	const { __ } = useI18n();
 
-	if ( isLoadingInteractions || ! recentConversations.length ) {
+	if ( ! recentConversations?.length ) {
 		return null;
 	}
 
 	const recentConversation = recentConversations[ 0 ];
 
-	const lastMessage = getLastMessage( { conversation: recentConversation } );
+	const lastMessage = getLastMessage( {
+		conversation: recentConversation,
+	} );
 
 	if ( ! lastMessage ) {
 		return null;
 	}
 
+	const { numberOfUnreadMessages } = calculateUnread( [
+		recentConversation as ZendeskConversation,
+	] );
+
 	return (
 		<div className="help-center-homepage-conversations">
 			<h3 className="help-center-search-results__title help-center__section-title">
@@ -29,6 +37,7 @@ const HelpCenterRecentConversations: React.FC = () => {
 			</h3>
 
 			<HelpCenterSupportChatMessage
+				numberOfUnreadMessages={ numberOfUnreadMessages }
 				sectionName="recent_conversations"
 				key={ recentConversation.id }
 				message={ lastMessage }
diff --git a/packages/help-center/src/components/help-center-search-results.scss b/packages/help-center/src/components/help-center-search-results.scss
index cf3778e2f227..1c0f59ee611a 100644
--- a/packages/help-center/src/components/help-center-search-results.scss
+++ b/packages/help-center/src/components/help-center-search-results.scss
@@ -48,7 +48,7 @@
 			border-radius: 4px;
 			border: 1.5px solid #C3C4C7;
 
-			&:hover {
+			&:hover, &.is-unread-message {
 				border: 1.5px solid $help-center-blue !important;
 			}
 		}
diff --git a/packages/help-center/src/components/help-center-smooch.tsx b/packages/help-center/src/components/help-center-smooch.tsx
index 652bef069307..a5e4723cdb56 100644
--- a/packages/help-center/src/components/help-center-smooch.tsx
+++ b/packages/help-center/src/components/help-center-smooch.tsx
@@ -81,7 +81,8 @@ const HelpCenterSmooch: React.FC< { enableAuth: boolean } > = ( { enableAuth } )
 	const { data: authData } = useAuthenticateZendeskMessaging( allowChat, 'messenger' );
 
 	const { isMessagingScriptLoaded } = useLoadZendeskMessaging( allowChat, allowChat );
-	const { setIsChatLoaded, setZendeskClientId } = useDataStoreDispatch( HELP_CENTER_STORE );
+	const { setIsChatLoaded, setZendeskClientId, setLastMessageReceivedAt } =
+		useDataStoreDispatch( HELP_CENTER_STORE );
 	const getUnreadNotifications = useGetUnreadConversations();
 
 	const getUnreadListener = useCallback(
@@ -90,13 +91,17 @@ const HelpCenterSmooch: React.FC< { enableAuth: boolean } > = ( { enableAuth } )
 				playNotificationSound();
 			}
 
+			if ( setLastMessageReceivedAt ) {
+				setLastMessageReceivedAt( Date.now() );
+			}
+
 			if ( isHelpCenterShown ) {
 				return;
 			}
 
 			Smooch.getConversationById( data?.conversation?.id ).then( () => getUnreadNotifications() );
 		},
-		[ isHelpCenterShown, areSoundNotificationsEnabled ]
+		[ isHelpCenterShown, areSoundNotificationsEnabled, setLastMessageReceivedAt ]
 	);
 
 	const clientIdListener = useCallback(
diff --git a/packages/help-center/src/components/help-center-support-chat-message.scss b/packages/help-center/src/components/help-center-support-chat-message.scss
index 6071fbc09be3..a6280766f938 100644
--- a/packages/help-center/src/components/help-center-support-chat-message.scss
+++ b/packages/help-center/src/components/help-center-support-chat-message.scss
@@ -23,8 +23,17 @@
 		}
 	}
 
-	&.is-closed, &.is-solved {
-		background-color: var(--studio-gray-0);
+	&.is-unread-message {
+		background-color: #EBF2FC;
+		.help-center-support-chat__open-conversation {
+			fill: $help-center-blue;
+		}
+	}
+
+	&:not(.is-unread-message) {
+		&.is-closed, &.is-solved {
+			background-color: var(--studio-gray-0);
+		}
 	}
 
 	& > * {
@@ -39,6 +48,28 @@
 		img {
 			border-radius: 50%;
 		}
+
+		&.has-unread-messages {
+			margin-right: 10px;
+		}
+
+		.help-center-support-chat__conversation-badge {
+			position: absolute;
+			top: 50%;
+			right: -0%;
+			transform: translate(50%, -50%);
+			display: flex;
+			align-items: center;
+			justify-content: center;
+			border: 2px solid #FFF;
+			border-radius: 12px; /* stylelint-disable-line scales/radii */
+			box-sizing: border-box;
+			min-width: 26px;
+			height: 20px;
+			font-size: 12px;
+			background-color: $help-center-blue;
+			color: #FFF;
+		}
 	}
 
 	.help-center-support-chat__conversation-information {
diff --git a/packages/help-center/src/components/help-center-support-chat-message.tsx b/packages/help-center/src/components/help-center-support-chat-message.tsx
index 932661ed39dd..54efaabe2613 100644
--- a/packages/help-center/src/components/help-center-support-chat-message.tsx
+++ b/packages/help-center/src/components/help-center-support-chat-message.tsx
@@ -32,10 +32,12 @@ export const HelpCenterSupportChatMessage = ( {
 	message,
 	sectionName,
 	conversation,
+	numberOfUnreadMessages = 0,
 }: {
 	message: OdieMessage | ZendeskMessage;
 	sectionName?: string;
 	conversation: OdieConversation | ZendeskConversation;
+	numberOfUnreadMessages?: number;
 } ) => {
 	const { __ } = useI18n();
 	const locale = useLocale();
@@ -86,6 +88,8 @@ export const HelpCenterSupportChatMessage = ( {
 		);
 	};
 
+	const hasUnreadMessages = numberOfUnreadMessages > 0;
+
 	return (
 		<Link
 			to="/odie"
@@ -100,11 +104,22 @@ export const HelpCenterSupportChatMessage = ( {
 				}
 			} }
 			className={ clsx( 'help-center-support-chat__conversation-container', {
+				'is-unread-message': hasUnreadMessages,
 				[ `is-${ conversationStatus }` ]: conversationStatus,
 			} ) }
 		>
-			<div className={ clsx( 'help-center-support-chat__conversation-avatar' ) }>
+			<div
+				className={ clsx( 'help-center-support-chat__conversation-avatar', {
+					'has-unread-messages': hasUnreadMessages,
+				} ) }
+			>
 				{ renderAvatar() }
+
+				{ hasUnreadMessages && (
+					<div className="help-center-support-chat__conversation-badge">
+						+{ numberOfUnreadMessages }
+					</div>
+				) }
 			</div>
 			<div className="help-center-support-chat__conversation-information">
 				<div className="help-center-support-chat__conversation-information-message">
diff --git a/packages/help-center/src/components/utils.tsx b/packages/help-center/src/components/utils.tsx
index 351c4f0449fc..2675240dd514 100644
--- a/packages/help-center/src/components/utils.tsx
+++ b/packages/help-center/src/components/utils.tsx
@@ -4,8 +4,10 @@ import Smooch from 'smooch';
 import type { ContactOption } from '../types';
 import type {
 	OdieConversation,
+	OdieMessage,
 	SupportInteraction,
 	ZendeskConversation,
+	ZendeskMessage,
 } from '@automattic/odie-client';
 
 const isMatchingInteraction = (
@@ -40,11 +42,15 @@ export const generateContactOnClickEvent = (
 	}
 };
 
+/**
+ * Returns the last message from a conversation.
+ * @returns The last message or null if there are no messages.
+ */
 export const getLastMessage = ( {
 	conversation,
 }: {
 	conversation: OdieConversation | ZendeskConversation;
-} ) => {
+} ): OdieMessage | ZendeskMessage | null => {
 	return Array.isArray( conversation.messages ) && conversation.messages.length > 0
 		? conversation.messages[ conversation.messages.length - 1 ]
 		: null;
diff --git a/packages/help-center/src/hooks/use-get-history-chats.ts b/packages/help-center/src/hooks/use-get-history-chats.ts
index a1139a9b0c46..6fcf44e7b11b 100644
--- a/packages/help-center/src/hooks/use-get-history-chats.ts
+++ b/packages/help-center/src/hooks/use-get-history-chats.ts
@@ -55,14 +55,14 @@ const getOdieConversationsWithNoSupportInteractions = (
  * Checks whether the last message from the specified conversation is not empty nor a predefined '--' token.
  */
 const isValidLastMessageContent = ( conversation: OdieConversation | ZendeskConversation ) => {
-	const { text } = getLastMessage( { conversation } ) || {};
+	const lastMessage = getLastMessage( { conversation } );
 
-	if ( text === null || text === undefined ) {
+	if ( ! lastMessage || lastMessage?.text === null || lastMessage?.text === undefined ) {
 		return false;
 	}
 
 	// '--' is a token returned for Odie conversations that should be forwarded to human support
-	return ! [ '', '--' ].includes( text.trim() );
+	return ! [ '', '--' ].includes( lastMessage.text.trim() );
 };
 
 /**
@@ -93,19 +93,22 @@ export const useGetHistoryChats = (): UseGetHistoryChatsResult => {
 	const [ recentConversations, setRecentConversations ] = useState< Conversations >( [] );
 	const [ archivedConversations, setArchivedConversations ] = useState< Conversations >( [] );
 
+	const { isChatLoaded, lastMessageReceivedAt } = useSelect( ( select ) => {
+		const store = select( HELP_CENTER_STORE ) as HelpCenterSelect;
+
+		return {
+			isChatLoaded: store.getIsChatLoaded(),
+			lastMessageReceivedAt: store.getLastMessageReceivedAt(),
+		};
+	}, [] );
+
 	const { data: openSupportInteraction, isLoading: isLoadingOpenInteractions } =
-		useGetSupportInteractions( 'zendesk', 10, 'open' );
+		useGetSupportInteractions( 'zendesk', 10, 'open', 10, true, lastMessageReceivedAt );
 	const { data: otherSupportInteractions, isLoading: isLoadingOtherSupportInteractions } =
 		useGetSupportInteractions( 'zendesk', 100, [ 'resolved', 'solved', 'closed' ] );
 	const { data: odieConversations, isLoading: isLoadingOdieConversations } =
 		useGetOdieConversations();
 
-	const { isChatLoaded } = useSelect( ( select ) => {
-		const store = select( HELP_CENTER_STORE ) as HelpCenterSelect;
-
-		return { isChatLoaded: store.getIsChatLoaded() };
-	}, [] );
-
 	const isLoadingInteractions =
 		isLoadingOpenInteractions || isLoadingOtherSupportInteractions || isLoadingOdieConversations;
 
diff --git a/packages/odie-client/src/data/use-get-support-interactions.ts b/packages/odie-client/src/data/use-get-support-interactions.ts
index 174b0c7cbb3c..c768947cacf1 100644
--- a/packages/odie-client/src/data/use-get-support-interactions.ts
+++ b/packages/odie-client/src/data/use-get-support-interactions.ts
@@ -11,13 +11,13 @@ export const useGetSupportInteractions = (
 	per_page = 10,
 	status: string | string[] = 'open',
 	page = 1,
-	enabled = true
+	enabled = true,
+	freshness = 0
 ) => {
 	const path = `?per_page=${ per_page }&page=${ page }&status=${ status }`;
-
 	return useQuery( {
 		// eslint-disable-next-line
-		queryKey: [ 'support-interactions', 'get-interactions', provider, status ],
+		queryKey: [ 'support-interactions', 'get-interactions', provider, status, freshness ],
 		queryFn: async () => {
 			const response = await handleSupportInteractionsFetch( 'GET', path );
 
diff --git a/packages/odie-client/src/data/use-get-unread-conversations.ts b/packages/odie-client/src/data/use-get-unread-conversations.ts
index eff489569a69..af02cdde82f4 100644
--- a/packages/odie-client/src/data/use-get-unread-conversations.ts
+++ b/packages/odie-client/src/data/use-get-unread-conversations.ts
@@ -4,20 +4,26 @@ import { useCallback } from 'react';
 import Smooch from 'smooch';
 import { ZendeskConversation } from '../types';
 
-const calculateUnread = ( conversations: Conversation[] | ZendeskConversation[] ) => {
-	let unreadConversations = 0;
-	let unreadMessages = 0;
+export const calculateUnread = (
+	conversations: Conversation[] | ZendeskConversation[] | undefined | null
+) => {
+	if ( ! Array.isArray( conversations ) || conversations.length === 0 ) {
+		return { numberOfUnreadConversations: 0, numberOfUnreadMessages: 0 };
+	}
+
+	let numberOfUnreadConversations = 0;
+	let numberOfUnreadMessages = 0;
 
 	conversations.forEach( ( conversation ) => {
-		const unreadCount = conversation.participants[ 0 ]?.unreadCount ?? 0;
+		const unreadCount = conversation?.participants?.[ 0 ]?.unreadCount ?? 0;
 
 		if ( unreadCount > 0 ) {
-			unreadConversations++;
-			unreadMessages += unreadCount;
+			numberOfUnreadConversations++;
+			numberOfUnreadMessages += unreadCount;
 		}
 	} );
 
-	return { unreadConversations, unreadMessages };
+	return { numberOfUnreadConversations, numberOfUnreadMessages };
 };
 
 export const useGetUnreadConversations = () => {
@@ -26,10 +32,11 @@ export const useGetUnreadConversations = () => {
 	return useCallback(
 		( conversations?: Conversation[] | ZendeskConversation[] ) => {
 			const conversationsToCheck = conversations ? conversations : Smooch?.getConversations?.();
-			const { unreadConversations, unreadMessages } = calculateUnread( conversationsToCheck );
-			setUnreadCount( unreadConversations );
+			const { numberOfUnreadConversations, numberOfUnreadMessages } =
+				calculateUnread( conversationsToCheck );
+			setUnreadCount( numberOfUnreadConversations );
 
-			return { unreadConversations, unreadMessages };
+			return { numberOfUnreadConversations, numberOfUnreadMessages };
 		},
 		[ setUnreadCount ]
 	);
diff --git a/packages/odie-client/src/hooks/use-get-most-recent-open-conversation.tsx b/packages/odie-client/src/hooks/use-get-most-recent-open-conversation.tsx
index a5551f220721..004451ec5871 100644
--- a/packages/odie-client/src/hooks/use-get-most-recent-open-conversation.tsx
+++ b/packages/odie-client/src/hooks/use-get-most-recent-open-conversation.tsx
@@ -9,10 +9,11 @@ export const useGetMostRecentOpenConversation = () => {
 	let mostRecentSupportInteractionId = null;
 	let totalNumberOfConversations = 0;
 
-	const { isChatLoaded } = useSelect( ( select ) => {
+	const { isChatLoaded, lastMessageReceivedAt } = useSelect( ( select ) => {
 		const store = select( HELP_CENTER_STORE ) as HelpCenterSelect;
 		return {
 			isChatLoaded: store.getIsChatLoaded(),
+			lastMessageReceivedAt: store.getLastMessageReceivedAt(),
 		};
 	}, [] );
 
@@ -20,7 +21,7 @@ export const useGetMostRecentOpenConversation = () => {
 		useGetSupportInteractions( 'zendesk', 100, 'resolved' );
 
 	const { data: supportInteractionsOpen, isLoading: isLoadingOpenInteractions } =
-		useGetSupportInteractions( 'zendesk', 10, 'open' );
+		useGetSupportInteractions( 'zendesk', 10, 'open', 10, true, lastMessageReceivedAt );
 
 	const isLoadingInteractions = isLoadingResolvedInteractions || isLoadingOpenInteractions;
 


Reviews:
user: login: escapemanuele
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T09:23:46Z
user: login: AllTerrainDeveloper
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T09:29:32Z
user: login: AllTerrainDeveloper
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T09:32:45Z
user: login: ehg
body: Code LGTM and it works! 
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-20T11:49:03Z
user: login: stephanethomas
body: I haven't tested those changes yet but I'd love to submit some feedback first about how we could improve the code in term of readability and duplication. I'll give those changes a try once you've decided if you want to follow those suggestions, otherwise feel free to merge them since Chris has already tested them.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T13:42:54Z
user: login: AllTerrainDeveloper
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T13:49:48Z
user: login: AllTerrainDeveloper
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T14:03:08Z
user: login: AllTerrainDeveloper
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T14:11:51Z
user: login: stephanethomas
body: I've tested those changes and they work great, time to ship it!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T15:47:21Z
user: login: AllTerrainDeveloper
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T08:39:32Z
user: login: AllTerrainDeveloper
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T08:40:20Z
user: login: stephanethomas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T08:43:54Z
user: login: AllTerrainDeveloper
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T08:50:13Z

Review comments:
diff_hunk: @@ -94,9 +95,10 @@ const HelpCenterSmooch: React.FC< { enableAuth: boolean } > = ( { enableAuth } )
 				return;
path: packages/help-center/src/components/help-center-smooch.tsx
user: login: escapemanuele
body: In my case, when receiving a message, the recent chat is not updated because `lastMessageReceivedAt` is not set, as this function returns 
created_at: 2025-05-20T09:23:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -94,9 +95,10 @@ const HelpCenterSmooch: React.FC< { enableAuth: boolean } > = ( { enableAuth } )
 				return;
path: packages/help-center/src/components/help-center-smooch.tsx
user: login: AllTerrainDeveloper
body: What do you mean? `isHelpCenterShown` or `setLastMessageReceivedAt`?
created_at: 2025-05-20T09:29:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -94,9 +95,10 @@ const HelpCenterSmooch: React.FC< { enableAuth: boolean } > = ( { enableAuth } )
 				return;
path: packages/help-center/src/components/help-center-smooch.tsx
user: login: AllTerrainDeveloper
body: But this would only work when Help Center is shown, isn't it? I mean, if it's not shown, we don't need to update any badge
created_at: 2025-05-20T09:32:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -55,7 +55,7 @@ const Conversations = ( {
 	return (
 		<>
 			{ conversations.map( ( conversation ) => {
-				const lastMessage = getLastMessage( { conversation } );
+				const { lastMessage, unreadMessages } = getLastMessageAndUnread( { conversation } );
path: packages/help-center/src/components/help-center-chat-history.tsx
user: login: stephanethomas
body: It feels like the name of that function is missing a bit. Since `getLastMessageAndUnreadMessages` is both long and repetitive, I would suggest a slightly different name:
```suggestion
				const { lastMessage, unreadMessages } = getLastAndUnreadMessages( { conversation } );
```
created_at: 2025-05-20T13:10:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -32,10 +32,12 @@ export const HelpCenterSupportChatMessage = ( {
 	message,
 	sectionName,
 	conversation,
+	unreadMessages = 0,
 }: {
 	message: OdieMessage | ZendeskMessage;
 	sectionName?: string;
 	conversation: OdieConversation | ZendeskConversation;
+	unreadMessages?: number;
path: packages/help-center/src/components/help-center-support-chat-message.tsx
user: login: stephanethomas
body: I would be more explicit here as it's not always clear looking at the code below what that variable contains without checking its type (e.g. one could expect a list of unread messages):
```suggestion
	numberOfUnreadMessages?: number;
```
created_at: 2025-05-20T13:16:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -86,6 +88,8 @@ export const HelpCenterSupportChatMessage = ( {
 		);
 	};
 
+	const hasUnread = unreadMessages > 0;
path: packages/help-center/src/components/help-center-support-chat-message.tsx
user: login: stephanethomas
body: Tangentially related to my previous message, and certainly a bit nitpicky but something you may want to consider:
```suggestion
	const hasUnreadMessages = unreadMessages > 0;
```
created_at: 2025-05-20T13:17:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -100,11 +104,20 @@ export const HelpCenterSupportChatMessage = ( {
 				}
 			} }
 			className={ clsx( 'help-center-support-chat__conversation-container', {
+				'is-unread-message': hasUnread,
 				[ `is-${ conversationStatus }` ]: conversationStatus,
 			} ) }
 		>
-			<div className={ clsx( 'help-center-support-chat__conversation-avatar' ) }>
+			<div
+				className={ clsx( 'help-center-support-chat__conversation-avatar', {
+					'has-unread-messages': unreadMessages > 0,
+				} ) }
+			>
 				{ renderAvatar() }
+
+				{ unreadMessages > 0 && (
+					<div className="help-center-support-chat__conversation-badge">+{ unreadMessages }</div>
+				) }
path: packages/help-center/src/components/help-center-support-chat-message.tsx
user: login: stephanethomas
body: What about using the _boolean_ variable instead of duplicating code 😅?

```suggestion
			<div
				className={ clsx( 'help-center-support-chat__conversation-avatar', {
					'has-unread-messages': hasUnreadMessages,
				} ) }
			>
				{ renderAvatar() }

				{ hasUnreadMessages && (
					<div className="help-center-support-chat__conversation-badge">+{ unreadMessages }</div>
				) }
```
created_at: 2025-05-20T13:18:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -40,14 +43,22 @@ export const generateContactOnClickEvent = (
 	}
 };
 
-export const getLastMessage = ( {
+/**
+ * Returns the last message from a conversation and the number of unread messages.
+ * @returns An object containing the last message and the number of unread messages.
+ */
+export const getLastMessageAndUnread = ( {
 	conversation,
 }: {
 	conversation: OdieConversation | ZendeskConversation;
-} ) => {
-	return Array.isArray( conversation.messages ) && conversation.messages.length > 0
-		? conversation.messages[ conversation.messages.length - 1 ]
-		: null;
+} ): { lastMessage: OdieMessage | ZendeskMessage | null; unreadMessages: number } => {
+	const lastMessage =
+		Array.isArray( conversation.messages ) && conversation.messages.length > 0
+			? conversation.messages[ conversation.messages.length - 1 ]
+			: null;
+
+	const { unreadMessages } = calculateUnread( [ conversation as ZendeskConversation ] );
+	return { lastMessage, unreadMessages };
path: packages/help-center/src/components/utils.tsx
user: login: stephanethomas
body: We still call `getLastMessageAndUnread()` to retrieve only the last message half of the time, so I don't see much value in grouping the retrieval of the last message with the computation of the number of messages. What about calling them separately?

If that was me, I would even rename `calculateUnread()` into something more explicit such as `getNumberOfUnreadConversationsAndMessages()`. With the latter it's easy to understand what that function does, with the former I would have to look at the code to see what output I should expect.
created_at: 2025-05-20T13:28:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4,12 +4,18 @@ import { useCallback } from 'react';
 import Smooch from 'smooch';
 import { ZendeskConversation } from '../types';
 
-const calculateUnread = ( conversations: Conversation[] | ZendeskConversation[] ) => {
+export const calculateUnread = (
+	conversations: Conversation[] | ZendeskConversation[] | undefined | null
+) => {
+	if ( ! Array.isArray( conversations ) || conversations.length === 0 ) {
+		return { unreadConversations: 0, unreadMessages: 0 };
path: packages/odie-client/src/data/use-get-unread-conversations.ts
user: login: stephanethomas
body: A small suggestion related to some previous feedback, if you decide to follow that approach:
```suggestion
		return { numberOfUnreadConversations: 0, numberOfUnreadMessages: 0 };
```
created_at: 2025-05-20T13:30:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -140,6 +143,7 @@ export const useGetHistoryChats = (): UseGetHistoryChatsResult => {
 		otherSupportInteractions,
 		odieConversations,
 		supportInteractions,
+		lastMessageReceivedAt,
path: packages/help-center/src/hooks/use-get-history-chats.ts
user: login: stephanethomas
body: We don't use that variable in the `useEffect()` clause, should we still need to define it as a dependency?
created_at: 2025-05-20T13:34:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -67,6 +67,7 @@ const Conversations = ( {
 						key={ conversation.id }
 						message={ lastMessage }
 						conversation={ conversation }
+						unreadMessages={ unreadMessages }
path: packages/help-center/src/components/help-center-chat-history.tsx
user: login: stephanethomas
body: You can leave it that way if you prefer, but since we already pass the whole `conversation` object, it would be really easy to retrieve the number of unread messages from inside `HelpCenterSupportChatMessage` components - which would simplify any calling code.
created_at: 2025-05-20T13:40:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -100,11 +104,20 @@ export const HelpCenterSupportChatMessage = ( {
 				}
 			} }
 			className={ clsx( 'help-center-support-chat__conversation-container', {
+				'is-unread-message': hasUnread,
 				[ `is-${ conversationStatus }` ]: conversationStatus,
 			} ) }
 		>
-			<div className={ clsx( 'help-center-support-chat__conversation-avatar' ) }>
+			<div
+				className={ clsx( 'help-center-support-chat__conversation-avatar', {
+					'has-unread-messages': unreadMessages > 0,
+				} ) }
+			>
 				{ renderAvatar() }
+
+				{ unreadMessages > 0 && (
+					<div className="help-center-support-chat__conversation-badge">+{ unreadMessages }</div>
+				) }
path: packages/help-center/src/components/help-center-support-chat-message.tsx
user: login: AllTerrainDeveloper
body: Oops :) 
created_at: 2025-05-20T13:49:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -140,6 +143,7 @@ export const useGetHistoryChats = (): UseGetHistoryChatsResult => {
 		otherSupportInteractions,
 		odieConversations,
 		supportInteractions,
+		lastMessageReceivedAt,
path: packages/help-center/src/hooks/use-get-history-chats.ts
user: login: AllTerrainDeveloper
body: We want to reorder the array for new data, but is true that the rest of the deps will change
created_at: 2025-05-20T14:03:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -67,6 +67,7 @@ const Conversations = ( {
 						key={ conversation.id }
 						message={ lastMessage }
 						conversation={ conversation }
+						unreadMessages={ unreadMessages }
path: packages/help-center/src/components/help-center-chat-history.tsx
user: login: AllTerrainDeveloper
body: From all the suggestions I'm going to leave this one, somehow I like it more modular too
created_at: 2025-05-20T14:11:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -55,6 +59,9 @@ const Conversations = ( {
 	return (
 		<>
 			{ conversations.map( ( conversation ) => {
+				const { numberOfUnreadMessages } = calculateUnread( [
+					conversation as ZendeskConversation,
path: packages/help-center/src/components/help-center-chat-history.tsx
user: login: stephanethomas
body: It's probably me not being familiar enough with TypeScript but what happens when the `conversation` is an `OdieConversation`?
created_at: 2025-05-21T15:11:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -40,14 +42,21 @@ export const generateContactOnClickEvent = (
 	}
 };
 
+/**
+ * Returns the last message from a conversation.
+ * @returns The last message or null if there are no messages.
+ */
 export const getLastMessage = ( {
 	conversation,
 }: {
 	conversation: OdieConversation | ZendeskConversation;
-} ) => {
-	return Array.isArray( conversation.messages ) && conversation.messages.length > 0
-		? conversation.messages[ conversation.messages.length - 1 ]
-		: null;
+} ): OdieMessage | ZendeskMessage | null => {
+	const lastMessage =
+		Array.isArray( conversation.messages ) && conversation.messages.length > 0
+			? conversation.messages[ conversation.messages.length - 1 ]
+			: null;
+
+	return lastMessage;
path: packages/help-center/src/components/utils.tsx
user: login: stephanethomas
body: I believe we could really do without that local variable, it's already pretty clear what that function returns:
```suggestion
	return Array.isArray( conversation.messages ) && conversation.messages.length > 0
		? conversation.messages[ conversation.messages.length - 1 ]
		: null;
```
created_at: 2025-05-21T15:15:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -55,6 +59,9 @@ const Conversations = ( {
 	return (
 		<>
 			{ conversations.map( ( conversation ) => {
+				const { numberOfUnreadMessages } = calculateUnread( [
+					conversation as ZendeskConversation,
path: packages/help-center/src/components/help-center-chat-history.tsx
user: login: AllTerrainDeveloper
body: As long as it has enough overlap it's ok. And actually the property is being used in a component that accepts either a `ZendeskConversation` or a `OdieConversation`, so it's ok.
created_at: 2025-05-22T08:39:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -40,14 +42,21 @@ export const generateContactOnClickEvent = (
 	}
 };
 
+/**
+ * Returns the last message from a conversation.
+ * @returns The last message or null if there are no messages.
+ */
 export const getLastMessage = ( {
 	conversation,
 }: {
 	conversation: OdieConversation | ZendeskConversation;
-} ) => {
-	return Array.isArray( conversation.messages ) && conversation.messages.length > 0
-		? conversation.messages[ conversation.messages.length - 1 ]
-		: null;
+} ): OdieMessage | ZendeskMessage | null => {
+	const lastMessage =
+		Array.isArray( conversation.messages ) && conversation.messages.length > 0
+			? conversation.messages[ conversation.messages.length - 1 ]
+			: null;
+
+	return lastMessage;
path: packages/help-center/src/components/utils.tsx
user: login: AllTerrainDeveloper
body: Right, enough information already in the method name.
created_at: 2025-05-22T08:40:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -55,6 +59,9 @@ const Conversations = ( {
 	return (
 		<>
 			{ conversations.map( ( conversation ) => {
+				const { numberOfUnreadMessages } = calculateUnread( [
+					conversation as ZendeskConversation,
path: packages/help-center/src/components/help-center-chat-history.tsx
user: login: stephanethomas
body: Well that's exactly my point - do we really need to cast that `conversation` into `ZendeskConversation`? If the conversation is of type `OdieConversation`, will it trigger a runtime error?
created_at: 2025-05-22T08:43:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -55,6 +59,9 @@ const Conversations = ( {
 	return (
 		<>
 			{ conversations.map( ( conversation ) => {
+				const { numberOfUnreadMessages } = calculateUnread( [
+					conversation as ZendeskConversation,
path: packages/help-center/src/components/help-center-chat-history.tsx
user: login: AllTerrainDeveloper
body: Oh well, just checked it. The parameter is actually used in `calculateUnread`. `CalculateUnread` uses a property that `OdieConversation` is missing: `participants`. That's why It's casted here.

`OdieConversation` won't have unread messages as it's always replying immediately and `OdieConversation` not having the participants property, when casted to `ZendeskConversation`, returns 0.

I hope I was able to clarify it enough 😄 
created_at: 2025-05-22T08:50:13Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-16T10:05:51Z
body: <details>
<summary>Calypso Live <a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-145305">(direct link)</a></summary>
<table>
<tr>
<td>
<img src="https://chart.googleapis.com/chart?chs=150x150&cht=qr&chl=https%3A%2F%2Fcalypso.live%3Fimage%3Dregistry.a8c.com%2Fcalypso%2Fapp%3Abuild-145305%26flags%3Doauth&choe=UTF-8" />
</td>
<td>
<a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-145305">https://calypso.live?image=registry.a8c.com/calypso/app:build-145305</a>
</td>
</tr>
</table>
</details>
<details>
<summary>Jetpack Cloud live <a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-145305&env=jetpack">(direct link)</a></summary>
<table>
<tr>
<td>
<img src="https://chart.googleapis.com/chart?chs=150x150&cht=qr&chl=https%3A%2F%2Fcalypso.live%3Fimage%3Dregistry.a8c.com%2Fcalypso%2Fapp%3Abuild-145305%26env%3Djetpack%26flags%3Doauth&choe=UTF-8" />
</td>
<td>
<a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-145305&env=jetpack">https://calypso.live?image=registry.a8c.com/calypso/app:build-145305&env=jetpack</a>
</td>
</tr>
</table>
</details>
<details>
<summary>Automattic for Agencies live <a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-145305&env=a8c-for-agencies">(direct link)</a></summary>
<table>
<tr>
<td>
<img src="https://chart.googleapis.com/chart?chs=150x150&cht=qr&chl=https%3A%2F%2Fcalypso.live%3Fimage%3Dregistry.a8c.com%2Fcalypso%2Fapp%3Abuild-145305%26env%3Da8c-for-agencies%26flags%3Doauth&choe=UTF-8" />
</td>
<td>
<a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-145305&env=a8c-for-agencies">https://calypso.live?image=registry.a8c.com/calypso/app:build-145305&env=a8c-for-agencies</a>
</td>
</tr>
</table>
</details>
<!--calypso-live-watermark:apr@v1-->
author_association: NONE
user: login: matticbot
created_at: 2025-05-16T10:06:01Z
body: <!-- icfy-watermark: c52822 -->
Here is how your PR affects size of JS and CSS bundles shipped to the user's browser:

**App Entrypoints** (~69 bytes added 📈 [gzipped])
<details>

```
name                   parsed_size           gzip_size
entry-stepper               +283 B  (+0.0%)      +69 B  (+0.0%)
entry-reauth-required       +283 B  (+0.0%)      +69 B  (+0.0%)
entry-main                  +283 B  (+0.0%)      +69 B  (+0.0%)
entry-login                 +283 B  (+0.0%)      +69 B  (+0.0%)
```

Common code that is always downloaded and parsed every time the app is loaded, no matter which route is used.
</details>

**Async-loaded Components** (~199 bytes added 📈 [gzipped])
<details>

```
name                                       parsed_size           gzip_size
async-load-automattic-help-center-stepper       +798 B  (+0.1%)     +199 B  (+0.1%)
async-load-automattic-help-center               +798 B  (+0.1%)     +199 B  (+0.1%)
```

React components that are loaded lazily, when a certain part of UI is displayed for the first time.
</details>

**Legend**
<details>
<summary>What is parsed and gzip size?</summary>

**Parsed Size:** Uncompressed size of the JS and CSS files. This much code needs to be parsed and stored in memory.
**Gzip Size:** Compressed size of the JS and CSS files. This much data needs to be downloaded over network.
</details>

Generated by performance advisor bot at [iscalypsofastyet.com](http://iscalypsofastyet.com).
author_association: CONTRIBUTOR
user: login: ehg
created_at: 2025-05-19T09:21:19Z
body: I took this for a spin, and couldn't see any of unread functionality. The message in the "Recent conversation" didn't update until I went to the conversation itself.
author_association: MEMBER
user: login: matticbot
created_at: 2025-05-19T14:16:48Z
body: **This PR modifies the release build for the following Calypso Apps:**

_For info about this notification, see here: PCYsg-OT6-p2_

* blaze-dashboard
* help-center
* odyssey-stats

To test WordPress.com changes, run `install-plugin.sh $pluginSlug Odie/restore-help-center-badge-and-unread-status` on your sandbox.
<!--calypso-app-artifacts-watermark:apr@v1-->
author_association: CONTRIBUTOR
user: login: AllTerrainDeveloper
created_at: 2025-05-20T11:04:23Z
body: I have added a set of aditional fixes that might need some explanation.

I have removed some loading guards that were returning null, this was making the screen to flicker, so now it will only return false, while is loading and no previous data. It has sense now, as if we receive a message, we were setting the help center to a blank state on some parts when we still need to show old data to present the new data, instead of the flow "old data -> blank (loading) -> new data"

Another thing was to add timestamps to the query cache, that will force fetching when new data is receied. I deally we should have integrated Smooch differently instead of relying on timestamps, but that would require quite a lot of work, and might be a nice work for a future a11n :)
author_association: CONTRIBUTOR