@@ -12,8 +12,10 @@ import type {TransactionDetails} from '@libs/ReportUtils';
1212import {
1313 buildOptimisticCreatedReportAction ,
1414 buildOptimisticModifiedExpenseReportAction ,
15+ buildTransactionThread ,
1516 canEditFieldOfMoneyRequest ,
1617 findSelfDMReportID ,
18+ generateReportID ,
1719 getOutstandingChildRequest ,
1820 getParsedComment ,
1921 getTransactionDetails ,
@@ -24,7 +26,6 @@ import {
2426} from '@libs/ReportUtils' ;
2527import { calculateTaxAmount , getAmount , getClearedPendingFields , getCurrency , getTaxValue , getUpdatedTransaction , isOnHold , isSplitChildTransaction } from '@libs/TransactionUtils' ;
2628import ViolationsUtils from '@libs/Violations/ViolationsUtils' ;
27- import { createTransactionThreadReport } from '@userActions/Report' ;
2829import CONST from '@src/CONST' ;
2930import ONYXKEYS from '@src/ONYXKEYS' ;
3031import type * as OnyxTypes from '@src/types/onyx' ;
@@ -79,9 +80,6 @@ type UpdateMultipleMoneyRequestsParams = {
7980 policyTags : OnyxCollection < OnyxTypes . PolicyTagLists > ;
8081 hash ?: number ;
8182 allPolicies ?: OnyxCollection < OnyxTypes . Policy > ;
82- introSelected : OnyxEntry < OnyxTypes . IntroSelected > ;
83- betas : OnyxEntry < OnyxTypes . Beta [ ] > ;
84- currentUserLogin : string ;
8583 currentUserAccountID : number ;
8684 delegateAccountID : number | undefined ;
8785} ;
@@ -97,10 +95,7 @@ function updateMultipleMoneyRequests({
9795 policyTags,
9896 hash,
9997 allPolicies,
100- introSelected,
101- betas,
10298 currentUserAccountID,
103- currentUserLogin,
10499 delegateAccountID,
105100} : UpdateMultipleMoneyRequestsParams ) {
106101 // Track running totals per report so multiple edits in the same report compound correctly.
@@ -132,23 +127,20 @@ function updateMultipleMoneyRequests({
132127 }
133128 }
134129
135- let transactionThreadReportID = transaction . transactionThreadReportID ?? reportAction ?. childReportID ;
130+ let transactionThreadReportID = reportAction ?. childReportID ?? transaction . transactionThreadReportID ;
136131 let transactionThread = reports ?. [ `${ ONYXKEYS . COLLECTION . REPORT } ${ transactionThreadReportID } ` ] ?? null ;
137132
138133 // Offline-created expenses can be missing a transaction thread until it's opened once.
139- // Ensure the thread exists before adding optimistic MODIFIED_EXPENSE actions so
134+ // Ensure the thread exists locally before adding optimistic MODIFIED_EXPENSE actions so
140135 // bulk-edit comments are visible immediately while still offline.
136+ // We intentionally avoid calling createTransactionThreadReport here because it fires
137+ // an OpenReport API command per expense, which floods the queue and hangs the UI on
138+ // large reports (40-50+ expenses). The backend already creates the transaction thread
139+ // when processing UpdateMoneyRequest, so we only need local Onyx state.
141140 let didCreateThreadInThisIteration = false ;
142141 if ( ! transactionThreadReportID && iouReport ?. reportID ) {
143- const optimisticTransactionThread = createTransactionThreadReport ( {
144- introSelected,
145- currentUserLogin,
146- currentUserAccountID,
147- betas,
148- iouReport,
149- iouReportAction : reportAction ,
150- transaction,
151- } ) ;
142+ const optimisticTransactionThreadReportID = generateReportID ( ) ;
143+ const optimisticTransactionThread = buildTransactionThread ( reportAction , iouReport , currentUserAccountID , undefined , optimisticTransactionThreadReportID ) ;
152144 if ( optimisticTransactionThread ?. reportID ) {
153145 transactionThreadReportID = optimisticTransactionThread . reportID ;
154146 transactionThread = optimisticTransactionThread ;
@@ -296,6 +288,38 @@ function updateMultipleMoneyRequests({
296288 const snapshotSuccessData : Array < OnyxUpdate < typeof ONYXKEYS . COLLECTION . SNAPSHOT > > = [ ] ;
297289 const snapshotFailureData : Array < OnyxUpdate < typeof ONYXKEYS . COLLECTION . SNAPSHOT > > = [ ] ;
298290
291+ // If we created the transaction thread optimistically above, seed it into Onyx
292+ // so the MODIFIED_EXPENSE action has somewhere to land. On success the server's
293+ // OpenReport data (triggered by UpdateMoneyRequest) will overwrite these values.
294+ // Also link the thread back: set childReportID on the parent IOU action and
295+ // transactionThreadReportID on the transaction so subsequent offline edits of the
296+ // same expense reuse this thread instead of generating a new one each time.
297+ if ( didCreateThreadInThisIteration && transactionThread && transactionThreadReportID ) {
298+ optimisticData . push ( {
299+ onyxMethod : Onyx . METHOD . SET ,
300+ key : `${ ONYXKEYS . COLLECTION . REPORT } ${ transactionThreadReportID } ` ,
301+ value : transactionThread ,
302+ } ) ;
303+ // Link childReportID on the parent IOU report action
304+ if ( reportAction ?. reportActionID && iouReport ?. reportID ) {
305+ optimisticData . push ( {
306+ onyxMethod : Onyx . METHOD . MERGE ,
307+ key : `${ ONYXKEYS . COLLECTION . REPORT_ACTIONS } ${ iouReport . reportID } ` ,
308+ value : { [ reportAction . reportActionID ] : { childReportID : transactionThreadReportID } } ,
309+ } ) ;
310+ failureData . push ( {
311+ onyxMethod : Onyx . METHOD . MERGE ,
312+ key : `${ ONYXKEYS . COLLECTION . REPORT_ACTIONS } ${ iouReport . reportID } ` ,
313+ value : { [ reportAction . reportActionID ] : { childReportID : null } } ,
314+ } ) ;
315+ }
316+ failureData . push ( {
317+ onyxMethod : Onyx . METHOD . SET ,
318+ key : `${ ONYXKEYS . COLLECTION . REPORT } ${ transactionThreadReportID } ` ,
319+ value : null ,
320+ } ) ;
321+ }
322+
299323 // Pending fields for the transaction
300324 const pendingFields : OnyxTypes . Transaction [ 'pendingFields' ] = Object . fromEntries ( Object . keys ( transactionChanges ) . map ( ( field ) => [ field , CONST . RED_BRICK_ROAD_PENDING_ACTION . UPDATE ] ) ) ;
301325 const clearedPendingFields = getClearedPendingFields ( transactionChanges ) ;
@@ -357,6 +381,9 @@ function updateMultipleMoneyRequests({
357381 pendingFields,
358382 isLoading : false ,
359383 errorFields : null ,
384+ // Link the optimistic thread back to the transaction so subsequent
385+ // offline edits reuse it instead of generating a new thread each time.
386+ ...( didCreateThreadInThisIteration && transactionThreadReportID ? { transactionThreadReportID} : { } ) ,
360387 } ,
361388 } ) ;
362389
@@ -460,9 +487,8 @@ function updateMultipleMoneyRequests({
460487 if ( transactionThreadReportID ) {
461488 // Backfill a CREATED action for threads never opened locally so
462489 // MoneyRequestView renders and the skeleton doesn't loop offline.
463- // Skip when the thread was just created above (openReport handles it).
464490 const threadReportActions = reportActions ?. [ `${ ONYXKEYS . COLLECTION . REPORT_ACTIONS } ${ transactionThreadReportID } ` ] ?? { } ;
465- const hasCreatedAction = didCreateThreadInThisIteration || Object . values ( threadReportActions ) . some ( ( action ) => action ?. actionName === CONST . REPORT . ACTIONS . TYPE . CREATED ) ;
491+ const hasCreatedAction = Object . values ( threadReportActions ) . some ( ( action ) => action ?. actionName === CONST . REPORT . ACTIONS . TYPE . CREATED ) ;
466492 const optimisticCreatedValue : Record < string , Partial < OnyxTypes . ReportAction > > = { } ;
467493 if ( ! hasCreatedAction ) {
468494 const optimisticCreatedAction = buildOptimisticCreatedReportAction ( { emailCreatingAction : CONST . REPORT . OWNER_EMAIL_FAKE } ) ;
@@ -528,6 +554,9 @@ function updateMultipleMoneyRequests({
528554 ...transaction ,
529555 pendingFields : clearedPendingFields ,
530556 errorFields,
557+ // Clear the optimistically added transactionThreadReportID so it doesn't
558+ // persist after a failed request — the server never created this thread.
559+ ...( didCreateThreadInThisIteration && transactionThreadReportID ? { transactionThreadReportID : null } : { } ) ,
531560 } ,
532561 } ) ;
533562
0 commit comments