diff --git a/src/libs/Network/SequentialQueue.ts b/src/libs/Network/SequentialQueue.ts index 5e5eda99a5377..b6992e6ac74f0 100644 --- a/src/libs/Network/SequentialQueue.ts +++ b/src/libs/Network/SequentialQueue.ts @@ -26,7 +26,15 @@ import type OnyxRequest from '@src/types/onyx/Request'; import type {AnyOnyxUpdate, AnyRequest, ConflictData} from '@src/types/onyx/Request'; import {isOffline, onReconnection} from './NetworkStore'; +// Commands that create visible report actions whose timestamps the server may reassign +const OUTGOING_MESSAGE_COMMANDS: ReadonlySet = new Set([WRITE_COMMANDS.ADD_COMMENT, WRITE_COMMANDS.ADD_ATTACHMENT, WRITE_COMMANDS.ADD_TEXT_AND_ATTACHMENT]); + +// Tracks report IDs that had offline outgoing message commands processed during the current flush cycle. +// Used to detect when an offline ReadNewestAction has a stale lastReadTime that needs refreshing. +const reportsWithOfflineSentMessages = new Set(); + let shouldFailAllRequests: boolean; +let networkTimeSkew = 0; // Use connectWithoutView since this is for network data and don't affect to any UI Onyx.connectWithoutView({ key: ONYXKEYS.NETWORK, @@ -35,6 +43,7 @@ Onyx.connectWithoutView({ return; } shouldFailAllRequests = !!network.shouldFailAllRequests; + networkTimeSkew = network?.timeSkew ?? 0; }, }); @@ -189,6 +198,35 @@ function process(): Promise { }, }); + // When offline messages were sent for a report and then ReadNewestAction was queued while still offline, + // the lastReadTime captured at that offline moment becomes stale. The server assigns new timestamps to the + // offline-sent messages during replay, which end up later than the stale lastReadTime, causing the report + // to incorrectly appear as unread. Refresh lastReadTime to current time so it covers the server-assigned + // timestamps. We only do this when the same report had offline messages earlier in this flush cycle to + // avoid auto-reading messages from other users in reports where the user didn't send anything offline. + if ( + requestToProcess.command === WRITE_COMMANDS.READ_NEWEST_ACTION && + requestToProcess.initiatedOffline && + typeof requestToProcess.data?.reportID === 'string' && + reportsWithOfflineSentMessages.has(requestToProcess.data.reportID) + ) { + // Inline the DB-time-with-skew logic here to avoid importing NetworkConnection, + // which triggers heavy module-level side effects (NetInfo, Onyx subscriptions) and + // causes a circular dependency: SequentialQueue → NetworkConnection → DateUtils → … → SequentialQueue. + // networkTimeSkew is already read from the ONYXKEYS.NETWORK subscription above. + const now = networkTimeSkew > 0 ? new Date(Date.now() + networkTimeSkew) : new Date(); + const refreshedLastReadTime = now.toISOString().replace('T', ' ').replace('Z', ''); + Log.info('[SequentialQueue] Refreshing lastReadTime for offline ReadNewestAction', false, { + reportID: requestToProcess.data.reportID, + originalLastReadTime: requestToProcess.data.lastReadTime, + refreshedLastReadTime, + }); + requestToProcess.data = { + ...requestToProcess.data, + lastReadTime: refreshedLastReadTime, + }; + } + // Set the current request to a promise awaiting its processing so that getCurrentRequest can be used to take some action after the current request has processed. currentRequestPromise = processWithMiddleware(requestToProcess, true) .then((response) => { @@ -211,6 +249,12 @@ function process(): Promise { }); endPersistedRequestAndRemoveFromQueue(requestToProcess); + // Track reports that had offline outgoing messages so we can refresh lastReadTime + // for any subsequent offline ReadNewestAction targeting the same report. + if (requestToProcess.initiatedOffline && OUTGOING_MESSAGE_COMMANDS.has(requestToProcess.command) && typeof requestToProcess.data?.reportID === 'string') { + reportsWithOfflineSentMessages.add(requestToProcess.data.reportID); + } + if (requestToProcess.queueFlushedData) { Log.info('[SequentialQueue] Will store queueFlushedData.', false, { command: requestToProcess.command, @@ -364,6 +408,7 @@ function flush(shouldResetPromise = true) { }); isSequentialQueueRunning = true; + reportsWithOfflineSentMessages.clear(); if (shouldResetPromise) { // Reset the isReadyPromise so that the queue will be flushed as soon as the request is finished