11import { useEffect , useRef } from 'react' ;
22import type { OnyxEntry } from 'react-native-onyx' ;
3- import { applyPendingConciergeAction , discardPendingConciergeAction } from '@libs/actions/Report/SuggestedFollowup' ;
3+ import { clearAgentZeroProcessingIndicator } from '@libs/actions/Report' ;
4+ import { applyPendingConciergeAction , clearPendingFollowupList , discardPendingConciergeAction , hidePendingFollowupList } from '@libs/actions/Report/SuggestedFollowup' ;
5+ import AgentZeroOptimisticStore , { MAX_AGE_MS } from '@libs/AgentZeroOptimisticStore' ;
46import Log from '@libs/Log' ;
57import { rand64 } from '@libs/NumberUtils' ;
68import type { ConciergeDraftEvent } from '@libs/Pusher/types' ;
9+ import { parseFollowupsFromHtml } from '@libs/ReportActionFollowupUtils' ;
710import tokenizeForReveal from '@libs/ReportActionFollowupUtils/tokenizeForReveal' ;
811import { getReportActionHtml } from '@libs/ReportActionsUtils' ;
912import { useConciergeDraftActions } from '@pages/inbox/ConciergeDraftContext' ;
1013import ONYXKEYS from '@src/ONYXKEYS' ;
1114import type { ReportAction , ReportActions } from '@src/types/onyx' ;
15+ import useNetwork from './useNetwork' ;
1216import useOnyx from './useOnyx' ;
1317
1418/** Default trickle duration. Targets ~19 chars/sec start (~7/sec end after ease-out) across a typical multi-paragraph response — visibly streaming without dragging the user past the moment they want to read. */
@@ -21,6 +25,8 @@ const TRICKLE_HARD_CAP_MS = 60_000;
2125const ACCELERATED_REMAINING_MS = 1_500 ;
2226/** Minimum char-level anchors before we opt into the trickle reveal. Replies under this fall back to the binary reveal at `displayAfter`. */
2327const MIN_TRICKLE_TOKEN_COUNT = 100 ;
28+ /** Hard cap on a pending followup-list skeleton. If the server never appends a real followup-list within this window, drop the marker so the UI stops showing a perpetual skeleton. */
29+ const PENDING_FOLLOWUP_LIST_HARD_CAP_MS = MAX_AGE_MS ;
2430
2531function easeOut ( t : number ) : number {
2632 const clamped = Math . max ( 0 , Math . min ( 1 , t ) ) ;
@@ -32,13 +38,19 @@ function easeOut(t: number): number {
3238 * the binary reveal at `displayAfter`. `REPORT_ACTIONS` is written at completion.
3339 */
3440function usePendingConciergeResponse ( reportID : string | undefined ) {
41+ const { isOffline} = useNetwork ( ) ;
3542 const [ pendingResponse ] = useOnyx ( `${ ONYXKEYS . COLLECTION . PENDING_CONCIERGE_RESPONSE } ${ reportID } ` ) ;
43+ const [ pendingFollowupList ] = useOnyx ( `${ ONYXKEYS . COLLECTION . CONCIERGE_PENDING_FOLLOWUP_LIST } ${ reportID } ` ) ;
3644 const reportActionID = pendingResponse ?. reportAction ?. reportActionID ;
3745 const fullHtml = pendingResponse ?. reportAction ? getReportActionHtml ( pendingResponse . reportAction ) : '' ;
3846 // React Compiler auto-memoizes the selector closure and the tokenize result;
3947 // explicit useCallback/useMemo would just shadow the compiler's analysis.
4048 const persistedActionSelector = ( actions : OnyxEntry < ReportActions > ) : ReportAction | undefined => ( reportActionID && actions ? actions [ reportActionID ] : undefined ) ;
4149 const [ persistedAction ] = useOnyx ( `${ ONYXKEYS . COLLECTION . REPORT_ACTIONS } ${ reportID } ` , { selector : persistedActionSelector } ) ;
50+ const pendingFollowupActionID = pendingFollowupList ?. reportActionID ;
51+ const pendingFollowupActionSelector = ( actions : OnyxEntry < ReportActions > ) : ReportAction | undefined =>
52+ pendingFollowupActionID && actions ? actions [ pendingFollowupActionID ] : undefined ;
53+ const [ pendingFollowupAction ] = useOnyx ( `${ ONYXKEYS . COLLECTION . REPORT_ACTIONS } ${ reportID } ` , { selector : pendingFollowupActionSelector } ) ;
4254 const { dispatchLocalDraftEvent} = useConciergeDraftActions ( ) ;
4355
4456 const tokens = tokenizeForReveal ( fullHtml ) ;
@@ -65,6 +77,58 @@ function usePendingConciergeResponse(reportID: string | undefined) {
6577 accelerateRef . current ( Date . now ( ) ) ;
6678 } , [ persistedAction ] ) ;
6779
80+ const lastOnlineTransitionAtRef = useRef < number > ( 0 ) ;
81+ const wasOfflineRef = useRef < boolean > ( isOffline ) ;
82+ useEffect ( ( ) => {
83+ if ( wasOfflineRef . current && ! isOffline ) {
84+ lastOnlineTransitionAtRef . current = Date . now ( ) ;
85+ }
86+ wasOfflineRef . current = isOffline ;
87+ } , [ isOffline ] ) ;
88+
89+ // Hide the followup-list skeleton when the user is offline.
90+ useEffect ( ( ) => {
91+ if ( ! reportID || ! pendingFollowupList || ! ! pendingFollowupList . hidden === isOffline ) {
92+ return ;
93+ }
94+ hidePendingFollowupList ( reportID , isOffline || null ) ;
95+ } , [ reportID , isOffline , pendingFollowupList ] ) ;
96+
97+ // Clear the pending followup-list skeleton flag as soon as the server reply
98+ // (with <followup-list>) overwrites the optimistic action.
99+ // A TTL fallback guards against the case where no followup-list ever arrives
100+ // so the skeleton won't get stuck.
101+ useEffect ( ( ) => {
102+ if ( ! reportID || ! pendingFollowupList ) {
103+ return ;
104+ }
105+ const html = pendingFollowupAction ? getReportActionHtml ( pendingFollowupAction ) : '' ;
106+ const hardClearIndicator = ( ) => {
107+ // Skip clearing agent thinking indicator when a newer agent request has kicked off.
108+ const optimisticEntry = AgentZeroOptimisticStore . getEntry ( reportID ) ;
109+ const hasNewerRequest = ! ! optimisticEntry && optimisticEntry . startedAt > pendingFollowupList . createdAt ;
110+ if ( ! hasNewerRequest ) {
111+ clearAgentZeroProcessingIndicator ( reportID ) ;
112+ }
113+ clearPendingFollowupList ( reportID ) ;
114+ } ;
115+ if ( parseFollowupsFromHtml ( html ) ?. length ) {
116+ hardClearIndicator ( ) ;
117+ return ;
118+ }
119+ if ( isOffline ) {
120+ return ;
121+ }
122+ const effectiveStart = Math . max ( pendingFollowupList . createdAt , lastOnlineTransitionAtRef . current ) ;
123+ const remainingTTL = effectiveStart + PENDING_FOLLOWUP_LIST_HARD_CAP_MS - Date . now ( ) ;
124+ if ( remainingTTL <= 0 ) {
125+ hardClearIndicator ( ) ;
126+ return ;
127+ }
128+ const ttlTimer = setTimeout ( hardClearIndicator , remainingTTL ) ;
129+ return ( ) => clearTimeout ( ttlTimer ) ;
130+ } , [ reportID , pendingFollowupList , pendingFollowupAction , isOffline ] ) ;
131+
68132 useEffect ( ( ) => {
69133 if ( ! reportID || ! reportActionID ) {
70134 return ;
0 commit comments