@@ -167,6 +167,7 @@ import {
167167 isOpenExpenseReport ,
168168 isProcessingReport ,
169169 isReportManuallyReimbursed ,
170+ isReportNotFound ,
170171 isSelfDM ,
171172 isValidReportIDFromPath ,
172173 prepareOnboardingOnyxData ,
@@ -400,6 +401,53 @@ type AddAttachmentWithCommentParams = {
400401const addNewMessageWithText = new Set < string > ( [ WRITE_COMMANDS . ADD_COMMENT , WRITE_COMMANDS . ADD_TEXT_AND_ATTACHMENT ] ) ;
401402// map of reportID to all reportActions for that report
402403const allReportActions : OnyxCollection < ReportActions > = { } ;
404+ const STALE_DM_RECOVERY_TARGET_TTL_MS = 30000 ;
405+ const staleDMRecoveryTargetBySourceReportID : Record < string , string > = { } ;
406+ const staleDMRecoverySourceByTargetReportID : Record < string , string > = { } ;
407+ const staleDMRecoveryCleanupTimersBySourceReportID : Record < string , ReturnType < typeof setTimeout > > = { } ;
408+
409+ function clearStaleDMRecoveryTargetBySourceReportID ( sourceReportID : string ) {
410+ const targetReportID = staleDMRecoveryTargetBySourceReportID [ sourceReportID ] ;
411+ if ( ! targetReportID ) {
412+ return ;
413+ }
414+
415+ delete staleDMRecoveryTargetBySourceReportID [ sourceReportID ] ;
416+ delete staleDMRecoverySourceByTargetReportID [ targetReportID ] ;
417+
418+ const timeoutID = staleDMRecoveryCleanupTimersBySourceReportID [ sourceReportID ] ;
419+ if ( timeoutID ) {
420+ clearTimeout ( timeoutID ) ;
421+ }
422+ delete staleDMRecoveryCleanupTimersBySourceReportID [ sourceReportID ] ;
423+ }
424+
425+ function setStaleDMRecoveryTarget ( sourceReportID : string , targetReportID : string ) {
426+ clearStaleDMRecoveryTargetBySourceReportID ( sourceReportID ) ;
427+ const existingSourceReportID = staleDMRecoverySourceByTargetReportID [ targetReportID ] ;
428+ if ( existingSourceReportID ) {
429+ clearStaleDMRecoveryTargetBySourceReportID ( existingSourceReportID ) ;
430+ }
431+
432+ staleDMRecoveryTargetBySourceReportID [ sourceReportID ] = targetReportID ;
433+ staleDMRecoverySourceByTargetReportID [ targetReportID ] = sourceReportID ;
434+ staleDMRecoveryCleanupTimersBySourceReportID [ sourceReportID ] = setTimeout ( ( ) => {
435+ clearStaleDMRecoveryTargetBySourceReportID ( sourceReportID ) ;
436+ } , STALE_DM_RECOVERY_TARGET_TTL_MS ) ;
437+ }
438+
439+ function getStaleDMRecoveryTarget ( reportID : string ) {
440+ return staleDMRecoveryTargetBySourceReportID [ reportID ] ;
441+ }
442+
443+ function clearStaleDMRecoveryTargetByTargetReportID ( targetReportID : string ) {
444+ const sourceReportID = staleDMRecoverySourceByTargetReportID [ targetReportID ] ;
445+ if ( ! sourceReportID ) {
446+ return ;
447+ }
448+
449+ clearStaleDMRecoveryTargetBySourceReportID ( sourceReportID ) ;
450+ }
403451
404452Onyx . connect ( {
405453 key : ONYXKEYS . COLLECTION . REPORT_ACTIONS ,
@@ -762,7 +810,18 @@ function addActions({
762810 if ( ! report ?. reportID ) {
763811 return ;
764812 }
765- const reportID = report . reportID ;
813+ const sourceReportID = report . reportID ;
814+ const reportID = getStaleDMRecoveryTarget ( sourceReportID ) ?? sourceReportID ;
815+ const reportForAction = reportID === sourceReportID ? report : ( allReports ?. [ `${ ONYXKEYS . COLLECTION . REPORT } ${ reportID } ` ] ?? report ) ;
816+
817+ let resolvedNotifyReportID : AddActionsParams [ 'notifyReportID' ] ;
818+ if ( typeof notifyReportID === 'string' ) {
819+ resolvedNotifyReportID = getStaleDMRecoveryTarget ( notifyReportID ) ?? notifyReportID ;
820+ } else if ( Array . isArray ( notifyReportID ) ) {
821+ resolvedNotifyReportID = notifyReportID . map ( ( id ) => getStaleDMRecoveryTarget ( id ) ?? id ) ;
822+ } else {
823+ resolvedNotifyReportID = notifyReportID ;
824+ }
766825 let reportCommentText = '' ;
767826 let reportCommentAction : OptimisticAddCommentReportAction | undefined ;
768827 let attachmentAction : OptimisticAddCommentReportAction | undefined ;
@@ -832,10 +891,10 @@ function addActions({
832891 lastActionType : CONST . REPORT . ACTIONS . TYPE . ADD_COMMENT ,
833892 } ;
834893
835- const shouldUpdateNotificationPreference = ! isEmptyObject ( report ) && isHiddenForCurrentUser ( report ) ;
894+ const shouldUpdateNotificationPreference = ! isEmptyObject ( reportForAction ) && isHiddenForCurrentUser ( reportForAction ) ;
836895 if ( shouldUpdateNotificationPreference ) {
837896 optimisticReport . participants = {
838- [ currentUserAccountID ] : { notificationPreference : getDefaultNotificationPreferenceForReport ( report ) } ,
897+ [ currentUserAccountID ] : { notificationPreference : getDefaultNotificationPreferenceForReport ( reportForAction ) } ,
839898 } ;
840899 }
841900
@@ -870,7 +929,7 @@ function addActions({
870929 idempotencyKey : Str . guid ( ) ,
871930 } ;
872931
873- const isConciergeChat = isConciergeChatReport ( report ) ;
932+ const isConciergeChat = isConciergeChatReport ( reportForAction ) ;
874933 if ( reportIDDeeplinkedFromOldDot === reportID && isConciergeChat ) {
875934 parameters . isOldDotConciergeChat = true ;
876935 }
@@ -879,7 +938,7 @@ function addActions({
879938 parameters . attachmentID = attachmentID ;
880939 }
881940
882- if ( isInSidePanel && ( isConciergeChat || isAdminRoom ( report ) ) ) {
941+ if ( isInSidePanel && ( isConciergeChat || isAdminRoom ( reportForAction ) ) ) {
883942 const pageHTML = capturePageHTML ( ) ;
884943 if ( pageHTML ) {
885944 parameters . pageHTML = pageHTML ;
@@ -1000,7 +1059,7 @@ function addActions({
10001059 successData,
10011060 failureData,
10021061 } ) ;
1003- notifyNewAction ( notifyReportID , lastAction , lastAction ?. actorAccountID === currentUserAccountID ) ;
1062+ notifyNewAction ( resolvedNotifyReportID , lastAction , lastAction ?. actorAccountID === currentUserAccountID ) ;
10041063}
10051064
10061065/** Add an attachment with an optional comment to a report */
@@ -1397,7 +1456,6 @@ function openReport(params: OpenReportActionParams) {
13971456 const participantLoginList = participants . map ( ( p ) => p . login ) . filter ( ( login ) => ! ! login ) ;
13981457 // TODO: allPersonalDetails fallback should be removed in follow-up PRs https://github.com/Expensify/App/issues/73656
13991458 const participantAccountIDList = participants . map ( ( p ) => p . accountID ) . filter ( ( id ) : id is number => id !== undefined ) ;
1400-
14011459 const optimisticReport = reportActionsExist ( reportID )
14021460 ? { }
14031461 : {
@@ -2101,17 +2159,10 @@ function createTransactionThreadReport(
21012159 */
21022160function navigateToReport ( reportID : string | undefined , shouldDismissModal = true ) {
21032161 if ( shouldDismissModal ) {
2104- Navigation . dismissModal ( {
2105- afterTransition : ( ) => {
2106- if ( ! reportID ) {
2107- return ;
2108- }
2109-
2110- Navigation . navigate ( ROUTES . REPORT_WITH_ID . getRoute ( reportID ) ) ;
2111- } ,
2112- } ) ;
2113- } else if ( reportID ) {
2114- Navigation . navigate ( ROUTES . REPORT_WITH_ID . getRoute ( reportID ) ) ;
2162+ Navigation . dismissModal ( ) ;
2163+ }
2164+ if ( ! reportID ) {
2165+ return ;
21152166 }
21162167 // In some cases when RHP modal gets hidden and then we navigate to report Composer focus breaks, wrapping navigation in setTimeout fixes this
21172168 setTimeout ( ( ) => {
@@ -2134,32 +2185,68 @@ function navigateToAndOpenReport(
21342185 isSelfTourViewed : boolean | undefined ,
21352186 betas : OnyxEntry < Beta [ ] > ,
21362187 shouldDismissModal = true ,
2188+ shouldRevalidateExistingChat = false ,
21372189) {
2138- let newChat : OptimisticChatReport | undefined ;
21392190 const participantAccountIDs = PersonalDetailsUtils . getAccountIDsByLogins ( userLogins ) ;
21402191 const chat = getChatByParticipants ( [ ...participantAccountIDs , currentUserAccountID ] ) ;
2141-
2142- if ( isEmptyObject ( chat ) ) {
2143- newChat = buildOptimisticChatReport ( {
2192+ const createAndOpenNewOptimisticChat = ( sourceCachedReportID ?: string ) => {
2193+ const fallbackChat = buildOptimisticChatReport ( {
21442194 participantList : [ ...participantAccountIDs , currentUserAccountID ] ,
21452195 notificationPreference : CONST . REPORT . NOTIFICATION_PREFERENCE . HIDDEN ,
21462196 currentUserAccountID,
21472197 } ) ;
2148- // We want to pass newChat here because if anything is passed in that param (even an existing chat), we will try to create a chat on the server
2198+ if ( sourceCachedReportID ) {
2199+ setStaleDMRecoveryTarget ( sourceCachedReportID , fallbackChat . reportID ) ;
2200+ }
2201+
2202+ // We pass newReportObject to force chat creation on the server.
21492203 openReport ( {
2150- reportID : newChat ? .reportID ,
2204+ reportID : fallbackChat . reportID ,
21512205 introSelected,
21522206 reportActionID : '' ,
21532207 participants : buildParticipantInfoFromLogins ( userLogins ) ,
21542208 personalDetails,
2155- newReportObject : newChat ,
2209+ newReportObject : fallbackChat ,
21562210 isSelfTourViewed,
21572211 betas,
21582212 } ) ;
2213+
2214+ navigateToReport ( fallbackChat . reportID , shouldDismissModal ) ;
2215+ } ;
2216+
2217+ if ( isEmptyObject ( chat ) || isReportNotFound ( chat ) ) {
2218+ createAndOpenNewOptimisticChat ( chat ?. reportID ) ;
2219+ return ;
21592220 }
2160- const report = isEmptyObject ( chat ) ? newChat : chat ;
21612221
2162- navigateToReport ( report ?. reportID , shouldDismissModal ) ;
2222+ if ( ! shouldRevalidateExistingChat ) {
2223+ navigateToReport ( chat . reportID , shouldDismissModal ) ;
2224+ return ;
2225+ }
2226+
2227+ let hasAttemptedFallback = false ;
2228+ const reportConnection = Onyx . connectWithoutView ( {
2229+ key : `${ ONYXKEYS . COLLECTION . REPORT } ${ chat . reportID } ` ,
2230+ callback : ( updatedReport ) => {
2231+ const notFoundError = updatedReport ?. errorFields ?. notFound ;
2232+ if ( notFoundError === null ) {
2233+ Onyx . disconnect ( reportConnection ) ;
2234+ return ;
2235+ }
2236+
2237+ if ( ! notFoundError || hasAttemptedFallback ) {
2238+ return ;
2239+ }
2240+
2241+ hasAttemptedFallback = true ;
2242+ Onyx . disconnect ( reportConnection ) ;
2243+ createAndOpenNewOptimisticChat ( chat . reportID ) ;
2244+ } ,
2245+ } ) ;
2246+
2247+ // Re-open existing chats to re-validate server-side access and refresh stale local state.
2248+ openReport ( { reportID : chat . reportID , introSelected, isSelfTourViewed, betas} ) ;
2249+ navigateToReport ( chat . reportID , shouldDismissModal ) ;
21632250}
21642251
21652252function navigateToAndCreateGroupChat (
@@ -2196,37 +2283,73 @@ function navigateToAndOpenReportWithAccountIDs(
21962283 betas : OnyxEntry < Beta [ ] > ,
21972284 // TODO: personalDetails should be a required field in follow-up PRs https://github.com/Expensify/App/issues/73656
21982285 personalDetails ?: OnyxEntry < PersonalDetailsList > ,
2286+ shouldRevalidateExistingChat = false ,
21992287) {
2200- let newChat : OptimisticChatReport | undefined ;
22012288 const participants = participantAccountIDs . map ( ( accountID ) : ParticipantInfo => {
22022289 return {
22032290 login : '' ,
22042291 accountID,
22052292 } ;
22062293 } ) ;
2207-
22082294 const chat = getChatByParticipants ( [ ...participantAccountIDs , currentUserAccountID ] ) ;
2209- if ( ! chat ) {
2210- newChat = buildOptimisticChatReport ( {
2295+ const createAndOpenNewOptimisticChat = ( sourceCachedReportID ?: string ) => {
2296+ const fallbackChat = buildOptimisticChatReport ( {
22112297 participantList : [ ...participantAccountIDs , currentUserAccountID ] ,
22122298 currentUserAccountID,
22132299 } ) ;
2214- // We want to pass newChat here because if anything is passed in that param (even an existing chat), we will try to create a chat on the server
2300+ if ( sourceCachedReportID ) {
2301+ setStaleDMRecoveryTarget ( sourceCachedReportID , fallbackChat . reportID ) ;
2302+ }
2303+
2304+ // We pass newReportObject to force chat creation on the server.
22152305 openReport ( {
2216- reportID : newChat ? .reportID ,
2306+ reportID : fallbackChat . reportID ,
22172307 introSelected,
22182308 isSelfTourViewed,
2219- newReportObject : newChat ,
2309+ newReportObject : fallbackChat ,
22202310 parentReportActionID : '0' ,
22212311 participants,
22222312 // TODO: allPersonalDetails fallback should be removed in follow-up PRs https://github.com/Expensify/App/issues/73656
22232313 personalDetails : personalDetails ?? allPersonalDetails ,
22242314 betas,
22252315 } ) ;
2316+
2317+ navigateToReport ( fallbackChat . reportID , false ) ;
2318+ } ;
2319+
2320+ if ( ! chat || isReportNotFound ( chat ) ) {
2321+ createAndOpenNewOptimisticChat ( chat ?. reportID ) ;
2322+ return ;
22262323 }
2227- const report = chat ?? newChat ;
22282324
2229- Navigation . navigate ( ROUTES . REPORT_WITH_ID . getRoute ( report ?. reportID ) ) ;
2325+ if ( ! shouldRevalidateExistingChat ) {
2326+ navigateToReport ( chat . reportID , false ) ;
2327+ return ;
2328+ }
2329+
2330+ let hasAttemptedFallback = false ;
2331+ const reportConnection = Onyx . connectWithoutView ( {
2332+ key : `${ ONYXKEYS . COLLECTION . REPORT } ${ chat . reportID } ` ,
2333+ callback : ( updatedReport ) => {
2334+ const notFoundError = updatedReport ?. errorFields ?. notFound ;
2335+ if ( notFoundError === null ) {
2336+ Onyx . disconnect ( reportConnection ) ;
2337+ return ;
2338+ }
2339+
2340+ if ( ! notFoundError || hasAttemptedFallback ) {
2341+ return ;
2342+ }
2343+
2344+ hasAttemptedFallback = true ;
2345+ Onyx . disconnect ( reportConnection ) ;
2346+ createAndOpenNewOptimisticChat ( chat . reportID ) ;
2347+ } ,
2348+ } ) ;
2349+
2350+ // Re-open existing chats to re-validate server-side access and refresh stale local state.
2351+ openReport ( { reportID : chat . reportID , introSelected, isSelfTourViewed, betas} ) ;
2352+ navigateToReport ( chat . reportID , false ) ;
22302353}
22312354
22322355/**
@@ -7686,6 +7809,7 @@ export {
76867809 setNewRoomFormLoading ,
76877810 clearPolicyRoomNameErrors ,
76887811 clearPrivateNotesError ,
7812+ clearStaleDMRecoveryTargetByTargetReportID ,
76897813 clearReportFieldKeyErrors ,
76907814 completeOnboarding ,
76917815 extractRHPVariantFromResponse ,
0 commit comments