diff --git a/.env.example b/.env.example index 2bdda890b2ef..3f516bdab889 100644 --- a/.env.example +++ b/.env.example @@ -30,6 +30,7 @@ EXPENSIFY_ACCOUNT_ID_RECEIPTS=-1 EXPENSIFY_ACCOUNT_ID_REWARDS=-1 EXPENSIFY_ACCOUNT_ID_STUDENT_AMBASSADOR=-1 EXPENSIFY_ACCOUNT_ID_SVFG=-1 +EXPENSIFY_ACCOUNT_ID_MANAGER_MCTEST=-1 FB_API_KEY=YOUR_API_KEY FB_APP_ID=YOUR_APP_ID diff --git a/.eslintrc.changed.js b/.eslintrc.changed.js index b2c9a46df078..356ad936e1fd 100644 --- a/.eslintrc.changed.js +++ b/.eslintrc.changed.js @@ -20,18 +20,6 @@ module.exports = { ], }, overrides: [ - { - files: [ - 'src/libs/actions/IOU.ts', - 'src/libs/actions/Report.ts', - 'src/pages/workspace/WorkspaceInitialPage.tsx', - 'src/pages/home/report/PureReportActionItem.tsx', - 'src/libs/SidebarUtils.ts', - ], - rules: { - 'rulesdir/no-default-id-values': 'off', - }, - }, { files: ['**/libs/**/*.{ts,tsx}'], rules: { diff --git a/.eslintrc.js b/.eslintrc.js index fefad92ce29d..aa98b7bdc464 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -3,7 +3,18 @@ const path = require('path'); const restrictedImportPaths = [ { name: 'react-native', - importNames: ['useWindowDimensions', 'StatusBar', 'TouchableOpacity', 'TouchableWithoutFeedback', 'TouchableNativeFeedback', 'TouchableHighlight', 'Pressable', 'Text', 'ScrollView'], + importNames: [ + 'useWindowDimensions', + 'StatusBar', + 'TouchableOpacity', + 'TouchableWithoutFeedback', + 'TouchableNativeFeedback', + 'TouchableHighlight', + 'Pressable', + 'Text', + 'ScrollView', + 'Animated', + ], message: [ '', "For 'useWindowDimensions', please use '@src/hooks/useWindowDimensions' instead.", @@ -11,6 +22,7 @@ const restrictedImportPaths = [ "For 'StatusBar', please use '@libs/StatusBar' instead.", "For 'Text', please use '@components/Text' instead.", "For 'ScrollView', please use '@components/ScrollView' instead.", + "For 'Animated', please use 'Animated' from 'react-native-reanimated' instead.", ].join('\n'), }, { @@ -75,6 +87,10 @@ const restrictedImportPaths = [ importNames: ['memoize'], message: "Please use '@src/libs/memoize' instead.", }, + { + name: 'react-native-animatable', + message: "Please use 'react-native-reanimated' instead.", + }, ]; const restrictedImportPatterns = [ @@ -134,6 +150,10 @@ module.exports = { { selector: ['variable', 'property'], format: ['camelCase', 'UPPER_CASE', 'PascalCase'], + filter: { + regex: '^private_[a-z][a-zA-Z0-9]+$', + match: false, + }, }, { selector: 'function', diff --git a/.github/ISSUE_TEMPLATE/TooltipsTemplate.md b/.github/ISSUE_TEMPLATE/TooltipsTemplate.md new file mode 100644 index 000000000000..882d88f6ccfb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/TooltipsTemplate.md @@ -0,0 +1,28 @@ +--- +name: 'Tooltips Template' +about: Create this issue when adding a new tooltip to New Expensify +labels: Daily, Design, WaitingForCopy +title: 'Tooltips Template' +--- +Refer to https://stackoverflowteams.com/c/expensify/questions/20762 for the full process to add a tooltip. + +### Problem +Enter the problem that currently exists without the tooltip. + +### Solution +Enter the solution that implementing the tooltip will achieve. + +### What is the purpose of the tooltip? +Enter the purpose. + +### How should the tooltip look +Add the Figma Mock Up that Design builds. + +### Condition +We should show this tooltip to: + +### Decide the prioritisation + +Priority score: + +NOTE: Only one tooltip is shown at a time. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 1446f1e4d851..2dfd9348d961 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -22,6 +22,13 @@ $ PROPOSAL: + + ### Tests + + + \ No newline at end of file diff --git a/assets/images/chatbubble-slash.svg b/assets/images/chatbubble-slash.svg new file mode 100644 index 000000000000..09d2b5bd3149 --- /dev/null +++ b/assets/images/chatbubble-slash.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/assets/images/magnifying-glass-spy-mouth-closed.svg b/assets/images/magnifying-glass-spy-mouth-closed.svg new file mode 100644 index 000000000000..d5b46a70270f --- /dev/null +++ b/assets/images/magnifying-glass-spy-mouth-closed.svg @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/electronBuilder.config.js b/config/electronBuilder.config.js index ad3a23407b89..5bfa9b75c463 100644 --- a/config/electronBuilder.config.js +++ b/config/electronBuilder.config.js @@ -42,9 +42,6 @@ module.exports = { entitlements: 'desktop/entitlements.mac.plist', entitlementsInherit: 'desktop/entitlements.mac.plist', type: 'distribution', - notarize: { - teamId: '368M544MTT', - }, target: [ { target: 'default', diff --git a/config/webpack/webpack.common.ts b/config/webpack/webpack.common.ts index e044c324837f..d50fa927fa95 100644 --- a/config/webpack/webpack.common.ts +++ b/config/webpack/webpack.common.ts @@ -179,7 +179,7 @@ const getCommonConfiguration = ({file = '.env', platform = 'web'}: Environment): // We are importing this worker as a string by using asset/source otherwise it will default to loading via an HTTPS request later. // This causes issues if we have gone offline before the pdfjs web worker is set up as we won't be able to load it from the server. { - test: new RegExp('node_modules/pdfjs-dist/build/pdf.worker.min.mjs'), + test: new RegExp('node_modules/pdfjs-dist/legacy/build/pdf.worker.min.mjs'), type: 'asset/source', }, diff --git a/contributingGuides/PERFORMANCE_METRICS.md b/contributingGuides/PERFORMANCE_METRICS.md index 9e942f21d918..d8bf79970006 100644 --- a/contributingGuides/PERFORMANCE_METRICS.md +++ b/contributingGuides/PERFORMANCE_METRICS.md @@ -24,6 +24,7 @@ Project is using Firebase for tracking these metrics. However, not all of them a | `open_report_from_preview` | ✅ | Time taken to open a report from preview.

(previously `switch_report_from_preview`)

**Platforms:** All | Starts when the user presses the Report Preview. | Stops when the `ReportActionsList` finishes laying out. | | `open_report_thread` | ✅ | Time taken to open a thread in a report.

**Platforms:** All | Starts when user presses Report Action Item. | Stops when the `ReportActionsList` finishes laying out. | | `send_message` | ✅ | Time taken to send a message.

**Platforms:** All | Starts when the new message is sent. | Stops when the message is being rendered in the chat. | +| `pusher_ping_pong` | ✅ | The time it takes to receive a PONG event through Pusher.

**Platforms:** All | Starts every minute and repeats on the minute. | Stops when the event is received from the server. | ## Documentation Maintenance diff --git a/contributingGuides/TS_CHEATSHEET.md b/contributingGuides/TS_CHEATSHEET.md index 77d316bb861d..e8c899a0aafb 100644 --- a/contributingGuides/TS_CHEATSHEET.md +++ b/contributingGuides/TS_CHEATSHEET.md @@ -101,22 +101,46 @@ - [1.4](#animated-style) **Animated styles** - ```ts - import {useRef} from 'react'; - import {Animated, StyleProp, ViewStyle} from 'react-native'; +The recommended approach to creating animations is by using the `react-native-reanimated` library, +as it offers greater efficiency and convenience compared to using the `Animated` API directly from +React Native. + ```ts + import React from 'react'; + import { View, StyleSheet, StyleProp, ViewStyle } from 'react-native'; + import Animated, { useAnimatedStyle, useSharedValue, withTiming, SharedValue, WithTimingConfig } from 'react-native-reanimated'; + type MyComponentProps = { - style?: Animated.WithAnimatedValue>; + opacity: Animated.SharedValue; + }; + + const MyComponent = ({ opacity }: MyComponentProps) => { + const animatedStyle = useAnimatedStyle(() => ({ + opacity: opacity.value, + })); + + return ( + + ); + }; + + const App = () => { + const opacity = useSharedValue(0); + + const startAnimation = () => { + opacity.value = withTiming(1, { + duration: 1000, + easing: Easing.inOut(Easing.quad), + }); + }; + + return ( + + + diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx index d1dcdb2f57f5..6354b69bc58e 100644 --- a/src/components/ReportActionItem/MoneyReportView.tsx +++ b/src/components/ReportActionItem/MoneyReportView.tsx @@ -16,12 +16,25 @@ import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as CurrencyUtils from '@libs/CurrencyUtils'; +import {convertToDisplayString} from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; -import * as ReportUtils from '@libs/ReportUtils'; +import { + getAvailableReportFields, + getFieldViolation, + getFieldViolationTranslation, + getMoneyRequestSpendBreakdown, + getReportFieldKey, + hasUpdatedTotal, + isClosedExpenseReportWithNoExpenses as isClosedExpenseReportWithNoExpensesReportUtils, + isInvoiceReport as isInvoiceReportUtils, + isPaidGroupPolicyExpenseReport as isPaidGroupPolicyExpenseReportUtils, + isReportFieldDisabled, + isReportFieldOfTypeTitle, + isSettled as isSettledReportUtils, +} from '@libs/ReportUtils'; import AnimatedEmptyStateBackground from '@pages/home/report/AnimatedEmptyStateBackground'; import variables from '@styles/variables'; -import * as reportActions from '@src/libs/actions/Report'; +import {clearReportFieldKeyErrors} from '@src/libs/actions/Report'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Policy, PolicyReportField, Report} from '@src/types/onyx'; @@ -29,7 +42,7 @@ import type {PendingAction} from '@src/types/onyx/OnyxCommon'; type MoneyReportViewProps = { /** The report currently being looked at */ - report: Report; + report: OnyxEntry; /** Policy that the report belongs to */ policy: OnyxEntry; @@ -52,15 +65,15 @@ function MoneyReportView({report, policy, isCombinedReport = false, shouldShowTo const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const {isOffline} = useNetwork(); - const isSettled = ReportUtils.isSettled(report.reportID); - const isTotalUpdated = ReportUtils.hasUpdatedTotal(report, policy); + const isSettled = isSettledReportUtils(report?.reportID); + const isTotalUpdated = hasUpdatedTotal(report, policy); - const {totalDisplaySpend, nonReimbursableSpend, reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(report); + const {totalDisplaySpend, nonReimbursableSpend, reimbursableSpend} = getMoneyRequestSpendBreakdown(report); const shouldShowBreakdown = nonReimbursableSpend && reimbursableSpend && shouldShowTotal; - const formattedTotalAmount = CurrencyUtils.convertToDisplayString(totalDisplaySpend, report.currency); - const formattedOutOfPocketAmount = CurrencyUtils.convertToDisplayString(reimbursableSpend, report.currency); - const formattedCompanySpendAmount = CurrencyUtils.convertToDisplayString(nonReimbursableSpend, report.currency); + const formattedTotalAmount = convertToDisplayString(totalDisplaySpend, report?.currency); + const formattedOutOfPocketAmount = convertToDisplayString(reimbursableSpend, report?.currency); + const formattedCompanySpendAmount = convertToDisplayString(nonReimbursableSpend, report?.currency); const isPartiallyPaid = !!report?.pendingFields?.partial; const subAmountTextStyles: StyleProp = [ @@ -70,25 +83,25 @@ function MoneyReportView({report, policy, isCombinedReport = false, shouldShowTo StyleUtils.getColorStyle(theme.textSupporting), ]; - const [violations] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_VIOLATIONS}${report.reportID}`); + const [violations] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_VIOLATIONS}${report?.reportID}`); const sortedPolicyReportFields = useMemo((): PolicyReportField[] => { - const fields = ReportUtils.getAvailableReportFields(report, Object.values(policy?.fieldList ?? {})); - return fields.filter((field) => field.target === report.type).sort(({orderWeight: firstOrderWeight}, {orderWeight: secondOrderWeight}) => firstOrderWeight - secondOrderWeight); + const fields = getAvailableReportFields(report, Object.values(policy?.fieldList ?? {})); + return fields.filter((field) => field.target === report?.type).sort(({orderWeight: firstOrderWeight}, {orderWeight: secondOrderWeight}) => firstOrderWeight - secondOrderWeight); }, [policy, report]); - const enabledReportFields = sortedPolicyReportFields.filter((reportField) => !ReportUtils.isReportFieldDisabled(report, reportField, policy)); - const isOnlyTitleFieldEnabled = enabledReportFields.length === 1 && ReportUtils.isReportFieldOfTypeTitle(enabledReportFields.at(0)); - const isClosedExpenseReportWithNoExpenses = ReportUtils.isClosedExpenseReportWithNoExpenses(report); - const isPaidGroupPolicyExpenseReport = ReportUtils.isPaidGroupPolicyExpenseReport(report); - const isInvoiceReport = ReportUtils.isInvoiceReport(report); + const enabledReportFields = sortedPolicyReportFields.filter((reportField) => !isReportFieldDisabled(report, reportField, policy)); + const isOnlyTitleFieldEnabled = enabledReportFields.length === 1 && isReportFieldOfTypeTitle(enabledReportFields.at(0)); + const isClosedExpenseReportWithNoExpenses = isClosedExpenseReportWithNoExpensesReportUtils(report); + const isPaidGroupPolicyExpenseReport = isPaidGroupPolicyExpenseReportUtils(report); + const isInvoiceReport = isInvoiceReportUtils(report); const shouldShowReportField = !isClosedExpenseReportWithNoExpenses && (isPaidGroupPolicyExpenseReport || isInvoiceReport) && (!isCombinedReport || !isOnlyTitleFieldEnabled); const renderThreadDivider = useMemo( () => shouldHideThreadDividerLine && !isCombinedReport ? ( ) : ( @@ -97,7 +110,7 @@ function MoneyReportView({report, policy, isCombinedReport = false, shouldShowTo style={[!shouldHideThreadDividerLine ? styles.reportHorizontalRule : {}]} /> ), - [shouldHideThreadDividerLine, report.reportID, styles.reportHorizontalRule, isCombinedReport], + [shouldHideThreadDividerLine, report?.reportID, styles.reportHorizontalRule, isCombinedReport], ); return ( @@ -110,39 +123,34 @@ function MoneyReportView({report, policy, isCombinedReport = false, shouldShowTo policy?.areReportFieldsEnabled && (!isCombinedReport || !isOnlyTitleFieldEnabled) && sortedPolicyReportFields.map((reportField) => { - if (ReportUtils.isReportFieldOfTypeTitle(reportField)) { + if (isReportFieldOfTypeTitle(reportField)) { return null; } const fieldValue = reportField.value ?? reportField.defaultValue; - const isFieldDisabled = ReportUtils.isReportFieldDisabled(report, reportField, policy); - const fieldKey = ReportUtils.getReportFieldKey(reportField.fieldID); + const isFieldDisabled = isReportFieldDisabled(report, reportField, policy); + const fieldKey = getReportFieldKey(reportField.fieldID); - const violation = ReportUtils.getFieldViolation(violations, reportField); - const violationTranslation = ReportUtils.getFieldViolationTranslation(reportField, violation); + const violation = getFieldViolation(violations, reportField); + const violationTranslation = getFieldViolationTranslation(reportField, violation); return ( reportActions.clearReportFieldKeyErrors(report.reportID, fieldKey)} + onClose={() => clearReportFieldKeyErrors(report?.reportID, fieldKey)} > + onPress={() => { Navigation.navigate( - ROUTES.EDIT_REPORT_FIELD_REQUEST.getRoute( - report.reportID, - report.policyID ?? '-1', - reportField.fieldID, - Navigation.getReportRHPActiveRoute(), - ), - ) - } + ROUTES.EDIT_REPORT_FIELD_REQUEST.getRoute(report?.reportID, report?.policyID, reportField.fieldID, Navigation.getReportRHPActiveRoute()), + ); + }} shouldShowRightIcon disabled={isFieldDisabled} wrapperStyle={[styles.pv2, styles.taskDescriptionMenuItem]} diff --git a/src/components/ReportActionItem/MoneyRequestAction.tsx b/src/components/ReportActionItem/MoneyRequestAction.tsx index af54e2940d3f..2936fddd0376 100644 --- a/src/components/ReportActionItem/MoneyRequestAction.tsx +++ b/src/components/ReportActionItem/MoneyRequestAction.tsx @@ -1,14 +1,18 @@ import React from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import type {OnyxEntry} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import RenderHTML from '@components/RenderHTML'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as IOUUtils from '@libs/IOUUtils'; +import {isIOUReportPendingCurrencyConversion} from '@libs/IOUUtils'; import Navigation from '@libs/Navigation/Navigation'; -import * as ReportActionsUtils from '@libs/ReportActionsUtils'; +import { + isDeletedParentAction as isDeletedParentActionReportActionsUtils, + isReversedTransaction as isReversedTransactionReportActionsUtils, + isSplitBillAction as isSplitBillActionReportActionsUtils, + isTrackExpenseAction as isTrackExpenseActionReportActionsUtils, +} from '@libs/ReportActionsUtils'; import type {ContextMenuAnchor} from '@pages/home/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; @@ -18,29 +22,18 @@ import type * as OnyxTypes from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import MoneyRequestPreview from './MoneyRequestPreview'; -type MoneyRequestActionOnyxProps = { - /** Chat report associated with iouReport */ - chatReport: OnyxEntry; - - /** IOU report data object */ - iouReport: OnyxEntry; - - /** Report actions for this report */ - reportActions: OnyxEntry; -}; - -type MoneyRequestActionProps = MoneyRequestActionOnyxProps & { +type MoneyRequestActionProps = { /** All the data of the action */ action: OnyxTypes.ReportAction; /** The ID of the associated chatReport */ - chatReportID: string; + chatReportID: string | undefined; /** The ID of the associated expense report */ - requestReportID: string; + requestReportID: string | undefined; /** The ID of the current report */ - reportID: string; + reportID: string | undefined; /** Is this IOUACTION the most recent? */ isMostRecentIOUReportAction: boolean; @@ -72,34 +65,33 @@ function MoneyRequestAction({ isMostRecentIOUReportAction, contextMenuAnchor, checkIfContextMenuActive = () => {}, - chatReport, - iouReport, - reportActions, isHovered = false, style, isWhisper = false, shouldDisplayContextMenu = true, }: MoneyRequestActionProps) { + const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`); + const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${requestReportID}`); + const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`, {canEvict: false}); + const styles = useThemeStyles(); const {translate} = useLocalize(); const {isOffline} = useNetwork(); - const isSplitBillAction = ReportActionsUtils.isSplitBillAction(action); - const isTrackExpenseAction = ReportActionsUtils.isTrackExpenseAction(action); + const isSplitBillAction = isSplitBillActionReportActionsUtils(action); + const isTrackExpenseAction = isTrackExpenseActionReportActionsUtils(action); const onMoneyRequestPreviewPressed = () => { if (isSplitBillAction) { - const reportActionID = action.reportActionID ?? '-1'; - Navigation.navigate(ROUTES.SPLIT_BILL_DETAILS.getRoute(chatReportID, reportActionID, Navigation.getReportRHPActiveRoute())); + Navigation.navigate(ROUTES.SPLIT_BILL_DETAILS.getRoute(chatReportID, action.reportActionID, Navigation.getReportRHPActiveRoute())); return; } - const childReportID = action?.childReportID ?? '-1'; - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(action?.childReportID)); }; let shouldShowPendingConversionMessage = false; - const isDeletedParentAction = ReportActionsUtils.isDeletedParentAction(action); - const isReversedTransaction = ReportActionsUtils.isReversedTransaction(action); + const isDeletedParentAction = isDeletedParentActionReportActionsUtils(action); + const isReversedTransaction = isReversedTransactionReportActionsUtils(action); if ( !isEmptyObject(iouReport) && !isEmptyObject(reportActions) && @@ -108,7 +100,7 @@ function MoneyRequestAction({ action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && isOffline ) { - shouldShowPendingConversionMessage = IOUUtils.isIOUReportPendingCurrencyConversion(iouReport); + shouldShowPendingConversionMessage = isIOUReportPendingCurrencyConversion(iouReport); } if (isDeletedParentAction || isReversedTransaction) { @@ -118,7 +110,7 @@ function MoneyRequestAction({ } else { message = 'parentReportAction.deletedExpense'; } - return ${translate(message)}`} />; + return ${translate(message)}`} />; } return ( ({ - chatReport: { - key: ({chatReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, - }, - iouReport: { - key: ({requestReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${requestReportID}`, - }, - reportActions: { - key: ({chatReportID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`, - canEvict: false, - }, -})(MoneyRequestAction); +export default MoneyRequestAction; diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx index 17874c9dd148..e20fe09058e1 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx @@ -103,9 +103,9 @@ function MoneyRequestPreviewContent({ const route = useRoute>(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); - const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID || CONST.DEFAULT_NUMBER_ID}`); + const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`); const [session] = useOnyx(ONYXKEYS.SESSION); - const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID || CONST.DEFAULT_NUMBER_ID}`); + const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`); const policy = usePolicy(iouReport?.policyID); const isMoneyRequestAction = isMoneyRequestActionReportActionsUtils(action); diff --git a/src/components/ReportActionItem/MoneyRequestPreview/index.tsx b/src/components/ReportActionItem/MoneyRequestPreview/index.tsx index f902948b2cb5..0b9d4e5f5629 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/index.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/index.tsx @@ -6,7 +6,7 @@ import MoneyRequestPreviewContent from './MoneyRequestPreviewContent'; import type {MoneyRequestPreviewProps} from './types'; function MoneyRequestPreview(props: MoneyRequestPreviewProps) { - const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${props.iouReportID || '-1'}`); + const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${props.iouReportID}`); // We should not render the component if there is no iouReport and it's not a split or track expense. // Moved outside of the component scope to allow for easier use of hooks in the main component. // eslint-disable-next-line react/jsx-props-no-spreading diff --git a/src/components/ReportActionItem/MoneyRequestPreview/types.ts b/src/components/ReportActionItem/MoneyRequestPreview/types.ts index c40b45c6d2bd..186c81a8c866 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/types.ts +++ b/src/components/ReportActionItem/MoneyRequestPreview/types.ts @@ -7,13 +7,13 @@ type MoneyRequestPreviewProps = { /** The active IOUReport, used for Onyx subscription */ // The iouReportID is used inside withOnyx HOC // eslint-disable-next-line react/no-unused-prop-types - iouReportID: string; + iouReportID: string | undefined; /** The associated chatReport */ - chatReportID: string; + chatReportID: string | undefined; /** The ID of the current report */ - reportID: string; + reportID: string | undefined; /** Callback for the preview pressed */ onPreviewPressed: (event?: GestureResponderEvent | KeyboardEvent) => void; diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 8f59cf311d86..10c72dbd8841 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -65,7 +65,7 @@ import AnimatedEmptyStateBackground from '@pages/home/report/AnimatedEmptyStateB import {cleanUpMoneyRequest, updateMoneyRequestBillable} from '@userActions/IOU'; import {navigateToConciergeChatAndDeleteReport} from '@userActions/Report'; import {clearAllRelatedReportActionErrors} from '@userActions/ReportActions'; -import {clearError} from '@userActions/Transaction'; +import {clearError, getLastModifiedExpense, revert} from '@userActions/Transaction'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -510,6 +510,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals clearAllRelatedReportActionErrors(report.reportID, parentReportAction); return; } + revert(transaction?.transactionID ?? linkedTransactionID, getLastModifiedExpense(report?.reportID)); clearError(transaction.transactionID); clearAllRelatedReportActionErrors(report.reportID, parentReportAction); }} diff --git a/src/components/ReportActionItem/ReportActionItemImages.tsx b/src/components/ReportActionItem/ReportActionItemImages.tsx index 9995b7f77860..633dca077070 100644 --- a/src/components/ReportActionItem/ReportActionItemImages.tsx +++ b/src/components/ReportActionItem/ReportActionItemImages.tsx @@ -1,4 +1,5 @@ /* eslint-disable react/no-array-index-key */ +import {Str} from 'expensify-common'; import React from 'react'; import {View} from 'react-native'; import {Polygon, Svg} from 'react-native-svg'; @@ -53,13 +54,17 @@ function ReportActionItemImages({images, size, total, isHovered = false, onPress const MAX_REMAINING = 9; // The height varies depending on the number of images we are displaying. - let heightStyle = {}; + let maxHeightStyle = {}; + let minHeightStyle = {}; if (numberOfShownImages === 1) { - heightStyle = StyleUtils.getHeight(variables.reportActionImagesSingleImageHeight); + maxHeightStyle = StyleUtils.getMaximumHeight(variables.reportActionImagesSingleImageHeight); + minHeightStyle = StyleUtils.getMinimumHeight(variables.reportActionImagesSingleImageHeight); } else if (numberOfShownImages === 2) { - heightStyle = StyleUtils.getHeight(variables.reportActionImagesDoubleImageHeight); + maxHeightStyle = StyleUtils.getMaximumHeight(variables.reportActionImagesDoubleImageHeight); + minHeightStyle = StyleUtils.getMinimumHeight(variables.reportActionImagesDoubleImageHeight); } else if (numberOfShownImages > 2) { - heightStyle = StyleUtils.getHeight(variables.reportActionImagesMultipleImageHeight); + maxHeightStyle = StyleUtils.getMaximumHeight(variables.reportActionImagesMultipleImageHeight); + minHeightStyle = StyleUtils.getMinimumHeight(variables.reportActionImagesMultipleImageHeight); } const hoverStyle = isHovered ? styles.reportPreviewBoxHoverBorder : undefined; @@ -68,13 +73,13 @@ function ReportActionItemImages({images, size, total, isHovered = false, onPress return ( - - - {shownImages.map(({thumbnail, isThumbnail, image, isEmptyReceipt, transaction, isLocalFile, fileExtension, filename}, index) => { - // Show a border to separate multiple images. Shown to the right for each except the last. - const shouldShowBorder = shownImages.length > 1 && index < shownImages.length - 1; - const borderStyle = shouldShowBorder ? styles.reportActionItemImageBorder : {}; - return ( + + {shownImages.map(({thumbnail, isThumbnail, image, isEmptyReceipt, transaction, isLocalFile, fileExtension, filename}, index) => { + // Show a border to separate multiple images. Shown to the right for each except the last. + const shouldShowBorder = shownImages.length > 1 && index < shownImages.length - 1; + const borderStyle = shouldShowBorder ? styles.reportActionItemImageBorder : {}; + return ( + - ); - })} - + + ); + })} {remaining > 0 && ( diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 47520c6b77c8..e975bcb94481 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -56,7 +56,7 @@ import { hasWarningTypeViolations, isAllowedToApproveExpenseReport, isAllowedToSubmitDraftExpenseReport, - isArchivedReport, + isArchivedReportWithID, isInvoiceReport as isInvoiceReportUtils, isInvoiceRoom as isInvoiceRoomReportUtils, isPayAtEndExpenseReport, @@ -64,10 +64,11 @@ import { isReportApproved, isReportOwner, isSettled, + isTripRoom as isTripRoomReportUtils, + reportTransactionsSelector, } from '@libs/ReportUtils'; import StringUtils from '@libs/StringUtils'; import { - getAllReportTransactions, getDescription, getMerchant, getTransactionViolations, @@ -97,13 +98,13 @@ type ReportPreviewProps = { action: ReportAction; /** The associated chatReport */ - chatReportID: string; + chatReportID: string | undefined; /** The active IOUReport, used for Onyx subscription */ iouReportID: string | undefined; /** The report's policyID, used for Onyx subscription */ - policyID: string; + policyID: string | undefined; /** Extra styles to pass to View wrapper */ containerStyles?: StyleProp; @@ -143,7 +144,9 @@ function ReportPreview({ const policy = usePolicy(policyID); const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`); const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`); - const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION); + const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, { + selector: (_transactions) => reportTransactionsSelector(_transactions, iouReportID), + }); const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); const [userWallet] = useOnyx(ONYXKEYS.USER_WALLET); const [invoiceReceiverPolicy] = useOnyx( @@ -157,7 +160,6 @@ function ReportPreview({ const styles = useThemeStyles(); const {translate} = useLocalize(); const {isOffline} = useNetwork(); - const allTransactions = useMemo(() => getAllReportTransactions(iouReportID, transactions), [iouReportID, transactions]); const {hasMissingSmartscanFields, areAllRequestsBeingSmartScanned, hasOnlyTransactionsWithPendingRoutes, hasNonReimbursableTransactions} = useMemo( () => ({ @@ -179,8 +181,8 @@ function ReportPreview({ const getCanIOUBePaid = useCallback( (onlyShowPayElsewhere = false, shouldCheckApprovedState = true) => - canIOUBePaidIOUActions(iouReport, chatReport, policy, allTransactions, onlyShowPayElsewhere, undefined, undefined, shouldCheckApprovedState), - [iouReport, chatReport, policy, allTransactions], + canIOUBePaidIOUActions(iouReport, chatReport, policy, transactions, onlyShowPayElsewhere, undefined, undefined, shouldCheckApprovedState), + [iouReport, chatReport, policy, transactions], ); const canIOUBePaid = useMemo(() => getCanIOUBePaid(), [getCanIOUBePaid]); @@ -193,7 +195,6 @@ function ReportPreview({ const {nonHeldAmount, fullAmount, hasValidNonHeldAmount} = getNonHeldAndFullAmount(iouReport, shouldShowPayButton); const hasOnlyHeldExpenses = hasOnlyHeldExpensesReportUtils(iouReport?.reportID); - const hasHeldExpenses = hasHeldExpensesReportUtils(iouReport?.reportID); const managerID = iouReport?.managerID ?? action.childManagerAccountID ?? CONST.DEFAULT_NUMBER_ID; const {totalDisplaySpend, reimbursableSpend} = getMoneyRequestSpendBreakdown(iouReport); @@ -215,9 +216,10 @@ function ReportPreview({ const moneyRequestComment = action?.childLastMoneyRequestComment ?? ''; const isPolicyExpenseChat = isPolicyExpenseChatReportUtils(chatReport); const isInvoiceRoom = isInvoiceRoomReportUtils(chatReport); + const isTripRoom = isTripRoomReportUtils(chatReport); const canAllowSettlement = hasUpdatedTotal(iouReport, policy); - const numberOfRequests = allTransactions.length; + const numberOfRequests = transactions?.length ?? 0; const transactionsWithReceipts = getTransactionsWithReceipts(iouReportID); const numberOfScanningReceipts = transactionsWithReceipts.filter((transaction) => isReceiptBeingScanned(transaction)).length; const numberOfPendingRequests = transactionsWithReceipts.filter((transaction) => isPending(transaction) && isCardTransaction(transaction)).length; @@ -232,21 +234,23 @@ function ReportPreview({ hasWarningTypeViolations(iouReportID, transactionViolations, true) || (isReportOwner(iouReport) && hasReportViolations(iouReportID)) || hasActionsWithErrors(iouReportID); - const lastThreeTransactions = allTransactions.slice(-3); + const lastThreeTransactions = transactions?.slice(-3) ?? []; + const lastTransaction = transactions?.at(0); const lastThreeReceipts = lastThreeTransactions.map((transaction) => ({...getThumbnailAndImageURIs(transaction), transaction})); - const showRTERViolationMessage = numberOfRequests === 1 && hasPendingUI(allTransactions.at(0), getTransactionViolations(allTransactions.at(0)?.transactionID, transactionViolations)); - const transactionIDList = [allTransactions.at(0)?.transactionID].filter((transactionID): transactionID is string => transactionID !== undefined); + const transactionIDList = transactions?.map((reportTransaction) => reportTransaction.transactionID) ?? []; + const showRTERViolationMessage = numberOfRequests === 1 && hasPendingUI(lastTransaction, getTransactionViolations(lastTransaction?.transactionID, transactionViolations)); const shouldShowBrokenConnectionViolation = numberOfRequests === 1 && shouldShowBrokenConnectionViolationTransactionUtils(transactionIDList, iouReport, policy); - let formattedMerchant = numberOfRequests === 1 ? getMerchant(allTransactions.at(0)) : null; - const formattedDescription = numberOfRequests === 1 ? getDescription(allTransactions.at(0)) : null; + let formattedMerchant = numberOfRequests === 1 ? getMerchant(lastTransaction) : null; + const formattedDescription = numberOfRequests === 1 ? getDescription(lastTransaction) : null; if (isPartialMerchant(formattedMerchant ?? '')) { formattedMerchant = null; } - const isArchived = isArchivedReport(iouReport); + const isArchived = isArchivedReportWithID(iouReport?.reportID); const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; - const shouldShowSubmitButton = canSubmitReport(iouReport, policy, transactionIDList, transactionViolations); + const filteredTransactions = transactions?.filter((transaction) => transaction) ?? []; + const shouldShowSubmitButton = canSubmitReport(iouReport, policy, filteredTransactions, transactionViolations); const shouldDisableSubmitButton = shouldShowSubmitButton && !isAllowedToSubmitDraftExpenseReport(iouReport); @@ -367,7 +371,7 @@ function ReportPreview({ } let payerOrApproverName; - if (isPolicyExpenseChat) { + if (isPolicyExpenseChat || isTripRoom) { payerOrApproverName = getPolicyName(chatReport, undefined, policy); } else if (isInvoiceRoom) { payerOrApproverName = getInvoicePayerName(chatReport, invoiceReceiverPolicy, invoiceReceiverPersonalDetail); @@ -389,22 +393,23 @@ function ReportPreview({ }, [ isScanning, isPolicyExpenseChat, - policy, - chatReport, + isTripRoom, isInvoiceRoom, - invoiceReceiverPolicy, - invoiceReceiverPersonalDetail, - managerID, isApproved, iouSettled, iouReport?.isWaitingOnBankAccount, hasNonReimbursableTransactions, translate, + chatReport, + policy, + invoiceReceiverPolicy, + invoiceReceiverPersonalDetail, + managerID, ]); const bankAccountRoute = getBankAccountRoute(chatReport); - const shouldShowSettlementButton = (shouldShowPayButton || shouldShowApproveButton) && !showRTERViolationMessage && !shouldShowBrokenConnectionViolation; + const shouldShowSettlementButton = !shouldShowSubmitButton && (shouldShowPayButton || shouldShowApproveButton) && !showRTERViolationMessage && !shouldShowBrokenConnectionViolation; const shouldPromptUserToAddBankAccount = (hasMissingPaymentMethod(userWallet, iouReportID) || hasMissingInvoiceBankAccount(iouReportID)) && !isSettled(iouReportID); const shouldShowRBR = hasErrors && !iouSettled; @@ -423,7 +428,7 @@ function ReportPreview({ const shouldShowScanningSubtitle = (numberOfScanningReceipts === 1 && numberOfRequests === 1) || (numberOfScanningReceipts >= 1 && Number(nonHeldAmount) === 0); const shouldShowPendingSubtitle = numberOfPendingRequests === 1 && numberOfRequests === 1; - const isPayAtEndExpense = isPayAtEndExpenseReport(iouReportID, allTransactions); + const isPayAtEndExpense = isPayAtEndExpenseReport(iouReportID, transactions); const [archiveReason] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportID}`, {selector: getArchiveReason}); const getPendingMessageProps: () => PendingMessageProps = () => { @@ -545,7 +550,7 @@ function ReportPreview({ {lastThreeReceipts.length > 0 && ( )} @@ -618,7 +623,6 @@ function ReportPreview({ {shouldShowSettlementButton && ( ; /** The chat report associated with taskReport */ - chatReportID: string; + chatReportID: string | undefined; /** Popover context menu anchor, used for showing context menu */ contextMenuAnchor: ContextMenuAnchor; @@ -74,27 +74,27 @@ function TaskPreview({taskReportID, action, contextMenuAnchor, chatReportID, che const isTaskCompleted = !isEmptyObject(taskReport) ? taskReport?.stateNum === CONST.REPORT.STATE_NUM.APPROVED && taskReport.statusNum === CONST.REPORT.STATUS_NUM.APPROVED : action?.childStateNum === CONST.REPORT.STATE_NUM.APPROVED && action?.childStatusNum === CONST.REPORT.STATUS_NUM.APPROVED; - const taskTitle = Str.htmlEncode(TaskUtils.getTaskTitleFromReport(taskReport, action?.childReportName ?? '')); - const taskAssigneeAccountID = Task.getTaskAssigneeAccountID(taskReport) ?? action?.childManagerAccountID ?? CONST.DEFAULT_NUMBER_ID; + const taskTitle = Str.htmlEncode(getTaskTitleFromReport(taskReport, action?.childReportName ?? '')); + const taskAssigneeAccountID = getTaskAssigneeAccountID(taskReport) ?? action?.childManagerAccountID ?? CONST.DEFAULT_NUMBER_ID; const taskOwnerAccountID = taskReport?.ownerAccountID ?? action?.actorAccountID ?? CONST.DEFAULT_NUMBER_ID; const hasAssignee = taskAssigneeAccountID > 0; const personalDetails = usePersonalDetails(); const avatar = personalDetails?.[taskAssigneeAccountID]?.avatar ?? Expensicons.FallbackAvatar; const avatarSize = CONST.AVATAR_SIZE.SMALL; - const isDeletedParentAction = ReportUtils.isCanceledTaskReport(taskReport, action); + const isDeletedParentAction = isCanceledTaskReport(taskReport, action); const iconWrapperStyle = StyleUtils.getTaskPreviewIconWrapper(hasAssignee ? avatarSize : undefined); const titleStyle = StyleUtils.getTaskPreviewTitleStyle(iconWrapperStyle.height, isTaskCompleted); - const shouldShowGreenDotIndicator = ReportUtils.isOpenTaskReport(taskReport, action) && ReportUtils.isReportManager(taskReport); + const shouldShowGreenDotIndicator = isOpenTaskReport(taskReport, action) && isReportManager(taskReport); if (isDeletedParentAction) { - return ${translate('parentReportAction.deletedTask')}`} />; + return ${translate('parentReportAction.deletedTask')}`} />; } return ( Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(taskReportID))} - onPressIn={() => DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} + onPressIn={() => canUseTouchScreen() && ControlSelection.block()} onPressOut={() => ControlSelection.unblock()} onLongPress={(event) => showContextMenuForReport(event, contextMenuAnchor, chatReportID, action, checkIfContextMenuActive)} shouldUseHapticsOnLongPress @@ -107,12 +107,12 @@ function TaskPreview({taskReportID, action, contextMenuAnchor, chatReportID, che { + disabled={!canActionTask(taskReport, currentUserPersonalDetails.accountID, taskOwnerAccountID, taskAssigneeAccountID)} + onPress={checkIfActionIsAllowed(() => { if (isTaskCompleted) { - Task.reopenTask(taskReport, taskReportID); + reopenTask(taskReport, taskReportID); } else { - Task.completeTask(taskReport, taskReportID); + completeTask(taskReport, taskReportID); } })} accessibilityLabel={translate('task.task')} diff --git a/src/components/ReportActionItem/TaskView.tsx b/src/components/ReportActionItem/TaskView.tsx index 7901426b33e0..3e077c2bda4a 100644 --- a/src/components/ReportActionItem/TaskView.tsx +++ b/src/components/ReportActionItem/TaskView.tsx @@ -1,5 +1,6 @@ import React, {useEffect} from 'react'; import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import Checkbox from '@components/Checkbox'; import Hoverable from '@components/Hoverable'; import Icon from '@components/Icon'; @@ -17,18 +18,18 @@ import useThemeStyles from '@hooks/useThemeStyles'; import convertToLTR from '@libs/convertToLTR'; import getButtonState from '@libs/getButtonState'; import Navigation from '@libs/Navigation/Navigation'; -import * as OptionsListUtils from '@libs/OptionsListUtils'; -import * as ReportUtils from '@libs/ReportUtils'; -import * as TaskUtils from '@libs/TaskUtils'; -import * as Session from '@userActions/Session'; -import * as Task from '@userActions/Task'; +import {getAvatarsForAccountIDs, getPersonalDetailsForAccountIDs} from '@libs/OptionsListUtils'; +import {getDisplayNameForParticipant, getDisplayNamesWithTooltips, isCompletedTaskReport, isOpenTaskReport} from '@libs/ReportUtils'; +import {isActiveTaskEditRoute} from '@libs/TaskUtils'; +import {checkIfActionIsAllowed} from '@userActions/Session'; +import {canActionTask as canActionTaskUtil, canModifyTask as canModifyTaskUtil, clearTaskErrors, completeTask, reopenTask, setTaskReport} from '@userActions/Task'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {Report} from '@src/types/onyx'; type TaskViewProps = { /** The report currently being looked at */ - report: Report; + report: OnyxEntry; }; function TaskView({report}: TaskViewProps) { @@ -37,17 +38,14 @@ function TaskView({report}: TaskViewProps) { const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const personalDetails = usePersonalDetails(); useEffect(() => { - Task.setTaskReport(report); + setTaskReport(report); }, [report]); - const taskTitle = convertToLTR(report.reportName ?? ''); - const assigneeTooltipDetails = ReportUtils.getDisplayNamesWithTooltips( - OptionsListUtils.getPersonalDetailsForAccountIDs(report.managerID ? [report.managerID] : [], personalDetails), - false, - ); - const isOpen = ReportUtils.isOpenTaskReport(report); - const isCompleted = ReportUtils.isCompletedTaskReport(report); - const canModifyTask = Task.canModifyTask(report, currentUserPersonalDetails.accountID); - const canActionTask = Task.canActionTask(report, currentUserPersonalDetails.accountID); + const taskTitle = convertToLTR(report?.reportName ?? ''); + const assigneeTooltipDetails = getDisplayNamesWithTooltips(getPersonalDetailsForAccountIDs(report?.managerID ? [report?.managerID] : [], personalDetails), false); + const isOpen = isOpenTaskReport(report); + const isCompleted = isCompletedTaskReport(report); + const canModifyTask = canModifyTaskUtil(report, currentUserPersonalDetails.accountID); + const canActionTask = canActionTaskUtil(report, currentUserPersonalDetails.accountID); const disableState = !canModifyTask; const isDisableInteractive = !canModifyTask || !isOpen; const {translate} = useLocalize(); @@ -56,14 +54,14 @@ function TaskView({report}: TaskViewProps) { Task.clearTaskErrors(report.reportID)} + errors={report?.errorFields?.editTask ?? report?.errorFields?.createTask} + onClose={() => clearTaskErrors(report?.reportID)} errorRowStyles={styles.ph5} > {(hovered) => ( { + onPress={checkIfActionIsAllowed((e) => { if (isDisableInteractive) { return; } @@ -71,7 +69,7 @@ function TaskView({report}: TaskViewProps) { (e.currentTarget as HTMLElement).blur(); } - Navigation.navigate(ROUTES.TASK_TITLE.getRoute(report.reportID, Navigation.getReportRHPActiveRoute())); + Navigation.navigate(ROUTES.TASK_TITLE.getRoute(report?.reportID, Navigation.getReportRHPActiveRoute())); })} style={({pressed}) => [ styles.ph5, @@ -83,19 +81,19 @@ function TaskView({report}: TaskViewProps) { disabled={isDisableInteractive} > {({pressed}) => ( - + {translate('task.title')} { + onPress={checkIfActionIsAllowed(() => { // If we're already navigating to these task editing pages, early return not to mark as completed, otherwise we would have not found page. - if (TaskUtils.isActiveTaskEditRoute(report.reportID)) { + if (isActiveTaskEditRoute(report?.reportID)) { return; } if (isCompleted) { - Task.reopenTask(report); + reopenTask(report); } else { - Task.completeTask(report); + completeTask(report); } })} isChecked={isCompleted} @@ -129,12 +127,12 @@ function TaskView({report}: TaskViewProps) { )} - + Navigation.navigate(ROUTES.REPORT_DESCRIPTION.getRoute(report.reportID, Navigation.getReportRHPActiveRoute()))} + title={report?.description ?? ''} + onPress={() => Navigation.navigate(ROUTES.REPORT_DESCRIPTION.getRoute(report?.reportID, Navigation.getReportRHPActiveRoute()))} shouldShowRightIcon={!isDisableInteractive} disabled={disableState} wrapperStyle={[styles.pv2, styles.taskDescriptionMenuItem]} @@ -144,16 +142,16 @@ function TaskView({report}: TaskViewProps) { shouldUseDefaultCursorWhenDisabled /> - - {report.managerID ? ( + + {report?.managerID ? ( Navigation.navigate(ROUTES.TASK_ASSIGNEE.getRoute(report.reportID, Navigation.getReportRHPActiveRoute()))} + onPress={() => Navigation.navigate(ROUTES.TASK_ASSIGNEE.getRoute(report?.reportID, Navigation.getReportRHPActiveRoute()))} shouldShowRightIcon={!isDisableInteractive} disabled={disableState} wrapperStyle={[styles.pv2]} @@ -166,7 +164,7 @@ function TaskView({report}: TaskViewProps) { ) : ( Navigation.navigate(ROUTES.TASK_ASSIGNEE.getRoute(report.reportID, Navigation.getReportRHPActiveRoute()))} + onPress={() => Navigation.navigate(ROUTES.TASK_ASSIGNEE.getRoute(report?.reportID, Navigation.getReportRHPActiveRoute()))} shouldShowRightIcon={!isDisableInteractive} disabled={disableState} wrapperStyle={[styles.pv2]} diff --git a/src/components/ReportActionItem/TripDetailsView.tsx b/src/components/ReportActionItem/TripDetailsView.tsx index e4137f74c276..72d71b39b9d7 100644 --- a/src/components/ReportActionItem/TripDetailsView.tsx +++ b/src/components/ReportActionItem/TripDetailsView.tsx @@ -15,10 +15,11 @@ import Navigation from '@libs/Navigation/Navigation'; import variables from '@styles/variables'; import * as Expensicons from '@src/components/Icon/Expensicons'; import CONST from '@src/CONST'; -import * as ReportUtils from '@src/libs/ReportUtils'; -import * as TripReservationUtils from '@src/libs/TripReservationUtils'; +import type {ReservationData} from '@src/libs/TripReservationUtils'; +import {getReservationsFromTripTransactions, getTripReservationIcon} from '@src/libs/TripReservationUtils'; import ROUTES from '@src/ROUTES'; import type {Reservation, ReservationTimeDetails} from '@src/types/onyx/Transaction'; +import type Transaction from '@src/types/onyx/Transaction'; type ReservationViewProps = { reservation: Reservation; @@ -33,7 +34,7 @@ function ReservationView({reservation, transactionID, tripRoomReportID, reservat const StyleUtils = useStyleUtils(); const {shouldUseNarrowLayout} = useResponsiveLayout(); - const reservationIcon = TripReservationUtils.getTripReservationIcon(reservation.type); + const reservationIcon = getTripReservationIcon(reservation.type); const formatAirportInfo = (reservationTimeDetails: ReservationTimeDetails) => { const longName = reservationTimeDetails?.longName ? `${reservationTimeDetails?.longName} ` : ''; @@ -140,14 +141,16 @@ type TripDetailsViewProps = { /** Whether we should display the horizontal rule below the component */ shouldShowHorizontalRule: boolean; + + /** Trip transactions associated with the report */ + tripTransactions: Transaction[]; }; -function TripDetailsView({tripRoomReportID, shouldShowHorizontalRule}: TripDetailsViewProps) { +function TripDetailsView({tripRoomReportID, shouldShowHorizontalRule, tripTransactions}: TripDetailsViewProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const tripTransactions = ReportUtils.getTripTransactions(tripRoomReportID); - const reservationsData: TripReservationUtils.ReservationData[] = TripReservationUtils.getReservationsFromTripTransactions(tripTransactions); + const reservationsData: ReservationData[] = getReservationsFromTripTransactions(tripTransactions); return ( diff --git a/src/components/ReportActionItem/TripRoomPreview.tsx b/src/components/ReportActionItem/TripRoomPreview.tsx index 764979651ee7..d85c19d21ee0 100644 --- a/src/components/ReportActionItem/TripRoomPreview.tsx +++ b/src/components/ReportActionItem/TripRoomPreview.tsx @@ -1,5 +1,5 @@ import React, {useMemo} from 'react'; -import type {StyleProp, ViewStyle} from 'react-native'; +import type {ListRenderItemInfo, StyleProp, ViewStyle} from 'react-native'; import {FlatList, View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import Button from '@components/Button'; @@ -13,13 +13,15 @@ import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import useTripTransactions from '@hooks/useTripTransactions'; import ControlSelection from '@libs/ControlSelection'; -import * as CurrencyUtils from '@libs/CurrencyUtils'; +import {convertToDisplayString} from '@libs/CurrencyUtils'; import DateUtils from '@libs/DateUtils'; -import * as DeviceCapabilities from '@libs/DeviceCapabilities'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import Navigation from '@libs/Navigation/Navigation'; -import * as ReportUtils from '@libs/ReportUtils'; -import * as TripReservationUtils from '@libs/TripReservationUtils'; +import {getMoneyRequestSpendBreakdown} from '@libs/ReportUtils'; +import type {ReservationData} from '@libs/TripReservationUtils'; +import {getReservationsFromTripTransactions, getTripReservationIcon} from '@libs/TripReservationUtils'; import type {ContextMenuAnchor} from '@pages/home/report/ContextMenu/ReportActionContextMenu'; import variables from '@styles/variables'; import * as Expensicons from '@src/components/Icon/Expensicons'; @@ -34,7 +36,7 @@ type TripRoomPreviewProps = { action: ReportAction; /** The associated chatReport */ - chatReportID: string; + chatReportID: string | undefined; /** Extra styles to pass to View wrapper */ containerStyles?: StyleProp; @@ -59,7 +61,7 @@ function ReservationView({reservation}: ReservationViewProps) { const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); - const reservationIcon = TripReservationUtils.getTripReservationIcon(reservation.type); + const reservationIcon = getTripReservationIcon(reservation.type); const title = reservation.type === CONST.RESERVATION_TYPE.CAR ? reservation.carInfo?.name : reservation.start.longName; let titleComponent = ( @@ -109,27 +111,27 @@ function ReservationView({reservation}: ReservationViewProps) { ); } -const renderItem = ({item}: {item: TripReservationUtils.ReservationData}) => ; +const renderItem = ({item}: ListRenderItemInfo) => ; function TripRoomPreview({action, chatReportID, containerStyles, contextMenuAnchor, isHovered = false, checkIfContextMenuActive = () => {}}: TripRoomPreviewProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`); const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReport?.iouReportID}`); + const tripTransactions = useTripTransactions(chatReportID); - const tripTransactions = ReportUtils.getTripTransactions(chatReport?.reportID); - const reservationsData: TripReservationUtils.ReservationData[] = TripReservationUtils.getReservationsFromTripTransactions(tripTransactions); + const reservationsData: ReservationData[] = getReservationsFromTripTransactions(tripTransactions); const dateInfo = chatReport?.tripData ? DateUtils.getFormattedDateRange(new Date(chatReport.tripData.startDate), new Date(chatReport.tripData.endDate)) : ''; - const {totalDisplaySpend} = ReportUtils.getMoneyRequestSpendBreakdown(chatReport); + const {totalDisplaySpend} = getMoneyRequestSpendBreakdown(chatReport); const currency = iouReport?.currency ?? chatReport?.currency; const displayAmount = useMemo(() => { if (totalDisplaySpend) { - return CurrencyUtils.convertToDisplayString(totalDisplaySpend, currency); + return convertToDisplayString(totalDisplaySpend, currency); } - return CurrencyUtils.convertToDisplayString( - tripTransactions.reduce((acc, transaction) => acc + Math.abs(transaction.amount), 0), + return convertToDisplayString( + tripTransactions?.reduce((acc, transaction) => acc + Math.abs(transaction.amount), 0), currency, ); }, [currency, totalDisplaySpend, tripTransactions]); @@ -142,7 +144,7 @@ function TripRoomPreview({action, chatReportID, containerStyles, contextMenuAnch > DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} + onPressIn={() => canUseTouchScreen() && ControlSelection.block()} onPressOut={() => ControlSelection.unblock()} onLongPress={(event) => showContextMenuForReport(event, contextMenuAnchor, chatReportID, action, checkIfContextMenuActive)} shouldUseHapticsOnLongPress diff --git a/src/components/ScreenWrapper.tsx b/src/components/ScreenWrapper.tsx index 464509b0a947..b22b4eac3fc6 100644 --- a/src/components/ScreenWrapper.tsx +++ b/src/components/ScreenWrapper.tsx @@ -12,9 +12,10 @@ import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets'; import useTackInputFocus from '@hooks/useTackInputFocus'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import * as Browser from '@libs/Browser'; +import {isMobile, isMobileWebKit, isSafari} from '@libs/Browser'; import type {PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types'; import type {AuthScreensParamList, RootStackParamList} from '@libs/Navigation/types'; +import addViewportResizeListener from '@libs/VisualViewport'; import toggleTestToolsModal from '@userActions/TestTool'; import CONST from '@src/CONST'; import CustomDevMenu from './CustomDevMenu'; @@ -22,6 +23,7 @@ import FocusTrapForScreens from './FocusTrap/FocusTrapForScreen'; import type FocusTrapForScreenProps from './FocusTrap/FocusTrapForScreen/FocusTrapProps'; import HeaderGap from './HeaderGap'; import ImportedStateIndicator from './ImportedStateIndicator'; +import {useInputBlurContext} from './InputBlurContext'; import KeyboardAvoidingView from './KeyboardAvoidingView'; import ModalContext from './Modal/ModalContext'; import OfflineIndicator from './OfflineIndicator'; @@ -159,12 +161,13 @@ function ScreenWrapper( const {isDevelopment} = useEnvironment(); const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); const maxHeight = shouldEnableMaxHeight ? windowHeight : undefined; - const minHeight = shouldEnableMinHeight && !Browser.isSafari() ? initialHeight : undefined; + const minHeight = shouldEnableMinHeight && !isSafari() ? initialHeight : undefined; const route = useRoute(); const shouldReturnToOldDot = useMemo(() => { return !!route?.params && 'singleNewDotEntry' in route.params && route.params.singleNewDotEntry === 'true'; }, [route?.params]); + const {isBlurred, setIsBlurred} = useInputBlurContext(); UNSTABLE_usePreventRemove(shouldReturnToOldDot, () => { NativeModules.HybridAppModule?.closeReactNativeApp(false, false); @@ -181,7 +184,7 @@ function ScreenWrapper( PanResponder.create({ onMoveShouldSetPanResponderCapture: (_e, gestureState) => { const isHorizontalSwipe = Math.abs(gestureState.dx) > Math.abs(gestureState.dy); - const shouldDismissKeyboard = shouldDismissKeyboardBeforeClose && Keyboard.isVisible() && Browser.isMobile(); + const shouldDismissKeyboard = shouldDismissKeyboardBeforeClose && Keyboard.isVisible() && isMobile(); return isHorizontalSwipe && shouldDismissKeyboard; }, @@ -189,6 +192,27 @@ function ScreenWrapper( }), ).current; + useEffect(() => { + /** + * Handler to manage viewport resize events specific to Safari. + * Disables the blur state when Safari is detected. + */ + const handleViewportResize = () => { + if (!isSafari()) { + return; // Exit early if not Safari + } + setIsBlurred(false); // Disable blur state for Safari + }; + + // Add the viewport resize listener + const removeResizeListener = addViewportResizeListener(handleViewportResize); + + // Cleanup function to remove the listener + return () => { + removeResizeListener(); + }; + }, [setIsBlurred]); + useEffect(() => { // On iOS, the transitionEnd event doesn't trigger some times. As such, we need to set a timeout const timeout = setTimeout(() => { @@ -249,7 +273,7 @@ function ScreenWrapper( paddingStyle.paddingBottom = unmodifiedPaddings.bottom; } - const isAvoidingViewportScroll = useTackInputFocus(isFocused && shouldEnableMaxHeight && shouldAvoidScrollOnVirtualViewport && Browser.isMobileWebKit()); + const isAvoidingViewportScroll = useTackInputFocus(isFocused && shouldEnableMaxHeight && shouldAvoidScrollOnVirtualViewport && isMobileWebKit()); const contextValue = useMemo( () => ({didScreenTransitionEnd, isSafeAreaTopPaddingApplied, isSafeAreaBottomPaddingApplied: includeSafeAreaPaddingBottom}), [didScreenTransitionEnd, includeSafeAreaPaddingBottom, isSafeAreaTopPaddingApplied], @@ -271,7 +295,7 @@ function ScreenWrapper( {...keyboardDismissPanResponder.panHandlers} > diff --git a/src/components/Search/SearchFiltersChatsSelector.tsx b/src/components/Search/SearchFiltersChatsSelector.tsx index 30c202295c87..8ee48d0895f6 100644 --- a/src/components/Search/SearchFiltersChatsSelector.tsx +++ b/src/components/Search/SearchFiltersChatsSelector.tsx @@ -68,7 +68,7 @@ function SearchFiltersChatsSelector({initialReportIDs, onFiltersUpdate, isScreen const chatOptions = useMemo(() => { return OptionsListUtils.filterAndOrderOptions(defaultOptions, cleanSearchTerm, { selectedOptions, - excludeLogins: CONST.EXPENSIFY_EMAILS, + excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT, }); }, [defaultOptions, cleanSearchTerm, selectedOptions]); diff --git a/src/components/Search/SearchFiltersParticipantsSelector.tsx b/src/components/Search/SearchFiltersParticipantsSelector.tsx index 6fec134a608a..fc12a20d758f 100644 --- a/src/components/Search/SearchFiltersParticipantsSelector.tsx +++ b/src/components/Search/SearchFiltersParticipantsSelector.tsx @@ -61,7 +61,7 @@ function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}: }, { selectedOptions, - excludeLogins: CONST.EXPENSIFY_EMAILS, + excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT, }, ); }, [areOptionsInitialized, options.personalDetails, options.reports, selectedOptions]); @@ -69,7 +69,7 @@ function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}: const chatOptions = useMemo(() => { return OptionsListUtils.filterAndOrderOptions(defaultOptions, cleanSearchTerm, { selectedOptions, - excludeLogins: CONST.EXPENSIFY_EMAILS, + excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT, maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, }); }, [defaultOptions, cleanSearchTerm, selectedOptions]); diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx index 00d32fa22676..a36368293cb2 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader.tsx @@ -392,6 +392,7 @@ function SearchPageHeader({queryJSON}: SearchPageHeaderProps) { shiftHorizontal={variables.searchFiltersTooltipShiftHorizontal} wrapperStyle={styles.productTrainingTooltipWrapper} renderTooltipContent={renderProductTrainingTooltip} + onTooltipPress={onFiltersButtonPress} >