-
Notifications
You must be signed in to change notification settings - Fork 3.8k
Remove Onyx.connect for the key REPORT_DRAFT_COMMENT #79456
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7ab394c
0ea695f
8a99a57
7775cf9
ea035db
d30d1ea
f3b7d6c
2ff058a
c5636dd
651ad02
a4b64b7
52bdae8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,172 @@ | ||
| import {DeviceEventEmitter, InteractionManager} from 'react-native'; | ||
| import type {OnyxCollection} from 'react-native-onyx'; | ||
| import Onyx from 'react-native-onyx'; | ||
| import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; | ||
| import {isMoneyRequest, isMoneyRequestReport} from '@libs/ReportUtils'; | ||
| import ONYXKEYS from '@src/ONYXKEYS'; | ||
| import type {Route} from '@src/ROUTES'; | ||
| import SCREENS from '@src/SCREENS'; | ||
| import type {Report, ReportActions} from '@src/types/onyx'; | ||
| import {saveReportDraftComment} from './Report'; | ||
|
|
||
| /** | ||
| * replaceOptimisticReportWithActualReport | ||
| * | ||
| * This module handles a specific edge case in the Expensify app's offline-first architecture. | ||
| * | ||
| * THE PROBLEM: | ||
| * When a user creates a new DM or group chat, we optimistically create a report with a temporary | ||
| * reportID so they can start using it immediately (offline-first UX). However, when the API request | ||
| * completes, the server might respond that a report already exists for that set of participants | ||
| * (e.g., if the user previously had a DM with that person). In this case, the API returns a | ||
| * `preexistingReportID` indicating which report should be used instead of the optimistic one. | ||
| * | ||
| * THE SOLUTION: | ||
| * This module listens to the REPORT collection in Onyx. When a report comes in with a | ||
| * `preexistingReportID` field set, it means we need to: | ||
| * 1. Delete the optimistically created report (the one with the temporary ID) | ||
| * 2. Redirect the user to the preexisting report (if they're currently viewing the optimistic one) | ||
| * 3. Transfer any draft comment from the optimistic report to the preexisting report | ||
| * 4. Clean up associated data like parent report actions for money request reports | ||
| * | ||
| */ | ||
|
|
||
| let allReportDraftComments: Record<string, string | undefined> = {}; | ||
| // Draft comments are cached only for transferring to the preexisting report; no UI subscribes, so connectWithoutView() is used. | ||
| Onyx.connectWithoutView({ | ||
| key: ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, | ||
| waitForCollectionCallback: true, | ||
| callback: (value) => (allReportDraftComments = value ?? {}), | ||
| }); | ||
|
|
||
| let allReports: OnyxCollection<Report>; | ||
|
|
||
| const allReportActions: OnyxCollection<ReportActions> = {}; | ||
| // Report actions are cached only to resolve parent actions for IOU cleanup; no UI subscribes, so connectWithoutView() is used. | ||
| Onyx.connectWithoutView({ | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Each ConnectWithoutView needs a comment on why we are using this. Can you please add one liner comments one each |
||
| key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, | ||
| callback: (actions, key) => { | ||
| if (!key || !actions) { | ||
| return; | ||
| } | ||
| const reportID = key.replace(ONYXKEYS.COLLECTION.REPORT_ACTIONS, ''); | ||
| allReportActions[reportID] = actions; | ||
| }, | ||
| }); | ||
|
|
||
| function replaceOptimisticReportWithActualReport(report: Report, draftReportComment: string | undefined) { | ||
| const {reportID, preexistingReportID, parentReportID, parentReportActionID} = report; | ||
|
|
||
| if (!reportID || !preexistingReportID) { | ||
| return; | ||
| } | ||
|
|
||
| // Handle cleanup of stale optimistic IOU report and its report preview separately | ||
| if ((isMoneyRequestReport(report) || isMoneyRequest(report)) && parentReportID && parentReportActionID) { | ||
| const parentReportAction = allReportActions?.[parentReportID]?.[parentReportActionID]; | ||
| if (parentReportAction?.childReportID === reportID) { | ||
| Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`, { | ||
| [parentReportActionID]: null, | ||
| }); | ||
| } | ||
| Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, null); | ||
| return; | ||
| } | ||
|
|
||
| // eslint-disable-next-line @typescript-eslint/no-deprecated | ||
| InteractionManager.runAfterInteractions(() => { | ||
| // It is possible that we optimistically created a DM/group-DM for a set of users for which a report already exists. | ||
| // In this case, the API will let us know by returning a preexistingReportID. | ||
| // We should clear out the optimistically created report and re-route the user to the preexisting report. | ||
| let callback = () => { | ||
| const existingReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${preexistingReportID}`]; | ||
|
|
||
| Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, null); | ||
| Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${preexistingReportID}`, { | ||
| ...report, | ||
| reportID: preexistingReportID, | ||
| preexistingReportID: null, | ||
| // Replacing the existing report's participants to avoid duplicates | ||
| participants: existingReport?.participants ?? report.participants, | ||
| }); | ||
| Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, null); | ||
| }; | ||
|
|
||
| if (!navigationRef.isReady()) { | ||
| callback(); | ||
| return; | ||
| } | ||
|
|
||
| // Use Navigation.getActiveRoute() instead of navigationRef.getCurrentRoute()?.path because | ||
| // getCurrentRoute().path can be undefined during first navigation. | ||
| // We still need getCurrentRoute() for params and screen name as getActiveRoute() only returns the path string. | ||
| const activeRoute = Navigation.getActiveRoute(); | ||
| const currentRouteInfo = navigationRef.getCurrentRoute(); | ||
| const backTo = (currentRouteInfo?.params as {backTo?: Route})?.backTo; | ||
| const screenName = currentRouteInfo?.name; | ||
|
|
||
| const isOptimisticReportFocused = activeRoute.includes(`/r/${reportID}`); | ||
|
|
||
| // Fix specific case: https://github.com/Expensify/App/pull/77657#issuecomment-3678696730. | ||
| // When user is editing a money request report (/e/:reportID route) and has | ||
| // an optimistic report in the background that should be replaced with preexisting report | ||
| const isOptimisticReportInBackground = screenName === SCREENS.RIGHT_MODAL.EXPENSE_REPORT && backTo && backTo.includes(`/r/${reportID}`); | ||
|
|
||
| // Only re-route them if they are still looking at the optimistically created report | ||
| if (isOptimisticReportFocused || isOptimisticReportInBackground) { | ||
| const currCallback = callback; | ||
| callback = () => { | ||
| currCallback(); | ||
| if (isOptimisticReportFocused) { | ||
| Navigation.setParams({reportID: preexistingReportID.toString()}); | ||
| } else if (isOptimisticReportInBackground) { | ||
| // Navigate to the correct backTo route with the preexisting report ID | ||
| Navigation.navigate(backTo.replace(`/r/${reportID}`, `/r/${preexistingReportID}`) as Route); | ||
| } | ||
| }; | ||
|
|
||
| // The report screen will listen to this event and transfer the draft comment to the existing report | ||
| // This will allow the newest draft comment to be transferred to the existing report | ||
| DeviceEventEmitter.emit(`switchToPreExistingReport_${reportID}`, { | ||
| preexistingReportID, | ||
| callback, | ||
| }); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| // In case the user is not on the report screen, we will transfer the report draft comment directly to the existing report | ||
| // after that clear the optimistically created report | ||
| if (!draftReportComment) { | ||
| callback(); | ||
| return; | ||
| } | ||
|
|
||
| saveReportDraftComment(preexistingReportID, draftReportComment, callback); | ||
| }); | ||
| } | ||
|
|
||
| // Reports are observed only to detect preexistingReportID and run replacement; no UI subscribes, so connectWithoutView() is used. | ||
| Onyx.connectWithoutView({ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is great, I really love seeing this here. Would you mind adding some pretty verbose comments to explain what the purpose of this is? I don't think I really understand why we have |
||
| key: ONYXKEYS.COLLECTION.REPORT, | ||
| waitForCollectionCallback: true, | ||
| callback: (value: OnyxCollection<Report>) => { | ||
| allReports = value; | ||
|
|
||
| if (!value) { | ||
| return; | ||
| } | ||
|
|
||
| for (const report of Object.values(value)) { | ||
| if (!report) { | ||
| continue; | ||
| } | ||
|
|
||
| replaceOptimisticReportWithActualReport(report, allReportDraftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${report.reportID}`]); | ||
| } | ||
| }, | ||
| }); | ||
|
|
||
| export {replaceOptimisticReportWithActualReport}; | ||
|
|
||
| export default {}; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
move it action folder.