diff --git a/android/app/build.gradle b/android/app/build.gradle index 2ec58aab1afa..9d6aed6e96c9 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -110,8 +110,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009006000 - versionName "9.0.60-0" + versionCode 1009006100 + versionName "9.0.61-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/attachment-not-found.svg b/assets/images/attachment-not-found.svg new file mode 100644 index 000000000000..25da973ce9cb --- /dev/null +++ b/assets/images/attachment-not-found.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/contributingGuides/CONTRIBUTING.md b/contributingGuides/CONTRIBUTING.md index be71cd4e115a..0a9417820190 100644 --- a/contributingGuides/CONTRIBUTING.md +++ b/contributingGuides/CONTRIBUTING.md @@ -11,6 +11,7 @@ You can create as many accounts as needed in order to test your changes directly 1. When testing chat functionality in the app please do this between accounts you or your fellow contributors own - **do not test chatting with Concierge**, as this diverts to our customer support team. Thank you. 2. A member of our customer onboarding team gets auto-assigned to every new policy created by a non-paying account to help them set up. Please **do not interact with these teams, ask for calls, or support on your issues.** If you do need to test functionality inside the defaultRooms (#admins & #announce) for any issues you’re working on, please let them know that you are a contributor and don’t need assistance. They will proceed to ignore the chat. +3. Please **do not post in any Expensify owned public room for testing** (e.g #exfy-roadmap, #new-expensify-feedback). These rooms include real customers and investors. You can create your own public rooms, or [use this test public room](https://staging.new.expensify.com/r/2091104345528462) on either staging or production. Thanks! #### Generating Multiple Test Accounts You can generate multiple test accounts by using a `+` postfix, for example if your email is test@test.com, you can create multiple New Expensify accounts connected to the same email address by using test+123@test.com, test+456@test.com, etc. diff --git a/contributingGuides/PERFORMANCE_METRICS.md b/contributingGuides/PERFORMANCE_METRICS.md index 6c40e346a3ce..ecebbaae4e0e 100644 --- a/contributingGuides/PERFORMANCE_METRICS.md +++ b/contributingGuides/PERFORMANCE_METRICS.md @@ -14,21 +14,16 @@ Project is using Firebase for tracking these metrics. However, not all of them a | `js_loaded` | ✅ | The time it takes for the JavaScript bundle to load.

**Platforms:** Android, iOS | **Android:** Starts in the `onCreate` method.

**iOS:** Starts in the AppDelegate's `didFinishLaunchingWithOptions` method. | Stops at the first render of the app via native module on the JS side. | | `_app_in_foreground` | ✅ | The time when the app is running in the foreground and available to the user.

**Platforms:** Android, iOS | **Android:** Starts when the first activity to reach the foreground has its `onResume()` method called.

**iOS:** Starts when the application receives the `UIApplicationDidBecomeActiveNotification` notification. | **Android:** Stops when the last activity to leave the foreground has its `onStop()` method called.

**iOS:** Stops when it receives the `UIApplicationWillResignActiveNotification` notification. | | `_app_in_background` | ✅ | Time when the app is running in the background.

**Platforms:** Android, iOS | **Android:** Starts when the last activity to leave the foreground has its `onStop()` method called.

**iOS:** Starts when the application receives the `UIApplicationWillResignActiveNotification` notification. | **Android:** Stops when the first activity to reach the foreground has its `onResume()` method called.

**iOS:** Stops when it receives the `UIApplicationDidBecomeActiveNotification` notification. | -| `homepage_initial_render` | ✅ | Time taken for the initial render of the app for a logged in user.

**Platforms:** All | Starts with the first render of the `AuthScreens` component. | Stops once the `AuthScreens` component is mounted. | -| `sidebar_loaded` | ❌ | Time taken for the Sidebar to load.

**Platforms:** All | Starts when the Sidebar is mounted. | Stops when the Splash Screen is hidden. | +| `sidebar_loaded` | ❌ | Time taken for the Sidebar to load.

**Platforms:** All | Starts when the Sidebar is mounted. | Stops when the LHN finishes laying out. | | `calc_most_recent_last_modified_action` | ✅ | Time taken to find the most recently modified report action or report.

**Platforms:** All | Starts when the app reconnects to the network | Ends when the app reconnects to the network and the most recent report action or report is found. | -| `search_render` | ✅ | Time taken to render the Chat Finder page.

