diff --git a/android/app/build.gradle b/android/app/build.gradle index c7fbf56f86e6..0f69346a44bc 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -107,8 +107,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009000303 - versionName "9.0.3-3" + versionCode 1009000305 + versionName "9.0.3-5" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index f400a6f43605..ad8cc982e052 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.3.3 + 9.0.3.5 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index b800b9f73118..8fc553fe8c0c 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 9.0.3.3 + 9.0.3.5 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 7e562b7de1f2..83e4d904584b 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 9.0.3 CFBundleVersion - 9.0.3.3 + 9.0.3.5 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 35dccc2de393..a5ffdcb4b63c 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1243,7 +1243,13 @@ PODS: - react-native-config (1.5.0): - react-native-config/App (= 1.5.0) - react-native-config/App (1.5.0): - - React-Core + - RCT-Folly + - RCTRequired + - RCTTypeSafety + - React + - React-Codegen + - React-RCTFabric + - ReactCommon/turbomodule/core - react-native-document-picker (9.1.1): - RCT-Folly - RCTRequired @@ -1974,7 +1980,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNScreens (3.30.1): + - RNScreens (3.32.0): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -1988,13 +1994,14 @@ PODS: - React-ImageManager - React-NativeModulesApple - React-RCTFabric + - React-RCTImage - React-rendererdebug - React-utils - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNScreens/common (= 3.30.1) + - RNScreens/common (= 3.32.0) - Yoga - - RNScreens/common (3.30.1): + - RNScreens/common (3.32.0): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -2008,6 +2015,7 @@ PODS: - React-ImageManager - React-NativeModulesApple - React-RCTFabric + - React-RCTImage - React-rendererdebug - React-utils - ReactCommon/turbomodule/bridging @@ -2552,7 +2560,7 @@ SPEC CHECKSUMS: react-native-airship: 38e2596999242b68c933959d6145512e77937ac0 react-native-blob-util: 1ddace5234c62e3e6e4e154d305ad07ef686599b react-native-cameraroll: f373bebbe9f6b7c3fd2a6f97c5171cda574cf957 - react-native-config: 5330c8258265c1e5fdb8c009d2cabd6badd96727 + react-native-config: 5ce986133b07fc258828b20b9506de0e683efc1c react-native-document-picker: 8532b8af7c2c930f9e202aac484ac785b0f4f809 react-native-geolocation: f9e92eb774cb30ac1e099f34b3a94f03b4db7eb3 react-native-image-picker: f8a13ff106bcc7eb00c71ce11fdc36aac2a44440 @@ -2612,7 +2620,7 @@ SPEC CHECKSUMS: RNPermissions: 0b61d30d21acbeafe25baaa47d9bae40a0c65216 RNReactNativeHapticFeedback: 616c35bdec7d20d4c524a7949ca9829c09e35f37 RNReanimated: 323436b1a5364dca3b5f8b1a13458455e0de9efe - RNScreens: 9ec969a95987a6caae170ef09313138abf3331e1 + RNScreens: abd354e98519ed267600b7ee64fdcb8e060b1218 RNShare: 2a4cdfc0626ad56b0ef583d424f2038f772afe58 RNSound: 6c156f925295bdc83e8e422e7d8b38d33bc71852 RNSVG: 18f1381e046be2f1c30b4724db8d0c966238089f diff --git a/package-lock.json b/package-lock.json index 07b13b5f078a..e9cc310d29e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.0.3-3", + "version": "9.0.3-5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.3-3", + "version": "9.0.3-5", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -115,7 +115,7 @@ "react-native-release-profiler": "^0.1.6", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "4.8.2", - "react-native-screens": "3.30.1", + "react-native-screens": "3.32.0", "react-native-share": "^10.0.2", "react-native-sound": "^0.11.2", "react-native-svg": "14.1.0", @@ -37506,9 +37506,9 @@ } }, "node_modules/react-native-screens": { - "version": "3.30.1", - "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-3.30.1.tgz", - "integrity": "sha512-/muEvjocCtFb+j5J3YmLvB25+f4rIU8hnnxgGTkXcAf2omPBY8uhPjJaaFUlvj64VEoEzJcRpugbXWsjfPPIFg==", + "version": "3.32.0", + "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-3.32.0.tgz", + "integrity": "sha512-wybqZAHX7v8ipOXhh90CqGLkBHw5JYqKNRBX7R/b0c2WQisTOgu0M0yGwBMM6LyXRBT+4k3NTGHdDbpJVpq0yQ==", "dependencies": { "react-freeze": "^1.0.0", "warn-once": "^0.1.0" diff --git a/package.json b/package.json index ddb75aae9700..e5e8f8fa8e60 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.3-3", + "version": "9.0.3-5", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -168,7 +168,7 @@ "react-native-release-profiler": "^0.1.6", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "4.8.2", - "react-native-screens": "3.30.1", + "react-native-screens": "3.32.0", "react-native-share": "^10.0.2", "react-native-sound": "^0.11.2", "react-native-svg": "14.1.0", diff --git a/patches/@expensify+react-native-live-markdown+0.1.88.patch b/patches/@expensify+react-native-live-markdown+0.1.85.patch similarity index 100% rename from patches/@expensify+react-native-live-markdown+0.1.88.patch rename to patches/@expensify+react-native-live-markdown+0.1.85.patch diff --git a/patches/react-native-keyboard-controller+1.12.2.patch b/patches/react-native-keyboard-controller+1.12.2.patch.patch similarity index 100% rename from patches/react-native-keyboard-controller+1.12.2.patch rename to patches/react-native-keyboard-controller+1.12.2.patch.patch diff --git a/patches/react-native-screens+3.30.1+001+fix-screen-type.patch b/patches/react-native-screens+3.30.1+001+fix-screen-type.patch deleted file mode 100644 index f282ec58b07b..000000000000 --- a/patches/react-native-screens+3.30.1+001+fix-screen-type.patch +++ /dev/null @@ -1,12 +0,0 @@ -diff --git a/node_modules/react-native-screens/src/components/Screen.tsx b/node_modules/react-native-screens/src/components/Screen.tsx -index 3f9a1cb..45767f7 100644 ---- a/node_modules/react-native-screens/src/components/Screen.tsx -+++ b/node_modules/react-native-screens/src/components/Screen.tsx -@@ -79,6 +79,7 @@ export class InnerScreen extends React.Component { - // Due to how Yoga resolves layout, we need to have different components for modal nad non-modal screens - const AnimatedScreen = - Platform.OS === 'android' || -+ stackPresentation === undefined || - stackPresentation === 'push' || - stackPresentation === 'containedModal' || - stackPresentation === 'containedTransparentModal' diff --git a/scripts/applyPatches.sh b/scripts/applyPatches.sh deleted file mode 100755 index 31e1b7f50e6a..000000000000 --- a/scripts/applyPatches.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash - -# This script is a simple wrapper around patch-package that fails if any errors or warnings are detected. -# This is useful because patch-package does not fail on errors or warnings by default, -# which means that broken patches are easy to miss, and leads to developer frustration and wasted time. - -SCRIPTS_DIR=$(dirname "${BASH_SOURCE[0]}") -source "$SCRIPTS_DIR/shellUtils.sh" - -# Run patch-package and capture its output and exit code, while still displaying the original output to the terminal -# (we use `script -q /dev/null` to preserve colorization in the output) -TEMP_OUTPUT="$(mktemp)" -script -q /dev/null npx patch-package --error-on-fail 2>&1 | tee "$TEMP_OUTPUT" -EXIT_CODE=${PIPESTATUS[0]} -OUTPUT="$(cat "$TEMP_OUTPUT")" -rm -f "$TEMP_OUTPUT" - -# Check if the output contains a warning message -echo "$OUTPUT" | grep -q "Warning:" -WARNING_FOUND=$? - -printf "\n"; - -# Determine the final exit code -if [ "$EXIT_CODE" -eq 0 ]; then - if [ $WARNING_FOUND -eq 0 ]; then - # patch-package succeeded but warning was found - error "It looks like you upgraded a dependency without upgrading the patch. Please review the patch, determine if it's still needed, and port it to the new version of the dependency." - exit 1 - else - # patch-package succeeded and no warning was found - success "patch-package succeeded without errors or warnings" - exit 0 - fi -else - # patch-package failed - error "patch-package failed to apply a patch" - exit "$EXIT_CODE" -fi diff --git a/scripts/postInstall.sh b/scripts/postInstall.sh index 782c8ef5822c..339fdf25cb10 100755 --- a/scripts/postInstall.sh +++ b/scripts/postInstall.sh @@ -1,14 +1,11 @@ #!/bin/bash -# Exit immediately if any command exits with a non-zero status -set -e - # Go to project root ROOT_DIR=$(dirname "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)") cd "$ROOT_DIR" || exit 1 -# Apply packages using patch-package -scripts/applyPatches.sh +# Run patch-package +npx patch-package # Install node_modules in subpackages, unless we're in a CI/CD environment, # where the node_modules for subpackages are cached separately. diff --git a/src/hooks/usePaginatedReportActions.ts b/src/hooks/usePaginatedReportActions.ts deleted file mode 100644 index 44a82253b7c0..000000000000 --- a/src/hooks/usePaginatedReportActions.ts +++ /dev/null @@ -1,33 +0,0 @@ -import {useMemo} from 'react'; -import {useOnyx} from 'react-native-onyx'; -import * as ReportActionsUtils from '@libs/ReportActionsUtils'; -import ONYXKEYS from '@src/ONYXKEYS'; - -/** - * Get the longest continuous chunk of reportActions including the linked reportAction. If not linking to a specific action, returns the continuous chunk of newest reportActions. - */ -function usePaginatedReportActions(reportID?: string, reportActionID?: string) { - // Use `||` instead of `??` to handle empty string. - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const reportIDWithDefault = reportID || '-1'; - const [sortedAllReportActions = []] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportIDWithDefault}`, { - canEvict: false, - selector: (allReportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, true), - }); - - const reportActions = useMemo(() => { - if (!sortedAllReportActions.length) { - return []; - } - return ReportActionsUtils.getContinuousReportActionChain(sortedAllReportActions, reportActionID); - }, [reportActionID, sortedAllReportActions]); - - const linkedAction = useMemo(() => sortedAllReportActions.find((obj) => String(obj.reportActionID) === String(reportActionID)), [reportActionID, sortedAllReportActions]); - - return { - reportActions, - linkedAction, - }; -} - -export default usePaginatedReportActions; diff --git a/src/libs/API/parameters/OpenPolicyInitialPageParams.ts b/src/libs/API/parameters/OpenPolicyInitialPageParams.ts deleted file mode 100644 index 764abe9a6a77..000000000000 --- a/src/libs/API/parameters/OpenPolicyInitialPageParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -type OpenPolicyInitialPageParams = { - policyID: string; -}; - -export default OpenPolicyInitialPageParams; diff --git a/src/libs/API/parameters/OpenPolicyProfilePageParams.ts b/src/libs/API/parameters/OpenPolicyProfilePageParams.ts deleted file mode 100644 index 55dce33a3dac..000000000000 --- a/src/libs/API/parameters/OpenPolicyProfilePageParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -type OpenPolicyProfilePageParams = { - policyID: string; -}; - -export default OpenPolicyProfilePageParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index ef44ce06bced..f032edf96e36 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -193,8 +193,6 @@ export type {default as OpenPolicyDistanceRatesPageParams} from './OpenPolicyDis export type {default as OpenPolicyTaxesPageParams} from './OpenPolicyTaxesPageParams'; export type {default as EnablePolicyTaxesParams} from './EnablePolicyTaxesParams'; export type {default as OpenPolicyMoreFeaturesPageParams} from './OpenPolicyMoreFeaturesPageParams'; -export type {default as OpenPolicyProfilePageParams} from './OpenPolicyProfilePageParams'; -export type {default as OpenPolicyInitialPageParams} from './OpenPolicyInitialPageParams'; export type {default as CreatePolicyDistanceRateParams} from './CreatePolicyDistanceRateParams'; export type {default as SetPolicyDistanceRatesUnitParams} from './SetPolicyDistanceRatesUnitParams'; export type {default as SetPolicyDistanceRatesDefaultCategoryParams} from './SetPolicyDistanceRatesDefaultCategoryParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 062f0e915873..1d6456f3df47 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -537,8 +537,6 @@ const READ_COMMANDS = { OPEN_POLICY_WORKFLOWS_PAGE: 'OpenPolicyWorkflowsPage', OPEN_POLICY_DISTANCE_RATES_PAGE: 'OpenPolicyDistanceRatesPage', OPEN_POLICY_MORE_FEATURES_PAGE: 'OpenPolicyMoreFeaturesPage', - OPEN_POLICY_PROFILE_PAGE: 'OpenPolicyProfilePage', - OPEN_POLICY_INITIAL_PAGE: 'OpenPolicyInitialPage', OPEN_POLICY_ACCOUNTING_PAGE: 'OpenPolicyAccountingPage', SEARCH: 'Search', OPEN_SUBSCRIPTION_PAGE: 'OpenSubscriptionPage', @@ -587,8 +585,6 @@ type ReadCommandParameters = { [READ_COMMANDS.OPEN_POLICY_WORKFLOWS_PAGE]: Parameters.OpenPolicyWorkflowsPageParams; [READ_COMMANDS.OPEN_POLICY_DISTANCE_RATES_PAGE]: Parameters.OpenPolicyDistanceRatesPageParams; [READ_COMMANDS.OPEN_POLICY_MORE_FEATURES_PAGE]: Parameters.OpenPolicyMoreFeaturesPageParams; - [READ_COMMANDS.OPEN_POLICY_PROFILE_PAGE]: Parameters.OpenPolicyProfilePageParams; - [READ_COMMANDS.OPEN_POLICY_INITIAL_PAGE]: Parameters.OpenPolicyInitialPageParams; [READ_COMMANDS.OPEN_POLICY_ACCOUNTING_PAGE]: Parameters.OpenPolicyAccountingPageParams; [READ_COMMANDS.SEARCH]: Parameters.SearchParams; [READ_COMMANDS.OPEN_SUBSCRIPTION_PAGE]: null; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index b17fe7266079..cf47864a779e 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -331,6 +331,7 @@ type OptimisticTaskReport = Pick< | 'notificationPreference' | 'parentReportActionID' | 'lastVisibleActionCreated' + | 'hasParentAccess' >; type TransactionDetails = { @@ -1080,7 +1081,7 @@ function doesReportBelongToWorkspace(report: OnyxEntry, policyMemberAcco /** * Given an array of reports, return them filtered by a policyID and policyMemberAccountIDs. */ -function filterReportsByPolicyIDAndMemberAccountIDs(reports: Report[], policyMemberAccountIDs: number[] = [], policyID?: string) { +function filterReportsByPolicyIDAndMemberAccountIDs(reports: Array>, policyMemberAccountIDs: number[] = [], policyID?: string) { return reports.filter((report) => !!report && doesReportBelongToWorkspace(report, policyMemberAccountIDs, policyID)); } @@ -1166,6 +1167,7 @@ function findLastAccessedReport( reportMetadata: OnyxCollection = {}, policyID?: string, policyMemberAccountIDs: number[] = [], + excludeReportID?: string, ): OnyxEntry { // If it's the user's first time using New Expensify, then they could either have: // - just a Concierge report, if so we'll return that @@ -1173,7 +1175,7 @@ function findLastAccessedReport( // If it's the latter, we'll use the deeplinked report over the Concierge report, // since the Concierge report would be incorrectly selected over the deep-linked report in the logic below. - let reportsValues = Object.values(reports ?? {}) as Report[]; + let reportsValues = Object.values(reports ?? {}); if (!!policyID || policyMemberAccountIDs.length > 0) { reportsValues = filterReportsByPolicyIDAndMemberAccountIDs(reportsValues, policyMemberAccountIDs, policyID); @@ -1189,13 +1191,28 @@ function findLastAccessedReport( }); } - if (ignoreDomainRooms) { - // We allow public announce rooms, admins, and announce rooms through since we bypass the default rooms beta for them. - // Check where ReportUtils.findLastAccessedReport is called in MainDrawerNavigator.js for more context. - // Domain rooms are now the only type of default room that are on the defaultRooms beta. - sortedReports = sortedReports.filter( - (report) => !isDomainRoom(report) || getPolicyType(report, policies) === CONST.POLICY.TYPE.FREE || hasExpensifyGuidesEmails(Object.keys(report?.participants ?? {}).map(Number)), - ); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const shouldFilter = excludeReportID || ignoreDomainRooms; + if (shouldFilter) { + sortedReports = sortedReports.filter((report) => { + if (excludeReportID && report?.reportID === excludeReportID) { + return false; + } + + // We allow public announce rooms, admins, and announce rooms through since we bypass the default rooms beta for them. + // Check where ReportUtils.findLastAccessedReport is called in MainDrawerNavigator.js for more context. + // Domain rooms are now the only type of default room that are on the defaultRooms beta. + if ( + ignoreDomainRooms && + isDomainRoom(report) && + getPolicyType(report, policies) !== CONST.POLICY.TYPE.FREE && + !hasExpensifyGuidesEmails(Object.keys(report?.participants ?? {}).map(Number)) + ) { + return false; + } + + return true; + }); } if (isFirstTimeNewExpensifyUser) { @@ -2279,7 +2296,15 @@ function getLastVisibleMessage(reportID: string | undefined, actionsToMerge: Rep * @param [parentReportAction] - The parent report action of the report (Used to check if the task has been canceled) */ function isWaitingForAssigneeToCompleteTask(report: OnyxEntry, parentReportAction: OnyxEntry): boolean { - return isTaskReport(report) && isReportManager(report) && isOpenTaskReport(report, parentReportAction); + if (report?.hasOutstandingChildTask) { + return true; + } + + if (isOpenTaskReport(report, parentReportAction) && !report?.hasParentAccess && isReportManager(report)) { + return true; + } + + return false; } function isUnreadWithMention(reportOrOption: OnyxEntry | OptionData): boolean { @@ -5105,6 +5130,7 @@ function buildOptimisticTaskReport( statusNum: CONST.REPORT.STATUS_NUM.OPEN, notificationPreference, lastVisibleActionCreated: DateUtils.getDBTime(), + hasParentAccess: true, }; } diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index d48404349057..b7d365a103ae 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -245,6 +245,8 @@ function getOptionData({ searchText: undefined, isPinned: false, hasOutstandingChildRequest: false, + hasOutstandingChildTask: false, + hasParentAccess: undefined, isIOUReportOwner: null, isChatRoom: false, isArchivedRoom: false, @@ -298,6 +300,8 @@ function getOptionData({ result.isDeletedParentAction = report.isDeletedParentAction; result.isSelfDM = ReportUtils.isSelfDM(report); result.tooltipText = ReportUtils.getReportParticipantsTitle(visibleParticipantAccountIDs); + result.hasOutstandingChildTask = report.hasOutstandingChildTask; + result.hasParentAccess = report.hasParentAccess; const hasMultipleParticipants = participantPersonalDetailList.length > 1 || result.isChatRoom || result.isPolicyExpenseChat || ReportUtils.isExpenseReport(report); const subtitle = ReportUtils.getChatRoomSubtitle(report); diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index b42b0c0d38a6..1bb53fbfa002 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -18,9 +18,7 @@ import type { EnablePolicyWorkflowsParams, LeavePolicyParams, OpenDraftWorkspaceRequestParams, - OpenPolicyInitialPageParams, OpenPolicyMoreFeaturesPageParams, - OpenPolicyProfilePageParams, OpenPolicyTaxesPageParams, OpenPolicyWorkflowsPageParams, OpenWorkspaceInvitePageParams, @@ -2823,18 +2821,6 @@ function openPolicyMoreFeaturesPage(policyID: string) { API.read(READ_COMMANDS.OPEN_POLICY_MORE_FEATURES_PAGE, params); } -function openPolicyProfilePage(policyID: string) { - const params: OpenPolicyProfilePageParams = {policyID}; - - API.read(READ_COMMANDS.OPEN_POLICY_PROFILE_PAGE, params); -} - -function openPolicyInitialPage(policyID: string) { - const params: OpenPolicyInitialPageParams = {policyID}; - - API.read(READ_COMMANDS.OPEN_POLICY_INITIAL_PAGE, params); -} - function setPolicyCustomTaxName(policyID: string, customTaxName: string) { const policy = getPolicy(policyID); const originalCustomTaxName = policy?.taxRates?.name; @@ -3042,8 +3028,6 @@ export { enablePolicyWorkflows, enableDistanceRequestTax, openPolicyMoreFeaturesPage, - openPolicyProfilePage, - openPolicyInitialPage, generateCustomUnitID, clearQBOErrorField, clearXeroErrorField, diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 3761f5b65e0d..31e801deeea4 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -961,6 +961,7 @@ function openReport( function navigateToAndOpenReport( userLogins: string[], shouldDismissModal = true, + actionType?: string, reportName?: string, avatarUri?: string, avatarFile?: File | CustomRNImageManipulatorResult | undefined, @@ -1003,7 +1004,7 @@ function navigateToAndOpenReport( Navigation.dismissModalWithReport(report); } else { Navigation.navigateWithSwitchPolicyID({route: ROUTES.HOME}); - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report?.reportID ?? '-1')); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report?.reportID ?? '-1'), actionType); } } @@ -2011,7 +2012,7 @@ function updateWriteCapabilityAndNavigate(report: Report, newValue: WriteCapabil /** * Navigates to the 1:1 report with Concierge */ -function navigateToConciergeChat(shouldDismissModal = false, checkIfCurrentPageActive = () => true) { +function navigateToConciergeChat(shouldDismissModal = false, checkIfCurrentPageActive = () => true, actionType?: string) { // If conciergeChatReportID contains a concierge report ID, we navigate to the concierge chat using the stored report ID. // Otherwise, we would find the concierge chat and navigate to it. if (!conciergeChatReportID) { @@ -2022,12 +2023,12 @@ function navigateToConciergeChat(shouldDismissModal = false, checkIfCurrentPageA if (!checkIfCurrentPageActive()) { return; } - navigateToAndOpenReport([CONST.EMAIL.CONCIERGE], shouldDismissModal); + navigateToAndOpenReport([CONST.EMAIL.CONCIERGE], shouldDismissModal, actionType); }); } else if (shouldDismissModal) { Navigation.dismissModal(conciergeChatReportID); } else { - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(conciergeChatReportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(conciergeChatReportID), actionType); } } @@ -2555,40 +2556,20 @@ function getCurrentUserAccountID(): number { } function navigateToMostRecentReport(currentReport: OnyxEntry) { - const reportID = currentReport?.reportID; - const sortedReportsByLastRead = ReportUtils.sortReportsByLastRead(Object.values(allReports ?? {}) as Report[], reportMetadata); - - // We want to filter out the current report, hidden reports and empty chats - const filteredReportsByLastRead = sortedReportsByLastRead.filter( - (sortedReport) => - sortedReport?.reportID !== reportID && - sortedReport?.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN && - ReportUtils.shouldReportBeInOptionList({ - report: sortedReport, - currentReportId: '', - isInFocusMode: false, - betas: [], - policies: {}, - excludeEmptyChats: true, - doesReportHaveViolations: false, - includeSelfDM: true, - }), - ); - const lastAccessedReportID = filteredReportsByLastRead.at(-1)?.reportID; - const isChatThread = ReportUtils.isChatThread(currentReport); + const lastAccessedReportID = ReportUtils.findLastAccessedReport(allReports, false, undefined, false, false, reportMetadata, undefined, [], currentReport?.reportID)?.reportID; + if (lastAccessedReportID) { const lastAccessedReportRoute = ROUTES.REPORT_WITH_ID.getRoute(lastAccessedReportID ?? '-1'); Navigation.goBack(lastAccessedReportRoute); } else { - const participantAccountIDs = PersonalDetailsUtils.getAccountIDsByLogins([CONST.EMAIL.CONCIERGE]); - const chat = ReportUtils.getChatByParticipants([...participantAccountIDs, currentUserAccountID]); - if (chat?.reportID) { - // If it is not a chat thread we should call Navigation.goBack to pop the current route first before navigating to Concierge. - if (!isChatThread) { - Navigation.goBack(); - } - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(chat?.reportID), CONST.NAVIGATION.TYPE.UP); + const isChatThread = ReportUtils.isChatThread(currentReport); + + // If it is not a chat thread we should call Navigation.goBack to pop the current route first before navigating to Concierge. + if (!isChatThread) { + Navigation.goBack(); } + + navigateToConciergeChat(false, () => true, CONST.NAVIGATION.TYPE.UP); } } @@ -3230,6 +3211,8 @@ function completeOnboarding( description: taskDescription ?? '', })); + const hasOutstandingChildTask = tasksData.some((task) => !task.completedTaskReportAction); + const tasksForOptimisticData = tasksData.reduce((acc, {currentTask, taskCreatedAction, taskReportAction, taskDescription, completedTaskReportAction}) => { acc.push( { @@ -3252,6 +3235,7 @@ function completeOnboarding( managerID: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, }, isOptimisticReport: true, + managerID: currentUserAccountID, }, }, { @@ -3278,6 +3262,7 @@ function completeOnboarding( value: { stateNum: CONST.REPORT.STATE_NUM.APPROVED, statusNum: CONST.REPORT.STATUS_NUM.APPROVED, + managerID: currentUserAccountID, }, }); } @@ -3362,6 +3347,7 @@ function completeOnboarding( key: `${ONYXKEYS.COLLECTION.REPORT}${targetChatReportID}`, value: { lastMentionedTime: DateUtils.getDBTime(), + hasOutstandingChildTask, }, }, { @@ -3393,6 +3379,7 @@ function completeOnboarding( lastMessageTranslationKey: '', lastMessageText: '', lastVisibleActionCreated: '', + hasOutstandingChildTask: false, }; const {lastMessageText = '', lastMessageTranslationKey = ''} = ReportActionsUtils.getLastVisibleMessage(targetChatReportID); if (lastMessageText || lastMessageTranslationKey) { diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index 83e6716accb9..013ae698ed3f 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -132,12 +132,14 @@ function createTaskAndNavigate( const currentTime = DateUtils.getDBTimeWithSkew(); const lastCommentText = ReportUtils.formatReportLastMessageText(ReportActionsUtils.getReportActionText(optimisticAddCommentReport.reportAction)); + const parentReport = getReport(parentReportID); const optimisticParentReport = { lastVisibleActionCreated: optimisticAddCommentReport.reportAction.created, lastMessageText: lastCommentText, lastActorAccountID: currentUserAccountID, lastReadTime: currentTime, lastMessageTranslationKey: '', + hasOutstandingChildTask: assigneeAccountID === currentUserAccountID ? true : parentReport?.hasOutstandingChildTask, }; // We're only setting onyx data for the task report here because it's possible for the parent report to not exist yet (if you're assigning a task to someone you haven't chatted with before) @@ -272,6 +274,13 @@ function createTaskAndNavigate( }, }, }); + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${parentReportID}`, + value: { + hasOutstandingChildTask: parentReport?.hasOutstandingChildTask, + }, + }); clearOutTaskInfo(); @@ -295,6 +304,29 @@ function createTaskAndNavigate( Report.notifyNewAction(parentReportID, currentUserAccountID); } +/** + * @returns the object to update `report.hasOutstandingChildTask` + */ +function getOutstandingChildTask(taskReport: OnyxEntry) { + const parentReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${taskReport?.parentReportID}`] ?? {}; + return Object.values(parentReportActions).some((reportAction) => { + if (String(reportAction.childReportID) === String(taskReport?.reportID)) { + return false; + } + + if ( + reportAction.childType === CONST.REPORT.TYPE.TASK && + reportAction?.childStateNum === CONST.REPORT.STATE_NUM.OPEN && + reportAction?.childStatusNum === CONST.REPORT.STATUS_NUM.OPEN && + ReportActionsUtils.getReportActionMessage(reportAction)?.isDeletedParentAction + ) { + return true; + } + + return false; + }); +} + /** * Complete a task */ @@ -302,7 +334,7 @@ function completeTask(taskReport: OnyxEntry) { const taskReportID = taskReport?.reportID ?? '-1'; const message = `marked as complete`; const completedTaskReportAction = ReportUtils.buildOptimisticTaskReportAction(taskReportID, CONST.REPORT.ACTIONS.TYPE.TASK_COMPLETED, message); - + const parentReport = getParentReport(taskReport); const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -353,6 +385,24 @@ function completeTask(taskReport: OnyxEntry) { }, ]; + if (parentReport?.hasOutstandingChildTask) { + const hasOutstandingChildTask = getOutstandingChildTask(taskReport); + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${taskReport?.parentReportID}`, + value: { + hasOutstandingChildTask, + }, + }); + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${taskReport?.parentReportID}`, + value: { + hasOutstandingChildTask: parentReport?.hasOutstandingChildTask, + }, + }); + } + const parameters: CompleteTaskParams = { taskReportID, completedTaskReportActionID: completedTaskReportAction.reportActionID, @@ -370,6 +420,8 @@ function reopenTask(taskReport: OnyxEntry) { const taskReportID = taskReport?.reportID ?? '-1'; const message = `marked as incomplete`; const reopenedTaskReportAction = ReportUtils.buildOptimisticTaskReportAction(taskReportID, CONST.REPORT.ACTIONS.TYPE.TASK_REOPENED, message); + const parentReport = getParentReport(taskReport); + const hasOutstandingChildTask = taskReport?.managerID === currentUserAccountID ? true : parentReport?.hasOutstandingChildTask; const optimisticData: OnyxUpdate[] = [ { @@ -384,6 +436,13 @@ function reopenTask(taskReport: OnyxEntry) { lastReadTime: reopenedTaskReportAction.created, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${taskReport?.parentReportID}`, + value: { + hasOutstandingChildTask, + }, + }, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${taskReportID}`, @@ -411,6 +470,13 @@ function reopenTask(taskReport: OnyxEntry) { statusNum: CONST.REPORT.STATUS_NUM.APPROVED, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${taskReport?.parentReportID}`, + value: { + hasOutstandingChildTask: taskReport?.hasOutstandingChildTask, + }, + }, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${taskReportID}`, @@ -566,6 +632,39 @@ function editTaskAssignee(report: OnyxTypes.Report, ownerAccountID: number, assi }, ]; + if (currentUserAccountID === assigneeAccountID) { + const parentReport = getParentReport(report); + if (!isEmptyObject(parentReport)) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${parentReport.reportID}`, + value: {hasOutstandingChildTask: true}, + }); + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${parentReport.reportID}`, + value: {hasOutstandingChildTask: parentReport?.hasOutstandingChildTask}, + }); + } + } + + if (report.managerID === currentUserAccountID) { + const hasOutstandingChildTask = getOutstandingChildTask(report); + const parentReport = getParentReport(report); + if (!isEmptyObject(parentReport)) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${parentReport.reportID}`, + value: {hasOutstandingChildTask}, + }); + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${parentReport.reportID}`, + value: {hasOutstandingChildTask: parentReport?.hasOutstandingChildTask}, + }); + } + } + // If we make a change to the assignee, we want to add a comment to the assignee's chat // Check if the assignee actually changed if (assigneeAccountID && assigneeAccountID !== report.managerID && assigneeAccountID !== ownerAccountID && assigneeChatReport) { @@ -813,6 +912,13 @@ function getParentReport(report: OnyxEntry): OnyxEntry { + return allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; +} + /** * Cancels a task by setting the report state to SUBMITTED and status to CLOSED */ @@ -847,6 +953,7 @@ function deleteTask(report: OnyxEntry) { const optimisticReportActions = { [parentReportAction?.reportActionID ?? '-1']: optimisticReportAction, }; + const hasOutstandingChildTask = getOutstandingChildTask(report); const optimisticData: OnyxUpdate[] = [ { @@ -865,6 +972,7 @@ function deleteTask(report: OnyxEntry) { value: { lastMessageText: ReportActionsUtils.getLastVisibleMessage(parentReport?.reportID ?? '-1', optimisticReportActions as OnyxTypes.ReportActions).lastMessageText ?? '', lastVisibleActionCreated: ReportActionsUtils.getLastVisibleAction(parentReport?.reportID ?? '-1', optimisticReportActions as OnyxTypes.ReportActions)?.created, + hasOutstandingChildTask, }, }, { @@ -927,6 +1035,13 @@ function deleteTask(report: OnyxEntry) { statusNum: report.statusNum ?? '', } as OnyxTypes.Report, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${parentReport?.reportID}`, + value: { + hasOutstandingChildTask: parentReport?.hasOutstandingChildTask, + }, + }, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, diff --git a/src/pages/NewChatConfirmPage.tsx b/src/pages/NewChatConfirmPage.tsx index 78e28cda332f..17e5708803cd 100644 --- a/src/pages/NewChatConfirmPage.tsx +++ b/src/pages/NewChatConfirmPage.tsx @@ -105,7 +105,7 @@ function NewChatConfirmPage({newGroupDraft, allPersonalDetails}: NewChatConfirmP } const logins: string[] = (newGroupDraft.participants ?? []).map((participant) => participant.login); - Report.navigateToAndOpenReport(logins, true, newGroupDraft.reportName ?? '', newGroupDraft.avatarUri ?? '', avatarFile, optimisticReportID.current, true); + Report.navigateToAndOpenReport(logins, true, undefined, newGroupDraft.reportName ?? '', newGroupDraft.avatarUri ?? '', avatarFile, optimisticReportID.current, true); }, [newGroupDraft, avatarFile]); const stashedLocalAvatarImage = newGroupDraft?.avatarUri; diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index b1ef9fbfdd97..dcbad36d1eda 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -22,7 +22,6 @@ import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; -import usePaginatedReportActions from '@hooks/usePaginatedReportActions'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import type {ReportDetailsNavigatorParamList} from '@libs/Navigation/types'; @@ -80,10 +79,21 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD const {translate} = useLocalize(); const {isOffline} = useNetwork(); const styles = useThemeStyles(); + // The app would crash due to subscribing to the entire report collection if parentReportID is an empty string. So we should have a fallback ID here. // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID || '-1'}`); - const {reportActions} = usePaginatedReportActions(report.reportID); + const [sortedAllReportActions = []] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID ?? '-1'}`, { + canEvict: false, + selector: (allReportActions: OnyxEntry) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, true), + }); + + const reportActions = useMemo(() => { + if (!sortedAllReportActions.length) { + return []; + } + return ReportActionsUtils.getContinuousReportActionChain(sortedAllReportActions); + }, [sortedAllReportActions]); const transactionThreadReportID = useMemo( () => ReportActionsUtils.getOneTransactionThreadReportID(report.reportID, reportActions ?? [], isOffline), diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 3076aeb87bd1..3c2ae7bbc6e6 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -26,11 +26,11 @@ import useIsReportOpenInRHP from '@hooks/useIsReportOpenInRHP'; import useLastAccessedReportID from '@hooks/useLastAccessedReportID'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; -import usePaginatedReportActions from '@hooks/usePaginatedReportActions'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; import useViewportOffsetTop from '@hooks/useViewportOffsetTop'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import {getCurrentUserAccountID} from '@libs/actions/Report'; import Timing from '@libs/actions/Timing'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; @@ -68,6 +68,9 @@ type ReportScreenOnyxProps = { /** The policies which the user has access to */ policies: OnyxCollection; + /** An array containing all report actions related to this report, sorted based on a date criterion */ + sortedAllReportActions: OnyxTypes.ReportAction[]; + /** Additional report details */ reportNameValuePairs: OnyxEntry; @@ -116,6 +119,7 @@ function ReportScreen({ betas = [], route, reportNameValuePairs, + sortedAllReportActions, reportMetadata = { isLoadingInitialReportActions: true, isLoadingOlderReportActions: false, @@ -279,14 +283,12 @@ function ReportScreen({ const prevReport = usePrevious(report); const prevUserLeavingStatus = usePrevious(userLeavingStatus); const [isLinkingToMessage, setIsLinkingToMessage] = useState(!!reportActionIDFromRoute); - - const [currentUserAccountID = -1] = useOnyx(ONYXKEYS.SESSION, {selector: (value) => value?.accountID}); - const {reportActions, linkedAction} = usePaginatedReportActions(report.reportID, reportActionIDFromRoute); - const isLinkedActionDeleted = useMemo(() => !!linkedAction && !ReportActionsUtils.shouldReportActionBeVisible(linkedAction, linkedAction.reportActionID), [linkedAction]); - const isLinkedActionInaccessibleWhisper = useMemo( - () => !!linkedAction && ReportActionsUtils.isWhisperAction(linkedAction) && !(linkedAction?.whisperedToAccountIDs ?? []).includes(currentUserAccountID), - [currentUserAccountID, linkedAction], - ); + const reportActions = useMemo(() => { + if (!sortedAllReportActions.length) { + return []; + } + return ReportActionsUtils.getContinuousReportActionChain(sortedAllReportActions, reportActionIDFromRoute); + }, [reportActionIDFromRoute, sortedAllReportActions]); // Define here because reportActions are recalculated before mount, allowing data to display faster than useEffect can trigger. // If we have cached reportActions, they will be shown immediately. @@ -311,6 +313,10 @@ function ReportScreen({ const screenWrapperStyle: ViewStyle[] = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}]; const isEmptyChat = useMemo(() => ReportUtils.isEmptyReport(report), [report]); const isOptimisticDelete = report.statusNum === CONST.REPORT.STATUS_NUM.CLOSED; + const isLinkedMessageAvailable = useMemo( + (): boolean => sortedAllReportActions.findIndex((obj) => String(obj.reportActionID) === String(reportActionIDFromRoute)) > -1, + [sortedAllReportActions, reportActionIDFromRoute], + ); // If there's a non-404 error for the report we should show it instead of blocking the screen const hasHelpfulErrors = Object.keys(report?.errorFields ?? {}).some((key) => key !== 'notFound'); @@ -402,12 +408,12 @@ function ReportScreen({ const isLoading = isLoadingApp ?? (!reportIDFromRoute || (!isSidebarLoaded && !isReportOpenInRHP) || PersonalDetailsUtils.isPersonalDetailsEmpty()); const shouldShowSkeleton = - !linkedAction && + !isLinkedMessageAvailable && (isLinkingToMessage || !isCurrentReportLoadedFromOnyx || (reportActions.length === 0 && !!reportMetadata?.isLoadingInitialReportActions) || isLoading || - (!!reportActionIDFromRoute && !!reportMetadata?.isLoadingInitialReportActions)); + (!!reportActionIDFromRoute && reportMetadata?.isLoadingInitialReportActions)); const shouldShowReportActionList = isCurrentReportLoadedFromOnyx && !isLoading; const currentReportIDFormRoute = route.params?.reportID; @@ -672,16 +678,28 @@ function ReportScreen({ fetchReport(); }, [fetchReport]); + const {isLinkedReportActionDeleted, isInaccessibleWhisper} = useMemo(() => { + const currentUserAccountID = getCurrentUserAccountID(); + if (!reportActionIDFromRoute || !sortedAllReportActions) { + return {isLinkedReportActionDeleted: false, isInaccessibleWhisper: false}; + } + const action = sortedAllReportActions.find((item) => item.reportActionID === reportActionIDFromRoute); + return { + isLinkedReportActionDeleted: action && !ReportActionsUtils.shouldReportActionBeVisible(action, action.reportActionID), + isInaccessibleWhisper: action && ReportActionsUtils.isWhisperAction(action) && !(action?.whisperedToAccountIDs ?? []).includes(currentUserAccountID), + }; + }, [reportActionIDFromRoute, sortedAllReportActions]); + // If user redirects to an inaccessible whisper via a deeplink, on a report they have access to, // then we set reportActionID as empty string, so we display them the report and not the "Not found page". useEffect(() => { - if (!isLinkedActionInaccessibleWhisper) { + if (!isInaccessibleWhisper) { return; } Navigation.isNavigationReady().then(() => { Navigation.setParams({reportActionID: ''}); }); - }, [isLinkedActionInaccessibleWhisper]); + }, [isInaccessibleWhisper]); useEffect(() => { if (!!report.lastReadTime || !ReportUtils.isTaskReport(report)) { @@ -691,7 +709,7 @@ function ReportScreen({ Report.readNewestAction(report.reportID); }, [report]); - if ((!isLinkedActionInaccessibleWhisper && isLinkedActionDeleted) ?? (!shouldShowSkeleton && reportActionIDFromRoute && reportActions?.length === 0 && !isLinkingToMessage)) { + if ((!isInaccessibleWhisper && isLinkedReportActionDeleted) ?? (!shouldShowSkeleton && reportActionIDFromRoute && reportActions?.length === 0 && !isLinkingToMessage)) { return ( `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getReportID(route)}`, + canEvict: false, + selector: (allReportActions: OnyxEntry) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, true), + }, reportNameValuePairs: { key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${getReportID(route)}`, allowStaleData: true, @@ -833,6 +856,7 @@ export default withCurrentReportID( ReportScreen, (prevProps, nextProps) => prevProps.isSidebarLoaded === nextProps.isSidebarLoaded && + lodashIsEqual(prevProps.sortedAllReportActions, nextProps.sortedAllReportActions) && lodashIsEqual(prevProps.reportMetadata, nextProps.reportMetadata) && lodashIsEqual(prevProps.betas, nextProps.betas) && lodashIsEqual(prevProps.policies, nextProps.policies) && diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index 29bd29de7b83..242bb5885d1d 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -1,4 +1,4 @@ -import {useFocusEffect, useNavigationState} from '@react-navigation/native'; +import {useNavigationState} from '@react-navigation/native'; import type {StackScreenProps} from '@react-navigation/stack'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; @@ -87,7 +87,7 @@ function dismissError(policyID: string, pendingAction: PendingAction | undefined } } -function WorkspaceInitialPage({policyDraft, policy: policyProp, reimbursementAccount, policyCategories, route}: WorkspaceInitialPageProps) { +function WorkspaceInitialPage({policyDraft, policy: policyProp, reimbursementAccount, policyCategories}: WorkspaceInitialPageProps) { const styles = useThemeStyles(); const policy = policyDraft?.id ? policyDraft : policyProp; const [isCurrencyModalOpen, setIsCurrencyModalOpen] = useState(false); @@ -135,18 +135,6 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reimbursementAcc setIsCurrencyModalOpen(false); }, [policy?.outputCurrency, isCurrencyModalOpen]); - const fetchPolicyData = useCallback(() => { - Policy.openPolicyInitialPage(route.params.policyID); - }, [route.params.policyID]); - - useNetwork({onReconnect: fetchPolicyData}); - - useFocusEffect( - useCallback(() => { - fetchPolicyData(); - }, [fetchPolicyData]), - ); - /** Call update workspace currency and hide the modal */ const confirmCurrencyChangeAndHideModal = useCallback(() => { Policy.updateGeneralSettings(policyID, policyName, CONST.CURRENCY.USD); diff --git a/src/pages/workspace/WorkspaceProfilePage.tsx b/src/pages/workspace/WorkspaceProfilePage.tsx index 6f2097688b5c..6a86a83b5d11 100644 --- a/src/pages/workspace/WorkspaceProfilePage.tsx +++ b/src/pages/workspace/WorkspaceProfilePage.tsx @@ -1,5 +1,3 @@ -import {useFocusEffect} from '@react-navigation/native'; -import type {StackScreenProps} from '@react-navigation/stack'; import {ExpensiMark} from 'expensify-common'; import React, {useCallback, useState} from 'react'; import type {ImageStyle, StyleProp} from 'react-native'; @@ -17,14 +15,12 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback'; import Section from '@components/Section'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; import useLocalize from '@hooks/useLocalize'; -import useNetwork from '@hooks/useNetwork'; import usePermissions from '@hooks/usePermissions'; import useThemeIllustrations from '@hooks/useThemeIllustrations'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; -import type {FullScreenNavigatorParamList} from '@libs/Navigation/types'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import StringUtils from '@libs/StringUtils'; @@ -33,23 +29,22 @@ import * as Policy from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import withPolicy from './withPolicy'; import type {WithPolicyProps} from './withPolicy'; import WorkspacePageWithSections from './WorkspacePageWithSections'; -type WorkspaceProfilePageOnyxProps = { +type WorkSpaceProfilePageOnyxProps = { /** Constant, list of available currencies */ currencyList: OnyxEntry; }; -type WorkspaceProfilePageProps = WithPolicyProps & WorkspaceProfilePageOnyxProps & StackScreenProps; +type WorkSpaceProfilePageProps = WithPolicyProps & WorkSpaceProfilePageOnyxProps; const parser = new ExpensiMark(); -function WorkspaceProfilePage({policy, currencyList = {}, route}: WorkspaceProfilePageProps) { +function WorkspaceProfilePage({policy, currencyList = {}, route}: WorkSpaceProfilePageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); @@ -87,20 +82,6 @@ function WorkspaceProfilePage({policy, currencyList = {}, route}: WorkspaceProfi const imageStyle: StyleProp = isSmallScreenWidth ? [styles.mhv12, styles.mhn5, styles.mbn5] : [styles.mhv8, styles.mhn8, styles.mbn5]; const shouldShowAddress = !readOnly || formattedAddress; - const fetchPolicyData = useCallback(() => { - Policy.openPolicyProfilePage(route.params.policyID); - }, [route.params.policyID]); - - useNetwork({onReconnect: fetchPolicyData}); - - // We have the same focus effect in the WorkspaceInitialPage, this way we can get the policy data in narrow - // as well as in the wide layout when looking at policy settings. - useFocusEffect( - useCallback(() => { - fetchPolicyData(); - }, [fetchPolicyData]), - ); - const DefaultAvatar = useCallback( () => ( ({ + withOnyx({ currencyList: {key: ONYXKEYS.CURRENCY_LIST}, })(WorkspaceProfilePage), ); diff --git a/src/pages/workspace/withPolicy.tsx b/src/pages/workspace/withPolicy.tsx index 4a908c0928ab..6300d3fcf280 100644 --- a/src/pages/workspace/withPolicy.tsx +++ b/src/pages/workspace/withPolicy.tsx @@ -16,7 +16,6 @@ type PolicyRoute = RouteProp< NavigatorsParamList, | typeof SCREENS.REIMBURSEMENT_ACCOUNT_ROOT | typeof SCREENS.WORKSPACE.INITIAL - | typeof SCREENS.WORKSPACE.PROFILE | typeof SCREENS.WORKSPACE.BILLS | typeof SCREENS.WORKSPACE.MORE_FEATURES | typeof SCREENS.WORKSPACE.MEMBERS diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 9b0b35a6da7c..b5d0a0a15d13 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -77,6 +77,9 @@ type Report = OnyxCommon.OnyxValueWithOfflineFeedback< /** Whether the report has a child that is an outstanding expense that is awaiting action from the current user */ hasOutstandingChildRequest?: boolean; + /** Whether the report has a child task that is awaiting action from the current user */ + hasOutstandingChildTask?: boolean; + /** List of icons for report participants */ icons?: OnyxCommon.Icon[]; diff --git a/tests/ui/UnreadIndicatorsTest.tsx b/tests/ui/UnreadIndicatorsTest.tsx index f84c75823753..b5990ee5d002 100644 --- a/tests/ui/UnreadIndicatorsTest.tsx +++ b/tests/ui/UnreadIndicatorsTest.tsx @@ -200,104 +200,81 @@ let reportAction9CreatedDate: string; /** * Sets up a test with a logged in user that has one unread chat from another user. Returns the test instance. */ -async function signInAndGetAppWithUnreadChat() { +function signInAndGetAppWithUnreadChat(): Promise { // Render the App and sign in as a test user. render(); - await waitForBatchedUpdatesWithAct(); - await waitForBatchedUpdatesWithAct(); - - const hintText = Localize.translateLocal('loginForm.loginForm'); - const loginForm = screen.queryAllByLabelText(hintText); - expect(loginForm).toHaveLength(1); - - await act(async () => { - await TestHelper.signInWithTestUser(USER_A_ACCOUNT_ID, USER_A_EMAIL, undefined, undefined, 'A'); - }); - await waitForBatchedUpdatesWithAct(); - - User.subscribeToUserEvents(); - await waitForBatchedUpdates(); - - const TEN_MINUTES_AGO = subMinutes(new Date(), 10); - reportAction3CreatedDate = format(addSeconds(TEN_MINUTES_AGO, 30), CONST.DATE.FNS_DB_FORMAT_STRING); - reportAction9CreatedDate = format(addSeconds(TEN_MINUTES_AGO, 90), CONST.DATE.FNS_DB_FORMAT_STRING); - - // Simulate setting an unread report and personal details - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, { - reportID: REPORT_ID, - reportName: CONST.REPORT.DEFAULT_REPORT_NAME, - lastReadTime: reportAction3CreatedDate, - lastVisibleActionCreated: reportAction9CreatedDate, - lastMessageText: 'Test', - participants: {[USER_B_ACCOUNT_ID]: {hidden: false}}, - lastActorAccountID: USER_B_ACCOUNT_ID, - type: CONST.REPORT.TYPE.CHAT, - }); - const createdReportActionID = NumberUtils.rand64().toString(); - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, { - [createdReportActionID]: { - actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, - automatic: false, - created: format(TEN_MINUTES_AGO, CONST.DATE.FNS_DB_FORMAT_STRING), - reportActionID: createdReportActionID, - message: [ - { - style: 'strong', - text: '__FAKE__', - type: 'TEXT', - }, - { - style: 'normal', - text: 'created this report', - type: 'TEXT', + return waitForBatchedUpdatesWithAct() + .then(async () => { + await waitForBatchedUpdatesWithAct(); + const hintText = Localize.translateLocal('loginForm.loginForm'); + const loginForm = screen.queryAllByLabelText(hintText); + expect(loginForm).toHaveLength(1); + + await act(async () => { + await TestHelper.signInWithTestUser(USER_A_ACCOUNT_ID, USER_A_EMAIL, undefined, undefined, 'A'); + }); + return waitForBatchedUpdatesWithAct(); + }) + .then(() => { + User.subscribeToUserEvents(); + return waitForBatchedUpdates(); + }) + .then(async () => { + const TEN_MINUTES_AGO = subMinutes(new Date(), 10); + reportAction3CreatedDate = format(addSeconds(TEN_MINUTES_AGO, 30), CONST.DATE.FNS_DB_FORMAT_STRING); + reportAction9CreatedDate = format(addSeconds(TEN_MINUTES_AGO, 90), CONST.DATE.FNS_DB_FORMAT_STRING); + + // Simulate setting an unread report and personal details + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, { + reportID: REPORT_ID, + reportName: CONST.REPORT.DEFAULT_REPORT_NAME, + lastReadTime: reportAction3CreatedDate, + lastVisibleActionCreated: reportAction9CreatedDate, + lastMessageText: 'Test', + participants: {[USER_B_ACCOUNT_ID]: {hidden: false}}, + lastActorAccountID: USER_B_ACCOUNT_ID, + type: CONST.REPORT.TYPE.CHAT, + }); + const createdReportActionID = NumberUtils.rand64().toString(); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, { + [createdReportActionID]: { + actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, + automatic: false, + created: format(TEN_MINUTES_AGO, CONST.DATE.FNS_DB_FORMAT_STRING), + reportActionID: createdReportActionID, + message: [ + { + style: 'strong', + text: '__FAKE__', + type: 'TEXT', + }, + { + style: 'normal', + text: 'created this report', + type: 'TEXT', + }, + ], }, - ], - }, - 1: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 10), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '1', createdReportActionID), - 2: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 20), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '2', '1'), - 3: TestHelper.buildTestReportComment(reportAction3CreatedDate, USER_B_ACCOUNT_ID, '3', '2'), - 4: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 40), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '4', '3'), - 5: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 50), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '5', '4'), - 6: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 60), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '6', '5'), - 7: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 70), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '7', '6'), - 8: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 80), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '8', '7'), - 9: TestHelper.buildTestReportComment(reportAction9CreatedDate, USER_B_ACCOUNT_ID, '9', '8'), - }); - await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, { - [USER_B_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_B_EMAIL, USER_B_ACCOUNT_ID, 'B'), - }); - - // We manually setting the sidebar as loaded since the onLayout event does not fire in tests - AppActions.setSidebarLoaded(); - - await waitForBatchedUpdatesWithAct(); -} - -let lastComment = 'Current User Comment 1'; -async function addComment() { - const num = Number.parseInt(lastComment.slice(-1), 10); - lastComment = `${lastComment.slice(0, -1)}${num + 1}`; - const comment = lastComment; - const reportActionsBefore = (await TestHelper.onyxGet(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`)) as Record; - Report.addComment(REPORT_ID, comment); - const reportActionsAfter = (await TestHelper.onyxGet(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`)) as Record; - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const newReportActionID = Object.keys(reportActionsAfter).find((reportActionID) => !reportActionsBefore[reportActionID])!; - await act(() => - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, { - [newReportActionID]: { - previousReportActionID: '9', - }, - }), - ); - await waitForBatchedUpdatesWithAct(); - - // Verify the comment is visible (it will appear twice, once in the LHN and once on the report screen) - expect(screen.getAllByText(comment)[0]).toBeOnTheScreen(); + 1: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 10), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '1', createdReportActionID), + 2: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 20), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '2', '1'), + 3: TestHelper.buildTestReportComment(reportAction3CreatedDate, USER_B_ACCOUNT_ID, '3', '2'), + 4: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 40), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '4', '3'), + 5: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 50), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '5', '4'), + 6: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 60), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '6', '5'), + 7: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 70), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '7', '6'), + 8: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 80), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '8', '7'), + 9: TestHelper.buildTestReportComment(reportAction9CreatedDate, USER_B_ACCOUNT_ID, '9', '8'), + }); + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, { + [USER_B_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_B_EMAIL, USER_B_ACCOUNT_ID, 'B'), + }); + + // We manually setting the sidebar as loaded since the onLayout event does not fire in tests + AppActions.setSidebarLoaded(); + return waitForBatchedUpdatesWithAct(); + }); } -const newMessageLineIndicatorHintText = Localize.translateLocal('accessibilityHints.newMessageLineIndicator'); - describe('Unread Indicators', () => { afterEach(() => { jest.clearAllMocks(); @@ -342,6 +319,7 @@ describe('Unread Indicators', () => { expect(reportComments).toHaveLength(9); // Since the last read timestamp is the timestamp of action 3 we should have an unread indicator above the next "unread" action which will // have actionID of 4 + const newMessageLineIndicatorHintText = Localize.translateLocal('accessibilityHints.newMessageLineIndicator'); const unreadIndicator = screen.queryAllByLabelText(newMessageLineIndicatorHintText); expect(unreadIndicator).toHaveLength(1); const reportActionID = unreadIndicator[0]?.props?.['data-action-id']; @@ -357,6 +335,7 @@ describe('Unread Indicators', () => { .then(async () => { await act(() => transitionEndCB?.()); // Verify the unread indicator is present + const newMessageLineIndicatorHintText = Localize.translateLocal('accessibilityHints.newMessageLineIndicator'); const unreadIndicator = screen.queryAllByLabelText(newMessageLineIndicatorHintText); expect(unreadIndicator).toHaveLength(1); }) @@ -379,6 +358,7 @@ describe('Unread Indicators', () => { }) .then(() => { // Verify the unread indicator is not present + const newMessageLineIndicatorHintText = Localize.translateLocal('accessibilityHints.newMessageLineIndicator'); const unreadIndicator = screen.queryAllByLabelText(newMessageLineIndicatorHintText); expect(unreadIndicator).toHaveLength(0); // Tap on the chat again @@ -386,6 +366,7 @@ describe('Unread Indicators', () => { }) .then(() => { // Verify the unread indicator is not present + const newMessageLineIndicatorHintText = Localize.translateLocal('accessibilityHints.newMessageLineIndicator'); const unreadIndicator = screen.queryAllByLabelText(newMessageLineIndicatorHintText); expect(unreadIndicator).toHaveLength(0); expect(areYouOnChatListScreen()).toBe(false); @@ -495,6 +476,7 @@ describe('Unread Indicators', () => { }) .then(() => { // Verify the indicator appears above the last action + const newMessageLineIndicatorHintText = Localize.translateLocal('accessibilityHints.newMessageLineIndicator'); const unreadIndicator = screen.queryAllByLabelText(newMessageLineIndicatorHintText); expect(unreadIndicator).toHaveLength(1); const reportActionID = unreadIndicator[0]?.props?.['data-action-id']; @@ -529,6 +511,7 @@ describe('Unread Indicators', () => { return navigateToSidebarOption(0); }) .then(() => { + const newMessageLineIndicatorHintText = Localize.translateLocal('accessibilityHints.newMessageLineIndicator'); const unreadIndicator = screen.queryAllByLabelText(newMessageLineIndicatorHintText); expect(unreadIndicator).toHaveLength(0); @@ -537,23 +520,30 @@ describe('Unread Indicators', () => { return waitFor(() => expect(isNewMessagesBadgeVisible()).toBe(false)); })); - it('Keep showing the new line indicator when a new message is created by the current user', async () => { - await signInAndGetAppWithUnreadChat(); - - // Verify we are on the LHN and that the chat shows as unread in the LHN - expect(areYouOnChatListScreen()).toBe(true); + it('Keep showing the new line indicator when a new message is created by the current user', () => + signInAndGetAppWithUnreadChat() + .then(() => { + // Verify we are on the LHN and that the chat shows as unread in the LHN + expect(areYouOnChatListScreen()).toBe(true); - // Navigate to the report and verify the indicator is present - await navigateToSidebarOption(0); - await act(() => transitionEndCB?.()); - let unreadIndicator = screen.queryAllByLabelText(newMessageLineIndicatorHintText); - expect(unreadIndicator).toHaveLength(1); + // Navigate to the report and verify the indicator is present + return navigateToSidebarOption(0); + }) + .then(async () => { + await act(() => transitionEndCB?.()); + const newMessageLineIndicatorHintText = Localize.translateLocal('accessibilityHints.newMessageLineIndicator'); + const unreadIndicator = screen.queryAllByLabelText(newMessageLineIndicatorHintText); + expect(unreadIndicator).toHaveLength(1); - // Leave a comment as the current user and verify the indicator is not removed - await addComment(); - unreadIndicator = screen.queryAllByLabelText(newMessageLineIndicatorHintText); - expect(unreadIndicator).toHaveLength(1); - }); + // Leave a comment as the current user and verify the indicator is removed + Report.addComment(REPORT_ID, 'Current User Comment 1'); + return waitForBatchedUpdates(); + }) + .then(() => { + const newMessageLineIndicatorHintText = Localize.translateLocal('accessibilityHints.newMessageLineIndicator'); + const unreadIndicator = screen.queryAllByLabelText(newMessageLineIndicatorHintText); + expect(unreadIndicator).toHaveLength(1); + })); xit('Keeps the new line indicator when the user moves the App to the background', () => signInAndGetAppWithUnreadChat() @@ -565,6 +555,7 @@ describe('Unread Indicators', () => { return navigateToSidebarOption(0); }) .then(() => { + const newMessageLineIndicatorHintText = Localize.translateLocal('accessibilityHints.newMessageLineIndicator'); const unreadIndicator = screen.queryAllByLabelText(newMessageLineIndicatorHintText); expect(unreadIndicator).toHaveLength(1); @@ -573,6 +564,7 @@ describe('Unread Indicators', () => { }) .then(() => navigateToSidebarOption(0)) .then(() => { + const newMessageLineIndicatorHintText = Localize.translateLocal('accessibilityHints.newMessageLineIndicator'); const unreadIndicator = screen.queryAllByLabelText(newMessageLineIndicatorHintText); expect(unreadIndicator).toHaveLength(0); @@ -581,6 +573,7 @@ describe('Unread Indicators', () => { return waitForBatchedUpdates(); }) .then(() => { + const newMessageLineIndicatorHintText = Localize.translateLocal('accessibilityHints.newMessageLineIndicator'); let unreadIndicator = screen.queryAllByLabelText(newMessageLineIndicatorHintText); expect(unreadIndicator).toHaveLength(1); @@ -604,10 +597,11 @@ describe('Unread Indicators', () => { signInAndGetAppWithUnreadChat() // Navigate to the chat and simulate leaving a comment from the current user .then(() => navigateToSidebarOption(0)) - .then(() => + .then(() => { // Leave a comment as the current user - addComment(), - ) + Report.addComment(REPORT_ID, 'Current User Comment 1'); + return waitForBatchedUpdates(); + }) .then(() => { // Simulate the response from the server so that the comment can be deleted in this test lastReportAction = reportActions ? CollectionUtils.lastItem(reportActions) : undefined; @@ -625,7 +619,7 @@ describe('Unread Indicators', () => { expect(alternateText).toHaveLength(1); // This message is visible on the sidebar and the report screen, so there are two occurrences. - expect(screen.getAllByText(lastComment)[0]).toBeOnTheScreen(); + expect(screen.getAllByText('Current User Comment 1')[0]).toBeOnTheScreen(); if (lastReportAction) { Report.deleteReportComment(REPORT_ID, lastReportAction); diff --git a/tests/utils/TestHelper.ts b/tests/utils/TestHelper.ts index dffb2b4e312a..9ca0969abc6a 100644 --- a/tests/utils/TestHelper.ts +++ b/tests/utils/TestHelper.ts @@ -1,6 +1,5 @@ import {Str} from 'expensify-common'; import Onyx from 'react-native-onyx'; -import type {ConnectOptions, OnyxKey} from 'react-native-onyx'; import CONST from '@src/CONST'; import * as Session from '@src/libs/actions/Session'; import HttpUtils from '@src/libs/HttpUtils'; @@ -248,33 +247,5 @@ const createAddListenerMock = () => { return {triggerTransitionEnd, addListener}; }; -/** - * Get an Onyx value. Only for use in tests for now. - */ -async function onyxGet(key: OnyxKey): Promise>['callback']>[0]> { - return new Promise((resolve) => { - // eslint-disable-next-line rulesdir/prefer-onyx-connect-in-libs - // @ts-expect-error This does not need more strict type checking as it's only for tests - const connectionID = Onyx.connect({ - key, - callback: (value) => { - Onyx.disconnect(connectionID); - resolve(value); - }, - waitForCollectionCallback: true, - }); - }); -} - export type {MockFetch, FormData}; -export { - assertFormDataMatchesObject, - buildPersonalDetails, - buildTestReportComment, - createAddListenerMock, - getGlobalFetchMock, - setPersonalDetails, - signInWithTestUser, - signOutTestUser, - onyxGet, -}; +export {assertFormDataMatchesObject, buildPersonalDetails, buildTestReportComment, createAddListenerMock, getGlobalFetchMock, setPersonalDetails, signInWithTestUser, signOutTestUser};