diff --git a/android/app/build.gradle b/android/app/build.gradle
index 77d179f23b48..b07c66308609 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -90,8 +90,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1001039503
- versionName "1.3.95-3"
+ versionCode 1001039505
+ versionName "1.3.95-5"
}
flavorDimensions "default"
diff --git a/assets/css/fonts.css b/assets/css/fonts.css
index 7834a0ebb861..078cec114c31 100644
--- a/assets/css/fonts.css
+++ b/assets/css/fonts.css
@@ -54,6 +54,11 @@
src: url('/fonts/ExpensifyNewKansas-MediumItalic.woff2') format('woff2'), url('/fonts/ExpensifyNewKansas-MediumItalic.woff') format('woff');
}
+@font-face {
+ font-family: Windows Segoe UI Emoji;
+ src: url('/fonts/seguiemj.ttf');
+}
+
* {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
diff --git a/assets/fonts/web/seguiemj.ttf b/assets/fonts/web/seguiemj.ttf
new file mode 100644
index 000000000000..3a455801aa0c
Binary files /dev/null and b/assets/fonts/web/seguiemj.ttf differ
diff --git a/docs/articles/expensify-classic/expense-and-report-features/Attendee-Tracking.md b/docs/articles/expensify-classic/expense-and-report-features/Attendee-Tracking.md
index ae367d25891e..7f3d83af1e6e 100644
--- a/docs/articles/expensify-classic/expense-and-report-features/Attendee-Tracking.md
+++ b/docs/articles/expensify-classic/expense-and-report-features/Attendee-Tracking.md
@@ -11,12 +11,14 @@ Every expense has an Attendees field and will list the expense creator’s name
## How to Add Additional Attendees to an Expense
* Go to the attendees field
* Search for the names of the attendees
- * The default list will be of internal attendees belonging to your workspace and domain.
+ * The default list will be of internal attendees belonging to your workspace and domain
* External attendees are not part of your workspace or domain, so you will need to enter their name or email
* Select the attendees you would like to add
* Save the expense
-* Once added, the list of attendees for each expense will be visible on the expense line.
-* An amount per employee expense will also be displayed on the report for easy viewing
+* Once added, the list of attendees for each expense will be visible on the expense line
+* An amount per employee expense will also be displayed on the report for easy viewing
+
+![image of an expense with attendee tracking]({{site.url}}/assets/images/attendee-tracking.png){:width="100%"}
# FAQ
diff --git a/docs/articles/expensify-classic/getting-started/Best-Practices.md b/docs/articles/expensify-classic/getting-started/Best-Practices.md
deleted file mode 100644
index b02ea9d68fe6..000000000000
--- a/docs/articles/expensify-classic/getting-started/Best-Practices.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Best Practices
-description: Best Practices
----
-## Resource Coming Soon!
diff --git a/docs/articles/expensify-classic/integrations/HR-integrations/Workday.md b/docs/articles/expensify-classic/integrations/HR-integrations/Workday.md
index e9077fc40a50..ecdea4699ee0 100644
--- a/docs/articles/expensify-classic/integrations/HR-integrations/Workday.md
+++ b/docs/articles/expensify-classic/integrations/HR-integrations/Workday.md
@@ -64,6 +64,8 @@ Before completing the steps below, you will need Workday Report Writer access to
- Note: _if there is field data you want to import that is not listed above, or you have any special requests, let your Expensify Account Manager know and we will work with you to accommodate the request._
4. Rename the columns so they match Expensify's API key names (The full list of names are found here):
- employeeID
+ - customField1
+ - customField2
- firstName
- lastName
- employeeEmail
diff --git a/docs/articles/expensify-classic/manage-employees-and-report-approvals/Adding-Users.md b/docs/articles/expensify-classic/manage-employees-and-report-approvals/Add-Members-to-your-Workspace.md
similarity index 61%
rename from docs/articles/expensify-classic/manage-employees-and-report-approvals/Adding-Users.md
rename to docs/articles/expensify-classic/manage-employees-and-report-approvals/Add-Members-to-your-Workspace.md
index 3ee1c8656b4b..f2978434959b 100644
--- a/docs/articles/expensify-classic/manage-employees-and-report-approvals/Adding-Users.md
+++ b/docs/articles/expensify-classic/manage-employees-and-report-approvals/Add-Members-to-your-Workspace.md
@@ -1,5 +1,5 @@
---
-title: Coming Soon
+title: Add Members to your Workspace
description: Coming Soon
---
## Resource Coming Soon!
diff --git a/docs/assets/images/attendee-tracking.png b/docs/assets/images/attendee-tracking.png
new file mode 100644
index 000000000000..1888851b2a13
Binary files /dev/null and b/docs/assets/images/attendee-tracking.png differ
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index ce6a52fc0912..1966f3862d59 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -40,7 +40,7 @@
CFBundleVersion
- 1.3.95.3
+ 1.3.95.5
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index fe03d4495b3d..387687a2beaa 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature
????
CFBundleVersion
- 1.3.95.3
+ 1.3.95.5
diff --git a/package-lock.json b/package-lock.json
index 70bfd1c9f6d6..a80022853a24 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.3.95-3",
+ "version": "1.3.95-5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.3.95-3",
+ "version": "1.3.95-5",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -96,7 +96,7 @@
"react-native-linear-gradient": "^2.8.1",
"react-native-localize": "^2.2.6",
"react-native-modal": "^13.0.0",
- "react-native-onyx": "1.0.100",
+ "react-native-onyx": "1.0.111",
"react-native-pager-view": "^6.2.0",
"react-native-pdf": "^6.7.1",
"react-native-performance": "^5.1.0",
@@ -44802,17 +44802,17 @@
}
},
"node_modules/react-native-onyx": {
- "version": "1.0.100",
- "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.100.tgz",
- "integrity": "sha512-m4bOF/uOtYpfL83fqoWhw7TYV4oKGXt0sfGoel/fhaT1HzXKheXc//ibt/G3VrTCf5nmRv7bXgsXkWjUYLH3UQ==",
+ "version": "1.0.111",
+ "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.111.tgz",
+ "integrity": "sha512-6drd5Grhkyq4oyt2+Bu6t7JYK5tqaARc0YP7taEHK9jLbhjdC4E9MPLJR2FVXiORkQCPOoyy1Gqmb4AUVIsvxg==",
"dependencies": {
"ascii-table": "0.0.9",
"fast-equals": "^4.0.3",
"underscore": "^1.13.1"
},
"engines": {
- "node": "16.15.1",
- "npm": "8.11.0"
+ "node": ">=16.15.1 <=18.17.1",
+ "npm": ">=8.11.0 <=9.6.7"
},
"peerDependencies": {
"idb-keyval": "^6.2.1",
@@ -85416,9 +85416,9 @@
}
},
"react-native-onyx": {
- "version": "1.0.100",
- "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.100.tgz",
- "integrity": "sha512-m4bOF/uOtYpfL83fqoWhw7TYV4oKGXt0sfGoel/fhaT1HzXKheXc//ibt/G3VrTCf5nmRv7bXgsXkWjUYLH3UQ==",
+ "version": "1.0.111",
+ "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.111.tgz",
+ "integrity": "sha512-6drd5Grhkyq4oyt2+Bu6t7JYK5tqaARc0YP7taEHK9jLbhjdC4E9MPLJR2FVXiORkQCPOoyy1Gqmb4AUVIsvxg==",
"requires": {
"ascii-table": "0.0.9",
"fast-equals": "^4.0.3",
diff --git a/package.json b/package.json
index 5d4e2371e578..f3462a2b63bb 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.3.95-3",
+ "version": "1.3.95-5",
"author": "Expensify, Inc.",
"homepage": "https://new.expensify.com",
"description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",
@@ -144,7 +144,7 @@
"react-native-linear-gradient": "^2.8.1",
"react-native-localize": "^2.2.6",
"react-native-modal": "^13.0.0",
- "react-native-onyx": "1.0.100",
+ "react-native-onyx": "1.0.111",
"react-native-pager-view": "^6.2.0",
"react-native-pdf": "^6.7.1",
"react-native-performance": "^5.1.0",
diff --git a/src/CONST.ts b/src/CONST.ts
index 29bb0b83aaee..9e7c1f007335 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -1312,6 +1312,7 @@ const CONST = {
TAX_ID: /^\d{9}$/,
NON_NUMERIC: /\D/g,
+ ANY_SPACE: /\s/g,
// Extract attachment's source from the data's html string
ATTACHMENT_DATA: /(data-expensify-source|data-name)="([^"]+)"/g,
diff --git a/src/components/DatePicker/index.android.js b/src/components/DatePicker/index.android.js
index d92869162d49..561fc700b6a5 100644
--- a/src/components/DatePicker/index.android.js
+++ b/src/components/DatePicker/index.android.js
@@ -1,5 +1,5 @@
import RNDatePicker from '@react-native-community/datetimepicker';
-import {format} from 'date-fns';
+import {format, parseISO} from 'date-fns';
import React, {forwardRef, useCallback, useImperativeHandle, useRef, useState} from 'react';
import {Keyboard} from 'react-native';
import TextInput from '@components/TextInput';
@@ -39,7 +39,7 @@ function DatePicker({value, defaultValue, label, placeholder, errorText, contain
);
const date = value || defaultValue;
- const dateAsText = date ? format(new Date(date), CONST.DATE.FNS_FORMAT_STRING) : '';
+ const dateAsText = date ? format(parseISO(date), CONST.DATE.FNS_FORMAT_STRING) : '';
return (
<>
diff --git a/src/components/DatePicker/index.ios.js b/src/components/DatePicker/index.ios.js
index 0f741e8db1ea..60307f70e954 100644
--- a/src/components/DatePicker/index.ios.js
+++ b/src/components/DatePicker/index.ios.js
@@ -1,5 +1,5 @@
import RNDatePicker from '@react-native-community/datetimepicker';
-import {format} from 'date-fns';
+import {format, parseISO} from 'date-fns';
import isFunction from 'lodash/isFunction';
import React, {useCallback, useEffect, useRef, useState} from 'react';
import {Button, Keyboard, View} from 'react-native';
@@ -77,7 +77,7 @@ function DatePicker({value, defaultValue, innerRef, onInputChange, preferredLoca
setSelectedDate(date);
};
- const dateAsText = dateValue ? format(new Date(dateValue), CONST.DATE.FNS_FORMAT_STRING) : '';
+ const dateAsText = dateValue ? format(parseISO(dateValue), CONST.DATE.FNS_FORMAT_STRING) : '';
return (
<>
diff --git a/src/components/DatePicker/index.js b/src/components/DatePicker/index.js
index 3bed9ca55321..33266242c5db 100644
--- a/src/components/DatePicker/index.js
+++ b/src/components/DatePicker/index.js
@@ -1,4 +1,4 @@
-import {format, isValid} from 'date-fns';
+import {format, isValid, parseISO} from 'date-fns';
import React, {useEffect, useRef} from 'react';
import _ from 'underscore';
import TextInput from '@components/TextInput';
@@ -29,7 +29,7 @@ function DatePicker({maxDate, minDate, onInputChange, innerRef, label, value, pl
return;
}
- const date = new Date(text);
+ const date = parseISO(text);
if (isValid(date)) {
onInputChange(format(date, CONST.DATE.FNS_FORMAT_STRING));
}
diff --git a/src/components/FlatList/MVCPFlatList.js b/src/components/FlatList/MVCPFlatList.js
new file mode 100644
index 000000000000..69c6b6767dae
--- /dev/null
+++ b/src/components/FlatList/MVCPFlatList.js
@@ -0,0 +1,205 @@
+/* eslint-disable es/no-optional-chaining, es/no-nullish-coalescing-operators, react/prop-types */
+import PropTypes from 'prop-types';
+import React from 'react';
+import {FlatList} from 'react-native';
+
+function mergeRefs(...args) {
+ return function forwardRef(node) {
+ args.forEach((ref) => {
+ if (ref == null) {
+ return;
+ }
+ if (typeof ref === 'function') {
+ ref(node);
+ return;
+ }
+ if (typeof ref === 'object') {
+ // eslint-disable-next-line no-param-reassign
+ ref.current = node;
+ return;
+ }
+ console.error(`mergeRefs cannot handle Refs of type boolean, number or string, received ref ${String(ref)}`);
+ });
+ };
+}
+
+function useMergeRefs(...args) {
+ return React.useMemo(
+ () => mergeRefs(...args),
+ // eslint-disable-next-line
+ [...args],
+ );
+}
+
+const MVCPFlatList = React.forwardRef(({maintainVisibleContentPosition, horizontal, inverted, onScroll, ...props}, forwardedRef) => {
+ const {minIndexForVisible: mvcpMinIndexForVisible, autoscrollToTopThreshold: mvcpAutoscrollToTopThreshold} = maintainVisibleContentPosition ?? {};
+ const scrollRef = React.useRef(null);
+ const prevFirstVisibleOffsetRef = React.useRef(null);
+ const firstVisibleViewRef = React.useRef(null);
+ const mutationObserverRef = React.useRef(null);
+ const lastScrollOffsetRef = React.useRef(0);
+
+ const getScrollOffset = React.useCallback(() => {
+ if (scrollRef.current == null) {
+ return 0;
+ }
+ return horizontal ? scrollRef.current.getScrollableNode().scrollLeft : scrollRef.current.getScrollableNode().scrollTop;
+ }, [horizontal]);
+
+ const getContentView = React.useCallback(() => scrollRef.current?.getScrollableNode().childNodes[0], []);
+
+ const scrollToOffset = React.useCallback(
+ (offset, animated) => {
+ const behavior = animated ? 'smooth' : 'instant';
+ scrollRef.current?.getScrollableNode().scroll(horizontal ? {left: offset, behavior} : {top: offset, behavior});
+ },
+ [horizontal],
+ );
+
+ const prepareForMaintainVisibleContentPosition = React.useCallback(() => {
+ if (mvcpMinIndexForVisible == null) {
+ return;
+ }
+
+ const contentView = getContentView();
+ if (contentView == null) {
+ return;
+ }
+
+ const scrollOffset = getScrollOffset();
+
+ const contentViewLength = contentView.childNodes.length;
+ for (let i = mvcpMinIndexForVisible; i < contentViewLength; i++) {
+ const subview = contentView.childNodes[inverted ? contentViewLength - i - 1 : i];
+ const subviewOffset = horizontal ? subview.offsetLeft : subview.offsetTop;
+ if (subviewOffset > scrollOffset || i === contentViewLength - 1) {
+ prevFirstVisibleOffsetRef.current = subviewOffset;
+ firstVisibleViewRef.current = subview;
+ break;
+ }
+ }
+ }, [getContentView, getScrollOffset, mvcpMinIndexForVisible, horizontal, inverted]);
+
+ const adjustForMaintainVisibleContentPosition = React.useCallback(() => {
+ if (mvcpMinIndexForVisible == null) {
+ return;
+ }
+
+ const firstVisibleView = firstVisibleViewRef.current;
+ const prevFirstVisibleOffset = prevFirstVisibleOffsetRef.current;
+ if (firstVisibleView == null || prevFirstVisibleOffset == null) {
+ return;
+ }
+
+ const firstVisibleViewOffset = horizontal ? firstVisibleView.offsetLeft : firstVisibleView.offsetTop;
+ const delta = firstVisibleViewOffset - prevFirstVisibleOffset;
+ if (Math.abs(delta) > 0.5) {
+ const scrollOffset = getScrollOffset();
+ prevFirstVisibleOffsetRef.current = firstVisibleViewOffset;
+ scrollToOffset(scrollOffset + delta, false);
+ if (mvcpAutoscrollToTopThreshold != null && scrollOffset <= mvcpAutoscrollToTopThreshold) {
+ scrollToOffset(0, true);
+ }
+ }
+ }, [getScrollOffset, scrollToOffset, mvcpMinIndexForVisible, mvcpAutoscrollToTopThreshold, horizontal]);
+
+ const setupMutationObserver = React.useCallback(() => {
+ const contentView = getContentView();
+ if (contentView == null) {
+ return;
+ }
+
+ mutationObserverRef.current?.disconnect();
+
+ const mutationObserver = new MutationObserver(() => {
+ // Chrome adjusts scroll position when elements are added at the top of the
+ // view. We want to have the same behavior as react-native / Safari so we
+ // reset the scroll position to the last value we got from an event.
+ const lastScrollOffset = lastScrollOffsetRef.current;
+ const scrollOffset = getScrollOffset();
+ if (lastScrollOffset !== scrollOffset) {
+ scrollToOffset(lastScrollOffset, false);
+ }
+
+ // This needs to execute after scroll events are dispatched, but
+ // in the same tick to avoid flickering. rAF provides the right timing.
+ requestAnimationFrame(() => {
+ adjustForMaintainVisibleContentPosition();
+ });
+ });
+ mutationObserver.observe(contentView, {
+ attributes: true,
+ childList: true,
+ subtree: true,
+ });
+
+ mutationObserverRef.current = mutationObserver;
+ }, [adjustForMaintainVisibleContentPosition, getContentView, getScrollOffset, scrollToOffset]);
+
+ React.useEffect(() => {
+ prepareForMaintainVisibleContentPosition();
+ setupMutationObserver();
+ }, [prepareForMaintainVisibleContentPosition, setupMutationObserver]);
+
+ const setMergedRef = useMergeRefs(scrollRef, forwardedRef);
+
+ const onRef = React.useCallback(
+ (newRef) => {
+ // Make sure to only call refs and re-attach listeners if the node changed.
+ if (newRef == null || newRef === scrollRef.current) {
+ return;
+ }
+
+ setMergedRef(newRef);
+ prepareForMaintainVisibleContentPosition();
+ setupMutationObserver();
+ },
+ [prepareForMaintainVisibleContentPosition, setMergedRef, setupMutationObserver],
+ );
+
+ React.useEffect(() => {
+ const mutationObserver = mutationObserverRef.current;
+ return () => {
+ mutationObserver?.disconnect();
+ };
+ }, []);
+
+ const onScrollInternal = React.useCallback(
+ (ev) => {
+ lastScrollOffsetRef.current = getScrollOffset();
+
+ prepareForMaintainVisibleContentPosition();
+
+ onScroll?.(ev);
+ },
+ [getScrollOffset, prepareForMaintainVisibleContentPosition, onScroll],
+ );
+
+ return (
+
+ );
+});
+
+MVCPFlatList.displayName = 'MVCPFlatList';
+MVCPFlatList.propTypes = {
+ maintainVisibleContentPosition: PropTypes.shape({
+ minIndexForVisible: PropTypes.number.isRequired,
+ autoscrollToTopThreshold: PropTypes.number,
+ }),
+ horizontal: PropTypes.bool,
+};
+
+MVCPFlatList.defaultProps = {
+ maintainVisibleContentPosition: null,
+ horizontal: false,
+};
+
+export default MVCPFlatList;
diff --git a/src/components/FlatList/index.web.js b/src/components/FlatList/index.web.js
new file mode 100644
index 000000000000..7299776db9bc
--- /dev/null
+++ b/src/components/FlatList/index.web.js
@@ -0,0 +1,3 @@
+import MVCPFlatList from './MVCPFlatList';
+
+export default MVCPFlatList;
diff --git a/src/components/InlineErrorText.js b/src/components/InlineErrorText.js
deleted file mode 100644
index 80438eea8b5f..000000000000
--- a/src/components/InlineErrorText.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-import _ from 'underscore';
-import styles from '@styles/styles';
-import Text from './Text';
-
-const propTypes = {
- /** Text to display */
- children: PropTypes.string.isRequired,
-
- /** Styling for inline error text */
- // eslint-disable-next-line react/forbid-prop-types
- styles: PropTypes.arrayOf(PropTypes.object),
-};
-
-const defaultProps = {
- styles: [],
-};
-
-function InlineErrorText(props) {
- if (_.isEmpty(props.children)) {
- return null;
- }
-
- return {props.children};
-}
-
-InlineErrorText.propTypes = propTypes;
-InlineErrorText.defaultProps = defaultProps;
-InlineErrorText.displayName = 'InlineErrorText';
-export default InlineErrorText;
diff --git a/src/components/LHNOptionsList/OptionRowLHNData.js b/src/components/LHNOptionsList/OptionRowLHNData.js
index cb64a135b264..ebba2ffe0587 100644
--- a/src/components/LHNOptionsList/OptionRowLHNData.js
+++ b/src/components/LHNOptionsList/OptionRowLHNData.js
@@ -168,17 +168,14 @@ export default React.memo(
},
fullReport: {
key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
- initialValue: {},
},
reportActions: {
key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
canEvict: false,
- initialValue: {},
},
personalDetails: {
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
selector: personalDetailsSelector,
- initialValue: {},
},
preferredLocale: {
key: ONYXKEYS.NVP_PREFERRED_LOCALE,
@@ -189,17 +186,15 @@ export default React.memo(
parentReportActions: {
key: ({fullReport}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${fullReport.parentReportID}`,
canEvict: false,
- initialValue: {},
},
policy: {
key: ({fullReport}) => `${ONYXKEYS.COLLECTION.POLICY}${fullReport.policyID}`,
- initialValue: {},
},
// Ideally, we aim to access only the last transaction for the current report by listening to changes in reportActions.
// In some scenarios, a transaction might be created after reportActions have been modified.
// This can lead to situations where `lastTransaction` doesn't update and retains the previous value.
// However, performance overhead of this is minimized by using memos inside the component.
- receiptTransactions: {key: ONYXKEYS.COLLECTION.TRANSACTION, initialValue: {}},
+ receiptTransactions: {key: ONYXKEYS.COLLECTION.TRANSACTION},
}),
// eslint-disable-next-line rulesdir/no-multiple-onyx-in-file
withOnyx({
diff --git a/src/components/OpacityView.js b/src/components/OpacityView.js
deleted file mode 100644
index ebd261916e65..000000000000
--- a/src/components/OpacityView.js
+++ /dev/null
@@ -1,69 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
-import shouldRenderOffscreen from '@libs/shouldRenderOffscreen';
-import * as StyleUtils from '@styles/StyleUtils';
-import variables from '@styles/variables';
-
-const propTypes = {
- /**
- * Should we dim the view
- */
- shouldDim: PropTypes.bool.isRequired,
-
- /**
- * Content to render
- */
- children: PropTypes.node.isRequired,
-
- /**
- * Array of style objects
- * @default []
- */
- // eslint-disable-next-line react/forbid-prop-types
- style: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
-
- /**
- * The value to use for the opacity when the view is dimmed
- * @default 0.5
- */
- dimmingValue: PropTypes.number,
-
- /** Whether the view needs to be rendered offscreen (for Android only) */
- needsOffscreenAlphaCompositing: PropTypes.bool,
-};
-
-const defaultProps = {
- style: [],
- dimmingValue: variables.hoverDimValue,
- needsOffscreenAlphaCompositing: false,
-};
-
-function OpacityView(props) {
- const opacity = useSharedValue(1);
- const opacityStyle = useAnimatedStyle(() => ({
- opacity: opacity.value,
- }));
-
- React.useEffect(() => {
- if (props.shouldDim) {
- opacity.value = withTiming(props.dimmingValue, {duration: 50});
- } else {
- opacity.value = withTiming(1, {duration: 50});
- }
- }, [props.shouldDim, props.dimmingValue, opacity]);
-
- return (
-
- {props.children}
-
- );
-}
-
-OpacityView.displayName = 'OpacityView';
-OpacityView.propTypes = propTypes;
-OpacityView.defaultProps = defaultProps;
-export default OpacityView;
diff --git a/src/components/OpacityView.tsx b/src/components/OpacityView.tsx
new file mode 100644
index 000000000000..6f82658bcac1
--- /dev/null
+++ b/src/components/OpacityView.tsx
@@ -0,0 +1,55 @@
+import React from 'react';
+import {StyleProp, ViewStyle} from 'react-native';
+import Animated, {AnimatedStyle, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
+import shouldRenderOffscreen from '@libs/shouldRenderOffscreen';
+import variables from '@styles/variables';
+
+type OpacityViewProps = {
+ /** Should we dim the view */
+ shouldDim: boolean;
+
+ /** Content to render */
+ children: React.ReactNode;
+
+ /**
+ * Array of style objects
+ * @default []
+ */
+ style?: StyleProp>;
+
+ /**
+ * The value to use for the opacity when the view is dimmed
+ * @default variables.hoverDimValue
+ */
+ dimmingValue?: number;
+
+ /** Whether the view needs to be rendered offscreen (for Android only) */
+ needsOffscreenAlphaCompositing?: boolean;
+};
+
+function OpacityView({shouldDim, children, style = [], dimmingValue = variables.hoverDimValue, needsOffscreenAlphaCompositing = false}: OpacityViewProps) {
+ const opacity = useSharedValue(1);
+ const opacityStyle = useAnimatedStyle(() => ({
+ opacity: opacity.value,
+ }));
+
+ React.useEffect(() => {
+ if (shouldDim) {
+ opacity.value = withTiming(dimmingValue, {duration: 50});
+ } else {
+ opacity.value = withTiming(1, {duration: 50});
+ }
+ }, [shouldDim, dimmingValue, opacity]);
+
+ return (
+
+ {children}
+
+ );
+}
+
+OpacityView.displayName = 'OpacityView';
+export default OpacityView;
diff --git a/src/components/PopoverProvider/index.native.js b/src/components/PopoverProvider/index.native.tsx
similarity index 63%
rename from src/components/PopoverProvider/index.native.js
rename to src/components/PopoverProvider/index.native.tsx
index 400b42ddea20..a87036c61808 100644
--- a/src/components/PopoverProvider/index.native.js
+++ b/src/components/PopoverProvider/index.native.tsx
@@ -1,20 +1,14 @@
-import PropTypes from 'prop-types';
import React from 'react';
+import {PopoverContextProps, PopoverContextValue} from './types';
-const propTypes = {
- children: PropTypes.node.isRequired,
-};
-
-const defaultProps = {};
-
-const PopoverContext = React.createContext({
+const PopoverContext = React.createContext({
onOpen: () => {},
popover: {},
close: () => {},
isOpen: false,
});
-function PopoverContextProvider(props) {
+function PopoverContextProvider(props: PopoverContextProps) {
const contextValue = React.useMemo(
() => ({
onOpen: () => {},
@@ -28,8 +22,6 @@ function PopoverContextProvider(props) {
return {props.children};
}
-PopoverContextProvider.defaultProps = defaultProps;
-PopoverContextProvider.propTypes = propTypes;
PopoverContextProvider.displayName = 'PopoverContextProvider';
export default PopoverContextProvider;
diff --git a/src/components/PopoverProvider/index.js b/src/components/PopoverProvider/index.tsx
similarity index 66%
rename from src/components/PopoverProvider/index.js
rename to src/components/PopoverProvider/index.tsx
index 3e245faceeef..06345ebdbc1c 100644
--- a/src/components/PopoverProvider/index.js
+++ b/src/components/PopoverProvider/index.tsx
@@ -1,24 +1,18 @@
-import PropTypes from 'prop-types';
import React from 'react';
+import {AnchorRef, PopoverContextProps, PopoverContextValue} from './types';
-const propTypes = {
- children: PropTypes.node.isRequired,
-};
-
-const defaultProps = {};
-
-const PopoverContext = React.createContext({
+const PopoverContext = React.createContext({
onOpen: () => {},
popover: {},
close: () => {},
isOpen: false,
});
-function PopoverContextProvider(props) {
+function PopoverContextProvider(props: PopoverContextProps) {
const [isOpen, setIsOpen] = React.useState(false);
- const activePopoverRef = React.useRef(null);
+ const activePopoverRef = React.useRef(null);
- const closePopover = React.useCallback((anchorRef) => {
+ const closePopover = React.useCallback((anchorRef?: React.RefObject) => {
if (!activePopoverRef.current || (anchorRef && anchorRef !== activePopoverRef.current.anchorRef)) {
return;
}
@@ -32,17 +26,12 @@ function PopoverContextProvider(props) {
}, []);
React.useEffect(() => {
- const listener = (e) => {
- if (
- !activePopoverRef.current ||
- !activePopoverRef.current.ref ||
- !activePopoverRef.current.ref.current ||
- activePopoverRef.current.ref.current.contains(e.target) ||
- (activePopoverRef.current.anchorRef && activePopoverRef.current.anchorRef.current && activePopoverRef.current.anchorRef.current.contains(e.target))
- ) {
+ const listener = (e: Event) => {
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ if (activePopoverRef.current?.ref?.current?.contains(e.target as Node) || activePopoverRef.current?.anchorRef?.current?.contains(e.target as Node)) {
return;
}
- const ref = activePopoverRef.current.anchorRef;
+ const ref = activePopoverRef.current?.anchorRef;
closePopover(ref);
};
document.addEventListener('click', listener, true);
@@ -52,8 +41,8 @@ function PopoverContextProvider(props) {
}, [closePopover]);
React.useEffect(() => {
- const listener = (e) => {
- if (!activePopoverRef.current || !activePopoverRef.current.ref || !activePopoverRef.current.ref.current || activePopoverRef.current.ref.current.contains(e.target)) {
+ const listener = (e: Event) => {
+ if (activePopoverRef.current?.ref?.current?.contains(e.target as Node)) {
return;
}
closePopover();
@@ -65,7 +54,7 @@ function PopoverContextProvider(props) {
}, [closePopover]);
React.useEffect(() => {
- const listener = (e) => {
+ const listener = (e: KeyboardEvent) => {
if (e.key !== 'Escape') {
return;
}
@@ -91,8 +80,8 @@ function PopoverContextProvider(props) {
}, [closePopover]);
React.useEffect(() => {
- const listener = (e) => {
- if (activePopoverRef.current && activePopoverRef.current.ref && activePopoverRef.current.ref.current && activePopoverRef.current.ref.current.contains(e.target)) {
+ const listener = (e: Event) => {
+ if (activePopoverRef.current?.ref?.current?.contains(e.target as Node)) {
return;
}
@@ -105,12 +94,12 @@ function PopoverContextProvider(props) {
}, [closePopover]);
const onOpen = React.useCallback(
- (popoverParams) => {
- if (activePopoverRef.current && activePopoverRef.current.ref !== popoverParams.ref) {
+ (popoverParams: AnchorRef) => {
+ if (activePopoverRef.current && activePopoverRef.current.ref !== popoverParams?.ref) {
closePopover(activePopoverRef.current.anchorRef);
}
activePopoverRef.current = popoverParams;
- if (popoverParams && popoverParams.onOpenCallback) {
+ if (popoverParams?.onOpenCallback) {
popoverParams.onOpenCallback();
}
setIsOpen(true);
@@ -131,8 +120,6 @@ function PopoverContextProvider(props) {
return {props.children};
}
-PopoverContextProvider.defaultProps = defaultProps;
-PopoverContextProvider.propTypes = propTypes;
PopoverContextProvider.displayName = 'PopoverContextProvider';
export default PopoverContextProvider;
diff --git a/src/components/PopoverProvider/types.ts b/src/components/PopoverProvider/types.ts
new file mode 100644
index 000000000000..ffd0087cd5ff
--- /dev/null
+++ b/src/components/PopoverProvider/types.ts
@@ -0,0 +1,20 @@
+type PopoverContextProps = {
+ children: React.ReactNode;
+};
+
+type PopoverContextValue = {
+ onOpen?: (popoverParams: AnchorRef) => void;
+ popover?: AnchorRef | Record | null;
+ close: (anchorRef?: React.RefObject) => void;
+ isOpen: boolean;
+};
+
+type AnchorRef = {
+ ref: React.RefObject;
+ close: (anchorRef?: React.RefObject) => void;
+ anchorRef: React.RefObject;
+ onOpenCallback?: () => void;
+ onCloseCallback?: () => void;
+};
+
+export type {PopoverContextProps, PopoverContextValue, AnchorRef};
diff --git a/src/components/withToggleVisibilityView.js b/src/components/withToggleVisibilityView.js
deleted file mode 100644
index 04c6ab8e8481..000000000000
--- a/src/components/withToggleVisibilityView.js
+++ /dev/null
@@ -1,52 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-import {View} from 'react-native';
-import getComponentDisplayName from '@libs/getComponentDisplayName';
-import styles from '@styles/styles';
-import refPropTypes from './refPropTypes';
-
-const toggleVisibilityViewPropTypes = {
- /** Whether the content is visible. */
- isVisible: PropTypes.bool,
-};
-
-export default function (WrappedComponent) {
- function WithToggleVisibilityView(props) {
- return (
-
-
-
- );
- }
-
- WithToggleVisibilityView.displayName = `WithToggleVisibilityView(${getComponentDisplayName(WrappedComponent)})`;
- WithToggleVisibilityView.propTypes = {
- forwardedRef: refPropTypes,
-
- /** Whether the content is visible. */
- isVisible: PropTypes.bool,
- };
- WithToggleVisibilityView.defaultProps = {
- forwardedRef: undefined,
- isVisible: false,
- };
-
- const WithToggleVisibilityViewWithRef = React.forwardRef((props, ref) => (
-
- ));
-
- WithToggleVisibilityViewWithRef.displayName = `WithToggleVisibilityViewWithRef`;
-
- return WithToggleVisibilityViewWithRef;
-}
-
-export {toggleVisibilityViewPropTypes};
diff --git a/src/components/withToggleVisibilityView.tsx b/src/components/withToggleVisibilityView.tsx
new file mode 100644
index 000000000000..5e0204f6e06f
--- /dev/null
+++ b/src/components/withToggleVisibilityView.tsx
@@ -0,0 +1,30 @@
+import React, {ComponentType, ForwardedRef, ReactElement, RefAttributes} from 'react';
+import {View} from 'react-native';
+import {SetOptional} from 'type-fest';
+import getComponentDisplayName from '@libs/getComponentDisplayName';
+import styles from '@styles/styles';
+
+type ToggleVisibilityViewProps = {
+ /** Whether the content is visible. */
+ isVisible: boolean;
+};
+
+export default function withToggleVisibilityView(
+ WrappedComponent: ComponentType>,
+): (props: TProps & RefAttributes) => ReactElement | null {
+ function WithToggleVisibilityView({isVisible = false, ...rest}: SetOptional, ref: ForwardedRef) {
+ return (
+
+
+
+ );
+ }
+
+ WithToggleVisibilityView.displayName = `WithToggleVisibilityViewWithRef(${getComponentDisplayName(WrappedComponent)})`;
+ return React.forwardRef(WithToggleVisibilityView);
+}
diff --git a/src/hooks/useDebounce.js b/src/hooks/useDebounce.js
index 874f9d72b276..62a919925a53 100644
--- a/src/hooks/useDebounce.js
+++ b/src/hooks/useDebounce.js
@@ -1,5 +1,5 @@
import lodashDebounce from 'lodash/debounce';
-import {useEffect, useRef} from 'react';
+import {useCallback, useEffect, useRef} from 'react';
/**
* Create and return a debounced function.
@@ -27,11 +27,13 @@ export default function useDebounce(func, wait, options) {
return debouncedFn.cancel;
}, [func, wait, leading, maxWait, trailing]);
- return (...args) => {
+ const debounceCallback = useCallback((...args) => {
const debouncedFn = debouncedFnRef.current;
if (debouncedFn) {
debouncedFn(...args);
}
- };
+ }, []);
+
+ return debounceCallback;
}
diff --git a/src/libs/Clipboard/index.native.js b/src/libs/Clipboard/index.native.js
deleted file mode 100644
index fe79e38585c4..000000000000
--- a/src/libs/Clipboard/index.native.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import Clipboard from '@react-native-clipboard/clipboard';
-
-/**
- * Sets a string on the Clipboard object via @react-native-clipboard/clipboard
- *
- * @param {String} text
- */
-const setString = (text) => {
- Clipboard.setString(text);
-};
-
-export default {
- setString,
-
- // We don't want to set HTML on native platforms so noop them.
- canSetHtml: () => false,
- setHtml: () => {},
-};
diff --git a/src/libs/Clipboard/index.native.ts b/src/libs/Clipboard/index.native.ts
new file mode 100644
index 000000000000..f78c5e4ab230
--- /dev/null
+++ b/src/libs/Clipboard/index.native.ts
@@ -0,0 +1,19 @@
+import Clipboard from '@react-native-clipboard/clipboard';
+import {CanSetHtml, SetHtml, SetString} from './types';
+
+/**
+ * Sets a string on the Clipboard object via @react-native-clipboard/clipboard
+ */
+const setString: SetString = (text) => {
+ Clipboard.setString(text);
+};
+
+// We don't want to set HTML on native platforms so noop them.
+const canSetHtml: CanSetHtml = () => false;
+const setHtml: SetHtml = () => {};
+
+export default {
+ setString,
+ canSetHtml,
+ setHtml,
+};
diff --git a/src/libs/Clipboard/index.js b/src/libs/Clipboard/index.ts
similarity index 68%
rename from src/libs/Clipboard/index.js
rename to src/libs/Clipboard/index.ts
index 3fb2091c5cb1..b703b0b4d7f5 100644
--- a/src/libs/Clipboard/index.js
+++ b/src/libs/Clipboard/index.ts
@@ -1,16 +1,34 @@
import Clipboard from '@react-native-clipboard/clipboard';
-import lodashGet from 'lodash/get';
import * as Browser from '@libs/Browser';
import CONST from '@src/CONST';
+import {CanSetHtml, SetHtml, SetString} from './types';
-const canSetHtml = () => lodashGet(navigator, 'clipboard.write');
+type ComposerSelection = {
+ start: number;
+ end: number;
+ direction: 'forward' | 'backward' | 'none';
+};
+
+type AnchorSelection = {
+ anchorOffset: number;
+ focusOffset: number;
+ anchorNode: Node;
+ focusNode: Node;
+};
+
+type NullableObject = {[K in keyof T]: T[K] | null};
+
+type OriginalSelection = ComposerSelection | NullableObject;
+
+const canSetHtml: CanSetHtml =
+ () =>
+ (...args: ClipboardItems) =>
+ navigator?.clipboard?.write([...args]);
/**
* Deprecated method to write the content as HTML to clipboard.
- * @param {String} html HTML representation
- * @param {String} text Plain text representation
*/
-function setHTMLSync(html, text) {
+function setHTMLSync(html: string, text: string) {
const node = document.createElement('span');
node.textContent = html;
node.style.all = 'unset';
@@ -21,16 +39,21 @@ function setHTMLSync(html, text) {
node.addEventListener('copy', (e) => {
e.stopPropagation();
e.preventDefault();
- e.clipboardData.clearData();
- e.clipboardData.setData('text/html', html);
- e.clipboardData.setData('text/plain', text);
+ e.clipboardData?.clearData();
+ e.clipboardData?.setData('text/html', html);
+ e.clipboardData?.setData('text/plain', text);
});
document.body.appendChild(node);
- const selection = window.getSelection();
- const firstAnchorChild = selection.anchorNode && selection.anchorNode.firstChild;
+ const selection = window?.getSelection();
+
+ if (selection === null) {
+ return;
+ }
+
+ const firstAnchorChild = selection.anchorNode?.firstChild;
const isComposer = firstAnchorChild instanceof HTMLTextAreaElement;
- let originalSelection = null;
+ let originalSelection: OriginalSelection | null = null;
if (isComposer) {
originalSelection = {
start: firstAnchorChild.selectionStart,
@@ -60,12 +83,14 @@ function setHTMLSync(html, text) {
selection.removeAllRanges();
- if (isComposer) {
+ const anchorSelection = originalSelection as AnchorSelection;
+
+ if (isComposer && 'start' in originalSelection) {
firstAnchorChild.setSelectionRange(originalSelection.start, originalSelection.end, originalSelection.direction);
- } else if (originalSelection.anchorNode && originalSelection.focusNode) {
+ } else if (anchorSelection.anchorNode && anchorSelection.focusNode) {
// When copying to the clipboard here, the original values of anchorNode and focusNode will be null since there will be no user selection.
// We are adding a check to prevent null values from being passed to setBaseAndExtent, in accordance with the standards of the Selection API as outlined here: https://w3c.github.io/selection-api/#dom-selection-setbaseandextent.
- selection.setBaseAndExtent(originalSelection.anchorNode, originalSelection.anchorOffset, originalSelection.focusNode, originalSelection.focusOffset);
+ selection.setBaseAndExtent(anchorSelection.anchorNode, anchorSelection.anchorOffset, anchorSelection.focusNode, anchorSelection.focusOffset);
}
document.body.removeChild(node);
@@ -73,10 +98,8 @@ function setHTMLSync(html, text) {
/**
* Writes the content as HTML if the web client supports it.
- * @param {String} html HTML representation
- * @param {String} text Plain text representation
*/
-const setHtml = (html, text) => {
+const setHtml: SetHtml = (html: string, text: string) => {
if (!html || !text) {
return;
}
@@ -93,8 +116,8 @@ const setHtml = (html, text) => {
setHTMLSync(html, text);
} else {
navigator.clipboard.write([
- // eslint-disable-next-line no-undef
new ClipboardItem({
+ /* eslint-disable @typescript-eslint/naming-convention */
'text/html': new Blob([html], {type: 'text/html'}),
'text/plain': new Blob([text], {type: 'text/plain'}),
}),
@@ -104,10 +127,8 @@ const setHtml = (html, text) => {
/**
* Sets a string on the Clipboard object via react-native-web
- *
- * @param {String} text
*/
-const setString = (text) => {
+const setString: SetString = (text) => {
Clipboard.setString(text);
};
diff --git a/src/libs/Clipboard/types.ts b/src/libs/Clipboard/types.ts
new file mode 100644
index 000000000000..1d899144a2ba
--- /dev/null
+++ b/src/libs/Clipboard/types.ts
@@ -0,0 +1,5 @@
+type SetString = (text: string) => void;
+type SetHtml = (html: string, text: string) => void;
+type CanSetHtml = (() => (...args: ClipboardItems) => Promise) | (() => boolean);
+
+export type {SetString, CanSetHtml, SetHtml};
diff --git a/src/libs/ComposerUtils/index.ts b/src/libs/ComposerUtils/index.ts
index 5a7da7ca08cf..58e1efa7aa65 100644
--- a/src/libs/ComposerUtils/index.ts
+++ b/src/libs/ComposerUtils/index.ts
@@ -32,7 +32,11 @@ function canSkipTriggerHotkeys(isSmallScreenWidth: boolean, isKeyboardShown: boo
*/
function getCommonSuffixLength(str1: string, str2: string): number {
let i = 0;
- while (str1[str1.length - 1 - i] === str2[str2.length - 1 - i]) {
+ if (str1.length === 0 || str2.length === 0) {
+ return 0;
+ }
+ const minLen = Math.min(str1.length, str2.length);
+ while (i < minLen && str1[str1.length - 1 - i] === str2[str2.length - 1 - i]) {
i++;
}
return i;
diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js
index 99853975f86a..54d09b75eff2 100644
--- a/src/libs/OptionsListUtils.js
+++ b/src/libs/OptionsListUtils.js
@@ -87,7 +87,6 @@ Onyx.connect({
const policyExpenseReports = {};
Onyx.connect({
key: ONYXKEYS.COLLECTION.REPORT,
- waitForCollectionCallback: true,
callback: (report, key) => {
if (!ReportUtils.isPolicyExpenseChat(report)) {
return;
diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts
index 45bdfb18b451..11e11f549682 100644
--- a/src/libs/ReportActionsUtils.ts
+++ b/src/libs/ReportActionsUtils.ts
@@ -406,12 +406,9 @@ function getLastVisibleMessage(reportID: string, actionsToMerge: ReportActions =
};
}
- let messageText = message?.text ?? '';
- if (messageText) {
- messageText = String(messageText).replace(CONST.REGEX.AFTER_FIRST_LINE_BREAK, '').substring(0, CONST.REPORT.LAST_MESSAGE_TEXT_MAX_LENGTH).trim();
- }
+ const messageText = message?.text ?? '';
return {
- lastMessageText: messageText,
+ lastMessageText: String(messageText).replace(CONST.REGEX.AFTER_FIRST_LINE_BREAK, '').substring(0, CONST.REPORT.LAST_MESSAGE_TEXT_MAX_LENGTH).trim(),
};
}
diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js
index 21fe9d54dfc7..1e3fc5297193 100644
--- a/src/libs/ReportUtils.js
+++ b/src/libs/ReportUtils.js
@@ -818,15 +818,8 @@ function isOneOnOneChat(report) {
* @returns {Object}
*/
function getReport(reportID) {
- /**
- * Using typical string concatenation here due to performance issues
- * with template literals.
- */
- if (!allReports) {
- return {};
- }
-
- return allReports[ONYXKEYS.COLLECTION.REPORT + reportID] || {};
+ // Deleted reports are set to null and lodashGet will still return null in that case, so we need to add an extra check
+ return lodashGet(allReports, `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {}) || {};
}
/**
@@ -1422,6 +1415,10 @@ function requiresAttentionFromCurrentUser(option, parentReportAction = {}) {
return false;
}
+ if (isArchivedRoom(option)) {
+ return false;
+ }
+
if (isArchivedRoom(getReport(option.parentReportID))) {
return false;
}
@@ -1495,15 +1492,18 @@ function getMoneyRequestSpendBreakdown(report, allReportsDict = null) {
}
if (moneyRequestReport) {
let nonReimbursableSpend = lodashGet(moneyRequestReport, 'nonReimbursableTotal', 0);
- let reimbursableSpend = lodashGet(moneyRequestReport, 'total', 0);
+ let totalSpend = lodashGet(moneyRequestReport, 'total', 0);
- if (nonReimbursableSpend + reimbursableSpend !== 0) {
+ if (nonReimbursableSpend + totalSpend !== 0) {
// There is a possibility that if the Expense report has a negative total.
// This is because there are instances where you can get a credit back on your card,
// or you enter a negative expense to “offset” future expenses
nonReimbursableSpend = isExpenseReport(moneyRequestReport) ? nonReimbursableSpend * -1 : Math.abs(nonReimbursableSpend);
- reimbursableSpend = isExpenseReport(moneyRequestReport) ? reimbursableSpend * -1 : Math.abs(reimbursableSpend);
- const totalDisplaySpend = nonReimbursableSpend + reimbursableSpend;
+ totalSpend = isExpenseReport(moneyRequestReport) ? totalSpend * -1 : Math.abs(totalSpend);
+
+ const totalDisplaySpend = totalSpend;
+ const reimbursableSpend = totalDisplaySpend - nonReimbursableSpend;
+
return {
nonReimbursableSpend,
reimbursableSpend,
@@ -1526,25 +1526,14 @@ function getMoneyRequestSpendBreakdown(report, allReportsDict = null) {
* @returns {String}
*/
function getPolicyExpenseChatName(report, policy = undefined) {
- const ownerAccountID = report.ownerAccountID;
- const personalDetails = allPersonalDetails[ownerAccountID];
- const login = personalDetails ? personalDetails.login : null;
- const reportOwnerDisplayName = getDisplayNameForParticipant(report.ownerAccountID) || login || report.reportName;
+ const reportOwnerDisplayName = getDisplayNameForParticipant(report.ownerAccountID) || lodashGet(allPersonalDetails, [report.ownerAccountID, 'login']) || report.reportName;
// If the policy expense chat is owned by this user, use the name of the policy as the report name.
if (report.isOwnPolicyExpenseChat) {
return getPolicyName(report, false, policy);
}
- let policyExpenseChatRole = 'user';
- /**
- * Using typical string concatenation here due to performance issues
- * with template literals.
- */
- const policyItem = allPolicies[ONYXKEYS.COLLECTION.POLICY + report.policyID];
- if (policyItem) {
- policyExpenseChatRole = policyItem.role || 'user';
- }
+ const policyExpenseChatRole = lodashGet(allPolicies, [`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`, 'role']) || 'user';
// If this user is not admin and this policy expense chat has been archived because of account merging, this must be an old workspace chat
// of the account which was merged into the current user's account. Use the name of the policy as the name of the report.
diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts
index 4951432bcd03..80ed96d25d65 100644
--- a/src/libs/SidebarUtils.ts
+++ b/src/libs/SidebarUtils.ts
@@ -38,6 +38,7 @@ Onyx.connect({
const reportActionsForDisplay = actionsArray.filter(
(reportAction, actionKey) =>
ReportActionsUtils.shouldReportActionBeVisible(reportAction, actionKey) &&
+ !ReportActionsUtils.isWhisperAction(reportAction) &&
reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED &&
reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE,
);
@@ -156,6 +157,18 @@ function getOrderedReportIDs(
}
}
+ // There are a few properties that need to be calculated for the report which are used when sorting reports.
+ reportsToDisplay.forEach((report) => {
+ // Normally, the spread operator would be used here to clone the report and prevent the need to reassign the params.
+ // However, this code needs to be very performant to handle thousands of reports, so in the interest of speed, we're just going to disable this lint rule and add
+ // the reportDisplayName property to the report object directly.
+ // eslint-disable-next-line no-param-reassign
+ report.displayName = ReportUtils.getReportName(report);
+
+ // eslint-disable-next-line no-param-reassign
+ report.iouReportAmount = ReportUtils.getMoneyRequestReimbursableTotal(report, allReports);
+ });
+
// The LHN is split into four distinct groups, and each group is sorted a little differently. The groups will ALWAYS be in this order:
// 1. Pinned/GBR - Always sorted by reportDisplayName
// 2. Drafts - Always sorted by reportDisplayName
@@ -169,17 +182,7 @@ function getOrderedReportIDs(
const draftReports: Report[] = [];
const nonArchivedReports: Report[] = [];
const archivedReports: Report[] = [];
-
reportsToDisplay.forEach((report) => {
- // Normally, the spread operator would be used here to clone the report and prevent the need to reassign the params.
- // However, this code needs to be very performant to handle thousands of reports, so in the interest of speed, we're just going to disable this lint rule and add
- // the reportDisplayName property to the report object directly.
- // eslint-disable-next-line no-param-reassign
- report.displayName = ReportUtils.getReportName(report);
-
- // eslint-disable-next-line no-param-reassign
- report.iouReportAmount = ReportUtils.getMoneyRequestReimbursableTotal(report, allReports);
-
const isPinned = report.isPinned ?? false;
if (isPinned || ReportUtils.requiresAttentionFromCurrentUser(report)) {
pinnedAndGBRReports.push(report);
@@ -450,7 +453,7 @@ function getOptionData(
} else if (lastAction?.actionName !== CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && lastActorDisplayName && lastMessageTextFromReport) {
result.alternateText = `${lastActorDisplayName}: ${lastMessageText}`;
} else {
- result.alternateText = lastAction && lastMessageTextFromReport.length > 0 ? lastMessageText : Localize.translate(preferredLocale, 'report.noActivityYet');
+ result.alternateText = lastMessageTextFromReport.length > 0 ? lastMessageText : Localize.translate(preferredLocale, 'report.noActivityYet');
}
} else {
if (!lastMessageText) {
diff --git a/src/libs/UnreadIndicatorUpdater/index.js b/src/libs/UnreadIndicatorUpdater/index.js
index bfa0cd911177..9af74f8313c3 100644
--- a/src/libs/UnreadIndicatorUpdater/index.js
+++ b/src/libs/UnreadIndicatorUpdater/index.js
@@ -1,4 +1,3 @@
-import {InteractionManager} from 'react-native';
import Onyx from 'react-native-onyx';
import _ from 'underscore';
import * as ReportUtils from '@libs/ReportUtils';
@@ -6,33 +5,11 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import updateUnread from './updateUnread/index';
-let previousUnreadCount = 0;
-
Onyx.connect({
key: ONYXKEYS.COLLECTION.REPORT,
waitForCollectionCallback: true,
callback: (reportsFromOnyx) => {
- if (!reportsFromOnyx) {
- return;
- }
-
- /**
- * We need to wait until after interactions have finished to update the unread count because otherwise
- * the unread count will be updated while the interactions/animations are in progress and we don't want
- * to put more work on the main thread.
- *
- * For eg. On web we are manipulating DOM and it makes it a better candidate to wait until after interactions
- * have finished.
- *
- * More info: https://reactnative.dev/docs/interactionmanager
- */
- InteractionManager.runAfterInteractions(() => {
- const unreadReports = _.filter(reportsFromOnyx, (report) => ReportUtils.isUnread(report) && report.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN);
- const unreadReportsCount = _.size(unreadReports);
- if (previousUnreadCount !== unreadReportsCount) {
- previousUnreadCount = unreadReportsCount;
- updateUnread(unreadReportsCount);
- }
- });
+ const unreadReports = _.filter(reportsFromOnyx, (report) => ReportUtils.isUnread(report) && report.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN);
+ updateUnread(_.size(unreadReports));
},
});
diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js
index e716c17de8b2..9b33ff9b086e 100644
--- a/src/libs/actions/Policy.js
+++ b/src/libs/actions/Policy.js
@@ -1,6 +1,7 @@
import {PUBLIC_DOMAINS} from 'expensify-common/lib/CONST';
import Str from 'expensify-common/lib/str';
import {escapeRegExp} from 'lodash';
+import filter from 'lodash/filter';
import lodashGet from 'lodash/get';
import lodashUnion from 'lodash/union';
import Onyx from 'react-native-onyx';
@@ -74,6 +75,12 @@ Onyx.connect({
callback: (val) => (allPersonalDetails = val),
});
+let reimbursementAccount;
+Onyx.connect({
+ key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
+ callback: (val) => (reimbursementAccount = val),
+});
+
let allRecentlyUsedCategories = {};
Onyx.connect({
key: ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES,
@@ -96,6 +103,36 @@ function updateLastAccessedWorkspace(policyID) {
Onyx.set(ONYXKEYS.LAST_ACCESSED_WORKSPACE_POLICY_ID, policyID);
}
+/**
+ * Check if the user has any active free policies (aka workspaces)
+ *
+ * @param {Array} policies
+ * @returns {Boolean}
+ */
+function hasActiveFreePolicy(policies) {
+ const adminFreePolicies = _.filter(policies, (policy) => policy && policy.type === CONST.POLICY.TYPE.FREE && policy.role === CONST.POLICY.ROLE.ADMIN);
+
+ if (adminFreePolicies.length === 0) {
+ return false;
+ }
+
+ if (_.some(adminFreePolicies, (policy) => !policy.pendingAction)) {
+ return true;
+ }
+
+ if (_.some(adminFreePolicies, (policy) => policy.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD)) {
+ return true;
+ }
+
+ if (_.some(adminFreePolicies, (policy) => policy.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE)) {
+ return false;
+ }
+
+ // If there are no add or delete pending actions the only option left is an update
+ // pendingAction, in which case we should return true.
+ return true;
+}
+
/**
* Delete the workspace
*
@@ -104,6 +141,7 @@ function updateLastAccessedWorkspace(policyID) {
* @param {String} policyName
*/
function deleteWorkspace(policyID, reports, policyName) {
+ const filteredPolicies = filter(allPolicies, (policy) => policy.id !== policyID);
const optimisticData = [
{
onyxMethod: Onyx.METHOD.MERGE,
@@ -146,6 +184,18 @@ function deleteWorkspace(policyID, reports, policyName) {
value: optimisticReportActions,
};
}),
+
+ ...(!hasActiveFreePolicy(filteredPolicies)
+ ? [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
+ value: {
+ errors: null,
+ },
+ },
+ ]
+ : []),
];
// Restore the old report stateNum and statusNum
@@ -160,6 +210,13 @@ function deleteWorkspace(policyID, reports, policyName) {
oldPolicyName,
},
})),
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
+ value: {
+ errors: lodashGet(reimbursementAccount, 'errors', null),
+ },
+ },
];
// We don't need success data since the push notification will update
@@ -183,36 +240,6 @@ function isAdminOfFreePolicy(policies) {
return _.some(policies, (policy) => policy && policy.type === CONST.POLICY.TYPE.FREE && policy.role === CONST.POLICY.ROLE.ADMIN);
}
-/**
- * Check if the user has any active free policies (aka workspaces)
- *
- * @param {Array} policies
- * @returns {Boolean}
- */
-function hasActiveFreePolicy(policies) {
- const adminFreePolicies = _.filter(policies, (policy) => policy && policy.type === CONST.POLICY.TYPE.FREE && policy.role === CONST.POLICY.ROLE.ADMIN);
-
- if (adminFreePolicies.length === 0) {
- return false;
- }
-
- if (_.some(adminFreePolicies, (policy) => !policy.pendingAction)) {
- return true;
- }
-
- if (_.some(adminFreePolicies, (policy) => policy.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD)) {
- return true;
- }
-
- if (_.some(adminFreePolicies, (policy) => policy.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE)) {
- return false;
- }
-
- // If there are no add or delete pending actions the only option left is an update
- // pendingAction, in which case we should return true.
- return true;
-}
-
/**
* Remove the passed members from the policy employeeList
*
diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js
index 4646e0e33da1..1de15c1184cb 100644
--- a/src/libs/actions/Report.js
+++ b/src/libs/actions/Report.js
@@ -65,7 +65,6 @@ Onyx.connect({
const currentReportData = {};
Onyx.connect({
key: ONYXKEYS.COLLECTION.REPORT,
- waitForCollectionCallback: true,
callback: (data, key) => {
if (!key || !data) {
return;
diff --git a/src/pages/ReimbursementAccount/AddressForm.js b/src/pages/ReimbursementAccount/AddressForm.js
index 36fbc6e4c59d..b8675fd9cc0e 100644
--- a/src/pages/ReimbursementAccount/AddressForm.js
+++ b/src/pages/ReimbursementAccount/AddressForm.js
@@ -107,6 +107,7 @@ function AddressForm(props) {
hint={props.translate('common.noPO')}
renamedInputKeys={props.inputKeys}
maxInputLength={CONST.FORM_CHARACTER_LIMIT}
+ isLimitedToUSA
/>
{
+ let startIndex = -1;
+ let endIndex = -1;
+ let currentIndex = 0;
+
+ // Find the first character mismatch with newText
+ while (currentIndex < newText.length && prevText.charAt(currentIndex) === newText.charAt(currentIndex) && selection.start > currentIndex) {
+ currentIndex++;
+ }
+
+ if (currentIndex < newText.length) {
+ startIndex = currentIndex;
+
+ // if text is getting pasted over find length of common suffix and subtract it from new text length
+ if (selection.end - selection.start > 0) {
+ const commonSuffixLength = ComposerUtils.getCommonSuffixLength(prevText, newText);
+ endIndex = newText.length - commonSuffixLength;
+ } else {
+ endIndex = currentIndex + (newText.length - prevText.length);
+ }
+ }
+
+ return {
+ startIndex,
+ endIndex,
+ diff: newText.substring(startIndex, endIndex),
+ };
+ },
+ [selection.end, selection.start],
+ );
+
+ const insertWhiteSpace = (text, index) => `${text.slice(0, index)} ${text.slice(index)}`;
+
const debouncedSaveReportComment = useMemo(
() =>
_.debounce((selectedReportID, newComment) => {
@@ -213,7 +258,14 @@ function ComposerWithSuggestions({
const updateComment = useCallback(
(commentValue, shouldDebounceSaveComment) => {
raiseIsScrollLikelyLayoutTriggered();
- const {text: newComment, emojis} = EmojiUtils.replaceAndExtractEmojis(commentValue, preferredSkinTone, preferredLocale);
+ const {startIndex, endIndex, diff} = findNewlyAddedChars(lastTextRef.current, commentValue);
+ const isEmojiInserted = diff.length && endIndex > startIndex && EmojiUtils.containsOnlyEmojis(diff);
+ const {text: newComment, emojis} = EmojiUtils.replaceAndExtractEmojis(
+ isEmojiInserted ? insertWhiteSpace(commentValue, endIndex) : commentValue,
+ preferredSkinTone,
+ preferredLocale,
+ );
+
if (!_.isEmpty(emojis)) {
const newEmojis = EmojiUtils.getAddedEmojis(emojis, emojisPresentBefore.current);
if (!_.isEmpty(newEmojis)) {
@@ -226,14 +278,8 @@ function ComposerWithSuggestions({
}
}
const newCommentConverted = convertToLTRForComposer(newComment);
- const isNewCommentEmpty = !!newCommentConverted.match(/^(\s)*$/);
- const isPrevCommentEmpty = !!commentRef.current.match(/^(\s)*$/);
-
- /** Only update isCommentEmpty state if it's different from previous one */
- if (isNewCommentEmpty !== isPrevCommentEmpty) {
- setIsCommentEmpty(isNewCommentEmpty);
- }
emojisPresentBefore.current = emojis;
+ setIsCommentEmpty(!!newCommentConverted.match(/^(\s)*$/));
setValue(newCommentConverted);
if (commentValue !== newComment) {
const remainder = ComposerUtils.getCommonSuffixLength(commentValue, newComment);
@@ -264,13 +310,14 @@ function ComposerWithSuggestions({
}
},
[
- debouncedUpdateFrequentlyUsedEmojis,
- preferredLocale,
+ raiseIsScrollLikelyLayoutTriggered,
+ findNewlyAddedChars,
preferredSkinTone,
- reportID,
+ preferredLocale,
setIsCommentEmpty,
+ debouncedUpdateFrequentlyUsedEmojis,
suggestionsRef,
- raiseIsScrollLikelyLayoutTriggered,
+ reportID,
debouncedSaveReportComment,
],
);
@@ -321,14 +368,8 @@ function ComposerWithSuggestions({
* @param {Boolean} shouldAddTrailSpace
*/
const replaceSelectionWithText = useCallback(
- (text, shouldAddTrailSpace = true) => {
- const updatedText = shouldAddTrailSpace ? `${text} ` : text;
- const selectionSpaceLength = shouldAddTrailSpace ? CONST.SPACE_LENGTH : 0;
- updateComment(ComposerUtils.insertText(commentRef.current, selection, updatedText));
- setSelection((prevSelection) => ({
- start: prevSelection.start + text.length + selectionSpaceLength,
- end: prevSelection.start + text.length + selectionSpaceLength,
- }));
+ (text) => {
+ updateComment(ComposerUtils.insertText(commentRef.current, selection, text));
},
[selection, updateComment],
);
@@ -452,7 +493,12 @@ function ComposerWithSuggestions({
}
focus();
- replaceSelectionWithText(e.key, false);
+ // Reset cursor to last known location
+ setSelection((prevSelection) => ({
+ start: prevSelection.start + 1,
+ end: prevSelection.end + 1,
+ }));
+ replaceSelectionWithText(e.key);
},
[checkComposerVisibility, focus, replaceSelectionWithText],
);
@@ -510,10 +556,16 @@ function ComposerWithSuggestions({
if (value.length === 0) {
return;
}
+
Report.setReportWithDraft(reportID, true);
+
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
+ useEffect(() => {
+ lastTextRef.current = value;
+ }, [value]);
+
useImperativeHandle(
forwardedRef,
() => ({
diff --git a/src/pages/home/report/ReportActionCompose/SuggestionMention.js b/src/pages/home/report/ReportActionCompose/SuggestionMention.js
index baf93da6ccc4..2ea2dd334528 100644
--- a/src/pages/home/report/ReportActionCompose/SuggestionMention.js
+++ b/src/pages/home/report/ReportActionCompose/SuggestionMention.js
@@ -1,17 +1,15 @@
import PropTypes from 'prop-types';
import React, {useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react';
-import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
import * as Expensicons from '@components/Icon/Expensicons';
import MentionSuggestions from '@components/MentionSuggestions';
+import {usePersonalDetails} from '@components/OnyxProvider';
import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager';
import useLocalize from '@hooks/useLocalize';
import usePrevious from '@hooks/usePrevious';
import * as SuggestionsUtils from '@libs/SuggestionUtils';
import * as UserUtils from '@libs/UserUtils';
-import personalDetailsPropType from '@pages/personalDetailsPropType';
import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
import * as SuggestionProps from './suggestionProps';
/**
@@ -29,9 +27,6 @@ const defaultSuggestionsValues = {
};
const propTypes = {
- /** Personal details of all users */
- personalDetails: PropTypes.objectOf(personalDetailsPropType),
-
/** A ref to this component */
forwardedRef: PropTypes.shape({current: PropTypes.shape({})}),
@@ -39,7 +34,6 @@ const propTypes = {
};
const defaultProps = {
- personalDetails: {},
forwardedRef: null,
};
@@ -49,7 +43,6 @@ function SuggestionMention({
selection,
setSelection,
isComposerFullSize,
- personalDetails,
updateComment,
composerHeight,
forwardedRef,
@@ -57,6 +50,7 @@ function SuggestionMention({
measureParentContainer,
isComposerFocused,
}) {
+ const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT;
const {translate} = useLocalize();
const previousValue = usePrevious(value);
const [suggestionValues, setSuggestionValues] = useState(defaultSuggestionsValues);
@@ -316,8 +310,4 @@ const SuggestionMentionWithRef = React.forwardRef((props, ref) => (
SuggestionMentionWithRef.displayName = 'SuggestionMentionWithRef';
-export default withOnyx({
- personalDetails: {
- key: ONYXKEYS.PERSONAL_DETAILS_LIST,
- },
-})(SuggestionMentionWithRef);
+export default SuggestionMentionWithRef;
diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js
index b3efb0388364..67c3d5f5c5ec 100644
--- a/src/pages/home/report/ReportActionItemMessageEdit.js
+++ b/src/pages/home/report/ReportActionItemMessageEdit.js
@@ -316,7 +316,7 @@ function ReportActionItemMessageEdit(props) {
// When user tries to save the empty message, it will delete it. Prompt the user to confirm deleting.
if (!trimmedNewDraft) {
textInputRef.current.blur();
- ReportActionContextMenu.showDeleteModal(props.reportID, props.action, false, deleteDraft, () => InteractionManager.runAfterInteractions(() => textInputRef.current.focus()));
+ ReportActionContextMenu.showDeleteModal(props.reportID, props.action, true, deleteDraft, () => InteractionManager.runAfterInteractions(() => textInputRef.current.focus()));
return;
}
Report.editReportComment(props.reportID, props.action, trimmedNewDraft);
diff --git a/src/pages/home/report/ReportActionItemSingle.js b/src/pages/home/report/ReportActionItemSingle.js
index 2b4526af98d1..e9e1ef39e417 100644
--- a/src/pages/home/report/ReportActionItemSingle.js
+++ b/src/pages/home/report/ReportActionItemSingle.js
@@ -108,7 +108,7 @@ function ReportActionItemSingle(props) {
// If this is a report preview, display names and avatars of both people involved
let secondaryAvatar = {};
- const primaryDisplayName = ReportUtils.getDisplayNameForParticipant(actorAccountID);
+ const primaryDisplayName = displayName;
if (displayAllActors) {
// The ownerAccountID and actorAccountID can be the same if the a user requests money back from the IOU's original creator, in that case we need to use managerID to avoid displaying the same user twice
const secondaryAccountId = props.iouReport.ownerAccountID === actorAccountID ? props.iouReport.managerID : props.iouReport.ownerAccountID;
diff --git a/src/pages/home/sidebar/SidebarLinksData.js b/src/pages/home/sidebar/SidebarLinksData.js
index 1e5e11fd9fcb..293dc3f5cd9d 100644
--- a/src/pages/home/sidebar/SidebarLinksData.js
+++ b/src/pages/home/sidebar/SidebarLinksData.js
@@ -198,28 +198,23 @@ export default compose(
chatReports: {
key: ONYXKEYS.COLLECTION.REPORT,
selector: chatReportSelector,
- initialValue: {},
},
isLoadingReportData: {
key: ONYXKEYS.IS_LOADING_REPORT_DATA,
},
priorityMode: {
key: ONYXKEYS.NVP_PRIORITY_MODE,
- initialValue: CONST.PRIORITY_MODE.DEFAULT,
},
betas: {
key: ONYXKEYS.BETAS,
- initialValue: [],
},
allReportActions: {
key: ONYXKEYS.COLLECTION.REPORT_ACTIONS,
selector: reportActionsSelector,
- initialValue: {},
},
policies: {
key: ONYXKEYS.COLLECTION.POLICY,
selector: policySelector,
- initialValue: {},
},
}),
)(SidebarLinksData);
diff --git a/src/pages/iou/IOUCurrencySelection.js b/src/pages/iou/IOUCurrencySelection.js
index c7b5885865df..20344a08a2c8 100644
--- a/src/pages/iou/IOUCurrencySelection.js
+++ b/src/pages/iou/IOUCurrencySelection.js
@@ -126,8 +126,12 @@ function IOUCurrencySelection(props) {
};
});
- const searchRegex = new RegExp(Str.escapeForRegExp(searchValue.trim()), 'i');
- const filteredCurrencies = _.filter(currencyOptions, (currencyOption) => searchRegex.test(currencyOption.text) || searchRegex.test(currencyOption.currencyName));
+ const searchRegex = new RegExp(Str.escapeForRegExp(searchValue.trim().replace(CONST.REGEX.ANY_SPACE, ' ')), 'i');
+ const filteredCurrencies = _.filter(
+ currencyOptions,
+ (currencyOption) =>
+ searchRegex.test(currencyOption.text.replace(CONST.REGEX.ANY_SPACE, ' ')) || searchRegex.test(currencyOption.currencyName.replace(CONST.REGEX.ANY_SPACE, ' ')),
+ );
const isEmpty = searchValue.trim() && !filteredCurrencies.length;
return {
diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js
index ec5ab3a678bd..8302564cfcb7 100644
--- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js
+++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js
@@ -1,15 +1,19 @@
import lodashGet from 'lodash/get';
+import lodashSize from 'lodash/size';
import PropTypes from 'prop-types';
import React, {useCallback, useEffect, useRef, useState} from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
+import transactionPropTypes from '@components/transactionPropTypes';
import useInitialValue from '@hooks/useInitialValue';
import useLocalize from '@hooks/useLocalize';
+import compose from '@libs/compose';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import * as MoneyRequestUtils from '@libs/MoneyRequestUtils';
import Navigation from '@libs/Navigation/Navigation';
+import * as TransactionUtils from '@libs/TransactionUtils';
import {iouDefaultProps, iouPropTypes} from '@pages/iou/propTypes';
import styles from '@styles/styles';
import * as IOU from '@userActions/IOU';
@@ -36,14 +40,18 @@ const propTypes = {
/** The current tab we have navigated to in the request modal. String that corresponds to the request type. */
selectedTab: PropTypes.oneOf([CONST.TAB.DISTANCE, CONST.TAB.MANUAL, CONST.TAB.SCAN]),
+
+ /** Transaction that stores the distance request data */
+ transaction: transactionPropTypes,
};
const defaultProps = {
iou: iouDefaultProps,
+ transaction: {},
selectedTab: undefined,
};
-function MoneyRequestParticipantsPage({iou, selectedTab, route}) {
+function MoneyRequestParticipantsPage({iou, selectedTab, route, transaction}) {
const {translate} = useLocalize();
const prevMoneyRequestId = useRef(iou.id);
const optionsSelectorRef = useRef();
@@ -54,7 +62,9 @@ function MoneyRequestParticipantsPage({iou, selectedTab, route}) {
const isScanRequest = MoneyRequestUtils.isScanRequest(selectedTab);
const isSplitRequest = iou.id === CONST.IOU.TYPE.SPLIT;
const [headerTitle, setHeaderTitle] = useState();
-
+ const waypoints = lodashGet(transaction, 'comment.waypoints', {});
+ const validatedWaypoints = TransactionUtils.getValidWaypoints(waypoints);
+ const isInvalidWaypoint = lodashSize(validatedWaypoints) < 2;
useEffect(() => {
if (isDistanceRequest) {
setHeaderTitle(translate('common.distance'));
@@ -85,10 +95,12 @@ function MoneyRequestParticipantsPage({iou, selectedTab, route}) {
}, []);
useEffect(() => {
+ const isInvalidDistanceRequest = !isDistanceRequest || isInvalidWaypoint;
+
// ID in Onyx could change by initiating a new request in a separate browser tab or completing a request
if (prevMoneyRequestId.current !== iou.id) {
// The ID is cleared on completing a request. In that case, we will do nothing
- if (iou.id && !isDistanceRequest && !isSplitRequest) {
+ if (iou.id && isInvalidDistanceRequest && !isSplitRequest) {
navigateBack(true);
}
return;
@@ -100,14 +112,14 @@ function MoneyRequestParticipantsPage({iou, selectedTab, route}) {
if (shouldReset) {
IOU.resetMoneyRequestInfo(moneyRequestId);
}
- if (!isDistanceRequest && ((iou.amount === 0 && !iou.receiptPath) || shouldReset)) {
+ if (isInvalidDistanceRequest && ((iou.amount === 0 && !iou.receiptPath) || shouldReset)) {
navigateBack(true);
}
return () => {
prevMoneyRequestId.current = iou.id;
};
- }, [iou.amount, iou.id, iou.receiptPath, isDistanceRequest, isSplitRequest, iouType, reportID, navigateBack]);
+ }, [iou.amount, iou.id, iou.receiptPath, isDistanceRequest, isSplitRequest, iouType, reportID, navigateBack, isInvalidWaypoint]);
return (
`${ONYXKEYS.COLLECTION.TRANSACTION}${iou.transactionID}`,
+ },
+ }),
+)(MoneyRequestParticipantsPage);
diff --git a/src/pages/signin/LoginForm/BaseLoginForm.js b/src/pages/signin/LoginForm/BaseLoginForm.js
index 9529d7fd0d60..2c96de33557e 100644
--- a/src/pages/signin/LoginForm/BaseLoginForm.js
+++ b/src/pages/signin/LoginForm/BaseLoginForm.js
@@ -15,7 +15,7 @@ import Text from '@components/Text';
import TextInput from '@components/TextInput';
import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
import withNavigationFocus from '@components/withNavigationFocus';
-import withToggleVisibilityView, {toggleVisibilityViewPropTypes} from '@components/withToggleVisibilityView';
+import withToggleVisibilityView from '@components/withToggleVisibilityView';
import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions';
import usePrevious from '@hooks/usePrevious';
import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus';
@@ -72,14 +72,14 @@ const propTypes = {
/** Whether or not the sign in page is being rendered in the RHP modal */
isInModal: PropTypes.bool,
+ isVisible: PropTypes.bool.isRequired,
+
/** Whether navigation is focused */
isFocused: PropTypes.bool.isRequired,
...windowDimensionsPropTypes,
...withLocalizePropTypes,
-
- ...toggleVisibilityViewPropTypes,
};
const defaultProps = {
diff --git a/src/pages/workspace/WorkspaceSettingsPage.js b/src/pages/workspace/WorkspaceSettingsPage.js
index 9d1000179291..d913ae26c170 100644
--- a/src/pages/workspace/WorkspaceSettingsPage.js
+++ b/src/pages/workspace/WorkspaceSettingsPage.js
@@ -101,8 +101,7 @@ function WorkspaceSettingsPage({policy, currencyList, windowWidth, route}) {
-
+
{
+ fontFamily[key as keyof FontFamilyStyles] = fontFamily[key as keyof FontFamilyStyles].replace('Segoe UI Emoji', 'Windows Segoe UI Emoji');
+ });
+}
+
export default fontFamily;
diff --git a/src/styles/styles.ts b/src/styles/styles.ts
index 404c5983d7f7..0b4f2af92867 100644
--- a/src/styles/styles.ts
+++ b/src/styles/styles.ts
@@ -3573,7 +3573,8 @@ const styles = (theme: ThemeDefault) =>
googlePillButtonContainer: {
colorScheme: 'light',
height: 40,
- width: 219,
+ width: 300,
+ overflow: 'hidden',
},
thirdPartyLoadingContainer: {
diff --git a/src/types/modules/react-native-web.d.ts b/src/types/modules/react-native-web.d.ts
new file mode 100644
index 000000000000..f7db951eadad
--- /dev/null
+++ b/src/types/modules/react-native-web.d.ts
@@ -0,0 +1,11 @@
+/* eslint-disable import/prefer-default-export */
+/* eslint-disable @typescript-eslint/consistent-type-definitions */
+declare module 'react-native-web' {
+ class Clipboard {
+ static isAvailable(): boolean;
+ static getString(): Promise;
+ static setString(text: string): boolean;
+ }
+
+ export {Clipboard};
+}