**Platforms:** All | Starts when the Chat Finder icon in LHN is pressed. | Stops when the list of available options is rendered for the first time. | -| `load_search_options` | ✅ | Time taken to generate the list of options used in Chat Finder.

**Platforms:** All | Starts when the `getSearchOptions` function is called. | Stops when the list of available options is generated. | -| `search_filter_options` | ✅ | Time taken to filter search options in Chat Finder by given search value.

**Platforms:** All | Starts when user types something in the Chat Finder search input. | Stops when the list of filtered options is generated. | +| `open_search` | ✅ | Time taken to open up the Search Router.

**Platforms:** All | Starts when the Search Router icon in LHN is pressed. | Stops when the list of available options finishes laying out. | +| `load_search_options` | ✅ | Time taken to generate the list of options used in the Search Router.

**Platforms:** All | Starts when the `getSearchOptions` function is called. | Stops when the list of available options is generated. | +| `search_filter_options` | ✅ | Time taken to filter search options in the Search Router by the given search value.

**Platforms:** All | Starts when user types something in the Search Router search input. | Stops when the list of filtered options is generated. | | `trie_initialization` | ✅ | Time taken to build the emoji trie.

**Platforms:** All | Starts when emoji trie begins to build. | Stops when emoji trie building is complete. | -| `open_report` | ❌ | Time taken to open a report.

**Platforms:** All | Starts when the row in the `LHNOptionsList` is pressed. | Stops when the `ReportActionsList` finishes laying out. | -| `switch_report` | ✅ | Time taken to open report.

**Platforms:** All | Starts when the chat in the LHN is pressed. | Stops when the `ReportActionsList` finishes laying out. | +| `open_report` | ✅ | Time taken to open a report.

**Platforms:** All | Starts when the row in the `LHNOptionsList` is pressed. | Stops when the `ReportActionsList` finishes laying out. | | `open_report_from_preview` | ✅ | Time taken to open a report from preview.

(previously `switch_report_from_preview`)

**Platforms:** All | Starts when the user presses the Report Preview. | Stops when the `ReportActionsList` finishes laying out. | -| `switch_report_from_preview` | ❌ | **[REMOVED]** Time taken to open a report from preview. | Starts when the user presses the Report Preview. | Stops when the `ReportActionsList` finishes laying out. | -| `chat_render` | ✅ | Time taken to render the Report screen.

**Platforms:** All | Starts when the `ReportScreen` is being rendered for the first time. | Stops once the `ReportScreen` component is mounted. | -| `report_initial_render` | ❌ | Time taken to render the Report screen.

**Platforms:** All | Starts when the first item is rendered in the `LHNOptionsList`. | Stops when the `ReportActionsList` finishes laying out. | | `open_report_thread` | ✅ | Time taken to open a thread in a report.

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

**Platforms:** All | Starts when the new message is sent. | Stops when the message is being rendered in the chat. | +| `send_message` | ✅ | Time taken to send a message.

