diff --git a/android/app/build.gradle b/android/app/build.gradle index c53ad2cd3cc7..d4cc7471a72b 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 1009000512 - versionName "9.0.5-12" + versionCode 1009000600 + versionName "9.0.6-0" // 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/assets/images/simple-illustrations/simple-illustration__empty-state.svg b/assets/images/simple-illustrations/simple-illustration__empty-state.svg new file mode 100644 index 000000000000..154b2269c285 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__empty-state.svg @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 2ccce98e6a21..2826f7e51db9 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 9.0.5 + 9.0.6 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.5.12 + 9.0.6.0 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 8248e7db0454..cf3e420de4e7 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 9.0.5 + 9.0.6 CFBundleSignature ???? CFBundleVersion - 9.0.5.12 + 9.0.6.0 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 87cdb420af38..58cdb65c40e9 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 9.0.5 + 9.0.6 CFBundleVersion - 9.0.5.12 + 9.0.6.0 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index c401dfe77198..64f5f7a6a94d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.0.5-12", + "version": "9.0.6-0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.5-12", + "version": "9.0.6-0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 5420a3e886ef..9f1c3ffca7a7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.5-12", + "version": "9.0.6-0", "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.", diff --git a/scripts/applyPatches.sh b/scripts/applyPatches.sh index a4be88984561..9145629015ee 100755 --- a/scripts/applyPatches.sh +++ b/scripts/applyPatches.sh @@ -8,24 +8,17 @@ SCRIPTS_DIR=$(dirname "${BASH_SOURCE[0]}") source "$SCRIPTS_DIR/shellUtils.sh" # Wrapper to run patch-package. -# We use `script` to preserve colorization when the output of patch-package is piped to tee -# and we provide /dev/null to discard the output rather than sending it to a file -# `script` has different syntax on macOS vs linux, so that's why we need a wrapper function function patchPackage { OS="$(uname)" - if [[ "$OS" == "Darwin" ]]; then - # macOS - script -q /dev/null npx patch-package --error-on-fail - elif [[ "$OS" == "Linux" ]]; then - # Ubuntu/Linux - script -q -c "npx patch-package --error-on-fail" /dev/null + if [[ "$OS" == "Darwin" || "$OS" == "Linux" ]]; then + npx patch-package --error-on-fail else error "Unsupported OS: $OS" + exit 1 fi } # 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)" patchPackage 2>&1 | tee "$TEMP_OUTPUT" EXIT_CODE=${PIPESTATUS[0]} @@ -36,7 +29,7 @@ rm -f "$TEMP_OUTPUT" echo "$OUTPUT" | grep -q "Warning:" WARNING_FOUND=$? -printf "\n"; +printf "\n" # Determine the final exit code if [ "$EXIT_CODE" -eq 0 ]; then diff --git a/src/CONST.ts b/src/CONST.ts index b809bdaacaf6..50df9118a74e 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -840,6 +840,8 @@ const CONST = { IOU: 'iou', TASK: 'task', INVOICE: 'invoice', + PAYCHECK: 'paycheck', + BILL: 'bill', }, CHAT_TYPE: chatTypes, WORKSPACE_CHAT_ROOMS: { @@ -1123,8 +1125,6 @@ const CONST = { // around each header. EMOJI_NUM_PER_ROW: 8, - EMOJI_FREQUENT_ROW_COUNT: 3, - EMOJI_DEFAULT_SKIN_TONE: -1, // Amount of emojis to render ahead at the end of the update cycle @@ -5250,6 +5250,13 @@ const CONST = { }, EXCLUDE_FROM_LAST_VISITED_PATH: [SCREENS.NOT_FOUND, SCREENS.SAML_SIGN_IN, SCREENS.VALIDATE_LOGIN] as string[], + + EMPTY_STATE_MEDIA: { + ANIMATION: 'animation', + ILLUSTRATION: 'illustration', + VIDEO: 'video', + }, + UPGRADE_FEATURE_INTRO_MAPPING: [ { id: 'reportFields', @@ -5260,6 +5267,7 @@ const CONST = { icon: 'Pencil', }, ], + REPORT_FIELD_TYPES: { TEXT: 'text', DATE: 'date', diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index bd4b294a6d68..8abb7738289c 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -445,6 +445,9 @@ const ONYXKEYS = { * So for example: card_12345_Expensify Card */ WORKSPACE_CARDS_LIST: 'card_', + + /** The bank account that Expensify Card payments will be reconciled against */ + SHARED_NVP_EXPENSIFY_CARD_CONTINUOUS_RECONCILIATION_CONNECTION: 'sharedNVP_expensifyCard_continuousReconciliationConnection_', }, /** List of Form ids */ @@ -535,8 +538,8 @@ const ONYXKEYS = { REPORT_VIRTUAL_CARD_FRAUD_DRAFT: 'reportVirtualCardFraudFormDraft', GET_PHYSICAL_CARD_FORM: 'getPhysicalCardForm', GET_PHYSICAL_CARD_FORM_DRAFT: 'getPhysicalCardFormDraft', - REPORT_FIELD_EDIT_FORM: 'reportFieldEditForm', - REPORT_FIELD_EDIT_FORM_DRAFT: 'reportFieldEditFormDraft', + REPORT_FIELDS_EDIT_FORM: 'reportFieldsEditForm', + REPORT_FIELDS_EDIT_FORM_DRAFT: 'reportFieldsEditFormDraft', REIMBURSEMENT_ACCOUNT_FORM: 'reimbursementAccount', REIMBURSEMENT_ACCOUNT_FORM_DRAFT: 'reimbursementAccountDraft', PERSONAL_BANK_ACCOUNT_FORM: 'personalBankAccount', @@ -622,7 +625,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD]: FormTypes.ReportVirtualCardFraudForm; [ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM]: FormTypes.ReportPhysicalCardForm; [ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM]: FormTypes.GetPhysicalCardForm; - [ONYXKEYS.FORMS.REPORT_FIELD_EDIT_FORM]: FormTypes.ReportFieldEditForm; + [ONYXKEYS.FORMS.REPORT_FIELDS_EDIT_FORM]: FormTypes.ReportFieldsEditForm; [ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM]: FormTypes.ReimbursementAccountForm; [ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM]: FormTypes.PersonalBankAccountForm; [ONYXKEYS.FORMS.WORKSPACE_DESCRIPTION_FORM]: FormTypes.WorkspaceDescriptionForm; @@ -692,6 +695,7 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END]: OnyxTypes.BillingGraceEndPeriod; [ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_EXPENSIFY_CARD_SETTINGS]: OnyxTypes.ExpensifyCardSettings; [ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST]: OnyxTypes.WorkspaceCardsList; + [ONYXKEYS.COLLECTION.SHARED_NVP_EXPENSIFY_CARD_CONTINUOUS_RECONCILIATION_CONNECTION]: OnyxTypes.BankAccount; }; type OnyxValuesMapping = { diff --git a/src/ROUTES.ts b/src/ROUTES.ts index c47083db48eb..fbebd459fff0 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -676,6 +676,10 @@ const ROUTES = { route: 'settings/workspaces/:policyID/accounting/quickbooks-online/invoice-account-selector', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/invoice-account-selector` as const, }, + WORKSPACE_ACCOUNTING_RECONCILIATION_ACCOUNT_SETTINGS: { + route: 'settings/workspaces/:policyID/accounting/:connection/card-reconciliation', + getRoute: (policyID: string, connection: ValueOf) => `settings/workspaces/${policyID}/accounting/${connection}/card-reconciliation` as const, + }, WORKSPACE_CATEGORIES: { route: 'settings/workspaces/:policyID/categories', getRoute: (policyID: string) => `settings/workspaces/${policyID}/categories` as const, @@ -797,30 +801,30 @@ const ROUTES = { route: 'settings/workspaces/:policyID/reportFields/new', getRoute: (policyID: string) => `settings/workspaces/${policyID}/reportFields/new` as const, }, - WORKSPACE_REPORT_FIELD_SETTINGS: { - route: 'settings/workspaces/:policyID/reportField/:reportFieldID/edit', - getRoute: (policyID: string, reportFieldID: string) => `settings/workspaces/${policyID}/reportField/${encodeURIComponent(reportFieldID)}/edit` as const, + WORKSPACE_REPORT_FIELDS_SETTINGS: { + route: 'settings/workspaces/:policyID/reportFields/:reportFieldID/edit', + getRoute: (policyID: string, reportFieldID: string) => `settings/workspaces/${policyID}/reportFields/${encodeURIComponent(reportFieldID)}/edit` as const, }, - WORKSPACE_REPORT_FIELD_LIST_VALUES: { - route: 'settings/workspaces/:policyID/reportField/listValues/:reportFieldID?', - getRoute: (policyID: string, reportFieldID?: string) => `settings/workspaces/${policyID}/reportField/listValues/${encodeURIComponent(reportFieldID ?? '')}` as const, + WORKSPACE_REPORT_FIELDS_LIST_VALUES: { + route: 'settings/workspaces/:policyID/reportFields/listValues/:reportFieldID?', + getRoute: (policyID: string, reportFieldID?: string) => `settings/workspaces/${policyID}/reportFields/listValues/${encodeURIComponent(reportFieldID ?? '')}` as const, }, - WORKSPACE_REPORT_FIELD_ADD_VALUE: { - route: 'settings/workspaces/:policyID/reportField/addValue/:reportFieldID?', - getRoute: (policyID: string, reportFieldID?: string) => `settings/workspaces/${policyID}/reportField/addValue/${encodeURIComponent(reportFieldID ?? '')}` as const, + WORKSPACE_REPORT_FIELDS_ADD_VALUE: { + route: 'settings/workspaces/:policyID/reportFields/addValue/:reportFieldID?', + getRoute: (policyID: string, reportFieldID?: string) => `settings/workspaces/${policyID}/reportFields/addValue/${encodeURIComponent(reportFieldID ?? '')}` as const, }, - WORKSPACE_REPORT_FIELD_VALUE_SETTINGS: { - route: 'settings/workspaces/:policyID/reportField/:valueIndex/:reportFieldID?', + WORKSPACE_REPORT_FIELDS_VALUE_SETTINGS: { + route: 'settings/workspaces/:policyID/reportFields/:valueIndex/:reportFieldID?', getRoute: (policyID: string, valueIndex: number, reportFieldID?: string) => - `settings/workspaces/${policyID}/reportField/${valueIndex}/${encodeURIComponent(reportFieldID ?? '')}` as const, + `settings/workspaces/${policyID}/reportFields/${valueIndex}/${encodeURIComponent(reportFieldID ?? '')}` as const, }, - WORKSPACE_REPORT_FIELD_EDIT_VALUE: { - route: 'settings/workspaces/:policyID/reportField/new/:valueIndex/edit', - getRoute: (policyID: string, valueIndex: number) => `settings/workspaces/${policyID}/reportField/new/${valueIndex}/edit` as const, + WORKSPACE_REPORT_FIELDS_EDIT_VALUE: { + route: 'settings/workspaces/:policyID/reportFields/new/:valueIndex/edit', + getRoute: (policyID: string, valueIndex: number) => `settings/workspaces/${policyID}/reportFields/new/${valueIndex}/edit` as const, }, - WORKSPACE_EDIT_REPORT_FIELD_INITIAL_VALUE: { - route: 'settings/workspaces/:policyID/reportField/:reportFieldID/edit/initialValue', - getRoute: (policyID: string, reportFieldID: string) => `settings/workspaces/${policyID}/reportField/${encodeURIComponent(reportFieldID)}/edit/initialValue` as const, + WORKSPACE_EDIT_REPORT_FIELDS_INITIAL_VALUE: { + route: 'settings/workspaces/:policyID/reportFields/:reportFieldID/edit/initialValue', + getRoute: (policyID: string, reportFieldID: string) => `settings/workspaces/${policyID}/reportFields/${encodeURIComponent(reportFieldID)}/edit/initialValue` as const, }, WORKSPACE_EXPENSIFY_CARD: { route: 'settings/workspaces/:policyID/expensify-card', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index d2a6b7c19ddd..0768ca8bb291 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -330,6 +330,7 @@ const SCREENS = { SAGE_INTACCT_NON_REIMBURSABLE_CREDIT_CARD_ACCOUNT: 'Policy_Accounting_Sage_Intacct_Non_Reimbursable_Credit_Card_Account', SAGE_INTACCT_ADVANCED: 'Policy_Accounting_Sage_Intacct_Advanced', SAGE_INTACCT_PAYMENT_ACCOUNT: 'Policy_Accounting_Sage_Intacct_Payment_Account', + RECONCILIATION_ACCOUNT_SETTINGS: 'Policy_Accounting_Reconciliation_Account_Settings', }, INITIAL: 'Workspace_Initial', PROFILE: 'Workspace_Profile', @@ -353,7 +354,7 @@ const SCREENS = { TAG_EDIT: 'Tag_Edit', TAXES: 'Workspace_Taxes', REPORT_FIELDS: 'Workspace_ReportFields', - REPORT_FIELD_SETTINGS: 'Workspace_ReportField_Settings', + REPORT_FIELDS_SETTINGS: 'Workspace_ReportFields_Settings', REPORT_FIELDS_CREATE: 'Workspace_ReportFields_Create', REPORT_FIELDS_LIST_VALUES: 'Workspace_ReportFields_ListValues', REPORT_FIELDS_ADD_VALUE: 'Workspace_ReportFields_AddValue', diff --git a/src/components/AccountingListSkeletonView.tsx b/src/components/AccountingListSkeletonView.tsx index b977903d3adc..dbe8ada6c4b7 100644 --- a/src/components/AccountingListSkeletonView.tsx +++ b/src/components/AccountingListSkeletonView.tsx @@ -4,12 +4,14 @@ import ItemListSkeletonView from './Skeletons/ItemListSkeletonView'; type AccountingListSkeletonViewProps = { shouldAnimate?: boolean; + gradientOpacityEnabled?: boolean; }; -function AccountingListSkeletonView({shouldAnimate = true}: AccountingListSkeletonViewProps) { +function AccountingListSkeletonView({shouldAnimate = true, gradientOpacityEnabled = false}: AccountingListSkeletonViewProps) { return ( ( <> { + if (!event) { + return; + } + + if ('naturalSize' in event) { + setVideoAspectRatio(event.naturalSize.width / event.naturalSize.height); + } else { + setVideoAspectRatio(event.srcElement.videoWidth / event.srcElement.videoHeight); + } + }; + + const HeaderComponent = useMemo(() => { + switch (headerMediaType) { + case CONST.EMPTY_STATE_MEDIA.VIDEO: + return ( + + ); + case CONST.EMPTY_STATE_MEDIA.ANIMATION: + return ( + + ); + case CONST.EMPTY_STATE_MEDIA.ILLUSTRATION: + return ( + + ); + default: + return null; + } + }, [headerMedia, headerMediaType, headerContentStyles, videoAspectRatio, styles.emptyStateVideo]); + + return ( + + + + + + + {HeaderComponent} + + {title} + {subtitle} + {!!buttonText && !!buttonAction && ( + + )} + + + + + ); +} + +EmptyStateComponent.displayName = 'EmptyStateComponent'; +export default EmptyStateComponent; diff --git a/src/components/EmptyStateComponent/types.ts b/src/components/EmptyStateComponent/types.ts new file mode 100644 index 000000000000..326b25542f42 --- /dev/null +++ b/src/components/EmptyStateComponent/types.ts @@ -0,0 +1,41 @@ +import type {ImageStyle} from 'expo-image'; +import type {StyleProp, ViewStyle} from 'react-native'; +import type {ValueOf} from 'type-fest'; +import type DotLottieAnimation from '@components/LottieAnimations/types'; +import type SearchRowSkeleton from '@components/Skeletons/SearchRowSkeleton'; +import type TableRowSkeleton from '@components/Skeletons/TableRowSkeleton'; +import type CONST from '@src/CONST'; +import type IconAsset from '@src/types/utils/IconAsset'; + +type ValidSkeletons = typeof SearchRowSkeleton | typeof TableRowSkeleton; +type MediaTypes = ValueOf; + +type SharedProps = { + SkeletonComponent: ValidSkeletons; + title: string; + subtitle: string; + buttonText?: string; + buttonAction?: () => void; + headerStyles?: StyleProp; + headerMediaType: T; + headerContentStyles?: StyleProp; +}; + +type MediaType = SharedProps & { + headerMedia: HeaderMedia; +}; + +type VideoProps = MediaType; +type IllustrationProps = MediaType; +type AnimationProps = MediaType; + +type EmptyStateComponentProps = VideoProps | IllustrationProps | AnimationProps; + +type VideoLoadedEventType = { + srcElement: { + videoWidth: number; + videoHeight: number; + }; +}; + +export type {EmptyStateComponentProps, VideoLoadedEventType}; diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts index 7a8186d2f38e..da72b3025340 100644 --- a/src/components/Icon/Illustrations.ts +++ b/src/components/Icon/Illustrations.ts @@ -53,6 +53,7 @@ import ConciergeNew from '@assets/images/simple-illustrations/simple-illustratio import CreditCardsNew from '@assets/images/simple-illustrations/simple-illustration__credit-cards.svg'; import CreditCardEyes from '@assets/images/simple-illustrations/simple-illustration__creditcardeyes.svg'; import EmailAddress from '@assets/images/simple-illustrations/simple-illustration__email-address.svg'; +import EmptyState from '@assets/images/simple-illustrations/simple-illustration__empty-state.svg'; import FolderOpen from '@assets/images/simple-illustrations/simple-illustration__folder-open.svg'; import Gears from '@assets/images/simple-illustrations/simple-illustration__gears.svg'; import HandCard from '@assets/images/simple-illustrations/simple-illustration__handcard.svg'; @@ -198,6 +199,7 @@ export { CheckmarkCircle, CreditCardEyes, LockClosedOrange, + EmptyState, FolderWithPapers, VirtualCard, }; diff --git a/src/components/OptionsListSkeletonView.tsx b/src/components/OptionsListSkeletonView.tsx index a11077f95bb5..6dede512f405 100644 --- a/src/components/OptionsListSkeletonView.tsx +++ b/src/components/OptionsListSkeletonView.tsx @@ -18,16 +18,18 @@ function getLinedWidth(index: number): string { type OptionsListSkeletonViewProps = { shouldAnimate?: boolean; + gradientOpacityEnabled?: boolean; shouldStyleAsTable?: boolean; }; -function OptionsListSkeletonView({shouldAnimate = true, shouldStyleAsTable = false}: OptionsListSkeletonViewProps) { +function OptionsListSkeletonView({shouldAnimate = true, shouldStyleAsTable = false, gradientOpacityEnabled = false}: OptionsListSkeletonViewProps) { const styles = useThemeStyles(); return ( { const lineWidth = getLinedWidth(itemIndex); diff --git a/src/components/PopoverWithoutOverlay/index.tsx b/src/components/PopoverWithoutOverlay/index.tsx index bcead42a64f2..7d58ad6d22be 100644 --- a/src/components/PopoverWithoutOverlay/index.tsx +++ b/src/components/PopoverWithoutOverlay/index.tsx @@ -124,6 +124,7 @@ function PopoverWithoutOverlay( ref={viewRef(withoutOverlayRef)} // Prevent the parent element to capture a click. This is useful when the modal component is put inside a pressable. onClick={(e) => e.stopPropagation()} + dataSet={{dragArea: false}} > { + // 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)) { + return; + } if (isCompleted) { Task.reopenTask(report); } else { diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index fc5c23d5c9ec..7d54d65b310e 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -1,11 +1,12 @@ import {useNavigation} from '@react-navigation/native'; import type {StackNavigationProp} from '@react-navigation/stack'; +import lodashMemoize from 'lodash/memoize'; import React, {useCallback, useEffect, useRef} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {useOnyx} from 'react-native-onyx'; import SearchTableHeader from '@components/SelectionList/SearchTableHeader'; import type {ReportListItemType, TransactionListItemType} from '@components/SelectionList/types'; -import TableListItemSkeleton from '@components/Skeletons/TableListItemSkeleton'; +import SearchRowSkeleton from '@components/Skeletons/SearchRowSkeleton'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; @@ -50,6 +51,9 @@ function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) { const lastSearchResultsRef = useRef>(); const {setCurrentSearchHash} = useSearchContext(); + const hash = SearchUtils.getQueryHash(query, policyIDs, sortBy, sortOrder); + const [currentSearchResults, searchResultsMeta] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`); + const getItemHeight = useCallback( (item: TransactionListItemType | ReportListItemType) => { if (SearchUtils.isTransactionListItemType(item)) { @@ -70,8 +74,15 @@ function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) { [isLargeScreenWidth], ); - const hash = SearchUtils.getQueryHash(query, policyIDs, sortBy, sortOrder); - const [currentSearchResults, searchResultsMeta] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`); + const getItemHeightMemoized = lodashMemoize( + (item: TransactionListItemType | ReportListItemType) => getItemHeight(item), + (item) => { + // List items are displayed differently on "L"arge and "N"arrow screens so the height will differ + // in addition the same items might be displayed as part of different Search screens ("Expenses", "All", "Finished") + const screenSizeHash = isLargeScreenWidth ? 'L' : 'N'; + return `${hash}-${item.keyForList}-${screenSizeHash}`; + }, + ); // save last non-empty search results to avoid ugly flash of loading screen when hash changes and onyx returns empty data if (currentSearchResults?.data && currentSearchResults !== lastSearchResultsRef.current) { @@ -101,7 +112,7 @@ function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) { query={query} hash={hash} /> - + ); } @@ -197,7 +208,7 @@ function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) { updateCellsBatchingPeriod={200} ListItem={ListItem} onSelectRow={openReport} - getItemHeight={getItemHeight} + getItemHeight={getItemHeightMemoized} shouldDebounceRowSelect shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} listHeaderWrapperStyle={[styles.ph8, styles.pv3, styles.pb5]} @@ -207,7 +218,7 @@ function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) { onEndReached={fetchMoreResults} listFooterContent={ isLoadingMoreItems ? ( - diff --git a/src/components/Skeletons/ItemListSkeletonView.tsx b/src/components/Skeletons/ItemListSkeletonView.tsx index 1ee2da8a8019..046cdfffbee5 100644 --- a/src/components/Skeletons/ItemListSkeletonView.tsx +++ b/src/components/Skeletons/ItemListSkeletonView.tsx @@ -1,6 +1,6 @@ -import React, {useMemo, useState} from 'react'; -import {View} from 'react-native'; -import type {StyleProp, ViewStyle} from 'react-native'; +import React, {useCallback, useMemo, useState} from 'react'; +import type {LayoutChangeEvent, StyleProp, ViewStyle} from 'react-native'; +import {StyleSheet, View} from 'react-native'; import SkeletonViewContentLoader from '@components/SkeletonViewContentLoader'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -10,22 +10,62 @@ type ListItemSkeletonProps = { shouldAnimate?: boolean; renderSkeletonItem: (args: {itemIndex: number}) => React.ReactNode; fixedNumItems?: number; + gradientOpacityEnabled?: boolean; itemViewStyle?: StyleProp; itemViewHeight?: number; }; -function ItemListSkeletonView({shouldAnimate = true, renderSkeletonItem, fixedNumItems, itemViewStyle = {}, itemViewHeight = CONST.LHN_SKELETON_VIEW_ITEM_HEIGHT}: ListItemSkeletonProps) { +const getVerticalMargin = (style: StyleProp): number => { + if (!style) { + return 0; + } + + const flattenStyle = StyleSheet.flatten(style); + const marginVertical = Number(flattenStyle?.marginVertical ?? 0); + const marginTop = Number(flattenStyle?.marginTop ?? 0); + const marginBottom = Number(flattenStyle?.marginBottom ?? 0); + + return marginVertical + marginTop + marginBottom; +}; + +function ItemListSkeletonView({ + shouldAnimate = true, + renderSkeletonItem, + fixedNumItems, + gradientOpacityEnabled = false, + itemViewStyle = {}, + itemViewHeight = CONST.LHN_SKELETON_VIEW_ITEM_HEIGHT, +}: ListItemSkeletonProps) { const theme = useTheme(); const themeStyles = useThemeStyles(); const [numItems, setNumItems] = useState(fixedNumItems ?? 0); + + const totalItemHeight = itemViewHeight + getVerticalMargin(itemViewStyle); + + const handleLayout = useCallback( + (event: LayoutChangeEvent) => { + if (fixedNumItems) { + return; + } + + const totalHeight = event.nativeEvent.layout.height; + const newNumItems = Math.ceil(totalHeight / totalItemHeight); + if (newNumItems !== numItems) { + setNumItems(newNumItems); + } + }, + [fixedNumItems, numItems, totalItemHeight], + ); + const skeletonViewItems = useMemo(() => { const items = []; for (let i = 0; i < numItems; i++) { + const opacity = gradientOpacityEnabled ? 1 - i / (numItems - 1) : 1; items.push( { - if (fixedNumItems) { - return; - } - - const newNumItems = Math.ceil(event.nativeEvent.layout.height / itemViewHeight); - if (newNumItems === numItems) { - return; - } - setNumItems(newNumItems); - }} + onLayout={handleLayout} > - {skeletonViewItems} + {skeletonViewItems} ); } diff --git a/src/components/Skeletons/TableListItemSkeleton.tsx b/src/components/Skeletons/SearchRowSkeleton.tsx similarity index 54% rename from src/components/Skeletons/TableListItemSkeleton.tsx rename to src/components/Skeletons/SearchRowSkeleton.tsx index 6ff3a3aedbb9..2359e47b7520 100644 --- a/src/components/Skeletons/TableListItemSkeleton.tsx +++ b/src/components/Skeletons/SearchRowSkeleton.tsx @@ -2,26 +2,41 @@ import React from 'react'; import {Circle, Rect} from 'react-native-svg'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import variables from '@styles/variables'; import CONST from '@src/CONST'; import ItemListSkeletonView from './ItemListSkeletonView'; -type TableListItemSkeletonProps = { +type SearchRowSkeletonProps = { shouldAnimate?: boolean; fixedNumItems?: number; + gradientOpacityEnabled?: boolean; }; -const barHeight = '10'; -const shortBarWidth = '40'; -const longBarWidth = '120'; +const barHeight = 8; +const longBarWidth = 120; +const leftPaneWidth = variables.sideBarWidth; -function TableListItemSkeleton({shouldAnimate = true, fixedNumItems}: TableListItemSkeletonProps) { +// 12 is the gap between the element and the right button +const gapWidth = 12; + +// 80 is the width of the element itself +const rightSideElementWidth = 80; + +// 24 is the padding of the central pane summing two sides +const centralPanePadding = 40; + +// 80 is the width of the button on the right side +const rightButtonWidth = 80; + +function SearchRowSkeleton({shouldAnimate = true, fixedNumItems, gradientOpacityEnabled = false}: SearchRowSkeletonProps) { const styles = useThemeStyles(); - const {windowWidth, isSmallScreenWidth} = useWindowDimensions(); + const {windowWidth, isSmallScreenWidth, isLargeScreenWidth} = useWindowDimensions(); if (isSmallScreenWidth) { return ( ( @@ -51,7 +66,7 @@ function TableListItemSkeleton({shouldAnimate = true, fixedNumItems}: TableListI height={4} /> ); } + return ( ( <> - + {isLargeScreenWidth && ( + <> + + + + + )} + + )} @@ -146,6 +181,6 @@ function TableListItemSkeleton({shouldAnimate = true, fixedNumItems}: TableListI ); } -TableListItemSkeleton.displayName = 'TableListItemSkeleton'; +SearchRowSkeleton.displayName = 'SearchRowSkeleton'; -export default TableListItemSkeleton; +export default SearchRowSkeleton; diff --git a/src/components/Skeletons/TableRowSkeleton.tsx b/src/components/Skeletons/TableRowSkeleton.tsx new file mode 100644 index 000000000000..865bffc5842f --- /dev/null +++ b/src/components/Skeletons/TableRowSkeleton.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import {Circle, Rect} from 'react-native-svg'; +import useThemeStyles from '@hooks/useThemeStyles'; +import ItemListSkeletonView from './ItemListSkeletonView'; + +type TableListItemSkeletonProps = { + shouldAnimate?: boolean; + fixedNumItems?: number; + gradientOpacityEnabled?: boolean; +}; + +const barHeight = '8'; +const shortBarWidth = '60'; +const longBarWidth = '124'; + +function TableListItemSkeleton({shouldAnimate = true, fixedNumItems, gradientOpacityEnabled = false}: TableListItemSkeletonProps) { + const styles = useThemeStyles(); + + return ( + ( + <> + + + + + )} + /> + ); +} + +TableListItemSkeleton.displayName = 'TableListItemSkeleton'; + +export default TableListItemSkeleton; diff --git a/src/components/TaskHeaderActionButton.tsx b/src/components/TaskHeaderActionButton.tsx index 0c7e603a4aa2..5e563ea99763 100644 --- a/src/components/TaskHeaderActionButton.tsx +++ b/src/components/TaskHeaderActionButton.tsx @@ -5,6 +5,7 @@ import {withOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; 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 ONYXKEYS from '@src/ONYXKEYS'; @@ -36,7 +37,17 @@ function TaskHeaderActionButton({report, session}: TaskHeaderActionButtonProps) isDisabled={!Task.canModifyTask(report, session?.accountID ?? -1)} medium text={translate(ReportUtils.isCompletedTaskReport(report) ? 'task.markAsIncomplete' : 'task.markAsComplete')} - onPress={Session.checkIfActionIsAllowed(() => (ReportUtils.isCompletedTaskReport(report) ? Task.reopenTask(report) : Task.completeTask(report)))} + onPress={Session.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)) { + return; + } + if (ReportUtils.isCompletedTaskReport(report)) { + Task.reopenTask(report); + } else { + Task.completeTask(report); + } + })} style={styles.flex1} /> diff --git a/src/hooks/useTackInputFocus/index.ts b/src/hooks/useTackInputFocus/index.ts index 124f8460127c..e6caa15f9dde 100644 --- a/src/hooks/useTackInputFocus/index.ts +++ b/src/hooks/useTackInputFocus/index.ts @@ -1,5 +1,6 @@ import {useCallback, useEffect} from 'react'; import useDebouncedState from '@hooks/useDebouncedState'; +import * as Browser from '@libs/Browser'; /** * Detects input or text area focus on browsers, to avoid scrolling on virtual viewports @@ -28,7 +29,13 @@ export default function useTackInputFocus(enable = false): boolean { ); const resetScrollPositionOnVisualViewport = useCallback(() => { - window.scrollTo({top: 0}); + if (Browser.isChromeIOS() && window.visualViewport?.offsetTop) { + // On Chrome iOS, the visual viewport triggers a scroll event when the keyboard is opened, but some time the scroll position is not correct. + // So this change is specific to Chrome iOS, helping to reset the viewport position correctly. + window.scrollTo({top: -window.visualViewport.offsetTop}); + } else { + window.scrollTo({top: 0}); + } }, []); useEffect(() => { diff --git a/src/hooks/useWindowDimensions/index.ts b/src/hooks/useWindowDimensions/index.ts index 25757fda17e5..b391e45a61aa 100644 --- a/src/hooks/useWindowDimensions/index.ts +++ b/src/hooks/useWindowDimensions/index.ts @@ -23,7 +23,7 @@ export default function (useCachedViewportHeight = false): WindowDimensions { unlockWindowDimensions: () => {}, }; - const isCachedViewportHeight = useCachedViewportHeight && Browser.isMobileSafari(); + const isCachedViewportHeight = useCachedViewportHeight && Browser.isMobileWebKit(); const cachedViewportHeightWithKeyboardRef = useRef(initalViewportHeight); const {width: windowWidth, height: windowHeight} = useWindowDimensions(); diff --git a/src/languages/en.ts b/src/languages/en.ts index 1ac9684ac22e..c7b5125d02fa 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -521,6 +521,7 @@ export default { replyInThread: 'Reply in thread', joinThread: 'Join thread', leaveThread: 'Leave thread', + copyOnyxData: 'Copy Onyx data', flagAsOffensive: 'Flag as offensive', menu: 'Menu', }, @@ -3125,6 +3126,13 @@ export default { defaultVendor: 'Default vendor', autoSync: 'Auto-sync', reimbursedReports: 'Sync reimbursed reports', + reconciliationAccount: 'Reconciliation account', + chooseReconciliationAccount: { + chooseBankAccount: 'Choose the bank account that your Expensify Card payments will be reconciled against.', + accountMatches: 'Make sure this account matches your ', + settlementAccount: 'Expensify Card settlement account ', + reconciliationWorks: (lastFourPAN: string) => `(ending in ${lastFourPAN}) so Continuous Reconciliation works properly.`, + }, }, bills: { manageYourBills: 'Manage your bills', @@ -4050,7 +4058,7 @@ export default { }, paymentCard: { addPaymentCard: 'Add payment card', - enterPaymentCardDetails: 'Enter your payment card details.', + enterPaymentCardDetails: 'Enter your payment card details', security: 'Expensify is PCI-DSS compliant, uses bank-level encryption, and utilizes redundant infrastructure to protect your data.', learnMoreAboutSecurity: 'Learn more about our security.', }, diff --git a/src/languages/es.ts b/src/languages/es.ts index ea9186daee78..075903d0f324 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -513,6 +513,7 @@ export default { replyInThread: 'Responder en el hilo', joinThread: 'Unirse al hilo', leaveThread: 'Dejar hilo', + copyOnyxData: 'Copiar datos de Onyx', flagAsOffensive: 'Marcar como ofensivo', menu: 'Menú', }, @@ -3111,6 +3112,13 @@ export default { defaultVendor: 'Proveedor predeterminado', autoSync: 'Autosincronización', reimbursedReports: 'Sincronizar informes reembolsados', + reconciliationAccount: 'Cuenta de conciliación', + chooseReconciliationAccount: { + chooseBankAccount: 'Elige la cuenta bancaria con la que se conciliarán los pagos de tu Tarjeta Expensify.', + accountMatches: 'Asegúrate de que esta cuenta coincide con ', + settlementAccount: 'la cuenta de liquidación de tu Tarjeta Expensify ', + reconciliationWorks: (lastFourPAN: string) => `(que termina en ${lastFourPAN}) para que la conciliación continua funcione correctamente.`, + }, }, card: { header: 'Desbloquea Tarjetas Expensify gratis', @@ -4565,7 +4573,7 @@ export default { }, paymentCard: { addPaymentCard: 'Añade tarjeta de pago', - enterPaymentCardDetails: 'Introduce los datos de tu tarjeta de pago.', + enterPaymentCardDetails: 'Introduce los datos de tu tarjeta de pago', security: 'Expensify es PCI-DSS obediente, utiliza cifrado a nivel bancario, y emplea infraestructura redundante para proteger tus datos.', learnMoreAboutSecurity: 'Conozca más sobre nuestra seguridad.', }, diff --git a/src/libs/API/parameters/PolicyReportFieldsReplace.ts b/src/libs/API/parameters/DeletePolicyReportField.ts similarity index 66% rename from src/libs/API/parameters/PolicyReportFieldsReplace.ts rename to src/libs/API/parameters/DeletePolicyReportField.ts index c6d1834f0789..d79e9b07249e 100644 --- a/src/libs/API/parameters/PolicyReportFieldsReplace.ts +++ b/src/libs/API/parameters/DeletePolicyReportField.ts @@ -1,4 +1,4 @@ -type PolicyReportFieldsReplace = { +type DeletePolicyReportField = { policyID: string; /** * Stringified JSON object with type of following structure: @@ -7,4 +7,4 @@ type PolicyReportFieldsReplace = { reportFields: string; }; -export default PolicyReportFieldsReplace; +export default DeletePolicyReportField; diff --git a/src/libs/API/parameters/UpdateFrequentlyUsedEmojisParams.ts b/src/libs/API/parameters/UpdateFrequentlyUsedEmojisParams.ts deleted file mode 100644 index f790ada3aad9..000000000000 --- a/src/libs/API/parameters/UpdateFrequentlyUsedEmojisParams.ts +++ /dev/null @@ -1,3 +0,0 @@ -type UpdateFrequentlyUsedEmojisParams = {value: string}; - -export default UpdateFrequentlyUsedEmojisParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index ff8465cfeec7..ff62d9b69ea6 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -64,7 +64,6 @@ export type {default as UpdateAutomaticTimezoneParams} from './UpdateAutomaticTi export type {default as UpdateChatPriorityModeParams} from './UpdateChatPriorityModeParams'; export type {default as UpdateDateOfBirthParams} from './UpdateDateOfBirthParams'; export type {default as UpdateDisplayNameParams} from './UpdateDisplayNameParams'; -export type {default as UpdateFrequentlyUsedEmojisParams} from './UpdateFrequentlyUsedEmojisParams'; export type {default as UpdateGroupChatNameParams} from './UpdateGroupChatNameParams'; export type {default as UpdateGroupChatMemberRolesParams} from './UpdateGroupChatMemberRolesParams'; export type {default as UpdateHomeAddressParams} from './UpdateHomeAddressParams'; @@ -242,7 +241,7 @@ export type {default as DeleteMoneyRequestOnSearchParams} from './DeleteMoneyReq export type {default as HoldMoneyRequestOnSearchParams} from './HoldMoneyRequestOnSearchParams'; export type {default as UnholdMoneyRequestOnSearchParams} from './UnholdMoneyRequestOnSearchParams'; export type {default as UpdateNetSuiteSubsidiaryParams} from './UpdateNetSuiteSubsidiaryParams'; -export type {default as PolicyReportFieldsReplace} from './PolicyReportFieldsReplace'; +export type {default as DeletePolicyReportField} from './DeletePolicyReportField'; export type {default as ConnectPolicyToNetSuiteParams} from './ConnectPolicyToNetSuiteParams'; export type {default as CreateWorkspaceReportFieldParams} from './CreateWorkspaceReportFieldParams'; export type {default as UpdateWorkspaceReportFieldInitialValueParams} from './UpdateWorkspaceReportFieldInitialValueParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index ca284321e3bb..948ed7f76373 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -57,7 +57,6 @@ const WRITE_COMMANDS = { VALIDATE_LOGIN: 'ValidateLogin', VALIDATE_SECONDARY_LOGIN: 'ValidateSecondaryLogin', UPDATE_PREFERRED_EMOJI_SKIN_TONE: 'UpdatePreferredEmojiSkinTone', - UPDATE_FREQUENTLY_USED_EMOJIS: 'UpdateFrequentlyUsedEmojis', UPDATE_CHAT_PRIORITY_MODE: 'UpdateChatPriorityMode', SET_CONTACT_METHOD_AS_DEFAULT: 'SetContactMethodAsDefault', UPDATE_THEME: 'UpdateTheme', @@ -351,7 +350,6 @@ type WriteCommandParameters = { [WRITE_COMMANDS.VALIDATE_LOGIN]: Parameters.ValidateLoginParams; [WRITE_COMMANDS.VALIDATE_SECONDARY_LOGIN]: Parameters.ValidateSecondaryLoginParams; [WRITE_COMMANDS.UPDATE_PREFERRED_EMOJI_SKIN_TONE]: Parameters.UpdatePreferredEmojiSkinToneParams; - [WRITE_COMMANDS.UPDATE_FREQUENTLY_USED_EMOJIS]: Parameters.UpdateFrequentlyUsedEmojisParams; [WRITE_COMMANDS.UPDATE_CHAT_PRIORITY_MODE]: Parameters.UpdateChatPriorityModeParams; [WRITE_COMMANDS.SET_CONTACT_METHOD_AS_DEFAULT]: Parameters.SetContactMethodAsDefaultParams; [WRITE_COMMANDS.UPDATE_THEME]: Parameters.UpdateThemeParams; @@ -426,7 +424,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.RENAME_WORKSPACE_CATEGORY]: Parameters.RenameWorkspaceCategoriesParams; [WRITE_COMMANDS.SET_WORKSPACE_REQUIRES_CATEGORY]: Parameters.SetWorkspaceRequiresCategoryParams; [WRITE_COMMANDS.DELETE_WORKSPACE_CATEGORIES]: Parameters.DeleteWorkspaceCategoriesParams; - [WRITE_COMMANDS.DELETE_POLICY_REPORT_FIELD]: Parameters.PolicyReportFieldsReplace; + [WRITE_COMMANDS.DELETE_POLICY_REPORT_FIELD]: Parameters.DeletePolicyReportField; [WRITE_COMMANDS.SET_POLICY_REQUIRES_TAG]: Parameters.SetPolicyRequiresTag; [WRITE_COMMANDS.SET_POLICY_TAGS_REQUIRED]: Parameters.SetPolicyTagsRequired; [WRITE_COMMANDS.RENAME_POLICY_TAG_LIST]: Parameters.RenamePolicyTaglistParams; diff --git a/src/libs/Browser/index.ts b/src/libs/Browser/index.ts index 98ad449c3dd0..aeec4f4def4a 100644 --- a/src/libs/Browser/index.ts +++ b/src/libs/Browser/index.ts @@ -1,4 +1,4 @@ -import type {GetBrowser, IsMobile, IsMobileChrome, IsMobileSafari, IsMobileWebKit, IsSafari, OpenRouteInDesktopApp} from './types'; +import type {GetBrowser, IsChromeIOS, IsMobile, IsMobileChrome, IsMobileSafari, IsMobileWebKit, IsSafari, OpenRouteInDesktopApp} from './types'; const getBrowser: GetBrowser = () => ''; @@ -10,8 +10,10 @@ const isMobileChrome: IsMobileChrome = () => false; const isMobileWebKit: IsMobileWebKit = () => false; +const isChromeIOS: IsChromeIOS = () => false; + const isSafari: IsSafari = () => false; const openRouteInDesktopApp: OpenRouteInDesktopApp = () => {}; -export {getBrowser, isMobile, isMobileSafari, isMobileWebKit, isSafari, isMobileChrome, openRouteInDesktopApp}; +export {getBrowser, isMobile, isMobileSafari, isMobileWebKit, isSafari, isMobileChrome, isChromeIOS, openRouteInDesktopApp}; diff --git a/src/libs/Browser/index.website.ts b/src/libs/Browser/index.website.ts index a83fa1cac70e..b89190dc7f78 100644 --- a/src/libs/Browser/index.website.ts +++ b/src/libs/Browser/index.website.ts @@ -1,7 +1,7 @@ import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; -import type {GetBrowser, IsMobile, IsMobileChrome, IsMobileSafari, IsMobileWebKit, IsSafari, OpenRouteInDesktopApp} from './types'; +import type {GetBrowser, IsChromeIOS, IsMobile, IsMobileChrome, IsMobileSafari, IsMobileWebKit, IsSafari, OpenRouteInDesktopApp} from './types'; /** * Fetch browser name from UA string @@ -66,6 +66,14 @@ const isMobileWebKit: IsMobileWebKit = () => { return /iP(ad|od|hone)/i.test(userAgent) && /WebKit/i.test(userAgent); }; +/** + * Checks if the requesting user agent is a Chrome browser on an iOS mobile device. + */ +const isChromeIOS: IsChromeIOS = () => { + const userAgent = navigator.userAgent; + return /iP(ad|od|hone)/i.test(userAgent) && /CriOS/i.test(userAgent); +}; + const isSafari: IsSafari = () => getBrowser() === 'safari' || isMobileSafari(); /** @@ -109,4 +117,4 @@ const openRouteInDesktopApp: OpenRouteInDesktopApp = (shortLivedAuthToken = '', } }; -export {getBrowser, isMobile, isMobileSafari, isMobileWebKit, isSafari, isMobileChrome, openRouteInDesktopApp}; +export {getBrowser, isMobile, isMobileSafari, isMobileWebKit, isSafari, isMobileChrome, isChromeIOS, openRouteInDesktopApp}; diff --git a/src/libs/Browser/types.ts b/src/libs/Browser/types.ts index 25f305953c87..cb242d3729aa 100644 --- a/src/libs/Browser/types.ts +++ b/src/libs/Browser/types.ts @@ -8,8 +8,10 @@ type IsMobileChrome = () => boolean; type IsMobileWebKit = () => boolean; +type IsChromeIOS = () => boolean; + type IsSafari = () => boolean; type OpenRouteInDesktopApp = (shortLivedAuthToken?: string, email?: string) => void; -export type {GetBrowser, IsMobile, IsMobileSafari, IsMobileChrome, IsMobileWebKit, IsSafari, OpenRouteInDesktopApp}; +export type {GetBrowser, IsMobile, IsMobileSafari, IsMobileChrome, IsMobileWebKit, IsSafari, IsChromeIOS, OpenRouteInDesktopApp}; diff --git a/src/libs/CurrencyUtils.ts b/src/libs/CurrencyUtils.ts index 862b0ae5e928..c3b80797d750 100644 --- a/src/libs/CurrencyUtils.ts +++ b/src/libs/CurrencyUtils.ts @@ -125,9 +125,11 @@ function convertToDisplayString(amountInCents = 0, currency: string = CONST.CURR style: 'currency', currency: currencyWithFallback, - // We are forcing the number of decimals because we override the default number of decimals in the backend for RSD + // We are forcing the number of decimals because we override the default number of decimals in the backend for some currencies // See: https://github.com/Expensify/PHP-Libs/pull/834 - minimumFractionDigits: currency === 'RSD' ? getCurrencyDecimals(currency) : undefined, + minimumFractionDigits: getCurrencyDecimals(currency), + // For currencies that have decimal places > 2, floor to 2 instead as we don't support more than 2 decimal places. + maximumFractionDigits: 2, }); } @@ -175,9 +177,11 @@ function convertToDisplayStringWithoutCurrency(amountInCents: number, currency: style: 'currency', currency, - // We are forcing the number of decimals because we override the default number of decimals in the backend for RSD + // We are forcing the number of decimals because we override the default number of decimals in the backend for some currencies // See: https://github.com/Expensify/PHP-Libs/pull/834 - minimumFractionDigits: currency === 'RSD' ? getCurrencyDecimals(currency) : undefined, + minimumFractionDigits: getCurrencyDecimals(currency), + // For currencies that have decimal places > 2, floor to 2 instead as we don't support more than 2 decimal places. + maximumFractionDigits: 2, }) .filter((x) => x.type !== 'currency') .filter((x) => x.type !== 'literal' || x.value.trim().length !== 0) diff --git a/src/libs/E2E/reactNativeLaunchingTest.ts b/src/libs/E2E/reactNativeLaunchingTest.ts index f952998f0aad..f61bee8dae6a 100644 --- a/src/libs/E2E/reactNativeLaunchingTest.ts +++ b/src/libs/E2E/reactNativeLaunchingTest.ts @@ -36,6 +36,7 @@ const tests: Tests = { [E2EConfig.TEST_NAMES.ChatOpening]: require('./tests/chatOpeningTest.e2e').default, [E2EConfig.TEST_NAMES.ReportTyping]: require('./tests/reportTypingTest.e2e').default, [E2EConfig.TEST_NAMES.Linking]: require('./tests/linkingTest.e2e').default, + [E2EConfig.TEST_NAMES.PreloadedLinking]: require('./tests/preloadedLinkingTest.e2e').default, }; // Once we receive the TII measurement we know that the app is initialized and ready to be used: diff --git a/src/libs/E2E/tests/preloadedLinkingTest.e2e.ts b/src/libs/E2E/tests/preloadedLinkingTest.e2e.ts new file mode 100644 index 000000000000..a36200b1a702 --- /dev/null +++ b/src/libs/E2E/tests/preloadedLinkingTest.e2e.ts @@ -0,0 +1,82 @@ +import {DeviceEventEmitter} from 'react-native'; +import type {NativeConfig} from 'react-native-config'; +import Config from 'react-native-config'; +import Timing from '@libs/actions/Timing'; +import E2ELogin from '@libs/E2E/actions/e2eLogin'; +import waitForAppLoaded from '@libs/E2E/actions/waitForAppLoaded'; +import E2EClient from '@libs/E2E/client'; +import getConfigValueOrThrow from '@libs/E2E/utils/getConfigValueOrThrow'; +import getPromiseWithResolve from '@libs/E2E/utils/getPromiseWithResolve'; +import Navigation from '@libs/Navigation/Navigation'; +import Performance from '@libs/Performance'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; + +type ViewableItem = { + reportActionID?: string; +}; +type ViewableItemResponse = Array<{item?: ViewableItem}>; + +const test = (config: NativeConfig) => { + console.debug('[E2E] Logging in for comment linking'); + + const reportID = getConfigValueOrThrow('reportID', config); + const linkedReportActionID = getConfigValueOrThrow('linkedReportActionID', config); + + E2ELogin().then((neededLogin) => { + if (neededLogin) { + return waitForAppLoaded().then(() => E2EClient.submitTestDone()); + } + + const [appearMessagePromise, appearMessageResolve] = getPromiseWithResolve(); + const [switchReportPromise, switchReportResolve] = getPromiseWithResolve(); + + Promise.all([appearMessagePromise, switchReportPromise]) + .then(() => { + console.debug('[E2E] Test completed successfully, exiting…'); + E2EClient.submitTestDone(); + }) + .catch((err) => { + console.debug('[E2E] Error while submitting test results:', err); + }); + + const subscription = DeviceEventEmitter.addListener('onViewableItemsChanged', (res: ViewableItemResponse) => { + console.debug('[E2E] Viewable items retrieved, verifying correct message…', res); + if (!!res && res?.[0]?.item?.reportActionID === linkedReportActionID) { + appearMessageResolve(); + subscription.remove(); + } else { + console.debug(`[E2E] Provided message id '${res?.[0]?.item?.reportActionID}' doesn't match to an expected '${linkedReportActionID}'. Waiting for a next one…`); + } + }); + + Performance.subscribeToMeasurements((entry) => { + if (entry.name === CONST.TIMING.SIDEBAR_LOADED) { + console.debug('[E2E] Sidebar loaded, navigating to a report…'); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID)); + return; + } + + if (entry.name === CONST.TIMING.REPORT_INITIAL_RENDER) { + console.debug('[E2E] Navigating to linked report action…'); + Timing.start(CONST.TIMING.SWITCH_REPORT); + Performance.markStart(CONST.TIMING.SWITCH_REPORT); + + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID, linkedReportActionID)); + return; + } + + if (entry.name === CONST.TIMING.CHAT_RENDER) { + E2EClient.submitTestResults({ + branch: Config.E2E_BRANCH, + name: 'Comment linking', + metric: entry.duration, + }); + + switchReportResolve(); + } + }); + }); +}; + +export default test; diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index 6fb5725addfc..007a892c048e 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -1,4 +1,3 @@ -import {getUnixTime} from 'date-fns'; import {Str} from 'expensify-common'; import memoize from 'lodash/memoize'; import Onyx from 'react-native-onyx'; @@ -235,37 +234,6 @@ function mergeEmojisWithFrequentlyUsedEmojis(emojis: PickerEmojis): EmojiPickerL return addSpacesToEmojiCategories(mergedEmojis); } -/** - * Get the updated frequently used emojis list by usage - */ -function getFrequentlyUsedEmojis(newEmoji: Emoji | Emoji[]): FrequentlyUsedEmoji[] { - let frequentEmojiList = [...frequentlyUsedEmojis]; - - const maxFrequentEmojiCount = CONST.EMOJI_FREQUENT_ROW_COUNT * CONST.EMOJI_NUM_PER_ROW - 1; - - const currentTimestamp = getUnixTime(new Date()); - (Array.isArray(newEmoji) ? [...newEmoji] : [newEmoji]).forEach((emoji) => { - let currentEmojiCount = 1; - const emojiIndex = frequentEmojiList.findIndex((e) => e.code === emoji.code); - if (emojiIndex >= 0) { - currentEmojiCount = frequentEmojiList[emojiIndex].count + 1; - frequentEmojiList.splice(emojiIndex, 1); - } - - const updatedEmoji = {...Emojis.emojiCodeTableWithSkinTones[emoji.code], count: currentEmojiCount, lastUpdatedAt: currentTimestamp}; - - // We want to make sure the current emoji is added to the list - // Hence, we take one less than the current frequent used emojis - frequentEmojiList = frequentEmojiList.slice(0, maxFrequentEmojiCount); - frequentEmojiList.push(updatedEmoji); - - // Sort the list by count and lastUpdatedAt in descending order - frequentEmojiList.sort((a, b) => b.count - a.count || b.lastUpdatedAt - a.lastUpdatedAt); - }); - - return frequentEmojiList; -} - /** * Given an emoji item object, return an emoji code based on its type. */ @@ -601,7 +569,6 @@ export { getLocalizedEmojiName, getHeaderEmojis, mergeEmojisWithFrequentlyUsedEmojis, - getFrequentlyUsedEmojis, containsOnlyEmojis, replaceEmojis, suggestEmojis, diff --git a/src/libs/Environment/Environment.ts b/src/libs/Environment/Environment.ts index 1f5a391d3b13..c343788bed05 100644 --- a/src/libs/Environment/Environment.ts +++ b/src/libs/Environment/Environment.ts @@ -38,6 +38,13 @@ function isDevelopment(): boolean { return (Config?.ENVIRONMENT ?? CONST.ENVIRONMENT.DEV) === CONST.ENVIRONMENT.DEV; } +/** + * Are we running the app in staging? + */ +function isStaging(): boolean { + return (Config?.ENVIRONMENT ?? CONST.ENVIRONMENT.DEV) === CONST.ENVIRONMENT.STAGING; +} + /** * Are we running the app in production? */ @@ -76,4 +83,4 @@ function getSpotnanaEnvironmentTMCID(): Promise { return getEnvironment().then((environment) => SPOTNANA_ENVIRONMENT_TMC_ID[environment]); } -export {getEnvironment, isInternalTestBuild, isDevelopment, isProduction, getEnvironmentURL, getOldDotEnvironmentURL, getTravelDotEnvironmentURL, getSpotnanaEnvironmentTMCID}; +export {getEnvironment, isInternalTestBuild, isDevelopment, isStaging, isProduction, getEnvironmentURL, getOldDotEnvironmentURL, getTravelDotEnvironmentURL, getSpotnanaEnvironmentTMCID}; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 0b94972b2aa9..ce43c78a6fee 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -396,6 +396,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/accounting/intacct/advanced/SageIntacctAdvancedPage').default, [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_PAYMENT_ACCOUNT]: () => require('../../../../pages/workspace/accounting/intacct/advanced/SageIntacctPaymentAccountPage').default, + [SCREENS.WORKSPACE.ACCOUNTING.RECONCILIATION_ACCOUNT_SETTINGS]: () => require('../../../../pages/workspace/accounting/ReconciliationAccountSettingsPage').default, [SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY]: () => require('../../../../pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage').default, [SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET]: () => require('../../../../pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage').default, [SCREENS.WORKSPACE.TAX_EDIT]: () => require('../../../../pages/workspace/taxes/WorkspaceEditTaxPage').default, @@ -408,13 +409,13 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/settings/Subscription/PaymentCard/ChangeBillingCurrency').default, [SCREENS.SETTINGS.SUBSCRIPTION.ADD_PAYMENT_CARD]: () => require('../../../../pages/settings/Subscription/PaymentCard').default, [SCREENS.SETTINGS.ADD_PAYMENT_CARD_CHANGE_CURRENCY]: () => require('../../../../pages/settings/PaymentCard/ChangeCurrency').default, - [SCREENS.WORKSPACE.REPORT_FIELDS_CREATE]: () => require('../../../../pages/workspace/reportFields/CreateReportFieldPage').default, - [SCREENS.WORKSPACE.REPORT_FIELD_SETTINGS]: () => require('../../../../pages/workspace/reportFields/ReportFieldSettingsPage').default, - [SCREENS.WORKSPACE.REPORT_FIELDS_LIST_VALUES]: () => require('../../../../pages/workspace/reportFields/ReportFieldListValuesPage').default, - [SCREENS.WORKSPACE.REPORT_FIELDS_ADD_VALUE]: () => require('../../../../pages/workspace/reportFields/ReportFieldAddListValuePage').default, - [SCREENS.WORKSPACE.REPORT_FIELDS_VALUE_SETTINGS]: () => require('../../../../pages/workspace/reportFields/ReportFieldValueSettingsPage').default, - [SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_INITIAL_VALUE]: () => require('../../../../pages/workspace/reportFields/ReportFieldInitialValuePage').default, - [SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_VALUE]: () => require('../../../../pages/workspace/reportFields/ReportFieldEditValuePage').default, + [SCREENS.WORKSPACE.REPORT_FIELDS_CREATE]: () => require('../../../../pages/workspace/reportFields/CreateReportFieldsPage').default, + [SCREENS.WORKSPACE.REPORT_FIELDS_SETTINGS]: () => require('../../../../pages/workspace/reportFields/ReportFieldsSettingsPage').default, + [SCREENS.WORKSPACE.REPORT_FIELDS_LIST_VALUES]: () => require('../../../../pages/workspace/reportFields/ReportFieldsListValuesPage').default, + [SCREENS.WORKSPACE.REPORT_FIELDS_ADD_VALUE]: () => require('../../../../pages/workspace/reportFields/ReportFieldsAddListValuePage').default, + [SCREENS.WORKSPACE.REPORT_FIELDS_VALUE_SETTINGS]: () => require('../../../../pages/workspace/reportFields/ReportFieldsValueSettingsPage').default, + [SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_INITIAL_VALUE]: () => require('../../../../pages/workspace/reportFields/ReportFieldsInitialValuePage').default, + [SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_VALUE]: () => require('../../../../pages/workspace/reportFields/ReportFieldsEditValuePage').default, [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_IMPORT]: () => require('../../../../pages/workspace/accounting/intacct/import/SageIntacctImportPage').default, [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_TOGGLE_MAPPING]: () => require('../../../../pages/workspace/accounting/intacct/import/SageIntacctToggleMappingsPage').default, diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts index 54804a495754..31b44a2681fd 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts @@ -112,6 +112,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_NON_REIMBURSABLE_CREDIT_CARD_ACCOUNT, SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_ADVANCED, SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_PAYMENT_ACCOUNT, + SCREENS.WORKSPACE.ACCOUNTING.RECONCILIATION_ACCOUNT_SETTINGS, ], [SCREENS.WORKSPACE.TAXES]: [ SCREENS.WORKSPACE.TAXES_SETTINGS, @@ -143,7 +144,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { ], [SCREENS.WORKSPACE.REPORT_FIELDS]: [ SCREENS.WORKSPACE.REPORT_FIELDS_CREATE, - SCREENS.WORKSPACE.REPORT_FIELD_SETTINGS, + SCREENS.WORKSPACE.REPORT_FIELDS_SETTINGS, SCREENS.WORKSPACE.REPORT_FIELDS_LIST_VALUES, SCREENS.WORKSPACE.REPORT_FIELDS_ADD_VALUE, SCREENS.WORKSPACE.REPORT_FIELDS_VALUE_SETTINGS, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 6dc3434ba3a2..1b224f1c0065 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -449,6 +449,7 @@ const config: LinkingOptions['config'] = { }, [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_ADVANCED]: {path: ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_ADVANCED.route}, [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_PAYMENT_ACCOUNT]: {path: ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_PAYMENT_ACCOUNT.route}, + [SCREENS.WORKSPACE.ACCOUNTING.RECONCILIATION_ACCOUNT_SETTINGS]: {path: ROUTES.WORKSPACE_ACCOUNTING_RECONCILIATION_ACCOUNT_SETTINGS.route}, [SCREENS.WORKSPACE.DESCRIPTION]: { path: ROUTES.WORKSPACE_PROFILE_DESCRIPTION.route, }, @@ -587,34 +588,34 @@ const config: LinkingOptions['config'] = { path: ROUTES.WORKSPACE_CREATE_REPORT_FIELD.route, }, [SCREENS.WORKSPACE.REPORT_FIELDS_LIST_VALUES]: { - path: ROUTES.WORKSPACE_REPORT_FIELD_LIST_VALUES.route, + path: ROUTES.WORKSPACE_REPORT_FIELDS_LIST_VALUES.route, parse: { reportFieldID: (reportFieldID: string) => decodeURIComponent(reportFieldID), }, }, [SCREENS.WORKSPACE.REPORT_FIELDS_ADD_VALUE]: { - path: ROUTES.WORKSPACE_REPORT_FIELD_ADD_VALUE.route, + path: ROUTES.WORKSPACE_REPORT_FIELDS_ADD_VALUE.route, parse: { reportFieldID: (reportFieldID: string) => decodeURIComponent(reportFieldID), }, }, [SCREENS.WORKSPACE.REPORT_FIELDS_VALUE_SETTINGS]: { - path: ROUTES.WORKSPACE_REPORT_FIELD_VALUE_SETTINGS.route, + path: ROUTES.WORKSPACE_REPORT_FIELDS_VALUE_SETTINGS.route, parse: { reportFieldID: (reportFieldID: string) => decodeURIComponent(reportFieldID), }, }, [SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_VALUE]: { - path: ROUTES.WORKSPACE_REPORT_FIELD_EDIT_VALUE.route, + path: ROUTES.WORKSPACE_REPORT_FIELDS_EDIT_VALUE.route, }, - [SCREENS.WORKSPACE.REPORT_FIELD_SETTINGS]: { - path: ROUTES.WORKSPACE_REPORT_FIELD_SETTINGS.route, + [SCREENS.WORKSPACE.REPORT_FIELDS_SETTINGS]: { + path: ROUTES.WORKSPACE_REPORT_FIELDS_SETTINGS.route, parse: { reportFieldID: (reportFieldID: string) => decodeURIComponent(reportFieldID), }, }, [SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_INITIAL_VALUE]: { - path: ROUTES.WORKSPACE_EDIT_REPORT_FIELD_INITIAL_VALUE.route, + path: ROUTES.WORKSPACE_EDIT_REPORT_FIELDS_INITIAL_VALUE.route, parse: { reportFieldID: (reportFieldID: string) => decodeURIComponent(reportFieldID), }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index fc67fe6b8cc0..00fd98dc51aa 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -294,7 +294,7 @@ type SettingsNavigatorParamList = { policyID: string; valueIndex: number; }; - [SCREENS.WORKSPACE.REPORT_FIELD_SETTINGS]: { + [SCREENS.WORKSPACE.REPORT_FIELDS_SETTINGS]: { policyID: string; reportFieldID: string; }; @@ -588,6 +588,10 @@ type SettingsNavigatorParamList = { [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_PAYMENT_ACCOUNT]: { policyID: string; }; + [SCREENS.WORKSPACE.ACCOUNTING.RECONCILIATION_ACCOUNT_SETTINGS]: { + policyID: string; + connection: ValueOf; + }; [SCREENS.GET_ASSISTANCE]: { backTo: Routes; }; diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 330d9d6ef61d..73b04742878a 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -657,10 +657,11 @@ function isSearchStringMatchUserDetails(personalDetail: PersonalDetails, searchV * Get the last message text from the report directly or from other sources for special cases. */ function getLastMessageTextForReport(report: OnyxEntry, lastActorDetails: Partial | null, policy?: OnyxEntry): string { - const lastReportAction = visibleReportActionItems[report?.reportID ?? '-1'] ?? null; + const reportID = report?.reportID ?? '-1'; + const lastReportAction = visibleReportActionItems[reportID] ?? null; // some types of actions are filtered out for lastReportAction, in some cases we need to check the actual last action - const lastOriginalReportAction = lastReportActions[report?.reportID ?? '-1'] ?? null; + const lastOriginalReportAction = lastReportActions[reportID] ?? null; let lastMessageTextFromReport = ''; if (ReportUtils.isArchivedRoom(report)) { @@ -720,8 +721,10 @@ function getLastMessageTextForReport(report: OnyxEntry, lastActorDetails lastMessageTextFromReport = ReportUtils.formatReportLastMessageText(TaskUtils.getTaskReportActionMessage(lastReportAction).text); } else if (ReportActionUtils.isCreatedTaskReportAction(lastReportAction)) { lastMessageTextFromReport = TaskUtils.getTaskCreatedMessage(lastReportAction); - } else if (ReportActionUtils.isApprovedOrSubmittedReportAction(lastReportAction)) { - lastMessageTextFromReport = ReportActionUtils.getReportActionMessageText(lastReportAction); + } else if (lastReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.SUBMITTED) { + lastMessageTextFromReport = ReportUtils.getIOUSubmittedMessage(reportID); + } else if (lastReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.APPROVED) { + lastMessageTextFromReport = ReportUtils.getIOUApprovedMessage(reportID); } return lastMessageTextFromReport || (report?.lastMessageText ?? ''); diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 3f8acd0e06fe..85850c15e534 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -260,6 +260,18 @@ function isRoomChangeLogAction(reportAction: OnyxEntry): reportAct return isActionOfType(reportAction, ...Object.values(CONST.REPORT.ACTIONS.TYPE.ROOM_CHANGE_LOG)); } +function isInviteOrRemovedAction( + reportAction: OnyxInputOrEntry, +): reportAction is ReportAction> { + return isActionOfType( + reportAction, + CONST.REPORT.ACTIONS.TYPE.ROOM_CHANGE_LOG.INVITE_TO_ROOM, + CONST.REPORT.ACTIONS.TYPE.ROOM_CHANGE_LOG.REMOVE_FROM_ROOM, + CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.INVITE_TO_ROOM, + CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.REMOVE_FROM_ROOM, + ); +} + /** * Returns whether the comment is a thread parent message/the first message in a thread */ @@ -1474,6 +1486,7 @@ export { isClosedAction, isRenamedAction, isRoomChangeLogAction, + isInviteOrRemovedAction, isChronosOOOListAction, isAddCommentAction, isPolicyChangeLogAction, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index fb3bebd75274..97d8c605b0ab 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -52,6 +52,7 @@ import AccountUtils from './AccountUtils'; import * as IOU from './actions/IOU'; import * as PolicyActions from './actions/Policy/Policy'; import * as store from './actions/ReimbursementAccount/store'; +import * as SessionUtils from './actions/Session'; import * as CurrencyUtils from './CurrencyUtils'; import DateUtils from './DateUtils'; import {hasValidDraftComment} from './DraftCommentUtils'; @@ -1065,7 +1066,7 @@ function isSystemChat(report: OnyxEntry): boolean { * Only returns true if this is our main 1:1 DM report with Concierge. */ function isConciergeChatReport(report: OnyxInputOrEntry): boolean { - const participantAccountIDs = Object.keys(report?.participants ?? {}); + const participantAccountIDs = Object.keys(report?.participants ?? {}).filter((accountID) => Number(accountID) !== currentUserAccountID); return participantAccountIDs.length === 1 && Number(participantAccountIDs[0]) === CONST.ACCOUNT_ID.CONCIERGE && !isChatThread(report); } @@ -4002,11 +4003,19 @@ function buildOptimisticExpenseReport(chatReportID: string, policyID: string, pa return expenseReport; } -function getIOUSubmittedMessage(reportID: string) { +function getFormattedAmount(reportID: string) { const report = getReportOrDraftReport(reportID); const linkedReport = isChatThread(report) ? getParentReport(report) : report; const formattedAmount = CurrencyUtils.convertToDisplayString(Math.abs(linkedReport?.total ?? 0), linkedReport?.currency); - return Localize.translateLocal('iou.submittedAmount', {formattedAmount}); + return formattedAmount; +} + +function getIOUSubmittedMessage(reportID: string) { + return Localize.translateLocal('iou.submittedAmount', {formattedAmount: getFormattedAmount(reportID)}); +} + +function getIOUApprovedMessage(reportID: string) { + return Localize.translateLocal('iou.approvedAmount', {amount: getFormattedAmount(reportID)}); } /** @@ -5451,6 +5460,10 @@ function shouldReportBeInOptionList({ return false; } + if (report?.type === CONST.REPORT.TYPE.PAYCHECK || report?.type === CONST.REPORT.TYPE.BILL) { + return false; + } + // Include the currently viewed report. If we excluded the currently viewed report, then there // would be no way to highlight it in the options list and it would be confusing to users because they lose // a sense of context. @@ -6922,6 +6935,10 @@ function canJoinChat(report: OnyxInputOrEntry, parentReportAction: OnyxI * Whether the user can leave a report */ function canLeaveChat(report: OnyxEntry, policy: OnyxEntry): boolean { + if (isPublicRoom(report) && SessionUtils.isAnonymousUser()) { + return false; + } + if (report?.notificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN) { return false; } @@ -7167,6 +7184,7 @@ export { getGroupChatName, getIOUReportActionDisplayMessage, getIOUReportActionMessage, + getIOUApprovedMessage, getIOUSubmittedMessage, getIcons, getIconsForParticipants, diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 80081c8f89c7..9efd1e584052 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -6,6 +6,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PersonalDetails, PersonalDetailsList, ReportActions, TransactionViolation} from '@src/types/onyx'; import type Beta from '@src/types/onyx/Beta'; +import type {OriginalMessageChangeLog} from '@src/types/onyx/OriginalMessage'; import type Policy from '@src/types/onyx/Policy'; import type PriorityMode from '@src/types/onyx/PriorityMode'; import type Report from '@src/types/onyx/Report'; @@ -353,7 +354,7 @@ function getOptionData({ result.alternateText = Localize.translate(preferredLocale, 'newRoomPage.roomRenamedTo', {newName}); } else if (ReportActionsUtils.isTaskAction(lastAction)) { result.alternateText = ReportUtils.formatReportLastMessageText(TaskUtils.getTaskReportActionMessage(lastAction).text); - } else if (ReportActionsUtils.isRoomChangeLogAction(lastAction)) { + } else if (ReportActionsUtils.isInviteOrRemovedAction(lastAction)) { const lastActionOriginalMessage = lastAction?.actionName ? ReportActionsUtils.getOriginalMessage(lastAction) : null; const targetAccountIDs = lastActionOriginalMessage?.targetAccountIDs ?? []; const targetAccountIDsLength = targetAccountIDs.length !== 0 ? targetAccountIDs.length : report.lastMessageHtml?.match(/]*><\/mention-user>/g)?.length ?? 0; @@ -372,11 +373,9 @@ function getOptionData({ : ` ${Localize.translate(preferredLocale, 'workspace.invite.from')}`; result.alternateText += `${preposition} ${roomName}`; } - if (lastActionName === CONST.REPORT.ACTIONS.TYPE.ROOM_CHANGE_LOG.UPDATE_ROOM_DESCRIPTION) { - result.alternateText = `${lastActorDisplayName} ${Localize.translate(preferredLocale, 'roomChangeLog.updateRoomDescription')} ${ - lastActionOriginalMessage?.description - }`.trim(); - } + } else if (lastActionName === CONST.REPORT.ACTIONS.TYPE.ROOM_CHANGE_LOG.UPDATE_ROOM_DESCRIPTION) { + const lastActionOriginalMessage = lastAction?.actionName ? (ReportActionsUtils.getOriginalMessage(lastAction) as OriginalMessageChangeLog | undefined) : null; + result.alternateText = `${lastActorDisplayName} ${Localize.translate(preferredLocale, 'roomChangeLog.updateRoomDescription')} ${lastActionOriginalMessage?.description}`.trim(); } else if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.LEAVE_POLICY) { result.alternateText = Localize.translateLocal('workspace.invite.leftWorkspace'); } else if (lastAction?.actionName !== CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW && lastActorDisplayName && lastMessageTextFromReport) { diff --git a/src/libs/TaskUtils.ts b/src/libs/TaskUtils.ts index bd0bd10cd83e..06745a49217b 100644 --- a/src/libs/TaskUtils.ts +++ b/src/libs/TaskUtils.ts @@ -1,12 +1,21 @@ import type {OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; import type {Message} from '@src/types/onyx/ReportAction'; import type ReportAction from '@src/types/onyx/ReportAction'; import * as Localize from './Localize'; +import Navigation from './Navigation/Navigation'; import {getReportActionHtml, getReportActionText} from './ReportActionsUtils'; import * as ReportConnection from './ReportConnection'; +/** + * Check if the active route belongs to task edit flow. + */ +function isActiveTaskEditRoute(reportID: string): boolean { + return [ROUTES.TASK_TITLE, ROUTES.TASK_ASSIGNEE, ROUTES.REPORT_DESCRIPTION].map((route) => route.getRoute(reportID)).some(Navigation.isActiveRoute); +} + /** * Given the Task reportAction name, return the appropriate message to be displayed and copied to clipboard. */ @@ -42,4 +51,4 @@ function getTaskCreatedMessage(reportAction: OnyxEntry) { return taskTitle ? Localize.translateLocal('task.messages.created', {title: taskTitle}) : ''; } -export {getTaskReportActionMessage, getTaskTitle, getTaskCreatedMessage}; +export {isActiveTaskEditRoute, getTaskReportActionMessage, getTaskTitle, getTaskCreatedMessage}; diff --git a/src/libs/actions/PersonalDetails.ts b/src/libs/actions/PersonalDetails.ts index 5870d642d8cd..8e2fff3868ae 100644 --- a/src/libs/actions/PersonalDetails.ts +++ b/src/libs/actions/PersonalDetails.ts @@ -25,7 +25,6 @@ import ROUTES from '@src/ROUTES'; import type {DateOfBirthForm} from '@src/types/form'; import type {PersonalDetails, PersonalDetailsList} from '@src/types/onyx'; import type {SelectedTimezone, Timezone} from '@src/types/onyx/PersonalDetails'; -import * as Session from './Session'; let currentUserEmail = ''; let currentUserAccountID = -1; @@ -191,10 +190,6 @@ function updateAddress(street: string, street2: string, city: string, state: str * selected timezone if set to automatically update. */ function updateAutomaticTimezone(timezone: Timezone) { - if (Session.isAnonymousUser()) { - return; - } - if (!currentUserAccountID) { return; } diff --git a/src/libs/actions/Policy/ReportField.ts b/src/libs/actions/Policy/ReportField.ts index bccb08c47c18..27b67c9fe686 100644 --- a/src/libs/actions/Policy/ReportField.ts +++ b/src/libs/actions/Policy/ReportField.ts @@ -5,8 +5,8 @@ import * as API from '@libs/API'; import type { CreateWorkspaceReportFieldListValueParams, CreateWorkspaceReportFieldParams, + DeletePolicyReportField, EnableWorkspaceReportFieldListValueParams, - PolicyReportFieldsReplace, RemoveWorkspaceReportFieldListValueParams, UpdateWorkspaceReportFieldInitialValueParams, } from '@libs/API/parameters'; @@ -260,7 +260,7 @@ function deleteReportFields(policyID: string, reportFieldsToUpdate: string[]) { ], }; - const parameters: PolicyReportFieldsReplace = { + const parameters: DeletePolicyReportField = { policyID, reportFields: JSON.stringify(Object.values(updatedReportFields)), }; diff --git a/src/libs/actions/Policy/Tag.ts b/src/libs/actions/Policy/Tag.ts index 2558969be2f3..9dc0f96c5886 100644 --- a/src/libs/actions/Policy/Tag.ts +++ b/src/libs/actions/Policy/Tag.ts @@ -1,4 +1,4 @@ -import type {NullishDeep, OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {NullishDeep, OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; import type {EnablePolicyTagsParams, OpenPolicyTagsPageParams, RenamePolicyTaglistParams, RenamePolicyTagsParams, SetPolicyTagsEnabled, SetPolicyTagsRequired} from '@libs/API/parameters'; @@ -624,6 +624,9 @@ function renamePolicyTaglist(policyID: string, policyTagListName: {oldName: stri } function setPolicyRequiresTag(policyID: string, requiresTag: boolean) { + const policyTags = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; + const isMultiLevelTags = PolicyUtils.isMultiLevelTags(policyTags); + const onyxData: OnyxData = { optimisticData: [ { @@ -667,6 +670,26 @@ function setPolicyRequiresTag(policyID: string, requiresTag: boolean) { ], }; + if (isMultiLevelTags) { + const getUpdatedTagsData = (required: boolean): OnyxUpdate => ({ + key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, + onyxMethod: Onyx.METHOD.MERGE, + value: { + ...Object.keys(policyTags).reduce((acc, key) => { + acc[key] = { + ...acc[key], + required, + }; + return acc; + }, {}), + }, + }); + + onyxData.optimisticData?.push(getUpdatedTagsData(requiresTag)); + onyxData.failureData?.push(getUpdatedTagsData(!requiresTag)); + onyxData.successData?.push(getUpdatedTagsData(requiresTag)); + } + const parameters = { policyID, requiresTag, diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 7acc79485f0c..7b3b1abd04ef 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -13,7 +13,6 @@ import type { SetContactMethodAsDefaultParams, SetNameValuePairParams, UpdateChatPriorityModeParams, - UpdateFrequentlyUsedEmojisParams, UpdateNewsletterSubscriptionParams, UpdatePreferredEmojiSkinToneParams, UpdateStatusParams, @@ -37,7 +36,7 @@ import Visibility from '@libs/Visibility'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {BlockedFromConcierge, CustomStatusDraft, FrequentlyUsedEmoji, Policy} from '@src/types/onyx'; +import type {BlockedFromConcierge, CustomStatusDraft, Policy} from '@src/types/onyx'; import type Login from '@src/types/onyx/Login'; import type {OnyxServerUpdate} from '@src/types/onyx/OnyxUpdatesFromServer'; import type OnyxPersonalDetails from '@src/types/onyx/PersonalDetails'; @@ -655,23 +654,6 @@ function updatePreferredSkinTone(skinTone: number) { API.write(WRITE_COMMANDS.UPDATE_PREFERRED_EMOJI_SKIN_TONE, parameters, {optimisticData}); } -/** - * Sync frequentlyUsedEmojis with Onyx and Server - */ -function updateFrequentlyUsedEmojis(frequentlyUsedEmojis: FrequentlyUsedEmoji[]) { - const optimisticData: OnyxUpdate[] = [ - { - onyxMethod: Onyx.METHOD.SET, - key: ONYXKEYS.FREQUENTLY_USED_EMOJIS, - value: frequentlyUsedEmojis, - }, - ]; - - const parameters: UpdateFrequentlyUsedEmojisParams = {value: JSON.stringify(frequentlyUsedEmojis)}; - - API.write(WRITE_COMMANDS.UPDATE_FREQUENTLY_USED_EMOJIS, parameters, {optimisticData}); -} - /** * Sync user chat priority mode with Onyx and Server * @param mode @@ -1045,7 +1027,6 @@ export { setShouldUseStagingServer, setMuteAllSounds, clearUserErrorMessage, - updateFrequentlyUsedEmojis, joinScreenShare, clearScreenShareRequest, generateStatementPDF, diff --git a/src/pages/EditReportFieldDate.tsx b/src/pages/EditReportFieldDate.tsx index 38209ba1083b..06ba24f780ec 100644 --- a/src/pages/EditReportFieldDate.tsx +++ b/src/pages/EditReportFieldDate.tsx @@ -24,7 +24,7 @@ type EditReportFieldDatePageProps = { isRequired: boolean; /** Callback to fire when the Save button is pressed */ - onSubmit: (form: FormOnyxValues) => void; + onSubmit: (form: FormOnyxValues) => void; }; function EditReportFieldDatePage({fieldName, isRequired, onSubmit, fieldValue, fieldKey}: EditReportFieldDatePageProps) { @@ -33,8 +33,8 @@ function EditReportFieldDatePage({fieldName, isRequired, onSubmit, fieldValue, f const inputRef = useRef(null); const validate = useCallback( - (value: FormOnyxValues) => { - const errors: FormInputErrors = {}; + (value: FormOnyxValues) => { + const errors: FormInputErrors = {}; if (isRequired && value[fieldKey].trim() === '') { errors[fieldKey] = translate('common.error.fieldRequired'); } @@ -46,7 +46,7 @@ function EditReportFieldDatePage({fieldName, isRequired, onSubmit, fieldValue, f return ( ) => { + const handleReportFieldChange = (form: FormOnyxValues) => { const value = form[fieldKey]; if (isReportFieldTitle) { ReportActions.updateReportName(report.reportID, value, report.reportName ?? ''); diff --git a/src/pages/EditReportFieldText.tsx b/src/pages/EditReportFieldText.tsx index d619eb52b695..b855acf3e1c0 100644 --- a/src/pages/EditReportFieldText.tsx +++ b/src/pages/EditReportFieldText.tsx @@ -24,7 +24,7 @@ type EditReportFieldTextPageProps = { isRequired: boolean; /** Callback to fire when the Save button is pressed */ - onSubmit: (form: FormOnyxValues) => void; + onSubmit: (form: FormOnyxValues) => void; }; function EditReportFieldTextPage({fieldName, onSubmit, fieldValue, isRequired, fieldKey}: EditReportFieldTextPageProps) { @@ -33,8 +33,8 @@ function EditReportFieldTextPage({fieldName, onSubmit, fieldValue, isRequired, f const {inputCallbackRef} = useAutoFocusInput(); const validate = useCallback( - (values: FormOnyxValues) => { - const errors: FormInputErrors = {}; + (values: FormOnyxValues) => { + const errors: FormInputErrors = {}; if (isRequired && values[fieldKey].trim() === '') { errors[fieldKey] = translate('common.error.fieldRequired'); } @@ -46,7 +46,7 @@ function EditReportFieldTextPage({fieldName, onSubmit, fieldValue, isRequired, f return ( diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 662e92658e9d..6491245469a1 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -171,6 +171,7 @@ function ReportScreen({ const isLoadingReportOnyx = isLoadingOnyxValue(reportResult); const permissions = useDeepCompareRef(reportOnyx?.permissions); + const lastAccessedReportID = ReportUtils.findLastAccessedReport(!canUseDefaultRooms, !!route.params.openOnAdminRoom, activeWorkspaceID)?.reportID; useEffect(() => { // Don't update if there is a reportID in the params already @@ -183,8 +184,6 @@ function ReportScreen({ return; } - const lastAccessedReportID = ReportUtils.findLastAccessedReport(!canUseDefaultRooms, !!route.params.openOnAdminRoom, activeWorkspaceID)?.reportID; - // It's possible that reports aren't fully loaded yet // in that case the reportID is undefined if (!lastAccessedReportID) { @@ -193,7 +192,7 @@ function ReportScreen({ Log.info(`[ReportScreen] no reportID found in params, setting it to lastAccessedReportID: ${lastAccessedReportID}`); navigation.setParams({reportID: lastAccessedReportID}); - }, [activeWorkspaceID, canUseDefaultRooms, navigation, route]); + }, [lastAccessedReportID, activeWorkspaceID, canUseDefaultRooms, navigation, route]); /** * Create a lightweight Report so as to keep the re-rendering as light as possible by diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index 218c382fd776..f5143fc9ba21 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -22,12 +22,14 @@ import Navigation from '@libs/Navigation/Navigation'; import Parser from '@libs/Parser'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; +import * as ReportConnection from '@libs/ReportConnection'; import * as ReportUtils from '@libs/ReportUtils'; import * as TaskUtils from '@libs/TaskUtils'; import * as Download from '@userActions/Download'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; +import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Beta, OnyxInputOrEntry, ReportAction, ReportActionReactions, Transaction} from '@src/types/onyx'; import type IconAsset from '@src/types/utils/IconAsset'; @@ -400,6 +402,9 @@ const ContextMenuActions: ContextMenuAction[] = [ } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.SUBMITTED) { const displayMessage = ReportUtils.getIOUSubmittedMessage(reportID); Clipboard.setString(displayMessage); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.APPROVED) { + const displayMessage = ReportUtils.getIOUApprovedMessage(reportID); + Clipboard.setString(displayMessage); } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.HOLD) { Clipboard.setString(Localize.translateLocal('iou.heldExpense')); } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.UNHOLD) { @@ -524,6 +529,20 @@ const ContextMenuActions: ContextMenuAction[] = [ }, getDescription: () => {}, }, + { + isAnonymousAction: true, + textTranslateKey: 'reportActionContextMenu.copyOnyxData', + icon: Expensicons.Copy, + successTextTranslateKey: 'reportActionContextMenu.copied', + successIcon: Expensicons.Checkmark, + shouldShow: (type) => type === CONST.CONTEXT_MENU_TYPES.REPORT && (Environment.isDevelopment() || Environment.isStaging() || Environment.isInternalTestBuild()), + onPress: (closePopover, {reportID}) => { + const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; + Clipboard.setString(JSON.stringify(report, null, 4)); + hideContextMenu(true, ReportActionComposeFocusManager.focus); + }, + getDescription: () => {}, + }, { isAnonymousAction: false, textTranslateKey: 'reportActionContextMenu.deleteAction', diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 6cc55c825983..1df45303694a 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -53,7 +53,6 @@ import * as EmojiPickerActions from '@userActions/EmojiPickerAction'; import * as InputFocus from '@userActions/InputFocus'; import * as Modal from '@userActions/Modal'; import * as Report from '@userActions/Report'; -import * as User from '@userActions/User'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; @@ -304,22 +303,12 @@ function ComposerWithSuggestions( const [composerHeight, setComposerHeight] = useState(0); const textInputRef = useRef(null); - const insertedEmojisRef = useRef([]); const syncSelectionWithOnChangeTextRef = useRef(null); // The ref to check whether the comment saving is in progress const isCommentPendingSaved = useRef(false); - /** - * Update frequently used emojis list. We debounce this method in the constructor so that UpdateFrequentlyUsedEmojis - * API is not called too often. - */ - const debouncedUpdateFrequentlyUsedEmojis = useCallback(() => { - User.updateFrequentlyUsedEmojis(EmojiUtils.getFrequentlyUsedEmojis(insertedEmojisRef.current)); - insertedEmojisRef.current = []; - }, []); - /** * Set the TextInput Ref */ @@ -421,8 +410,6 @@ function ComposerWithSuggestions( if (suggestionsRef.current) { suggestionsRef.current.resetSuggestions(); } - insertedEmojisRef.current = [...insertedEmojisRef.current, ...newEmojis]; - debouncedUpdateFrequentlyUsedEmojis(); } } const newCommentConverted = convertToLTRForComposer(newComment); @@ -461,18 +448,7 @@ function ComposerWithSuggestions( debouncedBroadcastUserIsTyping(reportID); } }, - [ - debouncedUpdateFrequentlyUsedEmojis, - findNewlyAddedChars, - preferredLocale, - preferredSkinTone, - reportID, - setIsCommentEmpty, - suggestionsRef, - raiseIsScrollLikelyLayoutTriggered, - debouncedSaveReportComment, - selection.end, - ], + [findNewlyAddedChars, preferredLocale, preferredSkinTone, reportID, setIsCommentEmpty, suggestionsRef, raiseIsScrollLikelyLayoutTriggered, debouncedSaveReportComment, selection.end], ); const prepareCommentAndResetComposer = useCallback((): string => { diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index e732c5793f96..9135e494794e 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -621,6 +621,8 @@ function ReportActionItem({ children = ; } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.SUBMITTED) { children = ; + } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.APPROVED) { + children = ; } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.HOLD) { children = ; } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.HOLD_COMMENT) { diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index 6618b20a5a6a..cb68410131ce 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -37,7 +37,6 @@ import * as ComposerActions from '@userActions/Composer'; import * as EmojiPickerAction from '@userActions/EmojiPickerAction'; import * as InputFocus from '@userActions/InputFocus'; import * as Report from '@userActions/Report'; -import * as User from '@userActions/User'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; @@ -105,7 +104,6 @@ function ReportActionItemMessageEdit( const textInputRef = useRef<(HTMLTextAreaElement & TextInput) | null>(null); const isFocusedRef = useRef(false); - const insertedEmojis = useRef([]); const draftRef = useRef(draft); const emojiPickerSelectionRef = useRef(undefined); // The ref to check whether the comment saving is in progress @@ -214,19 +212,6 @@ function ReportActionItemMessageEdit( [debouncedSaveDraft], ); - /** - * Update frequently used emojis list. We debounce this method in the constructor so that UpdateFrequentlyUsedEmojis - * API is not called too often. - */ - const debouncedUpdateFrequentlyUsedEmojis = useMemo( - () => - lodashDebounce(() => { - User.updateFrequentlyUsedEmojis(EmojiUtils.getFrequentlyUsedEmojis(insertedEmojis.current)); - insertedEmojis.current = []; - }, 1000), - [], - ); - /** * Update the value of the draft in Onyx * @@ -236,13 +221,6 @@ function ReportActionItemMessageEdit( (newDraftInput: string) => { const {text: newDraft, emojis, cursorPosition} = EmojiUtils.replaceAndExtractEmojis(newDraftInput, preferredSkinTone, preferredLocale); - if (emojis?.length > 0) { - const newEmojis = EmojiUtils.getAddedEmojis(emojis, emojisPresentBefore.current); - if (newEmojis?.length > 0) { - insertedEmojis.current = [...insertedEmojis.current, ...newEmojis]; - debouncedUpdateFrequentlyUsedEmojis(); - } - } emojisPresentBefore.current = emojis; setDraft(newDraft); @@ -261,7 +239,7 @@ function ReportActionItemMessageEdit( debouncedSaveDraft(newDraft); isCommentPendingSaved.current = true; }, - [debouncedSaveDraft, debouncedUpdateFrequentlyUsedEmojis, preferredSkinTone, preferredLocale, selection.end], + [debouncedSaveDraft, preferredSkinTone, preferredLocale, selection.end], ); useEffect(() => { diff --git a/src/pages/iou/request/step/IOURequestStepDistance.tsx b/src/pages/iou/request/step/IOURequestStepDistance.tsx index 05b228c73c76..9235c3293a6d 100644 --- a/src/pages/iou/request/step/IOURequestStepDistance.tsx +++ b/src/pages/iou/request/step/IOURequestStepDistance.tsx @@ -90,7 +90,8 @@ function IOURequestStepDistance({ const previousValidatedWaypoints = usePrevious(validatedWaypoints); const haveValidatedWaypointsChanged = !isEqual(previousValidatedWaypoints, validatedWaypoints); const isRouteAbsentWithoutErrors = !hasRoute && !hasRouteError; - const shouldFetchRoute = (isRouteAbsentWithoutErrors || haveValidatedWaypointsChanged) && !isLoadingRoute && Object.keys(validatedWaypoints).length > 1; + const isEmptyCoordinates = !transaction?.routes?.route0?.geometry?.coordinates?.length; + const shouldFetchRoute = (isEmptyCoordinates || isRouteAbsentWithoutErrors || haveValidatedWaypointsChanged) && !isLoadingRoute && Object.keys(validatedWaypoints).length > 1; const [shouldShowAtLeastTwoDifferentWaypointsError, setShouldShowAtLeastTwoDifferentWaypointsError] = useState(false); const nonEmptyWaypointsCount = useMemo(() => Object.keys(waypoints).filter((key) => !isEmpty(waypoints[key])).length, [waypoints]); const duplicateWaypointsError = useMemo( diff --git a/src/pages/signin/SignInPage.tsx b/src/pages/signin/SignInPage.tsx index 0b4310cce337..024d5a7c5610 100644 --- a/src/pages/signin/SignInPage.tsx +++ b/src/pages/signin/SignInPage.tsx @@ -266,6 +266,7 @@ function SignInPage({credentials, account, activeClients = [], preferredLocale, diff --git a/src/pages/workspace/accounting/ReconciliationAccountSettingsPage.tsx b/src/pages/workspace/accounting/ReconciliationAccountSettingsPage.tsx new file mode 100644 index 000000000000..7f0e45678471 --- /dev/null +++ b/src/pages/workspace/accounting/ReconciliationAccountSettingsPage.tsx @@ -0,0 +1,81 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React, {useMemo} from 'react'; +import {useOnyx} from 'react-native-onyx'; +import ConnectionLayout from '@components/ConnectionLayout'; +import SelectionList from '@components/SelectionList'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import Text from '@components/Text'; +import TextLink from '@components/TextLink'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import type {SettingsNavigatorParamList} from '@navigation/types'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; + +type ReconciliationAccountSettingsPageProps = StackScreenProps; + +function ReconciliationAccountSettingsPage({route}: ReconciliationAccountSettingsPageProps) { + const {policyID, connection} = route.params; + const settlementAccountEnding = '1234'; // TODO: use correct settlement account ending value https://github.com/Expensify/App/issues/44313 + + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); + const [reconciliationConnection] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_EXPENSIFY_CARD_CONTINUOUS_RECONCILIATION_CONNECTION}${policyID}`); + + const selectedBankAccount = useMemo(() => reconciliationConnection ?? Object.values(bankAccountList ?? {})[0], [reconciliationConnection, bankAccountList]); + + const sections = useMemo(() => { + const data = Object.values(bankAccountList ?? {}).map((bankAccount) => ({ + text: bankAccount.title, + value: bankAccount.accountData?.bankAccountID, + keyForList: bankAccount.accountData?.bankAccountID?.toString(), + isSelected: bankAccount.accountData?.bankAccountID === selectedBankAccount?.accountData?.bankAccountID, + })); + return [{data}]; + }, [bankAccountList, selectedBankAccount]); + + const selectBankAccount = () => { + // TODO: add API call when it's implemented https://github.com/Expensify/Expensify/issues/407836 + // Navigation.goBack(); + }; + + return ( + + {translate('workspace.accounting.chooseReconciliationAccount.chooseBankAccount')} + + {translate('workspace.accounting.chooseReconciliationAccount.accountMatches')} + { + // TODO: navigate to Settlement Account https://github.com/Expensify/App/issues/44313 + }} + > + {translate('workspace.accounting.chooseReconciliationAccount.settlementAccount')} + + {translate('workspace.accounting.chooseReconciliationAccount.reconciliationWorks', settlementAccountEnding)} + + + + + ); +} + +ReconciliationAccountSettingsPage.displayName = 'ReconciliationAccountSettingsPage'; + +export default ReconciliationAccountSettingsPage; diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index 633d1833e43f..174251a80d5f 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -7,6 +7,7 @@ import Button from '@components/Button'; import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu'; import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; import ConfirmModal from '@components/ConfirmModal'; +import EmptyStateComponent from '@components/EmptyStateComponent'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; @@ -15,9 +16,9 @@ import SelectionList from '@components/SelectionList'; import ListItemRightCaretWithLabel from '@components/SelectionList/ListItemRightCaretWithLabel'; import TableListItem from '@components/SelectionList/TableListItem'; import type {ListItem} from '@components/SelectionList/types'; +import TableListItemSkeleton from '@components/Skeletons/TableRowSkeleton'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; -import WorkspaceEmptyStateSection from '@components/WorkspaceEmptyStateSection'; import useEnvironment from '@hooks/useEnvironment'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -317,10 +318,15 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { color={theme.spinner} /> )} + {!hasVisibleCategories && !isLoading && ( - )} diff --git a/src/pages/workspace/reportFields/CreateReportFieldPage.tsx b/src/pages/workspace/reportFields/CreateReportFieldsPage.tsx similarity index 95% rename from src/pages/workspace/reportFields/CreateReportFieldPage.tsx rename to src/pages/workspace/reportFields/CreateReportFieldsPage.tsx index aaf3877f9490..4a02e701b058 100644 --- a/src/pages/workspace/reportFields/CreateReportFieldPage.tsx +++ b/src/pages/workspace/reportFields/CreateReportFieldsPage.tsx @@ -28,16 +28,16 @@ import INPUT_IDS from '@src/types/form/WorkspaceReportFieldForm'; import InitialListValueSelector from './InitialListValueSelector'; import TypeSelector from './TypeSelector'; -type CreateReportFieldPageProps = WithPolicyAndFullscreenLoadingProps & StackScreenProps; +type CreateReportFieldsPageProps = WithPolicyAndFullscreenLoadingProps & StackScreenProps; const defaultDate = DateUtils.extractDate(new Date().toString()); -function CreateReportFieldPage({ +function CreateReportFieldsPage({ policy, route: { params: {policyID}, }, -}: CreateReportFieldPageProps) { +}: CreateReportFieldsPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const formRef = useRef(null); @@ -101,7 +101,7 @@ function CreateReportFieldPage({ Navigation.navigate(ROUTES.WORKSPACE_REPORT_FIELD_LIST_VALUES.getRoute(policyID))} + onPress={() => Navigation.navigate(ROUTES.WORKSPACE_REPORT_FIELDS_LIST_VALUES.getRoute(policyID))} /> )} @@ -190,6 +190,6 @@ function CreateReportFieldPage({ ); } -CreateReportFieldPage.displayName = 'CreateReportFieldPage'; +CreateReportFieldsPage.displayName = 'CreateReportFieldsPage'; -export default withPolicyAndFullscreenLoading(CreateReportFieldPage); +export default withPolicyAndFullscreenLoading(CreateReportFieldsPage); diff --git a/src/pages/workspace/reportFields/ReportFieldAddListValuePage.tsx b/src/pages/workspace/reportFields/ReportFieldsAddListValuePage.tsx similarity index 90% rename from src/pages/workspace/reportFields/ReportFieldAddListValuePage.tsx rename to src/pages/workspace/reportFields/ReportFieldsAddListValuePage.tsx index 576dd1a6f6b0..a0969160365e 100644 --- a/src/pages/workspace/reportFields/ReportFieldAddListValuePage.tsx +++ b/src/pages/workspace/reportFields/ReportFieldsAddListValuePage.tsx @@ -21,13 +21,13 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; import INPUT_IDS from '@src/types/form/WorkspaceReportFieldForm'; -type ReportFieldAddListValuePageProps = StackScreenProps; +type ReportFieldsAddListValuePageProps = StackScreenProps; -function ReportFieldAddListValuePage({ +function ReportFieldsAddListValuePage({ route: { params: {policyID, reportFieldID}, }, -}: ReportFieldAddListValuePageProps) { +}: ReportFieldsAddListValuePageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {inputCallbackRef} = useAutoFocusInput(); @@ -61,7 +61,7 @@ function ReportFieldAddListValuePage({ ; +type ReportFieldsEditValuePageProps = StackScreenProps; -function ReportFieldEditValuePage({ +function ReportFieldsEditValuePage({ route: { params: {policyID, valueIndex}, }, -}: ReportFieldEditValuePageProps) { +}: ReportFieldsEditValuePageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {inputCallbackRef} = useAutoFocusInput(); @@ -67,7 +67,7 @@ function ReportFieldEditValuePage({ ; -function ReportFieldInitialValuePage({ +type ReportFieldsInitialValuePageProps = WithPolicyAndFullscreenLoadingProps & StackScreenProps; +function ReportFieldsInitialValuePage({ policy, route: { params: {policyID, reportFieldID}, }, -}: ReportFieldInitialValuePagePageProps) { +}: ReportFieldsInitialValuePageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {inputCallbackRef} = useAutoFocusInput(); @@ -98,7 +98,7 @@ function ReportFieldInitialValuePage({ ; +type ReportFieldsListValuesPageProps = WithPolicyAndFullscreenLoadingProps & StackScreenProps; -function ReportFieldListValuesPage({ +function ReportFieldsListValuesPage({ policy, route: { params: {policyID, reportFieldID}, }, -}: ReportFieldListValuesPageProps) { +}: ReportFieldsListValuesPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); @@ -140,7 +140,7 @@ function ReportFieldListValuesPage({ return; } - Navigation.navigate(ROUTES.WORKSPACE_REPORT_FIELD_VALUE_SETTINGS.getRoute(policyID, valueItem.index, reportFieldID)); + Navigation.navigate(ROUTES.WORKSPACE_REPORT_FIELDS_VALUE_SETTINGS.getRoute(policyID, valueItem.index, reportFieldID)); setSelectedValues({}); }; @@ -248,7 +248,7 @@ function ReportFieldListValuesPage({ success icon={Expensicons.Plus} text={translate('workspace.reportFields.addValue')} - onPress={() => Navigation.navigate(ROUTES.WORKSPACE_REPORT_FIELD_ADD_VALUE.getRoute(policyID, reportFieldID))} + onPress={() => Navigation.navigate(ROUTES.WORKSPACE_REPORT_FIELDS_ADD_VALUE.getRoute(policyID, reportFieldID))} /> ); }; @@ -262,7 +262,7 @@ function ReportFieldListValuesPage({ ; +type ReportFieldsSettingsPageProps = WithPolicyAndFullscreenLoadingProps & StackScreenProps; -function ReportFieldSettingsPage({ +function ReportFieldsSettingsPage({ policy, route: { params: {policyID, reportFieldID}, }, -}: ReportFieldSettingsPageProps) { +}: ReportFieldsSettingsPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); @@ -60,7 +60,7 @@ function ReportFieldSettingsPage({ Navigation.navigate(ROUTES.WORKSPACE_EDIT_REPORT_FIELD_INITIAL_VALUE.getRoute(policyID, reportFieldID))} + onPress={() => Navigation.navigate(ROUTES.WORKSPACE_EDIT_REPORT_FIELDS_INITIAL_VALUE.getRoute(policyID, reportFieldID))} /> {isListFieldType && ( Navigation.navigate(ROUTES.WORKSPACE_REPORT_FIELD_LIST_VALUES.getRoute(policyID, reportFieldID))} + onPress={() => Navigation.navigate(ROUTES.WORKSPACE_REPORT_FIELDS_LIST_VALUES.getRoute(policyID, reportFieldID))} /> )} @@ -121,6 +121,6 @@ function ReportFieldSettingsPage({ ); } -ReportFieldSettingsPage.displayName = 'ReportFieldSettingsPage'; +ReportFieldsSettingsPage.displayName = 'ReportFieldsSettingsPage'; -export default withPolicyAndFullscreenLoading(ReportFieldSettingsPage); +export default withPolicyAndFullscreenLoading(ReportFieldsSettingsPage); diff --git a/src/pages/workspace/reportFields/ReportFieldValueSettingsPage.tsx b/src/pages/workspace/reportFields/ReportFieldsValueSettingsPage.tsx similarity index 91% rename from src/pages/workspace/reportFields/ReportFieldValueSettingsPage.tsx rename to src/pages/workspace/reportFields/ReportFieldsValueSettingsPage.tsx index f99842fdf5a7..209be2be26b1 100644 --- a/src/pages/workspace/reportFields/ReportFieldValueSettingsPage.tsx +++ b/src/pages/workspace/reportFields/ReportFieldsValueSettingsPage.tsx @@ -25,14 +25,14 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -type ReportFieldValueSettingsPageProps = WithPolicyAndFullscreenLoadingProps & StackScreenProps; +type ReportFieldsValueSettingsPageProps = WithPolicyAndFullscreenLoadingProps & StackScreenProps; -function ReportFieldValueSettingsPage({ +function ReportFieldsValueSettingsPage({ policy, route: { params: {policyID, valueIndex, reportFieldID}, }, -}: ReportFieldValueSettingsPageProps) { +}: ReportFieldsValueSettingsPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const [formDraft] = useOnyx(ONYXKEYS.FORMS.WORKSPACE_REPORT_FIELDS_FORM_DRAFT); @@ -81,7 +81,7 @@ function ReportFieldValueSettingsPage({ }; const navigateToEditValue = () => { - Navigation.navigate(ROUTES.WORKSPACE_REPORT_FIELD_EDIT_VALUE.getRoute(policyID, valueIndex)); + Navigation.navigate(ROUTES.WORKSPACE_REPORT_FIELDS_EDIT_VALUE.getRoute(policyID, valueIndex)); }; return ( @@ -93,7 +93,7 @@ function ReportFieldValueSettingsPage({ { - Navigation.navigate(ROUTES.WORKSPACE_REPORT_FIELD_SETTINGS.getRoute(policyID, reportField.fieldID)); + const navigateToReportFieldsSettings = (reportField: ReportFieldForList) => { + Navigation.navigate(ROUTES.WORKSPACE_REPORT_FIELDS_SETTINGS.getRoute(policyID, reportField.fieldID)); }; const handleDeleteReportFields = () => { @@ -232,7 +232,7 @@ function WorkspaceReportFieldsPage({ canSelectMultiple sections={reportFieldsSections} onCheckboxPress={updateSelectedReportFields} - onSelectRow={navigateToReportFieldSettings} + onSelectRow={navigateToReportFieldsSettings} onSelectAll={toggleAllReportFields} ListItem={TableListItem} customListHeader={getCustomListHeader()} diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index 92b016766742..cf9952720fc9 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -7,6 +7,7 @@ import Button from '@components/Button'; import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu'; import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; import ConfirmModal from '@components/ConfirmModal'; +import EmptyStateComponent from '@components/EmptyStateComponent'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; @@ -14,9 +15,9 @@ import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import ListItemRightCaretWithLabel from '@components/SelectionList/ListItemRightCaretWithLabel'; import TableListItem from '@components/SelectionList/TableListItem'; +import TableListItemSkeleton from '@components/Skeletons/TableRowSkeleton'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; -import WorkspaceEmptyStateSection from '@components/WorkspaceEmptyStateSection'; import useEnvironment from '@hooks/useEnvironment'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -343,9 +344,13 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { /> )} {!hasVisibleTag && !isLoading && ( - )} diff --git a/src/styles/index.ts b/src/styles/index.ts index a0795e5d378a..e422b8bd3d1e 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -5083,6 +5083,59 @@ const styles = (theme: ThemeColors) => fontSize: variables.fontSizeNormal, fontWeight: FontUtils.fontWeight.bold, }, + + skeletonBackground: { + flex: 1, + position: 'absolute', + top: 0, + left: 0, + width: '100%', + height: '100%', + paddingRight: 8, + paddingLeft: 8, + }, + + emptyStateScrollView: { + minHeight: 400, + height: '100%', + flex: 1, + }, + + emptyStateForeground: (isSmallScreenWidth: boolean) => ({ + justifyContent: 'center', + alignItems: 'center', + height: '100%', + padding: isSmallScreenWidth ? 32 : 0, + width: '100%', + }), + + emptyStateContent: { + backgroundColor: theme.cardBG, + borderRadius: variables.componentBorderRadiusLarge, + maxWidth: 400, + }, + + emptyStateHeader: (isIllustration: boolean) => ({ + borderTopLeftRadius: variables.componentBorderRadiusLarge, + borderTopRightRadius: variables.componentBorderRadiusLarge, + minHeight: 200, + alignItems: isIllustration ? 'center' : undefined, + justifyContent: isIllustration ? 'center' : undefined, + }), + + emptyFolderBG: { + backgroundColor: theme.emptyFolderBG, + }, + + emptyStateVideo: { + borderTopLeftRadius: variables.componentBorderRadiusLarge, + borderTopRightRadius: variables.componentBorderRadiusLarge, + }, + + emptyStateFolderIconSize: { + width: 184, + height: 112, + }, } satisfies Styles); type ThemeStyles = ReturnType; diff --git a/src/styles/theme/themes/dark.ts b/src/styles/theme/themes/dark.ts index b316f116c805..7ed23c7c0991 100644 --- a/src/styles/theme/themes/dark.ts +++ b/src/styles/theme/themes/dark.ts @@ -93,6 +93,7 @@ const darkTheme = { white: colors.white, videoPlayerBG: `${colors.productDark100}cc`, transparentWhite: `${colors.white}51`, + emptyFolderBG: colors.yellow600, // Adding a color here will animate the status bar to the right color when the screen is opened. // Note that it needs to be a screen name, not a route url. diff --git a/src/styles/theme/themes/light.ts b/src/styles/theme/themes/light.ts index 05364515e264..2ebb558ee20b 100644 --- a/src/styles/theme/themes/light.ts +++ b/src/styles/theme/themes/light.ts @@ -93,6 +93,7 @@ const lightTheme = { white: colors.white, videoPlayerBG: `${colors.productDark100}cc`, transparentWhite: `${colors.white}51`, + emptyFolderBG: colors.yellow600, // Adding a color here will animate the status bar to the right color when the screen is opened. // Note that it needs to be a screen name, not a route url. diff --git a/src/styles/theme/types.ts b/src/styles/theme/types.ts index 2d8618c24ebb..ffa42e99777d 100644 --- a/src/styles/theme/types.ts +++ b/src/styles/theme/types.ts @@ -97,6 +97,7 @@ type ThemeColors = { white: Color; videoPlayerBG: Color; transparentWhite: Color; + emptyFolderBG: Color; PAGE_THEMES: Record; diff --git a/src/types/form/ReportFieldEditForm.ts b/src/types/form/ReportFieldEditForm.ts deleted file mode 100644 index 7befa2cb6502..000000000000 --- a/src/types/form/ReportFieldEditForm.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type Form from './Form'; - -type ReportFieldEditForm = Form>; - -// eslint-disable-next-line import/prefer-default-export -export type {ReportFieldEditForm}; diff --git a/src/types/form/ReportFieldsEditForm.ts b/src/types/form/ReportFieldsEditForm.ts new file mode 100644 index 000000000000..8315490d31c7 --- /dev/null +++ b/src/types/form/ReportFieldsEditForm.ts @@ -0,0 +1,6 @@ +import type Form from './Form'; + +type ReportFieldsEditForm = Form>; + +// eslint-disable-next-line import/prefer-default-export +export type {ReportFieldsEditForm}; diff --git a/src/types/form/index.ts b/src/types/form/index.ts index dbfc6e5095f6..450de82a60cf 100644 --- a/src/types/form/index.ts +++ b/src/types/form/index.ts @@ -25,7 +25,7 @@ export type {PrivateNotesForm} from './PrivateNotesForm'; export type {ProfileSettingsForm} from './ProfileSettingsForm'; export type {ReimbursementAccountForm} from './ReimbursementAccountForm'; export type {ReportDescriptionForm} from './ReportDescriptionForm'; -export type {ReportFieldEditForm} from './ReportFieldEditForm'; +export type {ReportFieldsEditForm} from './ReportFieldsEditForm'; export type {ReportVirtualCardFraudForm} from './ReportVirtualCardFraudForm'; export type {RequestPhysicalCardForm} from './RequestPhysicalCardForm'; export type {RoomNameForm} from './RoomNameForm'; diff --git a/tests/e2e/config.ts b/tests/e2e/config.ts index 8963e07c31c8..9241463f6a0c 100644 --- a/tests/e2e/config.ts +++ b/tests/e2e/config.ts @@ -8,6 +8,7 @@ const TEST_NAMES = { ReportTyping: 'Report typing', ChatOpening: 'Chat opening', Linking: 'Linking', + PreloadedLinking: 'Preloaded linking', }; /** @@ -89,6 +90,7 @@ export default { // #announce Chat with many messages reportID: '5421294415618529', }, + // linking from chat A to a specific message in chat B [TEST_NAMES.Linking]: { name: TEST_NAMES.Linking, reportScreen: { @@ -99,6 +101,16 @@ export default { linkedReportID: '5421294415618529', linkedReportActionID: '2845024374735019929', }, + // linking from chat A to a specific message in the same chat A + [TEST_NAMES.PreloadedLinking]: { + name: TEST_NAMES.PreloadedLinking, + reportScreen: { + autoFocus: true, + }, + // Crowded Policy (Do Not Delete) Report, has a input bar available: + reportID: '5421294415618529', + linkedReportActionID: '8984197495983183608', // Message 4897 + }, }, }; diff --git a/tests/unit/CurrencyUtilsTest.ts b/tests/unit/CurrencyUtilsTest.ts index 5322faff763f..9a86c9188d42 100644 --- a/tests/unit/CurrencyUtilsTest.ts +++ b/tests/unit/CurrencyUtilsTest.ts @@ -155,8 +155,8 @@ describe('CurrencyUtils', () => { ['JPY', 2500.5, '¥25'], ['RSD', 100, 'RSD\xa01.00'], ['RSD', 145, 'RSD\xa01.45'], - ['BHD', 12345, 'BHD\xa0123.450'], - ['BHD', 1, 'BHD\xa00.010'], + ['BHD', 12345, 'BHD\xa0123.45'], + ['BHD', 1, 'BHD\xa00.01'], ])('Correctly displays %s', (currency, amount, expectedResult) => { expect(CurrencyUtils.convertToDisplayString(amount, currency)).toBe(expectedResult); }); diff --git a/tests/unit/EmojiTest.ts b/tests/unit/EmojiTest.ts index 968e543363f2..84442db92553 100644 --- a/tests/unit/EmojiTest.ts +++ b/tests/unit/EmojiTest.ts @@ -1,15 +1,7 @@ -import {getUnixTime} from 'date-fns'; -import Onyx from 'react-native-onyx'; import Emojis, {importEmojiLocale} from '@assets/emojis'; import type {Emoji} from '@assets/emojis/types'; -import * as User from '@libs/actions/User'; import {buildEmojisTrie} from '@libs/EmojiTrie'; import * as EmojiUtils from '@libs/EmojiUtils'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type {FrequentlyUsedEmoji} from '@src/types/onyx'; -import * as TestHelper from '../utils/TestHelper'; -import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; describe('EmojiTest', () => { beforeAll(async () => { @@ -196,327 +188,4 @@ describe('EmojiTest', () => { }, ]); }); - - describe('update frequently used emojis', () => { - let spy: jest.SpyInstance; - - beforeAll(() => { - Onyx.init({keys: ONYXKEYS}); - global.fetch = TestHelper.getGlobalFetchMock(); - spy = jest.spyOn(User, 'updateFrequentlyUsedEmojis'); - }); - - beforeEach(() => { - spy.mockClear(); - return Onyx.clear(); - }); - - it('should put a less frequent and recent used emoji behind', () => { - // Given an existing frequently used emojis list with count > 1 - const frequentlyEmojisList: FrequentlyUsedEmoji[] = [ - { - code: '👋', - name: 'wave', - count: 2, - lastUpdatedAt: 4, - types: ['👋🏿', '👋🏾', '👋🏽', '👋🏼', '👋🏻'], - }, - { - code: '💤', - name: 'zzz', - count: 2, - lastUpdatedAt: 3, - }, - { - code: '💯', - name: '100', - count: 2, - lastUpdatedAt: 2, - }, - { - code: '👿', - name: 'imp', - count: 2, - lastUpdatedAt: 1, - }, - ]; - Onyx.merge(ONYXKEYS.FREQUENTLY_USED_EMOJIS, frequentlyEmojisList); - - return waitForBatchedUpdates().then(() => { - // When add a new emoji - const currentTime = getUnixTime(new Date()); - const smileEmoji: Emoji = {code: '😄', name: 'smile'}; - const newEmoji = [smileEmoji]; - User.updateFrequentlyUsedEmojis(EmojiUtils.getFrequentlyUsedEmojis(newEmoji)); - - // Then the new emoji should be at the last item of the list - const expectedSmileEmoji: FrequentlyUsedEmoji = {...smileEmoji, count: 1, lastUpdatedAt: currentTime}; - - const expectedFrequentlyEmojisList = [...frequentlyEmojisList, expectedSmileEmoji]; - expect(spy).toBeCalledWith(expectedFrequentlyEmojisList); - }); - }); - - it('should put more frequent and recent used emoji to the front', () => { - // Given an existing frequently used emojis list - const smileEmoji: Emoji = {code: '😄', name: 'smile'}; - const frequentlyEmojisList: FrequentlyUsedEmoji[] = [ - { - code: '😠', - name: 'angry', - count: 3, - lastUpdatedAt: 5, - }, - { - code: '👋', - name: 'wave', - count: 2, - lastUpdatedAt: 4, - types: ['👋🏿', '👋🏾', '👋🏽', '👋🏼', '👋🏻'], - }, - { - code: '💤', - name: 'zzz', - count: 2, - lastUpdatedAt: 3, - }, - { - code: '💯', - name: '100', - count: 1, - lastUpdatedAt: 2, - }, - {...smileEmoji, count: 1, lastUpdatedAt: 1}, - ]; - Onyx.merge(ONYXKEYS.FREQUENTLY_USED_EMOJIS, frequentlyEmojisList); - - return waitForBatchedUpdates().then(() => { - // When add an emoji that exists in the list - const currentTime = getUnixTime(new Date()); - const newEmoji = [smileEmoji]; - User.updateFrequentlyUsedEmojis(EmojiUtils.getFrequentlyUsedEmojis(newEmoji)); - - // Then the count should be increased and put into the very front of the other emoji within the same count - const expectedFrequentlyEmojisList = [frequentlyEmojisList[0], {...smileEmoji, count: 2, lastUpdatedAt: currentTime}, ...frequentlyEmojisList.slice(1, -1)]; - expect(spy).toBeCalledWith(expectedFrequentlyEmojisList); - }); - }); - - it('should sorted descending by count and lastUpdatedAt for multiple emoji added', () => { - // Given an existing frequently used emojis list - const smileEmoji: Emoji = {code: '😄', name: 'smile'}; - const zzzEmoji: Emoji = {code: '💤', name: 'zzz'}; - const impEmoji: Emoji = {code: '👿', name: 'imp'}; - const frequentlyEmojisList: FrequentlyUsedEmoji[] = [ - { - code: '😠', - name: 'angry', - count: 3, - lastUpdatedAt: 5, - }, - { - code: '👋', - name: 'wave', - count: 2, - lastUpdatedAt: 4, - types: ['👋🏿', '👋🏾', '👋🏽', '👋🏼', '👋🏻'], - }, - {...zzzEmoji, count: 2, lastUpdatedAt: 3}, - { - code: '💯', - name: '100', - count: 1, - lastUpdatedAt: 2, - }, - {...smileEmoji, count: 1, lastUpdatedAt: 1}, - ]; - Onyx.merge(ONYXKEYS.FREQUENTLY_USED_EMOJIS, frequentlyEmojisList); - - return waitForBatchedUpdates().then(() => { - // When add multiple emojis that either exist or not exist in the list - const currentTime = getUnixTime(new Date()); - const newEmoji = [smileEmoji, zzzEmoji, impEmoji]; - User.updateFrequentlyUsedEmojis(EmojiUtils.getFrequentlyUsedEmojis(newEmoji)); - - // Then the count should be increased for existing emoji and sorted descending by count and lastUpdatedAt - const expectedFrequentlyEmojisList = [ - {...zzzEmoji, count: 3, lastUpdatedAt: currentTime}, - frequentlyEmojisList[0], - {...smileEmoji, count: 2, lastUpdatedAt: currentTime}, - frequentlyEmojisList[1], - {...impEmoji, count: 1, lastUpdatedAt: currentTime}, - frequentlyEmojisList[3], - ]; - expect(spy).toBeCalledWith(expectedFrequentlyEmojisList); - }); - }); - - it('make sure the most recent new emoji is added to the list even it is full with count > 1', () => { - // Given an existing full (24 items) frequently used emojis list - const smileEmoji: Emoji = {code: '😄', name: 'smile'}; - const zzzEmoji: Emoji = {code: '💤', name: 'zzz'}; - const impEmoji: Emoji = {code: '👿', name: 'imp'}; - const bookEmoji: Emoji = {code: '📚', name: 'books'}; - const frequentlyEmojisList: FrequentlyUsedEmoji[] = [ - { - code: '😠', - name: 'angry', - count: 3, - lastUpdatedAt: 24, - }, - { - code: '👋', - name: 'wave', - count: 3, - lastUpdatedAt: 23, - types: ['👋🏿', '👋🏾', '👋🏽', '👋🏼', '👋🏻'], - }, - { - code: '😡', - name: 'rage', - count: 3, - lastUpdatedAt: 22, - }, - { - code: '😤', - name: 'triumph', - count: 3, - lastUpdatedAt: 21, - }, - { - code: '🥱', - name: 'yawning_face', - count: 3, - lastUpdatedAt: 20, - }, - { - code: '😫', - name: 'tired_face', - count: 3, - lastUpdatedAt: 19, - }, - { - code: '😩', - name: 'weary', - count: 3, - lastUpdatedAt: 18, - }, - { - code: '😓', - name: 'sweat', - count: 3, - lastUpdatedAt: 17, - }, - { - code: '😞', - name: 'disappointed', - count: 3, - lastUpdatedAt: 16, - }, - { - code: '😣', - name: 'persevere', - count: 3, - lastUpdatedAt: 15, - }, - { - code: '😖', - name: 'confounded', - count: 3, - lastUpdatedAt: 14, - }, - { - code: '👶', - name: 'baby', - count: 3, - lastUpdatedAt: 13, - types: ['👶🏿', '👶🏾', '👶🏽', '👶🏼', '👶🏻'], - }, - { - code: '👄', - name: 'lips', - count: 3, - lastUpdatedAt: 12, - }, - { - code: '🐶', - name: 'dog', - count: 3, - lastUpdatedAt: 11, - }, - { - code: '🦮', - name: 'guide_dog', - count: 3, - lastUpdatedAt: 10, - }, - { - code: '🐱', - name: 'cat', - count: 3, - lastUpdatedAt: 9, - }, - { - code: '🐈‍⬛', - name: 'black_cat', - count: 3, - lastUpdatedAt: 8, - }, - { - code: '🕞', - name: 'clock330', - count: 3, - lastUpdatedAt: 7, - }, - { - code: '🥎', - name: 'softball', - count: 3, - lastUpdatedAt: 6, - }, - { - code: '🏀', - name: 'basketball', - count: 3, - lastUpdatedAt: 5, - }, - { - code: '📟', - name: 'pager', - count: 3, - lastUpdatedAt: 4, - }, - { - code: '🎬', - name: 'clapper', - count: 3, - lastUpdatedAt: 3, - }, - { - code: '📺', - name: 'tv', - count: 3, - lastUpdatedAt: 2, - }, - {...bookEmoji, count: 3, lastUpdatedAt: 1}, - ]; - expect(frequentlyEmojisList.length).toBe(CONST.EMOJI_FREQUENT_ROW_COUNT * CONST.EMOJI_NUM_PER_ROW); - Onyx.merge(ONYXKEYS.FREQUENTLY_USED_EMOJIS, frequentlyEmojisList); - - return waitForBatchedUpdates().then(() => { - // When add new emojis - const currentTime = getUnixTime(new Date()); - const newEmoji = [bookEmoji, smileEmoji, zzzEmoji, impEmoji, smileEmoji]; - User.updateFrequentlyUsedEmojis(EmojiUtils.getFrequentlyUsedEmojis(newEmoji)); - - // Then the last emojis from the list should be replaced with the most recent new emoji (smile) - const expectedFrequentlyEmojisList = [ - {...bookEmoji, count: 4, lastUpdatedAt: currentTime}, - ...frequentlyEmojisList.slice(0, -2), - {...smileEmoji, count: 1, lastUpdatedAt: currentTime}, - ]; - expect(spy).toBeCalledWith(expectedFrequentlyEmojisList); - }); - }); - }); }); diff --git a/tests/unit/SidebarFilterTest.ts b/tests/unit/SidebarFilterTest.ts index c54c2cae54f8..1e0745916717 100644 --- a/tests/unit/SidebarFilterTest.ts +++ b/tests/unit/SidebarFilterTest.ts @@ -280,6 +280,35 @@ xdescribe('Sidebar', () => { ); }); + it('filter paycheck and bill report', () => { + const report1: Report = { + ...LHNTestUtils.getFakeReport(), + type: CONST.REPORT.TYPE.PAYCHECK, + }; + const report2: Report = { + ...LHNTestUtils.getFakeReport(), + type: CONST.REPORT.TYPE.BILL, + }; + const report3: Report = LHNTestUtils.getFakeReport(); + LHNTestUtils.getDefaultRenderedSidebarLinks(report1.reportID); + const reportCollectionDataSet: ReportCollectionDataSet = { + [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, + [`${ONYXKEYS.COLLECTION.REPORT}${report2.reportID}`]: report2, + [`${ONYXKEYS.COLLECTION.REPORT}${report3.reportID}`]: report3, + }; + return ( + waitForBatchedUpdates() + .then(() => Onyx.multiSet(reportCollectionDataSet)) + + // Then the reports 1 and 2 are hidden and 3 is not + .then(() => { + const hintText = Localize.translateLocal('accessibilityHints.navigatesToChat'); + const optionRows = screen.queryAllByAccessibilityHint(hintText); + expect(optionRows).toHaveLength(1); + }) + ); + }); + // NOTE: This is also for #focus mode, should we move this test block? describe('all combinations of isArchived, isUserCreatedPolicyRoom, hasAddWorkspaceError, isUnread, isPinned, hasDraft', () => { // Given a report that is the active report and doesn't change