Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions src/libs/Network/SequentialQueue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> = 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<string>();

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,
Expand All @@ -35,6 +43,7 @@ Onyx.connectWithoutView({
return;
}
shouldFailAllRequests = !!network.shouldFailAllRequests;
networkTimeSkew = network?.timeSkew ?? 0;
},
});

Expand Down Expand Up @@ -189,6 +198,35 @@ function process(): Promise<void> {
},
});

// 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) => {
Expand All @@ -211,6 +249,12 @@ function process(): Promise<void> {
});
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,
Expand Down Expand Up @@ -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
Expand Down
Loading