**Platforms:** All | Starts when the new message is sent. | Stops when the message is being rendered in the chat. | ## Documentation Maintenance @@ -46,4 +41,4 @@ To ensure this documentation remains accurate and useful, please adhere to the f ## Additional Resources - [Firebase Documentation](https://firebase.google.com/docs) -- [Firebase Performance Monitoring](https://firebase.google.com/docs/perf-mon) \ No newline at end of file +- [Firebase Performance Monitoring](https://firebase.google.com/docs/perf-mon) diff --git a/docs/articles/expensify-classic/connections/quickbooks-online/Configure-Quickbooks-Online.md b/docs/articles/expensify-classic/connections/quickbooks-online/Configure-Quickbooks-Online.md index 3fd1df0c0a1c..a6e19f8fd549 100644 --- a/docs/articles/expensify-classic/connections/quickbooks-online/Configure-Quickbooks-Online.md +++ b/docs/articles/expensify-classic/connections/quickbooks-online/Configure-Quickbooks-Online.md @@ -40,6 +40,7 @@ The following steps help you determine how data will be exported from Expensify - Journal Entries - This is a single itemized journal entry for each Expensify report. - _Non-reimbursable expenses_: Non-reimbursable expenses export to QuickBooks Online as: - Credit Card expenses - Each expense will be exported as a bank transaction with its transaction date. + - Note: The Expensify Card transactions will always export as Credit Card charges, even if the non-reimbursable setting is configured differently (such as a Vendor Bill.) - Debit Card Expenses - Each expense will be exported as a bank transaction with its transaction date. - Vendor Bills - A single detailed vendor bill is generated for each Expensify report. - If the accounting period is closed, the vendor bill will be posted on the first day of the next open period. If you choose to export non-reimbursable expenses as Vendor Bills, you can assign a default vendor to the bill. diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 7b263d98cf27..537de56b131c 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 9.0.60 + 9.0.61 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.60.0 + 9.0.61.0 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 90916937f184..328e27f2578f 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 9.0.60 + 9.0.61 CFBundleSignature ???? CFBundleVersion - 9.0.60.0 + 9.0.61.0 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 5e3c61e29256..7efe1888d4ae 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 9.0.60 + 9.0.61 CFBundleVersion - 9.0.60.0 + 9.0.61.0 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index 0a5aee49900d..b318c1a7f31c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.0.60-0", + "version": "9.0.61-0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.60-0", + "version": "9.0.61-0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 54b3a3c945cb..8d6612308505 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.60-0", + "version": "9.0.61-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/patches/react-native+0.75.2+020+keyboard-avoiding-view.patch b/patches/react-native+0.75.2+020+keyboard-avoiding-view.patch new file mode 100644 index 000000000000..2ee8aa1fd0de --- /dev/null +++ b/patches/react-native+0.75.2+020+keyboard-avoiding-view.patch @@ -0,0 +1,18 @@ +diff --git a/node_modules/react-native/Libraries/Components/Keyboard/KeyboardAvoidingView.js b/node_modules/react-native/Libraries/Components/Keyboard/KeyboardAvoidingView.js +index e26d677..597be5a 100644 +--- a/node_modules/react-native/Libraries/Components/Keyboard/KeyboardAvoidingView.js ++++ b/node_modules/react-native/Libraries/Components/Keyboard/KeyboardAvoidingView.js +@@ -175,6 +175,13 @@ class KeyboardAvoidingView extends React.Component { + } + + componentDidMount(): void { ++ // Fix KeyboardAvoidingView not aware of the keyboard closing after it is unmounted. ++ // Remove this patch after the upstream fix https://github.com/facebook/react-native/commit/08bd8ac47da60121225e7b281bbf566e2c5a291e is released. ++ if (!Keyboard.isVisible()) { ++ this._keyboardEvent = null; ++ this._setBottom(0); ++ } ++ + if (Platform.OS === 'ios') { + this._subscriptions = [ + Keyboard.addListener('keyboardWillChangeFrame', this._onKeyboardChange), diff --git a/src/CONST.ts b/src/CONST.ts index d9c3b72d2d1a..4e873163cc95 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -297,6 +297,7 @@ const CONST = { DEFAULT_TABLE_NAME: 'keyvaluepairs', DEFAULT_ONYX_DUMP_FILE_NAME: 'onyx-state.txt', DEFAULT_POLICY_ROOM_CHAT_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL], + DEFAULT_IMAGE_FILE_NAME: 'image', DISABLED_MAX_EXPENSE_VALUE: 10000000000, POLICY_BILLABLE_MODES: { BILLABLE: 'billable', @@ -1253,17 +1254,13 @@ const CONST = { }, TIMING: { CALCULATE_MOST_RECENT_LAST_MODIFIED_ACTION: 'calc_most_recent_last_modified_action', - SEARCH_ROUTER_RENDER: 'search_router_render', - CHAT_RENDER: 'chat_render', + OPEN_SEARCH: 'open_search', OPEN_REPORT: 'open_report', - HOMEPAGE_INITIAL_RENDER: 'homepage_initial_render', - REPORT_INITIAL_RENDER: 'report_initial_render', - SWITCH_REPORT: 'switch_report', OPEN_REPORT_FROM_PREVIEW: 'open_report_from_preview', OPEN_REPORT_THREAD: 'open_report_thread', SIDEBAR_LOADED: 'sidebar_loaded', LOAD_SEARCH_OPTIONS: 'load_search_options', - MESSAGE_SENT: 'message_sent', + SEND_MESSAGE: 'send_message', COLD: 'cold', WARM: 'warm', REPORT_ACTION_ITEM_LAYOUT_DEBOUNCE_TIME: 1500, @@ -1858,7 +1855,6 @@ const CONST = { JOBS: 'jobs', }, }, - NETSUITE_CUSTOM_LIST_LIMIT: 8, NETSUITE_ADD_CUSTOM_LIST_STEP_NAMES: ['1', '2,', '3', '4'], NETSUITE_ADD_CUSTOM_SEGMENT_STEP_NAMES: ['1', '2,', '3', '4', '5', '6,'], }, @@ -4547,9 +4543,6 @@ const CONST = { }, INDENTS: ' ', PARENT_CHILD_SEPARATOR: ': ', - CATEGORY_LIST_THRESHOLD: 8, - TAG_LIST_THRESHOLD: 8, - TAX_RATES_LIST_THRESHOLD: 8, COLON: ':', MAPBOX: { PADDING: 32, @@ -4633,11 +4626,6 @@ const CONST = { */ MAX_SELECTION_LIST_PAGE_LENGTH: 500, - /** - * We only include the members search bar when we have 8 or more members - */ - SHOULD_SHOW_MEMBERS_SEARCH_INPUT_BREAKPOINT: 8, - /** * Bank account names */ @@ -4811,7 +4799,6 @@ const CONST = { WORKSPACE_SWITCHER: { NAME: 'Expensify', SUBSCRIPT_ICON_SIZE: 8, - MINIMUM_WORKSPACES_TO_SHOW_SEARCH: 8, }, WELCOME_VIDEO_URL: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, @@ -5840,7 +5827,6 @@ const CONST = { MAX_TAX_RATE_INTEGER_PLACES: 4, MAX_TAX_RATE_DECIMAL_PLACES: 4, - MIN_TAX_RATE_DECIMAL_PLACES: 2, DOWNLOADS_PATH: '/Downloads', DOWNLOADS_TIMEOUT: 5000, @@ -5862,9 +5848,6 @@ const CONST = { ACTION_TYPES: { VIEW: 'view', REVIEW: 'review', - SUBMIT: 'submit', - APPROVE: 'approve', - PAY: 'pay', DONE: 'done', PAID: 'paid', }, diff --git a/src/Expensify.tsx b/src/Expensify.tsx index e07b03a6d405..1d0100add00f 100644 --- a/src/Expensify.tsx +++ b/src/Expensify.tsx @@ -30,7 +30,6 @@ import NavigationRoot from './libs/Navigation/NavigationRoot'; import NetworkConnection from './libs/NetworkConnection'; import PushNotification from './libs/Notification/PushNotification'; import './libs/Notification/PushNotification/subscribePushNotification'; -import Performance from './libs/Performance'; import setCrashlyticsUserId from './libs/setCrashlyticsUserId'; import StartupTimer from './libs/StartupTimer'; // This lib needs to be imported, but it has nothing to export since all it contains is an Onyx connection @@ -138,7 +137,6 @@ function Expensify() { const onSplashHide = useCallback(() => { setSplashScreenState(CONST.BOOT_SPLASH_STATE.HIDDEN); - Performance.markEnd(CONST.TIMING.SIDEBAR_LOADED); }, [setSplashScreenState]); useLayoutEffect(() => { diff --git a/src/ROUTES.ts b/src/ROUTES.ts index cd94035e0fff..103c4b2c3125 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -310,11 +310,13 @@ const ROUTES = { }, ATTACHMENTS: { route: 'attachment', - getRoute: (reportID: string, type: ValueOf, url: string, accountID?: number, isAuthTokenRequired?: boolean) => { + getRoute: (reportID: string, type: ValueOf, url: string, accountID?: number, isAuthTokenRequired?: boolean, fileName?: string) => { const reportParam = reportID ? `&reportID=${reportID}` : ''; const accountParam = accountID ? `&accountID=${accountID}` : ''; const authTokenParam = isAuthTokenRequired ? '&isAuthTokenRequired=true' : ''; - return `attachment?source=${encodeURIComponent(url)}&type=${type}${reportParam}${accountParam}${authTokenParam}` as const; + const fileNameParam = fileName ? `&fileName=${fileName}` : ''; + + return `attachment?source=${encodeURIComponent(url)}&type=${type}${reportParam}${accountParam}${authTokenParam}${fileNameParam}` as const; }, }, REPORT_PARTICIPANTS: { diff --git a/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx b/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx index 4de43a763231..5800e92cc4f4 100644 --- a/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx +++ b/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx @@ -4,6 +4,7 @@ import {View} from 'react-native'; import AttachmentView from '@components/Attachments/AttachmentView'; import type {Attachment} from '@components/Attachments/types'; import Button from '@components/Button'; +import * as Expensicons from '@components/Icon/Expensicons'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import SafeAreaConsumer from '@components/SafeAreaConsumer'; import Text from '@components/Text'; @@ -83,6 +84,7 @@ function CarouselItem({item, onPress, isFocused, isModalHovered}: CarouselItemPr isHovered={isModalHovered} isFocused={isFocused} duration={item.duration} + fallbackSource={Expensicons.AttachmentNotFound} /> diff --git a/src/components/Attachments/AttachmentView/index.tsx b/src/components/Attachments/AttachmentView/index.tsx index 0af1a86992e7..1281c017308d 100644 --- a/src/components/Attachments/AttachmentView/index.tsx +++ b/src/components/Attachments/AttachmentView/index.tsx @@ -10,6 +10,7 @@ import EReceipt from '@components/EReceipt'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import ScrollView from '@components/ScrollView'; +import Text from '@components/Text'; import {usePlaybackContext} from '@components/VideoPlayerContexts/PlaybackContext'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -127,7 +128,7 @@ function AttachmentView({ const [imageError, setImageError] = useState(false); - useNetwork({onReconnect: () => setImageError(false)}); + const {isOffline} = useNetwork({onReconnect: () => setImageError(false)}); useEffect(() => { FileUtils.getFileResolution(file).then((resolution) => { @@ -226,15 +227,20 @@ function AttachmentView({ if (isFileImage) { if (imageError && (typeof fallbackSource === 'number' || typeof fallbackSource === 'function')) { return ( - + + + + {translate('attachmentView.attachmentNotFound')} + + ); } + let imageSource = imageError && fallbackSource ? (fallbackSource as string) : (source as string); if (isHighResolution) { @@ -268,6 +274,9 @@ function AttachmentView({ isImage={isFileImage} onPress={onPress} onError={() => { + if (isOffline) { + return; + } setImageError(true); }} /> diff --git a/src/components/CategoryPicker.tsx b/src/components/CategoryPicker.tsx index 33d97c6909f5..0c855507371a 100644 --- a/src/components/CategoryPicker.tsx +++ b/src/components/CategoryPicker.tsx @@ -56,7 +56,7 @@ function CategoryPicker({selectedCategory, policyID, onSubmit}: CategoryPickerPr const categoryData = categoryOptions?.at(0)?.data ?? []; const header = OptionsListUtils.getHeaderMessageForNonUserList(categoryData.length > 0, debouncedSearchValue); const categoriesCount = OptionsListUtils.getEnabledCategoriesCount(categories); - const isCategoriesCountBelowThreshold = categoriesCount < CONST.CATEGORY_LIST_THRESHOLD; + const isCategoriesCountBelowThreshold = categoriesCount < CONST.STANDARD_LIST_ITEM_LIMIT; const showInput = !isCategoriesCountBelowThreshold; return [categoryOptions, header, showInput]; diff --git a/src/components/FlatList/index.tsx b/src/components/FlatList/index.tsx index be0227375470..52ccf50cd722 100644 --- a/src/components/FlatList/index.tsx +++ b/src/components/FlatList/index.tsx @@ -109,28 +109,31 @@ function MVCPFlatList({maintainVisibleContentPosition, horizontal = false } }, [getContentView, getScrollOffset, mvcpMinIndexForVisible, horizontal]); - const adjustForMaintainVisibleContentPosition = useCallback(() => { - if (mvcpMinIndexForVisible == null) { - return; - } + const adjustForMaintainVisibleContentPosition = useCallback( + (animated = true) => { + if (mvcpMinIndexForVisible == null) { + return; + } - const firstVisibleView = firstVisibleViewRef.current; - const prevFirstVisibleOffset = prevFirstVisibleOffsetRef.current; - if (firstVisibleView == null || !firstVisibleView.isConnected || prevFirstVisibleOffset == null) { - return; - } + const firstVisibleView = firstVisibleViewRef.current; + const prevFirstVisibleOffset = prevFirstVisibleOffsetRef.current; + if (firstVisibleView == null || !firstVisibleView.isConnected || prevFirstVisibleOffset == null) { + return; + } - const firstVisibleViewOffset = horizontal ? firstVisibleView.offsetLeft : firstVisibleView.offsetTop; - const delta = firstVisibleViewOffset - prevFirstVisibleOffset; - if (Math.abs(delta) > (IS_MOBILE_SAFARI ? 100 : 0.5)) { - const scrollOffset = lastScrollOffsetRef.current; - prevFirstVisibleOffsetRef.current = firstVisibleViewOffset; - scrollToOffset(scrollOffset + delta, false, true); - if (mvcpAutoscrollToTopThresholdRef.current != null && scrollOffset <= mvcpAutoscrollToTopThresholdRef.current) { - scrollToOffset(0, true, false); + const firstVisibleViewOffset = horizontal ? firstVisibleView.offsetLeft : firstVisibleView.offsetTop; + const delta = firstVisibleViewOffset - prevFirstVisibleOffset; + if (Math.abs(delta) > (IS_MOBILE_SAFARI ? 100 : 0.5)) { + const scrollOffset = lastScrollOffsetRef.current; + prevFirstVisibleOffsetRef.current = firstVisibleViewOffset; + scrollToOffset(scrollOffset + delta, false, true); + if (mvcpAutoscrollToTopThresholdRef.current != null && scrollOffset <= mvcpAutoscrollToTopThresholdRef.current) { + scrollToOffset(0, animated, false); + } } - } - }, [scrollToOffset, mvcpMinIndexForVisible, horizontal]); + }, + [scrollToOffset, mvcpMinIndexForVisible, horizontal], + ); const setupMutationObserver = useCallback(() => { const contentView = getContentView(); @@ -141,6 +144,7 @@ function MVCPFlatList({maintainVisibleContentPosition, horizontal = false mutationObserverRef.current?.disconnect(); const mutationObserver = new MutationObserver((mutations) => { + let isEditComposerAdded = false; // Check if the first visible view is removed and re-calculate it // if needed. mutations.forEach((mutation) => { @@ -150,6 +154,12 @@ function MVCPFlatList({maintainVisibleContentPosition, horizontal = false } firstVisibleViewRef.current = null; }); + mutation.addedNodes.forEach((node) => { + if (node.nodeType !== Node.ELEMENT_NODE || !(node as HTMLElement).querySelector('#composer')) { + return; + } + isEditComposerAdded = true; + }); }); if (firstVisibleViewRef.current == null) { @@ -162,7 +172,7 @@ function MVCPFlatList({maintainVisibleContentPosition, horizontal = false return; } - adjustForMaintainVisibleContentPosition(); + adjustForMaintainVisibleContentPosition(!isEditComposerAdded); prepareForMaintainVisibleContentPosition(); }); mutationObserver.observe(contentView, { diff --git a/src/components/FocusModeNotification.tsx b/src/components/FocusModeNotification.tsx index 7b3f567d256b..fe63fb4b487b 100644 --- a/src/components/FocusModeNotification.tsx +++ b/src/components/FocusModeNotification.tsx @@ -24,6 +24,8 @@ function FocusModeNotification() { confirmText={translate('common.buttonConfirm')} onConfirm={User.clearFocusModeNotification} shouldShowCancelButton={false} + onBackdropPress={User.clearFocusModeNotification} + onCancel={User.clearFocusModeNotification} prompt={ {translate('focusModeUpdateModal.prompt')} diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx index 17fbe1656020..f53e490dd0f9 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx @@ -1,4 +1,4 @@ -import React, {memo, useState} from 'react'; +import React, {memo} from 'react'; import {useOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import type {CustomRendererProps, TBlock} from 'react-native-render-html'; @@ -67,9 +67,14 @@ function ImageRenderer({tnode}: ImageRendererProps) { const fileType = FileUtils.getFileType(attachmentSourceAttribute); const fallbackIcon = fileType === CONST.ATTACHMENT_FILE_TYPE.FILE ? Expensicons.Document : Expensicons.GalleryNotFound; - const [hasLoadFailed, setHasLoadFailed] = useState(true); const theme = useTheme(); + let fileName = htmlAttribs[CONST.ATTACHMENT_ORIGINAL_FILENAME_ATTRIBUTE] || FileUtils.getFileName(`${isAttachmentOrReceipt ? attachmentSourceAttribute : htmlAttribs.src}`); + const fileInfo = FileUtils.splitExtensionFromFileName(fileName); + if (!fileInfo.fileExtension) { + fileName = `${fileInfo?.fileName || CONST.DEFAULT_IMAGE_FILE_NAME}.jpg`; + } + const thumbnailImageComponent = ( setHasLoadFailed(true)} - onMeasure={() => setHasLoadFailed(false)} fallbackIconBackground={theme.highlightBG} fallbackIconColor={theme.border} /> @@ -101,7 +104,7 @@ function ImageRenderer({tnode}: ImageRendererProps) { return; } - const route = ROUTES.ATTACHMENTS?.getRoute(reportID ?? '-1', type, source, accountID, isAttachmentOrReceipt); + const route = ROUTES.ATTACHMENTS?.getRoute(reportID ?? '-1', type, source, accountID, isAttachmentOrReceipt, fileName); Navigation.navigate(route); }} onLongPress={(event) => { @@ -113,7 +116,6 @@ function ImageRenderer({tnode}: ImageRendererProps) { shouldUseHapticsOnLongPress accessibilityRole={CONST.ROLE.BUTTON} accessibilityLabel={translate('accessibilityHints.viewAttachment')} - disabled={hasLoadFailed} > {thumbnailImageComponent} diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index fa531ce34adf..bd4bb64da050 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -8,6 +8,7 @@ import ArrowRight from '@assets/images/arrow-right.svg'; import ArrowUpLong from '@assets/images/arrow-up-long.svg'; import UpArrow from '@assets/images/arrow-up.svg'; import ArrowsUpDown from '@assets/images/arrows-updown.svg'; +import AttachmentNotFound from '@assets/images/attachment-not-found.svg'; import AdminRoomAvatar from '@assets/images/avatars/admin-room.svg'; import AnnounceRoomAvatar from '@assets/images/avatars/announce-room.svg'; import ConciergeAvatar from '@assets/images/avatars/concierge-avatar.svg'; @@ -217,6 +218,7 @@ export { ArrowsUpDown, ArrowUpLong, ArrowDownLong, + AttachmentNotFound, Wrench, BackArrow, Bank, diff --git a/src/components/ImageView/index.tsx b/src/components/ImageView/index.tsx index 266ed2eed16a..0bce2fd38432 100644 --- a/src/components/ImageView/index.tsx +++ b/src/components/ImageView/index.tsx @@ -196,8 +196,12 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV document.removeEventListener('mouseup', trackPointerPosition); }; }, [canUseTouchScreen, trackMovement, trackPointerPosition]); - - const isLocalFile = FileUtils.isLocalFile(url); + // isLocalToUserDeviceFile means the file is located on the user device, + // not loaded on the server yet (the user is offline when loading this file in fact) + let isLocalToUserDeviceFile = FileUtils.isLocalFile(url); + if (isLocalToUserDeviceFile && typeof url === 'string' && url.startsWith('/chat-attachments')) { + isLocalToUserDeviceFile = false; + } if (canUseTouchScreen) { return ( @@ -238,8 +242,8 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV /> - {isLoading && (!isOffline || isLocalFile) && } - {isLoading && !isLocalFile && } + {isLoading && (!isOffline || isLocalToUserDeviceFile) && } + {isLoading && !isLocalToUserDeviceFile && } ); } diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index 94116181bccb..3e3f4d1b8e5d 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -30,6 +30,7 @@ import * as ReportUtils from '@libs/ReportUtils'; import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu'; import FreeTrial from '@pages/settings/Subscription/FreeTrial'; import variables from '@styles/variables'; +import Timing from '@userActions/Timing'; import * as User from '@userActions/User'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -193,6 +194,7 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti ref={popoverAnchor} onPress={(event) => { Performance.markStart(CONST.TIMING.OPEN_REPORT); + Timing.start(CONST.TIMING.OPEN_REPORT); event?.preventDefault(); // Enable Composer to focus on clicking the same chat after opening the context menu. diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 9067f1abb11a..d476d1198808 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -27,6 +27,7 @@ import * as CurrencyUtils from '@libs/CurrencyUtils'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import HapticFeedback from '@libs/HapticFeedback'; import Navigation from '@libs/Navigation/Navigation'; +import Performance from '@libs/Performance'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReceiptUtils from '@libs/ReceiptUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; @@ -456,6 +457,7 @@ function ReportPreview({ { + Performance.markStart(CONST.TIMING.OPEN_REPORT_FROM_PREVIEW); Timing.start(CONST.TIMING.OPEN_REPORT_FROM_PREVIEW); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(iouReportID)); }} diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx index d73937aeadd9..a330be3d5ff6 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader.tsx @@ -182,7 +182,7 @@ function SearchPageHeader({queryJSON, hash}: SearchPageHeaderProps) { return; } - const reportIDList = selectedReports?.filter((report) => !!report) ?? []; + const reportIDList = (selectedReports?.filter((report) => !!report) as string[]) ?? []; SearchActions.exportSearchItemsToCSV( {query: status, jsonQuery: JSON.stringify(queryJSON), reportIDList, transactionIDList: selectedTransactionsKeys, policyIDs: [activeWorkspaceID ?? '']}, () => { diff --git a/src/components/Search/SearchRouter/SearchButton.tsx b/src/components/Search/SearchRouter/SearchButton.tsx index 76eacd8b991d..90699e951998 100644 --- a/src/components/Search/SearchRouter/SearchButton.tsx +++ b/src/components/Search/SearchRouter/SearchButton.tsx @@ -30,8 +30,8 @@ function SearchButton({style}: SearchButtonProps) { accessibilityLabel={translate('common.search')} style={[styles.flexRow, styles.touchableButtonImage, style]} onPress={Session.checkIfActionIsAllowed(() => { - Timing.start(CONST.TIMING.SEARCH_ROUTER_RENDER); - Performance.markStart(CONST.TIMING.SEARCH_ROUTER_RENDER); + Timing.start(CONST.TIMING.OPEN_SEARCH); + Performance.markStart(CONST.TIMING.OPEN_SEARCH); openSearchRouter(); })} diff --git a/src/components/Search/SearchRouter/SearchRouterList.tsx b/src/components/Search/SearchRouter/SearchRouterList.tsx index cc854ff926c3..45e30a6bad6d 100644 --- a/src/components/Search/SearchRouter/SearchRouterList.tsx +++ b/src/components/Search/SearchRouter/SearchRouterList.tsx @@ -69,8 +69,8 @@ type SearchRouterListProps = { }; const setPerformanceTimersEnd = () => { - Timing.end(CONST.TIMING.SEARCH_ROUTER_RENDER); - Performance.markEnd(CONST.TIMING.SEARCH_ROUTER_RENDER); + Timing.end(CONST.TIMING.OPEN_SEARCH); + Performance.markEnd(CONST.TIMING.OPEN_SEARCH); }; function getContextualSearchQuery(reportName: string) { diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts index 130ad7ae6f6e..74bf7b16d020 100644 --- a/src/components/Search/types.ts +++ b/src/components/Search/types.ts @@ -24,13 +24,6 @@ type SelectedTransactionInfo = { /** Model of selected results */ type SelectedTransactions = Record; -/** Model of payment data used by Search bulk actions */ -type PaymentData = { - reportID: string; - amount: number; - paymentType: ValueOf; -}; - type SortOrder = ValueOf; type SearchColumnType = ValueOf; type ExpenseSearchStatus = ValueOf; @@ -124,6 +117,5 @@ export type { TripSearchStatus, ChatSearchStatus, SearchAutocompleteResult, - PaymentData, SearchAutocompleteQueryRange, }; diff --git a/src/components/SelectionList/Search/ActionCell.tsx b/src/components/SelectionList/Search/ActionCell.tsx index 55e2cf6f849d..faafa6159dc1 100644 --- a/src/components/SelectionList/Search/ActionCell.tsx +++ b/src/components/SelectionList/Search/ActionCell.tsx @@ -15,9 +15,6 @@ import type {SearchTransactionAction} from '@src/types/onyx/SearchResults'; const actionTranslationsMap: Record = { view: 'common.view', review: 'common.review', - submit: 'common.submit', - approve: 'iou.approve', - pay: 'iou.pay', done: 'common.done', paid: 'iou.settledExpensify', }; @@ -29,18 +26,9 @@ type ActionCellProps = { goToItem: () => void; isChildListItem?: boolean; parentAction?: string; - isLoading?: boolean; }; -function ActionCell({ - action = CONST.SEARCH.ACTION_TYPES.VIEW, - isLargeScreenWidth = true, - isSelected = false, - goToItem, - isChildListItem = false, - parentAction = '', - isLoading = false, -}: ActionCellProps) { +function ActionCell({action = CONST.SEARCH.ACTION_TYPES.VIEW, isLargeScreenWidth = true, isSelected = false, goToItem, isChildListItem = false, parentAction = ''}: ActionCellProps) { const {translate} = useLocalize(); const theme = useTheme(); const styles = useThemeStyles(); @@ -73,8 +61,9 @@ function ActionCell({ ); } + const buttonInnerStyles = isSelected ? styles.buttonDefaultHovered : {}; + if (action === CONST.SEARCH.ACTION_TYPES.VIEW || shouldUseViewAction) { - const buttonInnerStyles = isSelected ? styles.buttonDefaultHovered : {}; return isLargeScreenWidth ? (