From acaa5ec244cce409fe6ab659a2bcdd309572a3dd Mon Sep 17 00:00:00 2001 From: Chuck Dries Date: Thu, 3 Jul 2025 13:44:29 -0700 Subject: [PATCH 01/13] Sync drafts between devices Sync drafts between devices --- src/ONYXKEYS.ts | 4 ++++ src/libs/API/types.ts | 1 + src/libs/actions/Report.ts | 15 ++++++++++-- .../ComposerWithSuggestions.tsx | 24 +++++++++++++------ src/types/onyx/DraftReportComments.ts | 3 +++ src/types/onyx/index.ts | 2 ++ 6 files changed, 40 insertions(+), 9 deletions(-) create mode 100644 src/types/onyx/DraftReportComments.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index d550a3bf6993..73ebf4e78179 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -553,6 +553,9 @@ const ONYXKEYS = { NVP_LAST_ECASH_ANDROID_LOGIN: 'nvp_lastECashAndroidLogin', NVP_LAST_ANDROID_LOGIN: 'nvp_lastAndroidLogin', + /** Draft report comments */ + NVP_DRAFT_REPORT_COMMENTS: 'nvp_draftReportComments', + /** Collection Keys */ COLLECTION: { DOWNLOAD: 'download_', @@ -1204,6 +1207,7 @@ type OnyxValuesMapping = { [ONYXKEYS.TRANSACTION_THREAD_NAVIGATION_REPORT_IDS]: string[]; [ONYXKEYS.ONBOARDING_USER_REPORTED_INTEGRATION]: OnboardingAccounting; [ONYXKEYS.HYBRID_APP]: OnyxTypes.HybridApp; + [ONYXKEYS.NVP_DRAFT_REPORT_COMMENTS]: OnyxTypes.DraftReportComments; }; type OnyxDerivedValuesMapping = { diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 480e753087fc..8451c06e59b1 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -11,6 +11,7 @@ import type UpdateBeneficialOwnersForBankAccountParams from './parameters/Update type ApiRequestType = ValueOf; const WRITE_COMMANDS = { + SAVE_REPORT_DRAFT_COMMENT: 'SaveReportDraftComment', // CQ TODO CLEAN_POLICY_TAGS: 'ClearPolicyTags', IMPORT_MULTI_LEVEL_TAGS: 'ImportMultiLevelTags', SET_WORKSPACE_AUTO_REPORTING_FREQUENCY: 'SetWorkspaceAutoReportingFrequency', diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 004ad36fda5a..0375a9780bec 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -1751,10 +1751,21 @@ function saveReportDraft(reportID: string, report: Report) { /** * Saves the comment left by the user as they are typing. By saving this data the user can switch between chats, close * tab, refresh etc without worrying about loosing what they typed out. - * When empty string or null is passed, it will delete the draft comment from Onyx store. */ function saveReportDraftComment(reportID: string, comment: string | null, callback: () => void = () => {}) { - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, prepareDraftComment(comment)).then(callback); + API.write( + WRITE_COMMANDS.SAVE_REPORT_DRAFT_COMMENT, + { + reportID, + // comment is quoted to intentionally preserve whitespace, otherwise it will be trimmed by the WAF _and_ Auth's SParseHTTP function + comment: `"${comment}"`, + }, + {optimisticData: [{onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, value: comment}]}, + { + checkAndFixConflictingRequest: (persistedRequests) => + resolveDuplicationConflictAction(persistedRequests, (request) => request.command === WRITE_COMMANDS.SAVE_REPORT_DRAFT_COMMENT && request.data?.reportID === reportID), + }, + ).then(callback); } /** Broadcasts whether or not a user is typing on a report over the report's private pusher channel. */ diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 081e610b7907..beff200b659e 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -248,14 +248,21 @@ function ComposerWithSuggestions( const mobileInputScrollPosition = useRef(0); const cursorPositionValue = useSharedValue({x: 0, y: 0}); const tag = useSharedValue(-1); - const draftComment = getDraftComment(reportID) ?? ''; + const [draftComment] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, {canBeMissing: true}); + const [draftReportComments] = useOnyx(ONYXKEYS.NVP_DRAFT_REPORT_COMMENTS, {canBeMissing: true, selector: (draftReportComments) => draftReportComments?.[reportID]}); + console.log('draftReportComments', draftReportComments); + console.log('draftComment', draftComment); const [value, setValue] = useState(() => { - if (draftComment) { - emojisPresentBefore.current = extractEmojis(draftComment); + if (draftReportComments) { + emojisPresentBefore.current = extractEmojis(draftReportComments); } - return draftComment; + return draftReportComments ?? ''; }); + useEffect(() => { + setValue(draftReportComments ?? ''); + }, [draftReportComments]); + const commentRef = useRef(value); const [modal] = useOnyx(ONYXKEYS.MODAL, {canBeMissing: true}); @@ -417,6 +424,7 @@ function ComposerWithSuggestions( } commentRef.current = newCommentConverted; + if (shouldDebounceSaveComment) { isCommentPendingSaved.current = true; debouncedSaveReportComment(reportID, newCommentConverted); @@ -726,7 +734,7 @@ function ComposerWithSuggestions( mobileInputScrollPosition.current = 0; // Note: use the value when the clear happened, not the current value which might have changed already onCleared(text); - updateComment('', true); + updateComment('', false); }, [onCleared, updateComment], ); @@ -844,7 +852,9 @@ function ComposerWithSuggestions( resetKeyboardInput={resetKeyboardInput} /> - {isValidReportIDFromPath(reportID) && ( + {/* CQ TODO do we actually need this? */} + + {/*{isValidReportIDFromPath(reportID) && ( - )} + )}*/} {/* Only used for testing so far */} {children} diff --git a/src/types/onyx/DraftReportComments.ts b/src/types/onyx/DraftReportComments.ts new file mode 100644 index 000000000000..e1ab82ab03a6 --- /dev/null +++ b/src/types/onyx/DraftReportComments.ts @@ -0,0 +1,3 @@ +type DraftReportComments = Record; + +export default DraftReportComments; \ No newline at end of file diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 1fb8fbb5f6f4..b0f235ad57d6 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -119,6 +119,7 @@ import type WalletOnfido from './WalletOnfido'; import type WalletStatement from './WalletStatement'; import type WalletTerms from './WalletTerms'; import type WalletTransfer from './WalletTransfer'; +import type DraftReportComments from './DraftReportComments'; export type { TryNewDot, @@ -264,4 +265,5 @@ export type { ValidateUserAndGetAccessiblePolicies, BillingReceiptDetails, HybridApp, + DraftReportComments, }; From 71eccaa4c8bd296c3ca504e6bc4f174484521e68 Mon Sep 17 00:00:00 2001 From: Chuck Dries Date: Wed, 9 Jul 2025 16:01:40 -0700 Subject: [PATCH 02/13] Migrate drafts from collection key to NVP key; also use for LHN Restore SilentCommentUpdater update import --- .../LHNOptionsList/LHNOptionsList.tsx | 6 +-- src/hooks/useSidebarOrderedReports.tsx | 10 +++-- src/libs/DraftCommentUtils.ts | 9 +++-- src/libs/ReportUtils.ts | 3 +- src/libs/SidebarUtils.ts | 14 +++++-- src/libs/actions/Report.ts | 5 ++- src/libs/migrateOnyx.ts | 2 + src/libs/migrations/MoveDraftsToNVP.ts | 37 +++++++++++++++++++ .../ComposerWithSuggestions.tsx | 22 ++++------- .../ReportActionCompose.tsx | 1 + src/types/onyx/DraftReportComments.ts | 3 ++ 11 files changed, 79 insertions(+), 33 deletions(-) create mode 100644 src/libs/migrations/MoveDraftsToNVP.ts diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index 6d52ab979b31..55d7573500b1 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -53,7 +53,7 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio const [policy] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: false}); const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: true}); const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {canBeMissing: false}); - const [draftComments] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, {canBeMissing: false}); + const [draftComments] = useOnyx(ONYXKEYS.NVP_DRAFT_REPORT_COMMENTS, {canBeMissing: true}); const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: false}); const [dismissedProductTraining, dismissedProductTrainingMetadata] = useOnyx(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING, {canBeMissing: true}); const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); @@ -189,7 +189,7 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio ? (getOriginalMessage(itemParentReportAction)?.IOUTransactionID ?? CONST.DEFAULT_NUMBER_ID) : CONST.DEFAULT_NUMBER_ID; const itemTransaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; - const hasDraftComment = isValidDraftComment(draftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`]); + const hasDraftComment = isValidDraftComment(draftComments?.[reportID]); const canUserPerformWrite = canUserPerformWriteAction(item); const sortedReportActions = getSortedReportActionsForDisplay(itemReportActions, canUserPerformWrite); @@ -284,7 +284,6 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio policy, personalDetails, data.length, - draftComments, optionMode, preferredLocale, transactions, @@ -301,7 +300,6 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio policy, personalDetails, data.length, - draftComments, optionMode, preferredLocale, transactions, diff --git a/src/hooks/useSidebarOrderedReports.tsx b/src/hooks/useSidebarOrderedReports.tsx index ed55aacb5918..c857c0b781da 100644 --- a/src/hooks/useSidebarOrderedReports.tsx +++ b/src/hooks/useSidebarOrderedReports.tsx @@ -61,7 +61,7 @@ function SidebarOrderedReportsContextProvider({ const [transactions, {sourceValue: transactionsUpdates}] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {canBeMissing: true}); const [transactionViolations, {sourceValue: transactionViolationsUpdates}] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); const [reportNameValuePairs, {sourceValue: reportNameValuePairsUpdates}] = useOnyx(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS, {canBeMissing: true}); - const [, {sourceValue: reportsDraftsUpdates}] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, {canBeMissing: true}); + const [reportsDraftsUpdates = {}] = useOnyx(ONYXKEYS.NVP_DRAFT_REPORT_COMMENTS, {canBeMissing: true}); const [betas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true}); const [reportAttributes] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, {selector: (value) => value?.reports, canBeMissing: true}); const [currentReportsToDisplay, setCurrentReportsToDisplay] = useState({}); @@ -95,7 +95,7 @@ function SidebarOrderedReportsContextProvider({ .map((key) => key.replace(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, ONYXKEYS.COLLECTION.TRANSACTION)) .map((key) => `${ONYXKEYS.COLLECTION.REPORT}${transactions?.[key]?.reportID}`); } else if (reportsDraftsUpdates) { - reportsToUpdate = Object.keys(reportsDraftsUpdates).map((key) => key.replace(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, ONYXKEYS.COLLECTION.REPORT)); + reportsToUpdate = Object.keys(reportsDraftsUpdates).map((key) => `${ONYXKEYS.COLLECTION.REPORT}${key}`); } else if (policiesUpdates) { const updatedPolicies = Object.keys(policiesUpdates).map((key) => key.replace(ONYXKEYS.COLLECTION.POLICY, '')); reportsToUpdate = Object.entries(chatReports ?? {}) @@ -150,6 +150,7 @@ function SidebarOrderedReportsContextProvider({ transactionViolations, reportNameValuePairs, reportAttributes, + reportsDraftsUpdates, ); } else { reportsToDisplay = SidebarUtils.getReportsToDisplayInLHN( @@ -161,19 +162,20 @@ function SidebarOrderedReportsContextProvider({ transactionViolations, reportNameValuePairs, reportAttributes, + reportsDraftsUpdates, ); } return reportsToDisplay; // Rule disabled intentionally — triggering a re-render on currentReportsToDisplay would cause an infinite loop // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, [getUpdatedReports, chatReports, derivedCurrentReportID, priorityMode, betas, policies, transactionViolations, reportNameValuePairs, reportAttributes]); + }, [getUpdatedReports, chatReports, derivedCurrentReportID, priorityMode, betas, policies, transactionViolations, reportNameValuePairs, reportAttributes, reportsDraftsUpdates]); useEffect(() => { setCurrentReportsToDisplay(reportsToDisplayInLHN); }, [reportsToDisplayInLHN]); const getOrderedReportIDs = useCallback( - () => SidebarUtils.sortReportsToDisplayInLHN(reportsToDisplayInLHN, priorityMode, reportNameValuePairs, reportAttributes), + () => SidebarUtils.sortReportsToDisplayInLHN(reportsToDisplayInLHN, priorityMode, reportNameValuePairs, reportAttributes, reportsDraftsUpdates), // Rule disabled intentionally - reports should be sorted only when the reportsToDisplayInLHN changes // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps [reportsToDisplayInLHN], diff --git a/src/libs/DraftCommentUtils.ts b/src/libs/DraftCommentUtils.ts index b3cb32498725..a04717767c8c 100644 --- a/src/libs/DraftCommentUtils.ts +++ b/src/libs/DraftCommentUtils.ts @@ -1,14 +1,14 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; +import { DraftReportComments } from '@src/types/onyx'; -let draftCommentCollection: OnyxCollection = {}; +let draftCommentCollection: OnyxEntry = {}; Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, + key: ONYXKEYS.NVP_DRAFT_REPORT_COMMENTS, callback: (nextVal) => { draftCommentCollection = nextVal; }, - waitForCollectionCallback: true, }); /** @@ -17,7 +17,7 @@ Onyx.connect({ * A valid use-case of this function is outside React components, like in utility functions. */ function getDraftComment(reportID: string): OnyxEntry | null | undefined { - return draftCommentCollection?.[ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT + reportID]; + return draftCommentCollection?.[reportID]; } /** @@ -30,6 +30,7 @@ function isValidDraftComment(comment?: string | null): boolean { /** * Returns true if the report has a valid draft comment. + * @deprecated prefer useOnyx to access when possible */ function hasValidDraftComment(reportID: string): boolean { return isValidDraftComment(getDraftComment(reportID)); diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index f299c252947b..bbf1f3e77c49 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -29,6 +29,7 @@ import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; import type { Beta, + DraftReportComments, IntroSelected, NewGroupChatDraft, OnyxInputOrEntry, @@ -80,7 +81,7 @@ import type {OnboardingCompanySize, OnboardingMessage, OnboardingPurpose, Onboar import type {AddCommentOrAttachmentParams} from './API/parameters'; import {convertToDisplayString} from './CurrencyUtils'; import DateUtils from './DateUtils'; -import {hasValidDraftComment} from './DraftCommentUtils'; +import {hasValidDraftComment, isValidDraftComment} from './DraftCommentUtils'; import {getEnvironment, getEnvironmentURL} from './Environment/Environment'; import type EnvironmentType from './Environment/getEnvironment/types'; import {getMicroSecondOnyxErrorWithTranslationKey, isReceiptError} from './ErrorUtils'; diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 3fa05841c51a..2170f50ad10b 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -5,7 +5,7 @@ import type {ValueOf} from 'type-fest'; import type {PartialPolicyForSidebar, ReportsToDisplayInLHN} from '@hooks/useSidebarOrderedReports'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PersonalDetails, PersonalDetailsList, ReportActions, ReportAttributesDerivedValue, ReportNameValuePairs, Transaction, TransactionViolation} from '@src/types/onyx'; +import type {DraftReportComments, PersonalDetails, PersonalDetailsList, ReportActions, ReportAttributesDerivedValue, ReportNameValuePairs, Transaction, TransactionViolation} from '@src/types/onyx'; import type Beta from '@src/types/onyx/Beta'; import type {ReportAttributes} from '@src/types/onyx/DerivedValues'; import type {Errors} from '@src/types/onyx/OnyxCommon'; @@ -15,7 +15,7 @@ import type Report from '@src/types/onyx/Report'; import type ReportAction from '@src/types/onyx/ReportAction'; import {getExpensifyCardFromReportAction} from './CardMessageUtils'; import {extractCollectionItemID} from './CollectionUtils'; -import {hasValidDraftComment} from './DraftCommentUtils'; +import {isValidDraftComment} from './DraftCommentUtils'; import localeCompare from './LocaleCompare'; import {translateLocal} from './Localize'; import {getLastActorDisplayName, getLastMessageTextForReport, getPersonalDetailsForAccountIDs, shouldShowLastActorDisplayName} from './OptionsListUtils'; @@ -207,6 +207,7 @@ function shouldDisplayReportInLHN( transactionViolations: OnyxCollection, reportNameValuePairs?: OnyxCollection, reportAttributes?: ReportAttributesDerivedValue['reports'], + draftReportComments?: DraftReportComments, ) { if (!report) { return {shouldDisplay: false}; @@ -238,7 +239,7 @@ function shouldDisplayReportInLHN( const isSystemChat = isSystemChatUtil(report); const isReportArchived = isArchivedReport(reportNameValuePairs); const shouldOverrideHidden = - hasValidDraftComment(report.reportID) || hasErrorsOtherThanFailedReceipt || isFocused || isSystemChat || !!report.isPinned || reportAttributes?.[report?.reportID]?.requiresAttention; + isValidDraftComment(draftReportComments?.[report.reportID]) || hasErrorsOtherThanFailedReceipt || isFocused || isSystemChat || !!report.isPinned || reportAttributes?.[report?.reportID]?.requiresAttention; if (isHidden && !shouldOverrideHidden) { return {shouldDisplay: false}; @@ -269,6 +270,7 @@ function getReportsToDisplayInLHN( transactionViolations: OnyxCollection, reportNameValuePairs?: OnyxCollection, reportAttributes?: ReportAttributesDerivedValue['reports'], + draftReportComments?: DraftReportComments, ) { const isInFocusMode = priorityMode === CONST.PRIORITY_MODE.GSD; const allReportsDictValues = reports ?? {}; @@ -288,6 +290,7 @@ function getReportsToDisplayInLHN( transactionViolations, reportNameValuePairs, reportAttributes, + draftReportComments, ); if (shouldDisplay) { @@ -309,6 +312,7 @@ function updateReportsToDisplayInLHN( transactionViolations: OnyxCollection, reportNameValuePairs?: OnyxCollection, reportAttributes?: ReportAttributesDerivedValue['reports'], + draftReportComments?: DraftReportComments, ) { const displayedReportsCopy = {...displayedReports}; updatedReportsKeys.forEach((reportID) => { @@ -326,6 +330,7 @@ function updateReportsToDisplayInLHN( transactionViolations, reportNameValuePairs, reportAttributes, + draftReportComments, ); if (shouldDisplay) { @@ -346,6 +351,7 @@ function sortReportsToDisplayInLHN( priorityMode: OnyxEntry, reportNameValuePairs?: OnyxCollection, reportAttributes?: ReportAttributesDerivedValue['reports'], + draftReportComments?: DraftReportComments, ): string[] { Performance.markStart(CONST.TIMING.GET_ORDERED_REPORT_IDS); const isInFocusMode = priorityMode === CONST.PRIORITY_MODE.GSD; @@ -382,7 +388,7 @@ function sortReportsToDisplayInLHN( pinnedAndGBRReports.push(miniReport); } else if (report?.hasErrorsOtherThanFailedReceipt) { errorReports.push(miniReport); - } else if (hasValidDraftComment(report?.reportID)) { + } else if (isValidDraftComment(draftReportComments?.[report?.reportID])) { draftReports.push(miniReport); } else if (isArchivedNonExpenseReport(report, !!rNVPs?.private_isArchived)) { archivedReports.push(miniReport); diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 0375a9780bec..ed1d255d081e 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -1757,10 +1757,11 @@ function saveReportDraftComment(reportID: string, comment: string | null, callba WRITE_COMMANDS.SAVE_REPORT_DRAFT_COMMENT, { reportID, - // comment is quoted to intentionally preserve whitespace, otherwise it will be trimmed by the WAF _and_ Auth's SParseHTTP function + // comment is quoted to intentionally preserve trailing whitespace as the user types + // otherwise it will be trimmed by the WAF _and_ Auth's SParseHTTP function comment: `"${comment}"`, }, - {optimisticData: [{onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, value: comment}]}, + {optimisticData: [{onyxMethod: Onyx.METHOD.SET, key: ONYXKEYS.NVP_DRAFT_REPORT_COMMENTS, value: {[reportID]: prepareDraftComment(comment)}}]}, { checkAndFixConflictingRequest: (persistedRequests) => resolveDuplicationConflictAction(persistedRequests, (request) => request.command === WRITE_COMMANDS.SAVE_REPORT_DRAFT_COMMENT && request.data?.reportID === reportID), diff --git a/src/libs/migrateOnyx.ts b/src/libs/migrateOnyx.ts index f0a9ec9db977..18cb6ca9195b 100644 --- a/src/libs/migrateOnyx.ts +++ b/src/libs/migrateOnyx.ts @@ -8,6 +8,7 @@ import RemoveEmptyReportActionsDrafts from './migrations/RemoveEmptyReportAction import RenameCardIsVirtual from './migrations/RenameCardIsVirtual'; import RenameReceiptFilename from './migrations/RenameReceiptFilename'; import TransactionBackupsToCollection from './migrations/TransactionBackupsToCollection'; +import MoveDraftsToNVP from './migrations/MoveDraftsToNVP'; export default function () { const startTime = Date.now(); @@ -25,6 +26,7 @@ export default function () { PronounsMigration, MoveIsOptimisticReportToMetadata, PendingMembersToMetadata, + MoveDraftsToNVP, ]; // Reduce all promises down to a single promise. All promises run in a linear fashion, waiting for the diff --git a/src/libs/migrations/MoveDraftsToNVP.ts b/src/libs/migrations/MoveDraftsToNVP.ts new file mode 100644 index 000000000000..5139a4a3e680 --- /dev/null +++ b/src/libs/migrations/MoveDraftsToNVP.ts @@ -0,0 +1,37 @@ +import Onyx from 'react-native-onyx'; +import type {OnyxCollection, OnyxKey} from 'react-native-onyx'; +import Log from '@libs/Log'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {DraftReportComments} from '@src/types/onyx'; + +// moves individual drafts from `${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}` to ONYXKEYS.NVP_DRAFT_REPORT_COMMENTS +export default function (): Promise { + return new Promise((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, + waitForCollectionCallback: true, + callback: (drafts: OnyxCollection) => { + Onyx.disconnect(connection); + + if (!drafts) { + Log.info('[Migrate Onyx] Skipped migration MoveDraftsToNVP because there were no drafts'); + return resolve(); + } + + const newDrafts: DraftReportComments = {}; + Object.entries(drafts).forEach(([reportOnyxKey, draft]) => { + if (!draft) { + return; + } + newDrafts[reportOnyxKey.replace(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, '')] = draft; + // eslint-disable-next-line rulesdir/prefer-actions-set-data + Onyx.set(reportOnyxKey as OnyxKey, null); + }); + + // eslint-disable-next-line rulesdir/prefer-actions-set-data + Onyx.set(ONYXKEYS.NVP_DRAFT_REPORT_COMMENTS, newDrafts); + resolve(); + }, + }); + }); +} diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index beff200b659e..8ca4dab5744f 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -34,7 +34,6 @@ import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; import {forceClearInput} from '@libs/ComponentUtils'; import {canSkipTriggerHotkeys, findCommonSuffixLength, insertText, insertWhiteSpaceAtIndex} from '@libs/ComposerUtils'; import convertToLTRForComposer from '@libs/convertToLTRForComposer'; -import {getDraftComment} from '@libs/DraftCommentUtils'; import {containsOnlyEmojis, extractEmojis, getAddedEmojis, getPreferredSkinToneIndex, replaceAndExtractEmojis} from '@libs/EmojiUtils'; import focusComposerWithDelay from '@libs/focusComposerWithDelay'; import getPlatform from '@libs/getPlatform'; @@ -248,20 +247,17 @@ function ComposerWithSuggestions( const mobileInputScrollPosition = useRef(0); const cursorPositionValue = useSharedValue({x: 0, y: 0}); const tag = useSharedValue(-1); - const [draftComment] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, {canBeMissing: true}); - const [draftReportComments] = useOnyx(ONYXKEYS.NVP_DRAFT_REPORT_COMMENTS, {canBeMissing: true, selector: (draftReportComments) => draftReportComments?.[reportID]}); - console.log('draftReportComments', draftReportComments); - console.log('draftComment', draftComment); + const [draftReportComment] = useOnyx(ONYXKEYS.NVP_DRAFT_REPORT_COMMENTS, {canBeMissing: true, selector: (draftReportComments) => draftReportComments?.[reportID]}); const [value, setValue] = useState(() => { - if (draftReportComments) { - emojisPresentBefore.current = extractEmojis(draftReportComments); + if (draftReportComment) { + emojisPresentBefore.current = extractEmojis(draftReportComment); } - return draftReportComments ?? ''; + return draftReportComment ?? ''; }); useEffect(() => { - setValue(draftReportComments ?? ''); - }, [draftReportComments]); + setValue(draftReportComment ?? ''); + }, [draftReportComment]); const commentRef = useRef(value); @@ -852,9 +848,7 @@ function ComposerWithSuggestions( resetKeyboardInput={resetKeyboardInput} /> - {/* CQ TODO do we actually need this? */} - - {/*{isValidReportIDFromPath(reportID) && ( + {isValidReportIDFromPath(reportID) && ( - )}*/} + )} {/* Only used for testing so far */} {children} diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index aae3713d7431..8903ab14ccaf 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -180,6 +180,7 @@ function ReportActionCompose({ debouncedLowerIsScrollLikelyLayoutTriggered(); }, [debouncedLowerIsScrollLikelyLayoutTriggered]); + // CQ TODO is this still necessary? const [isCommentEmpty, setIsCommentEmpty] = useState(() => { const draftComment = getDraftComment(reportID); return !draftComment || !!draftComment.match(CONST.REGEX.EMPTY_COMMENT); diff --git a/src/types/onyx/DraftReportComments.ts b/src/types/onyx/DraftReportComments.ts index e1ab82ab03a6..f139af0341af 100644 --- a/src/types/onyx/DraftReportComments.ts +++ b/src/types/onyx/DraftReportComments.ts @@ -1,3 +1,6 @@ +/** + * Map of reportID => comment + */ type DraftReportComments = Record; export default DraftReportComments; \ No newline at end of file From 7b009f74c159e87c0e0571aac59cfa1490c215cf Mon Sep 17 00:00:00 2001 From: Chuck Dries Date: Thu, 10 Jul 2025 09:11:41 -0700 Subject: [PATCH 03/13] Fix draft optimistic update clobbering other drafts --- src/libs/actions/Report.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index ed1d255d081e..53a7e224cddd 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -1761,7 +1761,7 @@ function saveReportDraftComment(reportID: string, comment: string | null, callba // otherwise it will be trimmed by the WAF _and_ Auth's SParseHTTP function comment: `"${comment}"`, }, - {optimisticData: [{onyxMethod: Onyx.METHOD.SET, key: ONYXKEYS.NVP_DRAFT_REPORT_COMMENTS, value: {[reportID]: prepareDraftComment(comment)}}]}, + {optimisticData: [{onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.NVP_DRAFT_REPORT_COMMENTS, value: {[reportID]: prepareDraftComment(comment)}}]}, { checkAndFixConflictingRequest: (persistedRequests) => resolveDuplicationConflictAction(persistedRequests, (request) => request.command === WRITE_COMMANDS.SAVE_REPORT_DRAFT_COMMENT && request.data?.reportID === reportID), From f8fe085d1a3e34eaab419c8da644f97575f6dff5 Mon Sep 17 00:00:00 2001 From: Chuck Dries Date: Fri, 11 Jul 2025 09:01:06 -0700 Subject: [PATCH 04/13] Clean up NVP drafts in local policy deletion cascades --- src/libs/actions/IOU.ts | 4 +++- src/libs/actions/Policy/Category.ts | 6 ++++-- src/libs/actions/Policy/DistanceRate.ts | 6 ++++-- src/libs/actions/Policy/Member.ts | 6 ++++-- src/libs/actions/Policy/PerDiem.ts | 6 ++++-- src/libs/actions/Policy/Policy.ts | 6 ++++-- src/libs/actions/Policy/ReportField.ts | 6 ++++-- src/libs/actions/Policy/Tag.ts | 7 +++---- src/libs/actions/Report.ts | 14 ++++++++------ 9 files changed, 38 insertions(+), 23 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 0829bc535080..fa386c99292a 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -752,16 +752,18 @@ Onyx.connect({ const policyReports = getAllPolicyReports(policyID); const cleanUpMergeQueries: Record<`${typeof ONYXKEYS.COLLECTION.REPORT}${string}`, NullishDeep> = {}; const cleanUpSetQueries: Record<`${typeof ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${string}` | `${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${string}`, null> = {}; + const cleanUpDrafts: Record = {}; policyReports.forEach((policyReport) => { if (!policyReport) { return; } const {reportID} = policyReport; - cleanUpSetQueries[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`] = null; + cleanUpDrafts[reportID] = null; cleanUpSetQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${reportID}`] = null; }); Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT, cleanUpMergeQueries); Onyx.multiSet(cleanUpSetQueries); + Onyx.merge(ONYXKEYS.NVP_DRAFT_REPORT_COMMENTS, cleanUpDrafts); delete allPolicies[key]; return; } diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index ea506ab923c3..232645ee2936 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -52,17 +52,19 @@ Onyx.connect({ const policyID = key.replace(ONYXKEYS.COLLECTION.POLICY, ''); const policyReports = getAllPolicyReports(policyID); const cleanUpMergeQueries: Record<`${typeof ONYXKEYS.COLLECTION.REPORT}${string}`, NullishDeep> = {}; - const cleanUpSetQueries: Record<`${typeof ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${string}` | `${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${string}`, null> = {}; + const cleanUpSetQueries: Record<`${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${string}`, null> = {}; + const cleanUpDrafts: Record = {}; policyReports.forEach((policyReport) => { if (!policyReport) { return; } const {reportID} = policyReport; - cleanUpSetQueries[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`] = null; + cleanUpDrafts[reportID] = null; cleanUpSetQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${reportID}`] = null; }); Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT, cleanUpMergeQueries); Onyx.multiSet(cleanUpSetQueries); + Onyx.merge(ONYXKEYS.NVP_DRAFT_REPORT_COMMENTS, cleanUpDrafts); delete allPolicies[key]; return; } diff --git a/src/libs/actions/Policy/DistanceRate.ts b/src/libs/actions/Policy/DistanceRate.ts index 21abeeec3c99..466abf0300e4 100644 --- a/src/libs/actions/Policy/DistanceRate.ts +++ b/src/libs/actions/Policy/DistanceRate.ts @@ -36,17 +36,19 @@ Onyx.connect({ const policyID = key.replace(ONYXKEYS.COLLECTION.POLICY, ''); const policyReports = ReportUtils.getAllPolicyReports(policyID); const cleanUpMergeQueries: Record<`${typeof ONYXKEYS.COLLECTION.REPORT}${string}`, NullishDeep> = {}; - const cleanUpSetQueries: Record<`${typeof ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${string}` | `${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${string}`, null> = {}; + const cleanUpSetQueries: Record<`${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${string}`, null> = {}; + const cleanUpDrafts: Record = {}; policyReports.forEach((policyReport) => { if (!policyReport) { return; } const {reportID} = policyReport; - cleanUpSetQueries[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`] = null; + cleanUpDrafts[reportID] = null; cleanUpSetQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${reportID}`] = null; }); Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT, cleanUpMergeQueries); Onyx.multiSet(cleanUpSetQueries); + Onyx.merge(ONYXKEYS.NVP_DRAFT_REPORT_COMMENTS, cleanUpDrafts); delete allPolicies[key]; return; } diff --git a/src/libs/actions/Policy/Member.ts b/src/libs/actions/Policy/Member.ts index fc4c83cb92b5..4a7ce7573db3 100644 --- a/src/libs/actions/Policy/Member.ts +++ b/src/libs/actions/Policy/Member.ts @@ -62,17 +62,19 @@ Onyx.connect({ const policyID = key.replace(ONYXKEYS.COLLECTION.POLICY, ''); const policyReports = ReportUtils.getAllPolicyReports(policyID); const cleanUpMergeQueries: Record<`${typeof ONYXKEYS.COLLECTION.REPORT}${string}`, NullishDeep> = {}; - const cleanUpSetQueries: Record<`${typeof ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${string}` | `${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${string}`, null> = {}; + const cleanUpSetQueries: Record<`${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${string}`, null> = {}; + const cleanUpDrafts: Record = {}; policyReports.forEach((policyReport) => { if (!policyReport) { return; } const {reportID} = policyReport; - cleanUpSetQueries[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`] = null; + cleanUpDrafts[reportID] = null; cleanUpSetQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${reportID}`] = null; }); Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT, cleanUpMergeQueries); Onyx.multiSet(cleanUpSetQueries); + Onyx.merge(ONYXKEYS.NVP_DRAFT_REPORT_COMMENTS, cleanUpDrafts); delete allPolicies[key]; return; } diff --git a/src/libs/actions/Policy/PerDiem.ts b/src/libs/actions/Policy/PerDiem.ts index c6994c24cbb5..e969782a431a 100644 --- a/src/libs/actions/Policy/PerDiem.ts +++ b/src/libs/actions/Policy/PerDiem.ts @@ -34,17 +34,19 @@ Onyx.connect({ const policyID = key.replace(ONYXKEYS.COLLECTION.POLICY, ''); const policyReports = ReportUtils.getAllPolicyReports(policyID); const cleanUpMergeQueries: Record<`${typeof ONYXKEYS.COLLECTION.REPORT}${string}`, NullishDeep> = {}; - const cleanUpSetQueries: Record<`${typeof ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${string}` | `${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${string}`, null> = {}; + const cleanUpSetQueries: Record<`${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${string}`, null> = {}; + const cleanUpDrafts: Record = {}; policyReports.forEach((policyReport) => { if (!policyReport) { return; } const {reportID} = policyReport; - cleanUpSetQueries[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`] = null; + cleanUpDrafts[reportID] = null; cleanUpSetQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${reportID}`] = null; }); Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT, cleanUpMergeQueries); Onyx.multiSet(cleanUpSetQueries); + Onyx.merge(ONYXKEYS.NVP_DRAFT_REPORT_COMMENTS, cleanUpDrafts); delete allPolicies[key]; return; } diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index e9cb1e8de9e3..54adade0f799 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -168,17 +168,19 @@ Onyx.connect({ const policyID = key.replace(ONYXKEYS.COLLECTION.POLICY, ''); const policyReports = ReportUtils.getAllPolicyReports(policyID); const cleanUpMergeQueries: Record<`${typeof ONYXKEYS.COLLECTION.REPORT}${string}`, NullishDeep> = {}; - const cleanUpSetQueries: Record<`${typeof ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${string}` | `${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${string}`, null> = {}; + const cleanUpDrafts: Record = {}; + const cleanUpSetQueries: Record<`${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${string}`, null> = {}; policyReports.forEach((policyReport) => { if (!policyReport) { return; } const {reportID} = policyReport; - cleanUpSetQueries[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`] = null; + cleanUpDrafts[reportID] = null; cleanUpSetQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${reportID}`] = null; }); Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT, cleanUpMergeQueries); Onyx.multiSet(cleanUpSetQueries); + Onyx.merge(ONYXKEYS.NVP_DRAFT_REPORT_COMMENTS, cleanUpDrafts); delete allPolicies[key]; return; } diff --git a/src/libs/actions/Policy/ReportField.ts b/src/libs/actions/Policy/ReportField.ts index 85a25754b7e1..f08280c5ae97 100644 --- a/src/libs/actions/Policy/ReportField.ts +++ b/src/libs/actions/Policy/ReportField.ts @@ -61,17 +61,19 @@ Onyx.connect({ const policyID = key.replace(ONYXKEYS.COLLECTION.POLICY, ''); const policyReports = ReportUtils.getAllPolicyReports(policyID); const cleanUpMergeQueries: Record<`${typeof ONYXKEYS.COLLECTION.REPORT}${string}`, NullishDeep> = {}; - const cleanUpSetQueries: Record<`${typeof ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${string}` | `${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${string}`, null> = {}; + const cleanUpDrafts: Record = {}; + const cleanUpSetQueries: Record<`${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${string}`, null> = {}; policyReports.forEach((policyReport) => { if (!policyReport) { return; } const {reportID} = policyReport; - cleanUpSetQueries[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`] = null; + cleanUpDrafts[reportID] = null; cleanUpSetQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${reportID}`] = null; }); Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT, cleanUpMergeQueries); Onyx.multiSet(cleanUpSetQueries); + Onyx.merge(ONYXKEYS.NVP_DRAFT_REPORT_COMMENTS, cleanUpDrafts); delete allPolicies[key]; return; } diff --git a/src/libs/actions/Policy/Tag.ts b/src/libs/actions/Policy/Tag.ts index 2f98884b9cfd..4fbaaec634b0 100644 --- a/src/libs/actions/Policy/Tag.ts +++ b/src/libs/actions/Policy/Tag.ts @@ -49,17 +49,16 @@ Onyx.connect({ const policyID = key.replace(ONYXKEYS.COLLECTION.POLICY, ''); const policyReports = ReportUtils.getAllPolicyReports(policyID); const cleanUpMergeQueries: Record<`${typeof ONYXKEYS.COLLECTION.REPORT}${string}`, NullishDeep> = {}; - const cleanUpSetQueries: Record<`${typeof ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${string}` | `${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${string}`, null> = {}; + const cleanUpDrafts: Record = {}; policyReports.forEach((policyReport) => { if (!policyReport) { return; } const {reportID} = policyReport; - cleanUpSetQueries[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`] = null; - cleanUpSetQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${reportID}`] = null; + cleanUpDrafts[reportID] = null; }); Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT, cleanUpMergeQueries); - Onyx.multiSet(cleanUpSetQueries); + Onyx.merge(ONYXKEYS.NVP_DRAFT_REPORT_COMMENTS, cleanUpDrafts); delete allPolicies[key]; return; } diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 53a7e224cddd..2f6e5790605c 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -169,6 +169,7 @@ import INPUT_IDS from '@src/types/form/NewRoomForm'; import type { Account, DismissedProductTraining, + DraftReportComments, IntroSelected, InvitedEmailsToAccountIDs, NewGroupChatDraft, @@ -397,10 +398,9 @@ Onyx.connect({ callback: (val) => (introSelected = val), }); -let allReportDraftComments: Record = {}; +let allReportDraftComments: OnyxEntry = {}; Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, - waitForCollectionCallback: true, + key: ONYXKEYS.NVP_DRAFT_REPORT_COMMENTS, callback: (value) => (allReportDraftComments = value), }); @@ -424,15 +424,17 @@ Onyx.connect({ const policyID = key.replace(ONYXKEYS.COLLECTION.POLICY, ''); const policyReports = getAllPolicyReports(policyID); const cleanUpSetQueries: Record<`${typeof ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${string}` | `${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${string}`, null> = {}; + const cleanUpDrafts: Record = {}; policyReports.forEach((policyReport) => { if (!policyReport) { return; } const {reportID} = policyReport; - cleanUpSetQueries[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`] = null; + cleanUpDrafts[reportID] = null; cleanUpSetQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${reportID}`] = null; }); Onyx.multiSet(cleanUpSetQueries); + Onyx.merge(ONYXKEYS.NVP_DRAFT_REPORT_COMMENTS, cleanUpDrafts); delete allPolicies[key]; return; } @@ -1819,7 +1821,7 @@ function handleReportChanged(report: OnyxEntry) { // Replacing the existing report's participants to avoid duplicates participants: existingReport?.participants ?? report.participants, }); - Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, null); + Onyx.merge(ONYXKEYS.NVP_DRAFT_REPORT_COMMENTS, {[reportID]: null}); }; // Only re-route them if they are still looking at the optimistically created report if (Navigation.getActiveRoute().includes(`/r/${reportID}`)) { @@ -1841,7 +1843,7 @@ function handleReportChanged(report: OnyxEntry) { // 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 - const draftReportComment = allReportDraftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`]; + const draftReportComment = allReportDraftComments?.[reportID]; if (!draftReportComment) { callback(); return; From 7c6fceb017eb6ffa117773b54aca7917f53dd204 Mon Sep 17 00:00:00 2001 From: Chuck Dries Date: Fri, 11 Jul 2025 09:14:49 -0700 Subject: [PATCH 05/13] Fix lint and style and types --- src/hooks/useSidebarOrderedReports.tsx | 1 - .../SaveReportDraftCommentParams.ts | 6 ++++++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 3 ++- src/libs/DraftCommentUtils.ts | 6 +++--- src/libs/ReportUtils.ts | 3 +-- src/libs/SidebarUtils.ts | 19 ++++++++++++++++--- src/libs/migrateOnyx.ts | 2 +- .../ReportActionCompose.tsx | 1 - src/types/onyx/DraftReportComments.ts | 2 +- src/types/onyx/index.ts | 2 +- tests/unit/DebugUtilsTest.ts | 2 +- 12 files changed, 33 insertions(+), 15 deletions(-) create mode 100644 src/libs/API/parameters/SaveReportDraftCommentParams.ts diff --git a/src/hooks/useSidebarOrderedReports.tsx b/src/hooks/useSidebarOrderedReports.tsx index c857c0b781da..342a53fd8ff1 100644 --- a/src/hooks/useSidebarOrderedReports.tsx +++ b/src/hooks/useSidebarOrderedReports.tsx @@ -146,7 +146,6 @@ function SidebarOrderedReportsContextProvider({ derivedCurrentReportID, priorityMode === CONST.PRIORITY_MODE.GSD, betas, - policies, transactionViolations, reportNameValuePairs, reportAttributes, diff --git a/src/libs/API/parameters/SaveReportDraftCommentParams.ts b/src/libs/API/parameters/SaveReportDraftCommentParams.ts new file mode 100644 index 000000000000..6b89e5c3ac05 --- /dev/null +++ b/src/libs/API/parameters/SaveReportDraftCommentParams.ts @@ -0,0 +1,6 @@ +type SaveReportDraftCommentParams = { + reportID: string; + comment: string; +}; + +export default SaveReportDraftCommentParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 1627158a2f6b..d47152254516 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -408,3 +408,4 @@ export type {default as ExportMultiLevelTagsSpreadSheetParams} from './ExportMul export type {default as ReopenReportParams} from './ReopenReportParams'; export type {default as OpenUnreportedExpensesPageParams} from './OpenUnreportedExpensesPageParams'; export type {default as VerifyTestDriveRecipientParams} from './VerifyTestDriveRecipientParams'; +export type {default as SaveReportDraftCommentParams} from './SaveReportDraftCommentParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 8451c06e59b1..06c785a5b058 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -11,7 +11,7 @@ import type UpdateBeneficialOwnersForBankAccountParams from './parameters/Update type ApiRequestType = ValueOf; const WRITE_COMMANDS = { - SAVE_REPORT_DRAFT_COMMENT: 'SaveReportDraftComment', // CQ TODO + SAVE_REPORT_DRAFT_COMMENT: 'SaveReportDraftComment', CLEAN_POLICY_TAGS: 'ClearPolicyTags', IMPORT_MULTI_LEVEL_TAGS: 'ImportMultiLevelTags', SET_WORKSPACE_AUTO_REPORTING_FREQUENCY: 'SetWorkspaceAutoReportingFrequency', @@ -843,6 +843,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.COMPLETE_CONCIERGE_CALL]: Parameters.CompleteConciergeCallParams; [WRITE_COMMANDS.FINISH_CORPAY_BANK_ACCOUNT_ONBOARDING]: Parameters.FinishCorpayBankAccountOnboardingParams; [WRITE_COMMANDS.REOPEN_REPORT]: Parameters.ReopenReportParams; + [WRITE_COMMANDS.SAVE_REPORT_DRAFT_COMMENT]: Parameters.SaveReportDraftCommentParams; [WRITE_COMMANDS.DELETE_MONEY_REQUEST_ON_SEARCH]: Parameters.DeleteMoneyRequestOnSearchParams; [WRITE_COMMANDS.HOLD_MONEY_REQUEST_ON_SEARCH]: Parameters.HoldMoneyRequestOnSearchParams; diff --git a/src/libs/DraftCommentUtils.ts b/src/libs/DraftCommentUtils.ts index a04717767c8c..3263f8431aec 100644 --- a/src/libs/DraftCommentUtils.ts +++ b/src/libs/DraftCommentUtils.ts @@ -1,7 +1,7 @@ -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; -import { DraftReportComments } from '@src/types/onyx'; +import type {DraftReportComments} from '@src/types/onyx'; let draftCommentCollection: OnyxEntry = {}; Onyx.connect({ @@ -30,7 +30,7 @@ function isValidDraftComment(comment?: string | null): boolean { /** * Returns true if the report has a valid draft comment. - * @deprecated prefer useOnyx to access when possible + * NOTE: please prefer useOnyx when possible */ function hasValidDraftComment(reportID: string): boolean { return isValidDraftComment(getDraftComment(reportID)); diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index bbf1f3e77c49..f299c252947b 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -29,7 +29,6 @@ import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; import type { Beta, - DraftReportComments, IntroSelected, NewGroupChatDraft, OnyxInputOrEntry, @@ -81,7 +80,7 @@ import type {OnboardingCompanySize, OnboardingMessage, OnboardingPurpose, Onboar import type {AddCommentOrAttachmentParams} from './API/parameters'; import {convertToDisplayString} from './CurrencyUtils'; import DateUtils from './DateUtils'; -import {hasValidDraftComment, isValidDraftComment} from './DraftCommentUtils'; +import {hasValidDraftComment} from './DraftCommentUtils'; import {getEnvironment, getEnvironmentURL} from './Environment/Environment'; import type EnvironmentType from './Environment/getEnvironment/types'; import {getMicroSecondOnyxErrorWithTranslationKey, isReceiptError} from './ErrorUtils'; diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 2170f50ad10b..52e44db837f7 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -5,7 +5,16 @@ import type {ValueOf} from 'type-fest'; import type {PartialPolicyForSidebar, ReportsToDisplayInLHN} from '@hooks/useSidebarOrderedReports'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {DraftReportComments, PersonalDetails, PersonalDetailsList, ReportActions, ReportAttributesDerivedValue, ReportNameValuePairs, Transaction, TransactionViolation} from '@src/types/onyx'; +import type { + DraftReportComments, + PersonalDetails, + PersonalDetailsList, + ReportActions, + ReportAttributesDerivedValue, + ReportNameValuePairs, + Transaction, + TransactionViolation, +} from '@src/types/onyx'; import type Beta from '@src/types/onyx/Beta'; import type {ReportAttributes} from '@src/types/onyx/DerivedValues'; import type {Errors} from '@src/types/onyx/OnyxCommon'; @@ -239,7 +248,12 @@ function shouldDisplayReportInLHN( const isSystemChat = isSystemChatUtil(report); const isReportArchived = isArchivedReport(reportNameValuePairs); const shouldOverrideHidden = - isValidDraftComment(draftReportComments?.[report.reportID]) || hasErrorsOtherThanFailedReceipt || isFocused || isSystemChat || !!report.isPinned || reportAttributes?.[report?.reportID]?.requiresAttention; + isValidDraftComment(draftReportComments?.[report.reportID]) || + hasErrorsOtherThanFailedReceipt || + isFocused || + isSystemChat || + !!report.isPinned || + reportAttributes?.[report?.reportID]?.requiresAttention; if (isHidden && !shouldOverrideHidden) { return {shouldDisplay: false}; @@ -308,7 +322,6 @@ function updateReportsToDisplayInLHN( currentReportId: string | undefined, isInFocusMode: boolean, betas: OnyxEntry, - policies: OnyxCollection, transactionViolations: OnyxCollection, reportNameValuePairs?: OnyxCollection, reportAttributes?: ReportAttributesDerivedValue['reports'], diff --git a/src/libs/migrateOnyx.ts b/src/libs/migrateOnyx.ts index 18cb6ca9195b..f928ff262048 100644 --- a/src/libs/migrateOnyx.ts +++ b/src/libs/migrateOnyx.ts @@ -1,5 +1,6 @@ import Log from './Log'; import KeyReportActionsDraftByReportActionID from './migrations/KeyReportActionsDraftByReportActionID'; +import MoveDraftsToNVP from './migrations/MoveDraftsToNVP'; import MoveIsOptimisticReportToMetadata from './migrations/MoveIsOptimisticReportToMetadata'; import NVPMigration from './migrations/NVPMigration'; import PendingMembersToMetadata from './migrations/PendingMembersToMetadata'; @@ -8,7 +9,6 @@ import RemoveEmptyReportActionsDrafts from './migrations/RemoveEmptyReportAction import RenameCardIsVirtual from './migrations/RenameCardIsVirtual'; import RenameReceiptFilename from './migrations/RenameReceiptFilename'; import TransactionBackupsToCollection from './migrations/TransactionBackupsToCollection'; -import MoveDraftsToNVP from './migrations/MoveDraftsToNVP'; export default function () { const startTime = Date.now(); diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 8903ab14ccaf..aae3713d7431 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -180,7 +180,6 @@ function ReportActionCompose({ debouncedLowerIsScrollLikelyLayoutTriggered(); }, [debouncedLowerIsScrollLikelyLayoutTriggered]); - // CQ TODO is this still necessary? const [isCommentEmpty, setIsCommentEmpty] = useState(() => { const draftComment = getDraftComment(reportID); return !draftComment || !!draftComment.match(CONST.REGEX.EMPTY_COMMENT); diff --git a/src/types/onyx/DraftReportComments.ts b/src/types/onyx/DraftReportComments.ts index f139af0341af..385a9623a36a 100644 --- a/src/types/onyx/DraftReportComments.ts +++ b/src/types/onyx/DraftReportComments.ts @@ -3,4 +3,4 @@ */ type DraftReportComments = Record; -export default DraftReportComments; \ No newline at end of file +export default DraftReportComments; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index b0f235ad57d6..3c73a96ec9f6 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -27,6 +27,7 @@ import type {ReportAttributesDerivedValue, ReportTransactionsAndViolationsDerive import type DismissedProductTraining from './DismissedProductTraining'; import type DismissedReferralBanners from './DismissedReferralBanners'; import type Download from './Download'; +import type DraftReportComments from './DraftReportComments'; import type ExpensifyCardBankAccountMetadata from './ExpensifyCardBankAccountMetadata'; import type ExpensifyCardSettings from './ExpensifyCardSettings'; import type FrequentlyUsedEmoji from './FrequentlyUsedEmoji'; @@ -119,7 +120,6 @@ import type WalletOnfido from './WalletOnfido'; import type WalletStatement from './WalletStatement'; import type WalletTerms from './WalletTerms'; import type WalletTransfer from './WalletTransfer'; -import type DraftReportComments from './DraftReportComments'; export type { TryNewDot, diff --git a/tests/unit/DebugUtilsTest.ts b/tests/unit/DebugUtilsTest.ts index b734111741c7..a0798a14b274 100644 --- a/tests/unit/DebugUtilsTest.ts +++ b/tests/unit/DebugUtilsTest.ts @@ -739,7 +739,7 @@ describe('DebugUtils', () => { expect(reason).toBeNull(); }); it('returns correct reason when report has a valid draft comment', async () => { - await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}1`, 'Hello world!'); + await Onyx.set(ONYXKEYS.NVP_DRAFT_REPORT_COMMENTS, {[baseReport.reportID]: 'Hello world!'}); const reason = DebugUtils.getReasonForShowingRowInLHN(baseReport, chatReportR14932); expect(reason).toBe('debug.reasonVisibleInLHN.hasDraftComment'); }); From ba40f3fdfc4f7037dc735f83c5c9541453954ec1 Mon Sep 17 00:00:00 2001 From: Chuck Dries Date: Fri, 11 Jul 2025 12:54:20 -0700 Subject: [PATCH 06/13] Use debounce:true for composer onClear to ensure sent messages actually clear out drafts --- .../ComposerWithSuggestions/ComposerWithSuggestions.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 8ca4dab5744f..a502ac16e990 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -730,7 +730,9 @@ function ComposerWithSuggestions( mobileInputScrollPosition.current = 0; // Note: use the value when the clear happened, not the current value which might have changed already onCleared(text); - updateComment('', false); + // debounce should be true here, otherwise the input will intially clear, but then the value the user had typed before they sent will return when the debounce timeout fires. + // It's okay for the Onyx value to lag a bit since we're using local state to feed the actual input. The only real impact is the pencil remains visible in the LHN for a moment + updateComment('', true); }, [onCleared, updateComment], ); From cd7c48a8647564d89a6375bcb9dbd9a81d337e4e Mon Sep 17 00:00:00 2001 From: Chuck Dries Date: Wed, 16 Jul 2025 15:03:47 -0700 Subject: [PATCH 07/13] Clean up shouldDebounceSavedComment --- src/libs/DraftCommentUtils.ts | 6 +++--- .../ComposerWithSuggestions.tsx | 20 +++++++------------ .../ReportActionCompose/SuggestionEmoji.tsx | 2 +- .../ReportActionCompose/SuggestionMention.tsx | 2 +- .../ReportActionCompose/Suggestions.tsx | 2 +- 5 files changed, 13 insertions(+), 19 deletions(-) diff --git a/src/libs/DraftCommentUtils.ts b/src/libs/DraftCommentUtils.ts index 3263f8431aec..8bc3edad8eb8 100644 --- a/src/libs/DraftCommentUtils.ts +++ b/src/libs/DraftCommentUtils.ts @@ -3,11 +3,11 @@ import Onyx from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; import type {DraftReportComments} from '@src/types/onyx'; -let draftCommentCollection: OnyxEntry = {}; +let draftComments: OnyxEntry = {}; Onyx.connect({ key: ONYXKEYS.NVP_DRAFT_REPORT_COMMENTS, callback: (nextVal) => { - draftCommentCollection = nextVal; + draftComments = nextVal; }, }); @@ -17,7 +17,7 @@ Onyx.connect({ * A valid use-case of this function is outside React components, like in utility functions. */ function getDraftComment(reportID: string): OnyxEntry | null | undefined { - return draftCommentCollection?.[reportID]; + return draftComments?.[reportID]; } /** diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index a502ac16e990..b8b410639fb7 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -378,7 +378,7 @@ function ComposerWithSuggestions( * Update the value of the comment in Onyx */ const updateComment = useCallback( - (commentValue: string, shouldDebounceSaveComment?: boolean) => { + (commentValue: string) => { raiseIsScrollLikelyLayoutTriggered(); const {startIndex, endIndex, diff} = findNewlyAddedChars(lastTextRef.current, commentValue); const isEmojiInserted = diff.length && endIndex > startIndex && diff.trim() === diff && containsOnlyEmojis(diff); @@ -421,12 +421,8 @@ function ComposerWithSuggestions( commentRef.current = newCommentConverted; - if (shouldDebounceSaveComment) { - isCommentPendingSaved.current = true; - debouncedSaveReportComment(reportID, newCommentConverted); - } else { - saveReportDraftComment(reportID, newCommentConverted); - } + isCommentPendingSaved.current = true; + debouncedSaveReportComment(reportID, newCommentConverted); if (newCommentConverted) { debouncedBroadcastUserIsTyping(reportID); } @@ -441,7 +437,7 @@ function ComposerWithSuggestions( (text: string) => { // selection replacement should be debounced to avoid conflicts with text typing // (f.e. when emoji is being picked and 1 second still did not pass after user finished typing) - updateComment(insertText(commentRef.current, selection, text), true); + updateComment(insertText(commentRef.current, selection, text)); }, [selection, updateComment], ); @@ -499,7 +495,7 @@ function ComposerWithSuggestions( positionX: prevSelection.positionX, positionY: prevSelection.positionY, })); - updateComment(newText, true); + updateComment(newText); } } }, @@ -508,7 +504,7 @@ function ComposerWithSuggestions( const onChangeText = useCallback( (commentValue: string) => { - updateComment(commentValue, true); + updateComment(commentValue); if (isIOSNative && syncSelectionWithOnChangeTextRef.current) { const positionSnapshot = syncSelectionWithOnChangeTextRef.current.position; @@ -730,9 +726,7 @@ function ComposerWithSuggestions( mobileInputScrollPosition.current = 0; // Note: use the value when the clear happened, not the current value which might have changed already onCleared(text); - // debounce should be true here, otherwise the input will intially clear, but then the value the user had typed before they sent will return when the debounce timeout fires. - // It's okay for the Onyx value to lag a bit since we're using local state to feed the actual input. The only real impact is the pencil remains visible in the LHN for a moment - updateComment('', true); + updateComment(''); }, [onCleared, updateComment], ); diff --git a/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx b/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx index 180718a4a670..dda9a41879be 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx +++ b/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx @@ -72,7 +72,7 @@ function SuggestionEmoji( const emojiCode = emojiObject?.types?.at(preferredSkinTone) && preferredSkinTone !== -1 ? emojiObject.types.at(preferredSkinTone) : emojiObject?.code; const commentAfterColonWithEmojiNameRemoved = value.slice(selection.end); - updateComment(`${commentBeforeColon}${emojiCode} ${trimLeadingSpace(commentAfterColonWithEmojiNameRemoved)}`, true); + updateComment(`${commentBeforeColon}${emojiCode} ${trimLeadingSpace(commentAfterColonWithEmojiNameRemoved)}`); // In some Android phones keyboard, the text to search for the emoji is not cleared // will be added after the user starts typing again on the keyboard. This package is diff --git a/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx b/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx index fed7945d9b74..d4d2be8705b5 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx +++ b/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx @@ -201,7 +201,7 @@ function SuggestionMention( suggestionValues.atSignIndex + Math.max(originalMention.length, suggestionValues.mentionPrefix.length + suggestionValues.prefixType.length), ); - updateComment(`${commentBeforeAtSign}${mentionCode} ${trimLeadingSpace(commentAfterMention)}`, true); + updateComment(`${commentBeforeAtSign}${mentionCode} ${trimLeadingSpace(commentAfterMention)}`); const selectionPosition = suggestionValues.atSignIndex + mentionCode.length + CONST.SPACE_LENGTH; setSelection({ start: selectionPosition, diff --git a/src/pages/home/report/ReportActionCompose/Suggestions.tsx b/src/pages/home/report/ReportActionCompose/Suggestions.tsx index 8b7171340b63..74efae903116 100644 --- a/src/pages/home/report/ReportActionCompose/Suggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/Suggestions.tsx @@ -21,7 +21,7 @@ type SuggestionProps = { setSelection: (newSelection: TextSelection) => void; /** Callback to update the comment draft */ - updateComment: (newComment: string, shouldDebounceSaveComment?: boolean) => void; + updateComment: (newComment: string) => void; /** Measures the parent container's position and dimensions. Also add cursor coordinates */ measureParentContainerAndReportCursor: (callback: MeasureParentContainerAndCursorCallback) => void; From dbcdd4c75cd00e03886def904a87a3072e2f4982 Mon Sep 17 00:00:00 2001 From: Chuck Dries Date: Wed, 16 Jul 2025 15:40:04 -0700 Subject: [PATCH 08/13] Clean up COLLECTION.REPORT_DRAFT_COMMENT --- src/libs/actions/IOU.ts | 2 +- src/libs/actions/Report.ts | 2 +- tests/ui/LHNItemsPresence.tsx | 2 +- tests/unit/ReportUtilsTest.ts | 2 +- tests/unit/SidebarFilterTest.ts | 7 ++++--- tests/unit/SidebarOrderTest.ts | 20 +++++++++++--------- 6 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index fa386c99292a..34c9a293bd13 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -751,7 +751,7 @@ Onyx.connect({ const policyID = key.replace(ONYXKEYS.COLLECTION.POLICY, ''); const policyReports = getAllPolicyReports(policyID); const cleanUpMergeQueries: Record<`${typeof ONYXKEYS.COLLECTION.REPORT}${string}`, NullishDeep> = {}; - const cleanUpSetQueries: Record<`${typeof ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${string}` | `${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${string}`, null> = {}; + const cleanUpSetQueries: Record<`${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${string}`, null> = {}; const cleanUpDrafts: Record = {}; policyReports.forEach((policyReport) => { if (!policyReport) { diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 2f6e5790605c..77512cd48af3 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -423,7 +423,7 @@ Onyx.connect({ // More info: https://github.com/Expensify/App/issues/14260 const policyID = key.replace(ONYXKEYS.COLLECTION.POLICY, ''); const policyReports = getAllPolicyReports(policyID); - const cleanUpSetQueries: Record<`${typeof ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${string}` | `${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${string}`, null> = {}; + const cleanUpSetQueries: Record<`${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${string}`, null> = {}; const cleanUpDrafts: Record = {}; policyReports.forEach((policyReport) => { if (!policyReport) { diff --git a/tests/ui/LHNItemsPresence.tsx b/tests/ui/LHNItemsPresence.tsx index 95374059dd7f..4e01be0ceafc 100644 --- a/tests/ui/LHNItemsPresence.tsx +++ b/tests/ui/LHNItemsPresence.tsx @@ -177,7 +177,7 @@ describe('SidebarLinksData', () => { await waitForBatchedUpdatesWithAct(); // And a draft message is added to the report. - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${draftReport.reportID}`, 'draft report message'); + await Onyx.merge(ONYXKEYS.NVP_DRAFT_REPORT_COMMENTS, {[draftReport.reportID]: 'draft report message'}); // Then the sidebar should display the draft report. expect(getDisplayNames()).toHaveLength(1); diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 837928f3458e..cceb7a728270 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -2187,7 +2187,7 @@ describe('ReportUtils', () => { const isInFocusMode = false; const betas = [CONST.BETAS.DEFAULT_ROOMS]; - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${report.reportID}`, 'fake draft'); + await Onyx.merge(ONYXKEYS.NVP_DRAFT_REPORT_COMMENTS, {[report.reportID]: 'fake draft'}); expect( shouldReportBeInOptionList({ diff --git a/tests/unit/SidebarFilterTest.ts b/tests/unit/SidebarFilterTest.ts index f2b3078994c1..7e35b7108cb0 100644 --- a/tests/unit/SidebarFilterTest.ts +++ b/tests/unit/SidebarFilterTest.ts @@ -27,6 +27,7 @@ const ONYXKEYS = { REPORT_DRAFT_COMMENT: 'reportDraftComment_', }, NETWORK: 'network', + NVP_DRAFT_REPORT_COMMENTS: 'nvp_draftReportComments', } as const; // We need to fix this test as a follow up. There seems to be some problems with memory after filtering got more complicated. @@ -120,7 +121,7 @@ xdescribe('Sidebar', () => { Onyx.multiSet({ [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, [ONYXKEYS.IS_LOADING_APP]: false, - [`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${report.reportID}`]: 'This is a draft message', + [ONYXKEYS.NVP_DRAFT_REPORT_COMMENTS]: {[report.reportID]: 'This is a draft message'}, ...reportCollectionDataSet, }), ) @@ -495,7 +496,7 @@ xdescribe('Sidebar', () => { [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.GSD, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, [ONYXKEYS.IS_LOADING_APP]: false, - [`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${draftReport.reportID}`]: 'draft report message', + [ONYXKEYS.NVP_DRAFT_REPORT_COMMENTS]: {[draftReport.reportID]: 'draft report message'}, ...reportCollectionDataSet, }), ) @@ -719,7 +720,7 @@ xdescribe('Sidebar', () => { [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, [ONYXKEYS.IS_LOADING_APP]: false, [`${ONYXKEYS.COLLECTION.POLICY}${policy.policyID}`]: policy, - [`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${report2.reportID}`]: hasDraft ? 'report2 draft' : null, + [ONYXKEYS.NVP_DRAFT_REPORT_COMMENTS]: {[report2.reportID]: hasDraft ? 'report2 draft' : null}, ...reportCollectionDataSet, }), ) diff --git a/tests/unit/SidebarOrderTest.ts b/tests/unit/SidebarOrderTest.ts index 57d35d757786..c80f2d173fcb 100644 --- a/tests/unit/SidebarOrderTest.ts +++ b/tests/unit/SidebarOrderTest.ts @@ -187,7 +187,7 @@ describe('Sidebar', () => { [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.DEFAULT, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, [ONYXKEYS.IS_LOADING_APP]: false, - [`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${report1.reportID}`]: 'report1 draft', + [ONYXKEYS.NVP_DRAFT_REPORT_COMMENTS]: {[report1.reportID]: 'report1 draft'}, ...reportCollectionDataSet, }), ) @@ -501,7 +501,7 @@ describe('Sidebar', () => { [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.DEFAULT, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, [ONYXKEYS.IS_LOADING_APP]: false, - [ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT + report2.reportID]: 'This is a draft', + [ONYXKEYS.NVP_DRAFT_REPORT_COMMENTS]: {[report2.reportID]: 'This is a draft'}, ...reportCollectionDataSet, }), ) @@ -548,7 +548,7 @@ describe('Sidebar', () => { [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.DEFAULT, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, [ONYXKEYS.IS_LOADING_APP]: false, - [`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${report.reportID}`]: 'This is a draft', + [ONYXKEYS.NVP_DRAFT_REPORT_COMMENTS]: {[report.reportID]: 'This is a draft'}, ...reportCollectionDataSet, }), ) @@ -559,7 +559,7 @@ describe('Sidebar', () => { }) // When the draft is removed - .then(() => Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${report.reportID}`, null)) + .then(() => Onyx.merge(ONYXKEYS.NVP_DRAFT_REPORT_COMMENTS, {[report.reportID]: null})) // Then the pencil icon goes away .then(() => { @@ -674,7 +674,7 @@ describe('Sidebar', () => { [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.DEFAULT, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, [ONYXKEYS.IS_LOADING_APP]: false, - [`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${report2.reportID}`]: 'Report2 draft comment', + [ONYXKEYS.NVP_DRAFT_REPORT_COMMENTS]: {[report2.reportID]: 'Report2 draft comment'}, ...reportCollectionDataSet, }), ) @@ -785,9 +785,11 @@ describe('Sidebar', () => { }; const reportDraftCommentCollectionDataSet = { - [`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${report1.reportID}`]: 'report1 draft', - [`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${report2.reportID}`]: 'report2 draft', - [`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${report3.reportID}`]: 'report3 draft', + [ONYXKEYS.NVP_DRAFT_REPORT_COMMENTS]: { + [report1.reportID]: 'report1 draft', + [report2.reportID]: 'report2 draft', + [report3.reportID]: 'report3 draft', + }, }; return ( @@ -819,7 +821,7 @@ describe('Sidebar', () => { .then(() => Onyx.multiSet({ ...reportDraftCommentCollectionDataSet, - [`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${report4.reportID}`]: 'report4 draft', + [ONYXKEYS.NVP_DRAFT_REPORT_COMMENTS]: {[report4.reportID]: 'report4 draft'}, [`${ONYXKEYS.COLLECTION.REPORT}${report4.reportID}`]: report4, ...reportCollectionDataSet, }), From 5bf301c2fd2dbe408151fd6afb57ea87ce004807 Mon Sep 17 00:00:00 2001 From: Chuck Dries Date: Wed, 16 Jul 2025 15:49:44 -0700 Subject: [PATCH 09/13] Address lint error --- src/hooks/useSidebarOrderedReports.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/hooks/useSidebarOrderedReports.tsx b/src/hooks/useSidebarOrderedReports.tsx index 342a53fd8ff1..9e3642caf03d 100644 --- a/src/hooks/useSidebarOrderedReports.tsx +++ b/src/hooks/useSidebarOrderedReports.tsx @@ -7,6 +7,7 @@ import SidebarUtils from '@libs/SidebarUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; +import {getEmptyObject} from '@src/types/utils/EmptyObject'; import mapOnyxCollectionItems from '@src/utils/mapOnyxCollectionItems'; import useCurrentReportID from './useCurrentReportID'; import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails'; @@ -61,7 +62,7 @@ function SidebarOrderedReportsContextProvider({ const [transactions, {sourceValue: transactionsUpdates}] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {canBeMissing: true}); const [transactionViolations, {sourceValue: transactionViolationsUpdates}] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); const [reportNameValuePairs, {sourceValue: reportNameValuePairsUpdates}] = useOnyx(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS, {canBeMissing: true}); - const [reportsDraftsUpdates = {}] = useOnyx(ONYXKEYS.NVP_DRAFT_REPORT_COMMENTS, {canBeMissing: true}); + const [reportsDraftsUpdates = getEmptyObject()] = useOnyx(ONYXKEYS.NVP_DRAFT_REPORT_COMMENTS, {canBeMissing: true}); const [betas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true}); const [reportAttributes] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, {selector: (value) => value?.reports, canBeMissing: true}); const [currentReportsToDisplay, setCurrentReportsToDisplay] = useState({}); From 88fe5c9609ff7e6971fc60362e42607aa27ab6cd Mon Sep 17 00:00:00 2001 From: Chuck Dries Date: Thu, 24 Jul 2025 13:51:20 -0700 Subject: [PATCH 10/13] Address PR feedback, add useDiffPrevious hook --- src/ONYXKEYS.ts | 1 + src/components/LHNOptionsList/LHNOptionsList.tsx | 3 +-- src/hooks/useDiffPrevious.ts | 16 ++++++++++++++++ src/hooks/useSidebarOrderedReports.tsx | 14 ++++++++------ src/libs/DraftCommentUtils.ts | 12 ++---------- src/libs/SidebarUtils.ts | 5 ++--- src/libs/migrations/MoveDraftsToNVP.ts | 16 ++++++++++------ 7 files changed, 40 insertions(+), 27 deletions(-) create mode 100644 src/hooks/useDiffPrevious.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 73ebf4e78179..2641a9a32c1d 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -589,6 +589,7 @@ const ONYXKEYS = { REPORT_ACTIONS_DRAFTS: 'reportActionsDrafts_', REPORT_ACTIONS_PAGES: 'reportActionsPages_', REPORT_ACTIONS_REACTIONS: 'reportActionsReactions_', + /** @deprecated */ REPORT_DRAFT_COMMENT: 'reportDraftComment_', REPORT_IS_COMPOSER_FULL_SIZE: 'reportIsComposerFullSize_', REPORT_USER_IS_TYPING: 'reportUserIsTyping_', diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index 55d7573500b1..3ccb3156c04b 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -19,7 +19,6 @@ import useRootNavigationState from '@hooks/useRootNavigationState'; import useScrollEventEmitter from '@hooks/useScrollEventEmitter'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import {isValidDraftComment} from '@libs/DraftCommentUtils'; import getPlatform from '@libs/getPlatform'; import Log from '@libs/Log'; import {getIOUReportIDOfLastAction, getLastMessageTextForReport} from '@libs/OptionsListUtils'; @@ -189,7 +188,7 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio ? (getOriginalMessage(itemParentReportAction)?.IOUTransactionID ?? CONST.DEFAULT_NUMBER_ID) : CONST.DEFAULT_NUMBER_ID; const itemTransaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; - const hasDraftComment = isValidDraftComment(draftComments?.[reportID]); + const hasDraftComment = !!draftComments?.[reportID]; const canUserPerformWrite = canUserPerformWriteAction(item); const sortedReportActions = getSortedReportActionsForDisplay(itemReportActions, canUserPerformWrite); diff --git a/src/hooks/useDiffPrevious.ts b/src/hooks/useDiffPrevious.ts new file mode 100644 index 000000000000..95b98d99fc8e --- /dev/null +++ b/src/hooks/useDiffPrevious.ts @@ -0,0 +1,16 @@ +import union from 'lodash-es/union'; +import {useMemo} from 'react'; +import usePrevious from './usePrevious'; + +/** This hook provides a list of which keys changed in an object vs the previous render + * akin to `sourceValue` for collections. Generally, using this hook at all is an anti-pattern. + * Avoid it at all costs */ +export default function useDiffPrevious>(value: T): string[] { + const previous = usePrevious(value); + const diff = useMemo(() => { + const allKeys = union(Object.keys(value), Object.keys(previous)); + return allKeys.filter((key) => value[key] !== previous[key]); + }, [value, previous]); + + return diff; +} diff --git a/src/hooks/useSidebarOrderedReports.tsx b/src/hooks/useSidebarOrderedReports.tsx index 9e3642caf03d..253c59ed9bb7 100644 --- a/src/hooks/useSidebarOrderedReports.tsx +++ b/src/hooks/useSidebarOrderedReports.tsx @@ -11,6 +11,7 @@ import {getEmptyObject} from '@src/types/utils/EmptyObject'; import mapOnyxCollectionItems from '@src/utils/mapOnyxCollectionItems'; import useCurrentReportID from './useCurrentReportID'; import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails'; +import useDiffPrevious from './useDiffPrevious'; import useOnyx from './useOnyx'; import usePrevious from './usePrevious'; import useResponsiveLayout from './useResponsiveLayout'; @@ -62,7 +63,8 @@ function SidebarOrderedReportsContextProvider({ const [transactions, {sourceValue: transactionsUpdates}] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {canBeMissing: true}); const [transactionViolations, {sourceValue: transactionViolationsUpdates}] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); const [reportNameValuePairs, {sourceValue: reportNameValuePairsUpdates}] = useOnyx(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS, {canBeMissing: true}); - const [reportsDraftsUpdates = getEmptyObject()] = useOnyx(ONYXKEYS.NVP_DRAFT_REPORT_COMMENTS, {canBeMissing: true}); + const [drafts = getEmptyObject()] = useOnyx(ONYXKEYS.NVP_DRAFT_REPORT_COMMENTS, {canBeMissing: true}); + const reportsDraftsUpdates = useDiffPrevious(drafts); const [betas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true}); const [reportAttributes] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, {selector: (value) => value?.reports, canBeMissing: true}); const [currentReportsToDisplay, setCurrentReportsToDisplay] = useState({}); @@ -95,8 +97,8 @@ function SidebarOrderedReportsContextProvider({ reportsToUpdate = Object.keys(transactionViolationsUpdates ?? {}) .map((key) => key.replace(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, ONYXKEYS.COLLECTION.TRANSACTION)) .map((key) => `${ONYXKEYS.COLLECTION.REPORT}${transactions?.[key]?.reportID}`); - } else if (reportsDraftsUpdates) { - reportsToUpdate = Object.keys(reportsDraftsUpdates).map((key) => `${ONYXKEYS.COLLECTION.REPORT}${key}`); + } else if (reportsDraftsUpdates.length > 0) { + reportsToUpdate = reportsDraftsUpdates.map((key) => `${ONYXKEYS.COLLECTION.REPORT}${key}`); } else if (policiesUpdates) { const updatedPolicies = Object.keys(policiesUpdates).map((key) => key.replace(ONYXKEYS.COLLECTION.POLICY, '')); reportsToUpdate = Object.entries(chatReports ?? {}) @@ -150,7 +152,7 @@ function SidebarOrderedReportsContextProvider({ transactionViolations, reportNameValuePairs, reportAttributes, - reportsDraftsUpdates, + drafts, ); } else { reportsToDisplay = SidebarUtils.getReportsToDisplayInLHN( @@ -162,7 +164,7 @@ function SidebarOrderedReportsContextProvider({ transactionViolations, reportNameValuePairs, reportAttributes, - reportsDraftsUpdates, + drafts, ); } return reportsToDisplay; @@ -175,7 +177,7 @@ function SidebarOrderedReportsContextProvider({ }, [reportsToDisplayInLHN]); const getOrderedReportIDs = useCallback( - () => SidebarUtils.sortReportsToDisplayInLHN(reportsToDisplayInLHN, priorityMode, reportNameValuePairs, reportAttributes, reportsDraftsUpdates), + () => SidebarUtils.sortReportsToDisplayInLHN(reportsToDisplayInLHN, priorityMode, reportNameValuePairs, reportAttributes, drafts), // Rule disabled intentionally - reports should be sorted only when the reportsToDisplayInLHN changes // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps [reportsToDisplayInLHN], diff --git a/src/libs/DraftCommentUtils.ts b/src/libs/DraftCommentUtils.ts index 8bc3edad8eb8..b95294820b1e 100644 --- a/src/libs/DraftCommentUtils.ts +++ b/src/libs/DraftCommentUtils.ts @@ -20,20 +20,12 @@ function getDraftComment(reportID: string): OnyxEntry | null | undefined return draftComments?.[reportID]; } -/** - * Returns true if the report has a valid draft comment. - * A valid draft comment is a non-empty string. - */ -function isValidDraftComment(comment?: string | null): boolean { - return !!comment; -} - /** * Returns true if the report has a valid draft comment. * NOTE: please prefer useOnyx when possible */ function hasValidDraftComment(reportID: string): boolean { - return isValidDraftComment(getDraftComment(reportID)); + return !!getDraftComment(reportID); } /** @@ -45,4 +37,4 @@ function prepareDraftComment(comment: string | null) { return comment || null; } -export {getDraftComment, isValidDraftComment, hasValidDraftComment, prepareDraftComment}; +export {getDraftComment, hasValidDraftComment, prepareDraftComment}; diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 52e44db837f7..e380c03205be 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -24,7 +24,6 @@ import type Report from '@src/types/onyx/Report'; import type ReportAction from '@src/types/onyx/ReportAction'; import {getExpensifyCardFromReportAction} from './CardMessageUtils'; import {extractCollectionItemID} from './CollectionUtils'; -import {isValidDraftComment} from './DraftCommentUtils'; import localeCompare from './LocaleCompare'; import {translateLocal} from './Localize'; import {getLastActorDisplayName, getLastMessageTextForReport, getPersonalDetailsForAccountIDs, shouldShowLastActorDisplayName} from './OptionsListUtils'; @@ -248,7 +247,7 @@ function shouldDisplayReportInLHN( const isSystemChat = isSystemChatUtil(report); const isReportArchived = isArchivedReport(reportNameValuePairs); const shouldOverrideHidden = - isValidDraftComment(draftReportComments?.[report.reportID]) || + !!draftReportComments?.[report.reportID] || hasErrorsOtherThanFailedReceipt || isFocused || isSystemChat || @@ -401,7 +400,7 @@ function sortReportsToDisplayInLHN( pinnedAndGBRReports.push(miniReport); } else if (report?.hasErrorsOtherThanFailedReceipt) { errorReports.push(miniReport); - } else if (isValidDraftComment(draftReportComments?.[report?.reportID])) { + } else if (draftReportComments?.[report?.reportID]) { draftReports.push(miniReport); } else if (isArchivedNonExpenseReport(report, !!rNVPs?.private_isArchived)) { archivedReports.push(miniReport); diff --git a/src/libs/migrations/MoveDraftsToNVP.ts b/src/libs/migrations/MoveDraftsToNVP.ts index 5139a4a3e680..136b38a574f8 100644 --- a/src/libs/migrations/MoveDraftsToNVP.ts +++ b/src/libs/migrations/MoveDraftsToNVP.ts @@ -1,5 +1,5 @@ import Onyx from 'react-native-onyx'; -import type {OnyxCollection, OnyxKey} from 'react-native-onyx'; +import type {OnyxCollection, OnyxKey, OnyxMultiSetInput} from 'react-native-onyx'; import Log from '@libs/Log'; import ONYXKEYS from '@src/ONYXKEYS'; import type {DraftReportComments} from '@src/types/onyx'; @@ -7,6 +7,7 @@ import type {DraftReportComments} from '@src/types/onyx'; // moves individual drafts from `${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}` to ONYXKEYS.NVP_DRAFT_REPORT_COMMENTS export default function (): Promise { return new Promise((resolve) => { + // eslint-disable-next-line rulesdir/no-onyx-connect const connection = Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, waitForCollectionCallback: true, @@ -19,17 +20,20 @@ export default function (): Promise { } const newDrafts: DraftReportComments = {}; - Object.entries(drafts).forEach(([reportOnyxKey, draft]) => { + const draftsToClear: OnyxMultiSetInput = {}; + for (const [reportOnyxKey, draft] of Object.entries(drafts)) { if (!draft) { - return; + continue; } newDrafts[reportOnyxKey.replace(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, '')] = draft; - // eslint-disable-next-line rulesdir/prefer-actions-set-data - Onyx.set(reportOnyxKey as OnyxKey, null); - }); + draftsToClear[reportOnyxKey as OnyxKey] = null; + } // eslint-disable-next-line rulesdir/prefer-actions-set-data Onyx.set(ONYXKEYS.NVP_DRAFT_REPORT_COMMENTS, newDrafts); + + // eslint-disable-next-line rulesdir/prefer-actions-set-data + Onyx.multiSet(draftsToClear); resolve(); }, }); From ea529647455879522b2093f007056fb76044e5d8 Mon Sep 17 00:00:00 2001 From: Chuck Dries Date: Thu, 24 Jul 2025 15:31:34 -0700 Subject: [PATCH 11/13] Address lint errors --- src/ONYXKEYS.ts | 3 ++- src/libs/migrations/MoveDraftsToNVP.ts | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 2641a9a32c1d..722b495dc71b 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -590,7 +590,7 @@ const ONYXKEYS = { REPORT_ACTIONS_PAGES: 'reportActionsPages_', REPORT_ACTIONS_REACTIONS: 'reportActionsReactions_', /** @deprecated */ - REPORT_DRAFT_COMMENT: 'reportDraftComment_', + REPORT_DRAFT_COMMENT: 'reportDraftComment_', // eslint-disable-line deprecation/deprecation REPORT_IS_COMPOSER_FULL_SIZE: 'reportIsComposerFullSize_', REPORT_USER_IS_TYPING: 'reportUserIsTyping_', REPORT_USER_IS_LEAVING_ROOM: 'reportUserIsLeavingRoom_', @@ -988,6 +988,7 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS]: OnyxTypes.ReportActionsDrafts; [ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES]: OnyxTypes.Pages; [ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS]: OnyxTypes.ReportActionReactions; + // eslint-disable-next-line deprecation/deprecation [ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT]: string; [ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE]: boolean; [ONYXKEYS.COLLECTION.REPORT_USER_IS_TYPING]: OnyxTypes.ReportUserIsTyping; diff --git a/src/libs/migrations/MoveDraftsToNVP.ts b/src/libs/migrations/MoveDraftsToNVP.ts index 136b38a574f8..eb2b1851ee5e 100644 --- a/src/libs/migrations/MoveDraftsToNVP.ts +++ b/src/libs/migrations/MoveDraftsToNVP.ts @@ -9,6 +9,7 @@ export default function (): Promise { return new Promise((resolve) => { // eslint-disable-next-line rulesdir/no-onyx-connect const connection = Onyx.connect({ + // eslint-disable-next-line deprecation/deprecation key: ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, waitForCollectionCallback: true, callback: (drafts: OnyxCollection) => { @@ -25,6 +26,7 @@ export default function (): Promise { if (!draft) { continue; } + // eslint-disable-next-line deprecation/deprecation newDrafts[reportOnyxKey.replace(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, '')] = draft; draftsToClear[reportOnyxKey as OnyxKey] = null; } From fb51045eb19d0ea488f7c3a5f2438cf4ee42e878 Mon Sep 17 00:00:00 2001 From: Chuck Dries Date: Thu, 24 Jul 2025 16:02:15 -0700 Subject: [PATCH 12/13] Fix lodash import statement --- src/hooks/useDiffPrevious.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useDiffPrevious.ts b/src/hooks/useDiffPrevious.ts index 95b98d99fc8e..2687a17b7670 100644 --- a/src/hooks/useDiffPrevious.ts +++ b/src/hooks/useDiffPrevious.ts @@ -1,4 +1,4 @@ -import union from 'lodash-es/union'; +import union from 'lodash/union'; import {useMemo} from 'react'; import usePrevious from './usePrevious'; From 31687c6c685f1ddfcdc246cf61265270b6dad581 Mon Sep 17 00:00:00 2001 From: Chuck Dries Date: Fri, 25 Jul 2025 15:25:45 -0700 Subject: [PATCH 13/13] Remove SilentCommentUpdater --- .../ComposerWithSuggestions.tsx | 13 +----- .../SilentCommentUpdater/index.android.tsx | 29 ------------- .../SilentCommentUpdater/index.tsx | 43 ------------------- .../SilentCommentUpdater/types.ts | 18 -------- 4 files changed, 1 insertion(+), 102 deletions(-) delete mode 100644 src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.android.tsx delete mode 100644 src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx delete mode 100644 src/pages/home/report/ReportActionCompose/SilentCommentUpdater/types.ts diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index b8b410639fb7..14e0c6e1c792 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -40,13 +40,12 @@ import getPlatform from '@libs/getPlatform'; import {addKeyDownPressListener, removeKeyDownPressListener} from '@libs/KeyboardShortcut/KeyDownPressListener'; import Parser from '@libs/Parser'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import {isValidReportIDFromPath, shouldAutoFocusOnKeyPress} from '@libs/ReportUtils'; +import {shouldAutoFocusOnKeyPress} from '@libs/ReportUtils'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import willBlurTextInputOnTapOutsideFunc from '@libs/willBlurTextInputOnTapOutside'; import getCursorPosition from '@pages/home/report/ReportActionCompose/getCursorPosition'; import getScrollPosition from '@pages/home/report/ReportActionCompose/getScrollPosition'; import type {SuggestionsRef} from '@pages/home/report/ReportActionCompose/ReportActionCompose'; -import SilentCommentUpdater from '@pages/home/report/ReportActionCompose/SilentCommentUpdater'; import Suggestions from '@pages/home/report/ReportActionCompose/Suggestions'; import type {FileObject} from '@pages/media/AttachmentModalScreen/types'; import {isEmojiPickerVisible} from '@userActions/EmojiPickerAction'; @@ -844,16 +843,6 @@ function ComposerWithSuggestions( resetKeyboardInput={resetKeyboardInput} /> - {isValidReportIDFromPath(reportID) && ( - - )} - {/* Only used for testing so far */} {children} diff --git a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.android.tsx b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.android.tsx deleted file mode 100644 index b64672b28f9f..000000000000 --- a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.android.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import {useEffect} from 'react'; -import useOnyx from '@hooks/useOnyx'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type SilentCommentUpdaterProps from './types'; - -/** - * Adding .android component to disable updating comment when prev comment is different - * it fixes issue on Android, assuming we don't need tab sync on mobiles - https://github.com/Expensify/App/issues/28562 - */ - -/** - * This component doesn't render anything. It runs a side effect to update the comment of a report under certain conditions. - * It is connected to the actual draft comment in onyx. The comment in onyx might updates multiple times, and we want to avoid - * re-rendering a UI component for that. That's why the side effect was moved down to a separate component. - */ -function SilentCommentUpdater({updateComment, reportID}: SilentCommentUpdaterProps) { - const [comment = ''] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, {canBeMissing: true}); - - useEffect(() => { - updateComment(comment); - // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- We need to run this on mount - }, []); - - return null; -} - -SilentCommentUpdater.displayName = 'SilentCommentUpdater'; - -export default SilentCommentUpdater; diff --git a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx deleted file mode 100644 index efe1c79c28d8..000000000000 --- a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import {useEffect} from 'react'; -import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; -import usePrevious from '@hooks/usePrevious'; -import ONYXKEYS from '@src/ONYXKEYS'; -import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; -import type SilentCommentUpdaterProps from './types'; - -/** - * This component doesn't render anything. It runs a side effect to update the comment of a report under certain conditions. - * It is connected to the actual draft comment in onyx. The comment in onyx might updates multiple times, and we want to avoid - * re-rendering a UI component for that. That's why the side effect was moved down to a separate component. - */ -function SilentCommentUpdater({commentRef, reportID, value, updateComment, isCommentPendingSaved}: SilentCommentUpdaterProps) { - const [comment = '', commentResult] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, {canBeMissing: true}); - const prevCommentProp = usePrevious(comment); - const prevReportId = usePrevious(reportID); - const {preferredLocale} = useLocalize(); - const prevPreferredLocale = usePrevious(preferredLocale); - - useEffect(() => { - if (isLoadingOnyxValue(commentResult)) { - return; - } - // Value state does not have the same value as comment props when the comment gets changed from another tab. - // In this case, we should synchronize the value between tabs. - const shouldSyncComment = prevCommentProp !== comment && value !== comment && !isCommentPendingSaved.current; - - // As the report IDs change, make sure to update the composer comment as we need to make sure - // we do not show incorrect data in there (ie. draft of message from other report). - if (preferredLocale === prevPreferredLocale && reportID === prevReportId && !shouldSyncComment) { - return; - } - - updateComment(comment ?? ''); - }, [prevCommentProp, prevPreferredLocale, prevReportId, comment, preferredLocale, reportID, updateComment, value, commentRef, isCommentPendingSaved, commentResult]); - - return null; -} - -SilentCommentUpdater.displayName = 'SilentCommentUpdater'; - -export default SilentCommentUpdater; diff --git a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/types.ts b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/types.ts deleted file mode 100644 index 2768dfc1250a..000000000000 --- a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/types.ts +++ /dev/null @@ -1,18 +0,0 @@ -type SilentCommentUpdaterProps = { - /** Updates the comment */ - updateComment: (comment: string) => void; - - /** The ID of the report associated with the comment */ - reportID: string; - - /** The value of the comment */ - value: string; - - /** The ref of the comment */ - commentRef: React.RefObject; - - /** The ref to check whether the comment saving is in progress */ - isCommentPendingSaved: React.RefObject; -}; - -export default SilentCommentUpdaterProps;