diff --git a/.github/actions/composite/setupNode/action.yml b/.github/actions/composite/setupNode/action.yml
index 0b32d8ee6dc1..c6a6029e06e0 100644
--- a/.github/actions/composite/setupNode/action.yml
+++ b/.github/actions/composite/setupNode/action.yml
@@ -18,13 +18,13 @@ runs:
desktop/package-lock.json
- id: cache-node-modules
- uses: actions/cache@v3
+ uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node-modules-${{ hashFiles('package-lock.json', 'patches/**') }}
- id: cache-desktop-node-modules
- uses: actions/cache@v3
+ uses: actions/cache@v4
with:
path: desktop/node_modules
key: ${{ runner.os }}-desktop-node-modules-${{ hashFiles('desktop/package-lock.json', 'desktop/patches/**') }}
diff --git a/.github/workflows/checkE2ETestCode.yml b/.github/workflows/checkE2ETestCode.yml
new file mode 100644
index 000000000000..090b7a7f23e4
--- /dev/null
+++ b/.github/workflows/checkE2ETestCode.yml
@@ -0,0 +1,23 @@
+name: Check e2e test code builds correctly
+
+on:
+ workflow_call:
+ pull_request:
+ types: [opened, synchronize]
+ paths:
+ - 'tests/e2e/**'
+ - 'src/libs/E2E/**'
+
+jobs:
+ lint:
+ if: ${{ github.actor != 'OSBotify' || github.event_name == 'workflow_call' }}
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Node
+ uses: ./.github/actions/composite/setupNode
+
+ - name: Verify e2e tests compile correctly
+ run: npm run e2e-test-runner-build
\ No newline at end of file
diff --git a/.github/workflows/e2ePerformanceTests.yml b/.github/workflows/e2ePerformanceTests.yml
index 338cb8313465..8a47ea4bb220 100644
--- a/.github/workflows/e2ePerformanceTests.yml
+++ b/.github/workflows/e2ePerformanceTests.yml
@@ -15,6 +15,10 @@ on:
type: string
required: true
+concurrency:
+ group: "${{ github.ref }}-e2e"
+ cancel-in-progress: true
+
jobs:
buildBaseline:
runs-on: ubuntu-latest-xl
@@ -175,23 +179,11 @@ jobs:
- name: Rename delta APK
run: mv "${{steps.downloadDeltaAPK.outputs.download-path}}/app-e2edelta-release.apk" "${{steps.downloadDeltaAPK.outputs.download-path}}/app-e2edeltaRelease.apk"
- - name: Copy e2e code into zip folder
- run: cp -r tests/e2e zip
+ - name: Compile test runner to be executable in a nodeJS environment
+ run: npm run e2e-test-runner-build
- # Note: we can't reuse the apps tsconfig, as it depends on modules that aren't available in the AWS Device Farm environment
- - name: Write tsconfig.json to zip folder
- run: |
- echo '{
- "compilerOptions": {
- "target": "ESNext",
- "module": "commonjs",
- "strict": true,
- "esModuleInterop": true,
- "skipLibCheck": true,
- "forceConsistentCasingInFileNames": true,
- "allowJs": true,
- }
- }' > zip/tsconfig.json
+ - name: Copy e2e code into zip folder
+ run: cp tests/e2e/dist/index.js zip/testRunner.js
- name: Zip everything in the zip directory up
run: zip -qr App.zip ./zip
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 33c850823413..50e886942c98 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -7,8 +7,13 @@ on:
branches-ignore: [staging, production]
paths: ['**.js', '**.ts', '**.tsx', '**.json', '**.mjs', '**.cjs', 'config/.editorconfig', '.watchmanconfig', '.imgbotconfig']
+concurrency:
+ group: "${{ github.ref }}-lint"
+ cancel-in-progress: true
+
jobs:
lint:
+ name: Run ESLint
if: ${{ github.actor != 'OSBotify' || github.event_name == 'workflow_call' }}
runs-on: ubuntu-latest
steps:
diff --git a/.github/workflows/platformDeploy.yml b/.github/workflows/platformDeploy.yml
index 818441828bf0..4d6597334447 100644
--- a/.github/workflows/platformDeploy.yml
+++ b/.github/workflows/platformDeploy.yml
@@ -194,7 +194,7 @@ jobs:
bundler-cache: true
- name: Cache Pod dependencies
- uses: actions/cache@v3
+ uses: actions/cache@v4
id: pods-cache
with:
path: ios/Pods
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index bdc14950a337..71b4bc3d8fc3 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -7,6 +7,10 @@ on:
branches-ignore: [staging, production]
paths: ['**.js', '**.ts', '**.tsx', '**.sh', 'package.json', 'package-lock.json']
+concurrency:
+ group: "${{ github.ref }}-jest"
+ cancel-in-progress: true
+
jobs:
jest:
if: ${{ github.actor != 'OSBotify' && github.actor != 'imgbot[bot]' || github.event_name == 'workflow_call' }}
@@ -31,7 +35,7 @@ jobs:
- name: Cache Jest cache
id: cache-jest-cache
- uses: actions/cache@ac25611caef967612169ab7e95533cf932c32270
+ uses: actions/cache@v4
with:
path: .jest-cache
key: ${{ runner.os }}-jest
diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml
index 9548c3a6e595..3f02430f3c1f 100644
--- a/.github/workflows/testBuild.yml
+++ b/.github/workflows/testBuild.yml
@@ -167,7 +167,7 @@ jobs:
bundler-cache: true
- name: Cache Pod dependencies
- uses: actions/cache@v3
+ uses: actions/cache@v4
id: pods-cache
with:
path: ios/Pods
diff --git a/__mocks__/react-native-safe-area-context.js b/__mocks__/react-native-safe-area-context.js
deleted file mode 100644
index b31ed670b81c..000000000000
--- a/__mocks__/react-native-safe-area-context.js
+++ /dev/null
@@ -1,52 +0,0 @@
-import React, {forwardRef} from 'react';
-import {View} from 'react-native';
-
-const insets = {
- top: 0,
- right: 0,
- bottom: 0,
- left: 0,
-};
-
-function withSafeAreaInsets(WrappedComponent) {
- function WithSafeAreaInsets(props) {
- return (
-
- );
- }
-
- const WithSafeAreaInsetsWithRef = forwardRef((props, ref) => (
-
- ));
-
- WithSafeAreaInsetsWithRef.displayName = 'WithSafeAreaInsetsWithRef';
-
- return WithSafeAreaInsetsWithRef;
-}
-
-const SafeAreaView = View;
-const SafeAreaProvider = (props) => props.children;
-const SafeAreaConsumer = (props) => props.children(insets);
-const SafeAreaInsetsContext = {
- Consumer: SafeAreaConsumer,
-};
-
-const useSafeAreaFrame = jest.fn(() => ({
- x: 0,
- y: 0,
- width: 390,
- height: 844,
-}));
-const useSafeAreaInsets = jest.fn(() => insets);
-
-export {SafeAreaProvider, SafeAreaConsumer, SafeAreaInsetsContext, withSafeAreaInsets, SafeAreaView, useSafeAreaFrame, useSafeAreaInsets};
diff --git a/__mocks__/react-native-safe-area-context.tsx b/__mocks__/react-native-safe-area-context.tsx
new file mode 100644
index 000000000000..b789c90f87e8
--- /dev/null
+++ b/__mocks__/react-native-safe-area-context.tsx
@@ -0,0 +1,61 @@
+import type {ForwardedRef, ReactNode} from 'react';
+import React, {forwardRef} from 'react';
+import {View} from 'react-native';
+import type {EdgeInsets, useSafeAreaFrame as LibUseSafeAreaFrame, WithSafeAreaInsetsProps} from 'react-native-safe-area-context';
+import type ChildrenProps from '@src/types/utils/ChildrenProps';
+
+type SafeAreaProviderProps = ChildrenProps;
+type SafeAreaConsumerProps = {
+ children?: (insets: EdgeInsets) => ReactNode;
+};
+type SafeAreaInsetsContextValue = {
+ Consumer: (props: SafeAreaConsumerProps) => ReactNode;
+};
+
+const insets: EdgeInsets = {
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: 0,
+};
+
+function withSafeAreaInsets(WrappedComponent: React.ComponentType}>) {
+ function WithSafeAreaInsets(props: WithSafeAreaInsetsProps & {forwardedRef: React.ForwardedRef}) {
+ return (
+
+ );
+ }
+
+ const WithSafeAreaInsetsWithRef = forwardRef((props: WithSafeAreaInsetsProps, ref: ForwardedRef) => (
+
+ ));
+
+ return WithSafeAreaInsetsWithRef;
+}
+
+const SafeAreaView = View;
+const SafeAreaProvider = (props: SafeAreaProviderProps) => props.children;
+const SafeAreaConsumer = (props: SafeAreaConsumerProps) => props.children?.(insets);
+const SafeAreaInsetsContext: SafeAreaInsetsContextValue = {
+ Consumer: SafeAreaConsumer,
+};
+
+const useSafeAreaFrame: jest.Mock> = jest.fn(() => ({
+ x: 0,
+ y: 0,
+ width: 390,
+ height: 844,
+}));
+const useSafeAreaInsets: jest.Mock = jest.fn(() => insets);
+
+export {SafeAreaProvider, SafeAreaConsumer, SafeAreaInsetsContext, withSafeAreaInsets, SafeAreaView, useSafeAreaFrame, useSafeAreaInsets};
diff --git a/docs/assets/images/moderation-context-menu.png b/docs/assets/images/moderation-context-menu.png
index e76a09ce8153..bc96aa55037f 100644
Binary files a/docs/assets/images/moderation-context-menu.png and b/docs/assets/images/moderation-context-menu.png differ
diff --git a/docs/assets/images/moderation-flag-page.png b/docs/assets/images/moderation-flag-page.png
index e28ff940322d..256c1678fe7d 100644
Binary files a/docs/assets/images/moderation-flag-page.png and b/docs/assets/images/moderation-flag-page.png differ
diff --git a/package-lock.json b/package-lock.json
index a18846dec51a..44cabdeb2cb7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -97,7 +97,7 @@
"react-native-linear-gradient": "^2.8.1",
"react-native-localize": "^2.2.6",
"react-native-modal": "^13.0.0",
- "react-native-onyx": "2.0.7",
+ "react-native-onyx": "2.0.10",
"react-native-pager-view": "6.2.2",
"react-native-pdf": "6.7.3",
"react-native-performance": "^5.1.0",
@@ -202,7 +202,7 @@
"css-loader": "^6.7.2",
"diff-so-fancy": "^1.3.0",
"dotenv": "^16.0.3",
- "electron": "^26.6.8",
+ "electron": "^29.0.0",
"electron-builder": "24.6.4",
"eslint": "^7.6.0",
"eslint-config-airbnb-typescript": "^17.1.0",
@@ -24894,20 +24894,22 @@
}
},
"node_modules/browserify-sign": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz",
- "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==",
- "license": "ISC",
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.2.tgz",
+ "integrity": "sha512-1rudGyeYY42Dk6texmv7c4VcQ0EsvVbLwZkA+AQB7SxvXxmcD93jcHie8bzecJ+ChDlmAm2Qyu0+Ccg5uhZXCg==",
"dependencies": {
- "bn.js": "^5.1.1",
- "browserify-rsa": "^4.0.1",
+ "bn.js": "^5.2.1",
+ "browserify-rsa": "^4.1.0",
"create-hash": "^1.2.0",
"create-hmac": "^1.1.7",
- "elliptic": "^6.5.3",
+ "elliptic": "^6.5.4",
"inherits": "^2.0.4",
- "parse-asn1": "^5.1.5",
- "readable-stream": "^3.6.0",
- "safe-buffer": "^5.2.0"
+ "parse-asn1": "^5.1.6",
+ "readable-stream": "^3.6.2",
+ "safe-buffer": "^5.2.1"
+ },
+ "engines": {
+ "node": ">= 4"
}
},
"node_modules/browserify-sign/node_modules/readable-stream": {
@@ -28644,14 +28646,14 @@
}
},
"node_modules/electron": {
- "version": "26.6.8",
- "resolved": "https://registry.npmjs.org/electron/-/electron-26.6.8.tgz",
- "integrity": "sha512-nuzJ5nVButL1jErc97IVb+A6jbContMg5Uuz5fhmZ4NLcygLkSW8FZpnOT7A4k8Saa95xDJOvqGZyQdI/OPNFw==",
+ "version": "29.0.0",
+ "resolved": "https://registry.npmjs.org/electron/-/electron-29.0.0.tgz",
+ "integrity": "sha512-HhrRC5vWb6fAbWXP3A6ABwKUO9JvYSC4E141RzWFgnDBqNiNtabfmgC8hsVeCR65RQA2MLSDgC8uP52I9zFllQ==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
"@electron/get": "^2.0.0",
- "@types/node": "^18.11.18",
+ "@types/node": "^20.9.0",
"extract-zip": "^2.0.1"
},
"bin": {
@@ -28932,15 +28934,6 @@
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.435.tgz",
"integrity": "sha512-B0CBWVFhvoQCW/XtjRzgrmqcgVWg6RXOEM/dK59+wFV93BFGR6AeNKc4OyhM+T3IhJaOOG8o/V+33Y2mwJWtzw=="
},
- "node_modules/electron/node_modules/@types/node": {
- "version": "18.19.8",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.8.tgz",
- "integrity": "sha512-g1pZtPhsvGVTwmeVoexWZLTQaOvXwoSq//pTL0DHeNzUDrFnir4fgETdhjhIxjVnN+hKOuh98+E1eMLnUXstFg==",
- "dev": true,
- "dependencies": {
- "undici-types": "~5.26.4"
- }
- },
"node_modules/element-resize-detector": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/element-resize-detector/-/element-resize-detector-1.2.4.tgz",
@@ -44490,9 +44483,9 @@
}
},
"node_modules/react-native-onyx": {
- "version": "2.0.7",
- "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.7.tgz",
- "integrity": "sha512-UGMUTSFxYEzNn3wuCGzaf0t6D5XwcE+3J2pYj7wPlbskdcHVLijZZEwgSSDBF7hgNfCuZ+ImetskPNktnf9hkg==",
+ "version": "2.0.10",
+ "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.10.tgz",
+ "integrity": "sha512-XHJdKBZnUyoRKrBgZlv/p6ehuFvqXqwqQlapmVwwIU40KQQes58gPy+8HnRndT3CdAeElVWZnw/BUMtiD/F3Xw==",
"dependencies": {
"ascii-table": "0.0.9",
"fast-equals": "^4.0.3",
diff --git a/package.json b/package.json
index e6d311934524..5ce77f47aedc 100644
--- a/package.json
+++ b/package.json
@@ -55,7 +55,8 @@
"gh-actions-unused-styles": "./.github/scripts/findUnusedKeys.sh",
"workflow-test": "./workflow_tests/scripts/runWorkflowTests.sh",
"workflow-test:generate": "ts-node workflow_tests/utils/preGenerateTest.js",
- "setup-https": "mkcert -install && mkcert -cert-file config/webpack/certificate.pem -key-file config/webpack/key.pem dev.new.expensify.com localhost 127.0.0.1"
+ "setup-https": "mkcert -install && mkcert -cert-file config/webpack/certificate.pem -key-file config/webpack/key.pem dev.new.expensify.com localhost 127.0.0.1",
+ "e2e-test-runner-build": "ncc build tests/e2e/testRunner.js -o tests/e2e/dist/"
},
"dependencies": {
"@dotlottie/react-player": "^1.6.3",
@@ -145,7 +146,7 @@
"react-native-linear-gradient": "^2.8.1",
"react-native-localize": "^2.2.6",
"react-native-modal": "^13.0.0",
- "react-native-onyx": "2.0.7",
+ "react-native-onyx": "2.0.10",
"react-native-pager-view": "6.2.2",
"react-native-pdf": "6.7.3",
"react-native-performance": "^5.1.0",
@@ -250,7 +251,7 @@
"css-loader": "^6.7.2",
"diff-so-fancy": "^1.3.0",
"dotenv": "^16.0.3",
- "electron": "^26.6.8",
+ "electron": "^29.0.0",
"electron-builder": "24.6.4",
"eslint": "^7.6.0",
"eslint-config-airbnb-typescript": "^17.1.0",
diff --git a/src/CONST.ts b/src/CONST.ts
index 1489df5c051a..6e49ad9c12bc 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -691,6 +691,7 @@ const CONST = {
DOMAIN_ALL: 'domainAll',
POLICY_ROOM: 'policyRoom',
POLICY_EXPENSE_CHAT: 'policyExpenseChat',
+ SELF_DM: 'selfDM',
},
WORKSPACE_CHAT_ROOMS: {
ANNOUNCE: '#announce',
@@ -1471,6 +1472,8 @@ const CONST = {
ALPHABETIC_AND_LATIN_CHARS: /^[\p{Script=Latin} ]*$/u,
NON_ALPHABETIC_AND_NON_LATIN_CHARS: /[^\p{Script=Latin}]/gu,
ACCENT_LATIN_CHARS: /[\u00C0-\u017F]/g,
+ INVALID_DISPLAY_NAME_LHN: /[^\p{L}\p{N}\u00C0-\u017F\s-]/gu,
+ INVALID_DISPLAY_NAME_ONLY_LHN: /^[^\p{L}\p{N}\u00C0-\u017F]$/gu,
POSITIVE_INTEGER: /^\d+$/,
PO_BOX: /\b[P|p]?(OST|ost)?\.?\s*[O|o|0]?(ffice|FFICE)?\.?\s*[B|b][O|o|0]?[X|x]?\.?\s+[#]?(\d+)\b/,
ANY_VALUE: /^.+$/,
diff --git a/src/components/FocusModeNotification.js b/src/components/FocusModeNotification.tsx
similarity index 98%
rename from src/components/FocusModeNotification.js
rename to src/components/FocusModeNotification.tsx
index 9ec16beead15..7b3f567d256b 100644
--- a/src/components/FocusModeNotification.js
+++ b/src/components/FocusModeNotification.tsx
@@ -28,7 +28,6 @@ function FocusModeNotification() {
{translate('focusModeUpdateModal.prompt')}
{
User.clearFocusModeNotification();
diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx
index 27f424ad1b70..f5545f402b14 100644
--- a/src/components/LHNOptionsList/LHNOptionsList.tsx
+++ b/src/components/LHNOptionsList/LHNOptionsList.tsx
@@ -32,6 +32,7 @@ function LHNOptionsList({
draftComments = {},
transactionViolations = {},
onFirstItemRendered = () => {},
+ reportIDsWithErrors = {},
}: LHNOptionsListProps) {
const styles = useThemeStyles();
const {canUseViolations} = usePermissions();
@@ -63,6 +64,7 @@ function LHNOptionsList({
const itemComment = draftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`] ?? '';
const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(itemReportActions);
const lastReportAction = sortedReportActions[0];
+ const reportErrors = reportIDsWithErrors[reportID] ?? {};
// Get the transaction for the last report action
let lastReportActionTransactionID = '';
@@ -91,6 +93,7 @@ function LHNOptionsList({
transactionViolations={transactionViolations}
canUseViolations={canUseViolations}
onLayout={onLayoutItem}
+ reportErrors={reportErrors}
/>
);
},
@@ -109,6 +112,7 @@ function LHNOptionsList({
transactionViolations,
canUseViolations,
onLayoutItem,
+ reportIDsWithErrors,
],
);
diff --git a/src/components/LHNOptionsList/OptionRowLHNData.tsx b/src/components/LHNOptionsList/OptionRowLHNData.tsx
index a18d5a8ec1ec..a3394190d0c1 100644
--- a/src/components/LHNOptionsList/OptionRowLHNData.tsx
+++ b/src/components/LHNOptionsList/OptionRowLHNData.tsx
@@ -28,6 +28,7 @@ function OptionRowLHNData({
lastReportActionTransaction = {},
transactionViolations,
canUseViolations,
+ reportErrors,
...propsToForward
}: OptionRowLHNDataProps) {
const reportID = propsToForward.reportID;
@@ -40,11 +41,11 @@ function OptionRowLHNData({
// Note: ideally we'd have this as a dependent selector in onyx!
const item = SidebarUtils.getOptionData({
report: fullReport,
- reportActions,
personalDetails,
preferredLocale: preferredLocale ?? CONST.LOCALES.DEFAULT,
policy,
parentReportAction,
+ reportErrors,
hasViolations: !!hasViolations,
});
if (deepEqual(item, optionItemRef.current)) {
@@ -69,6 +70,7 @@ function OptionRowLHNData({
transactionViolations,
canUseViolations,
receiptTransactions,
+ reportErrors,
]);
useEffect(() => {
diff --git a/src/components/LHNOptionsList/types.ts b/src/components/LHNOptionsList/types.ts
index 58bea97f04c9..c122ab018392 100644
--- a/src/components/LHNOptionsList/types.ts
+++ b/src/components/LHNOptionsList/types.ts
@@ -7,6 +7,7 @@ import type {CurrentReportIDContextValue} from '@components/withCurrentReportID'
import type CONST from '@src/CONST';
import type {OptionData} from '@src/libs/ReportUtils';
import type {Locale, PersonalDetailsList, Policy, Report, ReportAction, ReportActions, Transaction, TransactionViolation} from '@src/types/onyx';
+import type * as OnyxCommon from '@src/types/onyx/OnyxCommon';
import type {EmptyObject} from '@src/types/utils/EmptyObject';
type OptionMode = ValueOf;
@@ -58,6 +59,9 @@ type CustomLHNOptionsListProps = {
/** Callback to fire when the list is laid out */
onFirstItemRendered: () => void;
+
+ /** Report IDs with errors mapping to their corresponding error objects */
+ reportIDsWithErrors: Record;
};
type LHNOptionsListProps = CustomLHNOptionsListProps & CurrentReportIDContextValue & LHNOptionsListOnyxProps;
@@ -113,6 +117,9 @@ type OptionRowLHNDataProps = {
/** Callback to execute when the OptionList lays out */
onLayout?: (event: LayoutChangeEvent) => void;
+
+ /** The report errors */
+ reportErrors: OnyxCommon.Errors | undefined;
};
type OptionRowLHNProps = {
diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js
index df2781d3ea89..dfe1d96b0e5d 100755
--- a/src/components/MoneyRequestConfirmationList.js
+++ b/src/components/MoneyRequestConfirmationList.js
@@ -282,7 +282,7 @@ function MoneyRequestConfirmationList(props) {
}, [props.isEditingSplitBill, props.hasSmartScanFailed, transaction, didConfirmSplit]);
const isMerchantEmpty = !props.iouMerchant || props.iouMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT;
- const shouldDisplayMerchantError = props.isPolicyExpenseChat && !props.isScanRequest && isMerchantEmpty;
+ const shouldDisplayMerchantError = props.isPolicyExpenseChat && shouldDisplayFieldError && isMerchantEmpty;
useEffect(() => {
if (shouldDisplayFieldError && didConfirmSplit) {
@@ -750,14 +750,8 @@ function MoneyRequestConfirmationList(props) {
}}
disabled={didConfirm}
interactive={!props.isReadOnly}
- brickRoadIndicator={
- props.isPolicyExpenseChat && shouldDisplayFieldError && TransactionUtils.isMerchantMissing(transaction) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''
- }
- error={
- shouldDisplayMerchantError || (props.isPolicyExpenseChat && shouldDisplayFieldError && TransactionUtils.isMerchantMissing(transaction))
- ? translate('common.error.enterMerchant')
- : ''
- }
+ brickRoadIndicator={shouldDisplayMerchantError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''}
+ error={shouldDisplayMerchantError ? translate('common.error.enterMerchant') : ''}
/>
)}
{shouldShowCategories && (
diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js
index c5c7c3ec50b0..c0258f1252ef 100755
--- a/src/components/OptionsSelector/BaseOptionsSelector.js
+++ b/src/components/OptionsSelector/BaseOptionsSelector.js
@@ -125,8 +125,11 @@ class BaseOptionsSelector extends Component {
// Unregister the shortcut before registering a new one to avoid lingering shortcut listener
this.unSubscribeFromKeyboardShortcut();
if (this.props.isFocused) {
+ this.subscribeActiveElement();
this.subscribeToEnterShortcut();
this.subscribeToCtrlEnterShortcut();
+ } else {
+ this.unSubscribeActiveElement();
}
}
diff --git a/src/components/ReportWelcomeText.tsx b/src/components/ReportWelcomeText.tsx
index 6ade416e82ca..e9bbd0f27bdc 100644
--- a/src/components/ReportWelcomeText.tsx
+++ b/src/components/ReportWelcomeText.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, {useMemo} from 'react';
import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
@@ -35,7 +35,8 @@ function ReportWelcomeText({report, policy, personalDetails}: ReportWelcomeTextP
const styles = useThemeStyles();
const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(report);
const isChatRoom = ReportUtils.isChatRoom(report);
- const isDefault = !(isChatRoom || isPolicyExpenseChat);
+ const isSelfDM = ReportUtils.isSelfDM(report);
+ const isDefault = !(isChatRoom || isPolicyExpenseChat || isSelfDM);
const participantAccountIDs = report?.participantAccountIDs ?? [];
const isMultipleParticipant = participantAccountIDs.length > 1;
const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(OptionsListUtils.getPersonalDetailsForAccountIDs(participantAccountIDs, personalDetails), isMultipleParticipant);
@@ -44,6 +45,7 @@ function ReportWelcomeText({report, policy, personalDetails}: ReportWelcomeTextP
const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, policy, participantAccountIDs);
const additionalText = moneyRequestOptions.map((item) => translate(`reportActionsView.iouTypes.${item}`)).join(', ');
const canEditPolicyDescription = ReportUtils.canEditPolicyDescription(policy);
+ const reportName = ReportUtils.getReportName(report);
const navigateToReport = () => {
if (!report?.reportID) {
@@ -53,12 +55,22 @@ function ReportWelcomeText({report, policy, personalDetails}: ReportWelcomeTextP
Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report.reportID));
};
+ const welcomeHeroText = useMemo(() => {
+ if (isChatRoom) {
+ return translate('reportActionsView.welcomeToRoom', {roomName: reportName});
+ }
+
+ if (isSelfDM) {
+ return translate('reportActionsView.yourSpace');
+ }
+
+ return translate('reportActionsView.sayHello');
+ }, [isChatRoom, isSelfDM, translate, reportName]);
+
return (
<>
-
- {isChatRoom ? translate('reportActionsView.welcomeToRoom', {roomName: ReportUtils.getReportName(report)}) : translate('reportActionsView.sayHello')}
-
+ {welcomeHeroText}
{isPolicyExpenseChat &&
@@ -114,6 +126,11 @@ function ReportWelcomeText({report, policy, personalDetails}: ReportWelcomeTextP
{roomWelcomeMessage.phrase2 !== undefined && {roomWelcomeMessage.phrase2}}
))}
+ {isSelfDM && (
+
+ {translate('reportActionsView.beginningOfChatHistorySelfDM')}
+
+ )}
{isDefault && (
{translate('reportActionsView.beginningOfChatHistory')}
diff --git a/src/components/SAMLLoadingIndicator.js b/src/components/SAMLLoadingIndicator.tsx
similarity index 79%
rename from src/components/SAMLLoadingIndicator.js
rename to src/components/SAMLLoadingIndicator.tsx
index 84f9098e564f..2be7b76e6cae 100644
--- a/src/components/SAMLLoadingIndicator.js
+++ b/src/components/SAMLLoadingIndicator.tsx
@@ -3,6 +3,7 @@ import {StyleSheet, View} from 'react-native';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
+import variables from '@styles/variables';
import Icon from './Icon';
import * as Expensicons from './Icon/Expensicons';
import * as Illustrations from './Icon/Illustrations';
@@ -23,8 +24,13 @@ function SAMLLoadingIndicator() {
/>
{translate('samlSignIn.launching')}
-
- {translate('samlSignIn.oneMoment')}
+
+
+ {translate('samlSignIn.oneMoment')}
+
diff --git a/src/components/SelectionList/UserListItem.tsx b/src/components/SelectionList/UserListItem.tsx
index 0cfe4c1a509a..4619a2e54f74 100644
--- a/src/components/SelectionList/UserListItem.tsx
+++ b/src/components/SelectionList/UserListItem.tsx
@@ -85,7 +85,7 @@ function UserListItem({
style={[
styles.optionDisplayName,
isFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText,
- styles.sidebarLinkTextBold,
+ item.isBold !== false && styles.sidebarLinkTextBold,
styles.pre,
item.alternateText ? styles.mb1 : null,
]}
diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts
index eaaed4e572cb..005a8ab21cc1 100644
--- a/src/components/SelectionList/types.ts
+++ b/src/components/SelectionList/types.ts
@@ -61,6 +61,9 @@ type ListItem = {
/** Whether this option is disabled for selection */
isDisabled?: boolean;
+ /** List title is bold by default. Use this props to customize it */
+ isBold?: boolean;
+
/** User accountID */
accountID?: number | null;
diff --git a/src/components/ShowMoreButton/index.js b/src/components/ShowMoreButton.tsx
similarity index 74%
rename from src/components/ShowMoreButton/index.js
rename to src/components/ShowMoreButton.tsx
index 28c33d185cff..3411066a5376 100644
--- a/src/components/ShowMoreButton/index.js
+++ b/src/components/ShowMoreButton.tsx
@@ -1,42 +1,34 @@
-import PropTypes from 'prop-types';
import React from 'react';
import {View} from 'react-native';
-import _ from 'underscore';
-import Button from '@components/Button';
-import * as Expensicons from '@components/Icon/Expensicons';
-import Text from '@components/Text';
+import type {StyleProp, ViewStyle} from 'react-native';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import * as NumberFormatUtils from '@libs/NumberFormatUtils';
-import stylePropTypes from '@styles/stylePropTypes';
+import Button from './Button';
+import * as Expensicons from './Icon/Expensicons';
+import Text from './Text';
-const propTypes = {
+type ShowMoreButtonProps = {
/** Additional styles for container */
- containerStyle: stylePropTypes,
+ containerStyle?: StyleProp;
/** The number of currently shown items */
- currentCount: PropTypes.number,
+ currentCount?: number;
/** The total number of items that could be shown */
- totalCount: PropTypes.number,
+ totalCount?: number;
/** A handler that fires when button has been pressed */
- onPress: PropTypes.func.isRequired,
+ onPress: () => void;
};
-const defaultProps = {
- containerStyle: {},
- currentCount: undefined,
- totalCount: undefined,
-};
-
-function ShowMoreButton({containerStyle, currentCount, totalCount, onPress}) {
+function ShowMoreButton({containerStyle, currentCount, totalCount, onPress}: ShowMoreButtonProps) {
const {translate, preferredLocale} = useLocalize();
const theme = useTheme();
const styles = useThemeStyles();
- const shouldShowCounter = _.isNumber(currentCount) && _.isNumber(totalCount);
+ const shouldShowCounter = !!(currentCount && totalCount);
return (
@@ -67,7 +59,5 @@ function ShowMoreButton({containerStyle, currentCount, totalCount, onPress}) {
}
ShowMoreButton.displayName = 'ShowMoreButton';
-ShowMoreButton.propTypes = propTypes;
-ShowMoreButton.defaultProps = defaultProps;
export default ShowMoreButton;
diff --git a/src/components/TaxPicker/index.js b/src/components/TaxPicker.tsx
similarity index 68%
rename from src/components/TaxPicker/index.js
rename to src/components/TaxPicker.tsx
index be15cd546b36..664aa741c400 100644
--- a/src/components/TaxPicker/index.js
+++ b/src/components/TaxPicker.tsx
@@ -1,16 +1,32 @@
-import lodashGet from 'lodash/get';
import React, {useMemo, useState} from 'react';
-import _ from 'underscore';
-import OptionsSelector from '@components/OptionsSelector';
+import type {EdgeInsets} from 'react-native-safe-area-context';
import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as TransactionUtils from '@libs/TransactionUtils';
import CONST from '@src/CONST';
-import {defaultProps, propTypes} from './taxPickerPropTypes';
+import type {TaxRatesWithDefault} from '@src/types/onyx';
+import OptionsSelector from './OptionsSelector';
-function TaxPicker({selectedTaxRate, taxRates, insets, onSubmit}) {
+type TaxPickerProps = {
+ /** Collection of tax rates attached to a policy */
+ taxRates: TaxRatesWithDefault;
+
+ /** The selected tax rate of an expense */
+ selectedTaxRate?: string;
+
+ /**
+ * Safe area insets required for reflecting the portion of the view,
+ * that is not covered by navigation bars, tab bars, toolbars, and other ancestor views.
+ */
+ insets?: EdgeInsets;
+
+ /** Callback to fire when a tax is pressed */
+ onSubmit: () => void;
+};
+
+function TaxPicker({selectedTaxRate = '', taxRates, insets, onSubmit}: TaxPickerProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const {translate} = useLocalize();
@@ -40,10 +56,11 @@ function TaxPicker({selectedTaxRate, taxRates, insets, onSubmit}) {
return taxRatesOptions;
}, [taxRates, searchValue, selectedOptions]);
- const selectedOptionKey = lodashGet(_.filter(lodashGet(sections, '[0].data', []), (taxRate) => taxRate.searchText === selectedTaxRate)[0], 'keyForList');
+ const selectedOptionKey = sections?.[0]?.data?.find((taxRate) => taxRate.searchText === selectedTaxRate)?.keyForList;
return (
`Welcome to ${roomName}!`,
usePlusButton: ({additionalText}: UsePlusButtonParams) => `\nYou can also use the + button to ${additionalText}, or assign a task!`,
iouTypes: {
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 20f4cf8aeac8..83ed2ca1c89c 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -485,8 +485,10 @@ export default {
beginningOfChatHistoryPolicyExpenseChatPartOne: '¡La colaboración entre ',
beginningOfChatHistoryPolicyExpenseChatPartTwo: ' y ',
beginningOfChatHistoryPolicyExpenseChatPartThree: ' empieza aquí! 🎉 Este es el lugar donde chatear, pedir dinero y pagar.',
+ beginningOfChatHistorySelfDM: 'Este es tu espacio personal. Úsalo para notas, tareas, borradores y recordatorios.',
chatWithAccountManager: 'Chatea con tu gestor de cuenta aquí',
sayHello: '¡Saluda!',
+ yourSpace: 'Tu espacio',
welcomeToRoom: ({roomName}: WelcomeToRoomParams) => `¡Bienvenido a ${roomName}!`,
usePlusButton: ({additionalText}: UsePlusButtonParams) => `\n¡También puedes usar el botón + de abajo para ${additionalText}, o asignar una tarea!`,
iouTypes: {
diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts
index 342006eca710..631bbfe4aa4a 100644
--- a/src/libs/OptionsListUtils.ts
+++ b/src/libs/OptionsListUtils.ts
@@ -114,6 +114,7 @@ type GetOptionsConfig = {
includeMultipleParticipantReports?: boolean;
includePersonalDetails?: boolean;
includeRecentReports?: boolean;
+ includeSelfDM?: boolean;
sortByReportTypeInSearch?: boolean;
searchInputValue?: string;
showChatPreviewLine?: boolean;
@@ -480,7 +481,7 @@ function getSearchText(
/**
* Get an object of error messages keyed by microtime by combining all error objects related to the report.
*/
-function getAllReportErrors(report: OnyxEntry, reportActions: OnyxEntry): OnyxCommon.Errors {
+function getAllReportErrors(report: OnyxEntry, reportActions: OnyxEntry, transactions: OnyxCollection = allTransactions): OnyxCommon.Errors {
const reportErrors = report?.errors ?? {};
const reportErrorFields = report?.errorFields ?? {};
const reportActionErrors: OnyxCommon.ErrorFields = Object.values(reportActions ?? {}).reduce(
@@ -492,7 +493,7 @@ function getAllReportErrors(report: OnyxEntry, reportActions: OnyxEntry<
if (parentReportAction?.actorAccountID === currentUserAccountID && ReportActionUtils.isTransactionThread(parentReportAction)) {
const transactionID = parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? parentReportAction?.originalMessage?.IOUTransactionID : null;
- const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
+ const transaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
if (TransactionUtils.hasMissingSmartscanFields(transaction ?? null) && !ReportUtils.isSettled(transaction?.reportID)) {
reportActionErrors.smartscan = ErrorUtils.getMicroSecondOnyxError('report.genericSmartscanFailureMessage');
}
@@ -675,6 +676,8 @@ function createOption(
result.tooltipText = ReportUtils.getReportParticipantsTitle(report.visibleChatMemberAccountIDs ?? []);
result.isWaitingOnBankAccount = report.isWaitingOnBankAccount;
result.policyID = report.policyID;
+ result.isSelfDM = ReportUtils.isSelfDM(report);
+
hasMultipleParticipants = personalDetailList.length > 1 || result.isChatRoom || result.isPolicyExpenseChat;
subtitle = ReportUtils.getChatRoomSubtitle(report);
@@ -1368,6 +1371,7 @@ function getOptions(
transactionViolations = {},
includeTaxRates,
taxRates,
+ includeSelfDM = false,
}: GetOptionsConfig,
): GetOptions {
if (includeCategories) {
@@ -1444,8 +1448,8 @@ function getOptions(
policies,
doesReportHaveViolations,
isInGSDMode: false,
-
excludeEmptyChats: false,
+ includeSelfDM,
});
});
@@ -1472,7 +1476,9 @@ function getOptions(
const isTaskReport = ReportUtils.isTaskReport(report);
const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(report);
const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report);
- const accountIDs = report.visibleChatMemberAccountIDs ?? [];
+ const isSelfDM = ReportUtils.isSelfDM(report);
+ // Currently, currentUser is not included in visibleChatMemberAccountIDs, so for selfDM we need to add the currentUser as participants.
+ const accountIDs = isSelfDM ? [currentUserAccountID ?? 0] : report.visibleChatMemberAccountIDs ?? [];
if (isPolicyExpenseChat && report.isOwnPolicyExpenseChat && !includeOwnedWorkspaceChats) {
return;
@@ -1483,6 +1489,10 @@ function getOptions(
return;
}
+ if (isSelfDM && !includeSelfDM) {
+ return;
+ }
+
if (isThread && !includeThreads) {
return;
}
@@ -1733,6 +1743,7 @@ function getSearchOptions(reports: Record, personalDetails: Onyx
includeThreads: true,
includeMoneyRequests: true,
includeTasks: true,
+ includeSelfDM: true,
});
Timing.end(CONST.TIMING.LOAD_SEARCH_OPTIONS);
Performance.markEnd(CONST.TIMING.LOAD_SEARCH_OPTIONS);
@@ -1808,6 +1819,7 @@ function getFilteredOptions(
includeSelectedOptions = false,
includeTaxRates = false,
taxRates: TaxRatesWithDefault = {} as TaxRatesWithDefault,
+ includeSelfDM = false,
) {
return getOptions(reports, personalDetails, {
betas,
@@ -1829,6 +1841,7 @@ function getFilteredOptions(
includeSelectedOptions,
includeTaxRates,
taxRates,
+ includeSelfDM,
});
}
@@ -1862,6 +1875,7 @@ function getShareDestinationOptions(
excludeLogins,
includeOwnedWorkspaceChats,
excludeUnknownUsers,
+ includeSelfDM: true,
});
}
diff --git a/src/libs/PersonalDetailsUtils.ts b/src/libs/PersonalDetailsUtils.ts
index 4be9ad81184b..9dd60eeebcef 100644
--- a/src/libs/PersonalDetailsUtils.ts
+++ b/src/libs/PersonalDetailsUtils.ts
@@ -24,9 +24,14 @@ Onyx.connect({
},
});
-function getDisplayNameOrDefault(passedPersonalDetails?: Partial | null, defaultValue = '', shouldFallbackToHidden = true): string {
- const displayName = passedPersonalDetails?.displayName ? passedPersonalDetails.displayName.replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, '') : '';
+function getDisplayNameOrDefault(passedPersonalDetails?: Partial | null, defaultValue = '', shouldFallbackToHidden = true, shouldAddCurrentUserPostfix = false): string {
+ let displayName = passedPersonalDetails?.displayName ? passedPersonalDetails.displayName.replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, '') : '';
+ if (shouldAddCurrentUserPostfix && !!displayName) {
+ displayName = `${displayName} (${Localize.translateLocal('common.you').toLowerCase()})`;
+ }
+
const fallbackValue = shouldFallbackToHidden ? Localize.translateLocal('common.hidden') : '';
+
return displayName || defaultValue || fallbackValue;
}
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index f64123aceaf8..284c56ce2c1e 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -399,6 +399,7 @@ type OptionData = {
notificationPreference?: NotificationPreference | null;
isDisabled?: boolean | null;
name?: string | null;
+ isSelfDM?: boolean | null;
} & Report;
type OnyxDataTaskAssigneeChat = {
@@ -909,6 +910,10 @@ function isDM(report: OnyxEntry): boolean {
return isChatReport(report) && !getChatType(report);
}
+function isSelfDM(report: OnyxEntry): boolean {
+ return getChatType(report) === CONST.REPORT.CHAT_TYPE.SELF_DM;
+}
+
/**
* Only returns true if this is our main 1:1 DM report with Concierge
*/
@@ -1611,6 +1616,10 @@ function getIcons(
return isPayer ? [managerIcon, ownerIcon] : [ownerIcon, managerIcon];
}
+ if (isSelfDM(report)) {
+ return getIconsForParticipants([currentUserAccountID ?? 0], personalDetails);
+ }
+
return getIconsForParticipants(report?.participantAccountIDs ?? [], personalDetails);
}
@@ -1633,7 +1642,7 @@ function getPersonalDetailsForAccountID(accountID: number): Partial {
const accountID = Number(user?.accountID);
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- const displayName = getDisplayNameForParticipant(accountID, isMultipleParticipantReport, shouldFallbackToHidden) || user?.login || '';
+ const displayName = getDisplayNameForParticipant(accountID, isMultipleParticipantReport, shouldFallbackToHidden, shouldAddCurrentUserPostfix) || user?.login || '';
const avatar = UserUtils.getDefaultAvatar(accountID);
let pronouns = user?.pronouns ?? undefined;
@@ -2563,6 +2576,10 @@ function getReportName(report: OnyxEntry, policy: OnyxEntry = nu
formattedName += ` (${Localize.translateLocal('common.archived')})`;
}
+ if (isSelfDM(report)) {
+ formattedName = getDisplayNameForParticipant(currentUserAccountID, undefined, undefined, true);
+ }
+
if (formattedName) {
return formattedName;
}
@@ -2619,6 +2636,11 @@ function getParentNavigationSubtitle(report: OnyxEntry): ParentNavigatio
function navigateToDetailsPage(report: OnyxEntry) {
const participantAccountIDs = report?.participantAccountIDs ?? [];
+ if (isSelfDM(report)) {
+ Navigation.navigate(ROUTES.PROFILE.getRoute(currentUserAccountID ?? 0));
+ return;
+ }
+
if (isOneOnOneChat(report)) {
Navigation.navigate(ROUTES.PROFILE.getRoute(participantAccountIDs[0]));
return;
@@ -3926,6 +3948,7 @@ function shouldReportBeInOptionList({
policies,
excludeEmptyChats,
doesReportHaveViolations,
+ includeSelfDM = false,
}: {
report: OnyxEntry;
currentReportId: string;
@@ -3934,6 +3957,7 @@ function shouldReportBeInOptionList({
policies: OnyxCollection;
excludeEmptyChats: boolean;
doesReportHaveViolations: boolean;
+ includeSelfDM?: boolean;
}) {
const isInDefaultMode = !isInGSDMode;
// Exclude reports that have no data because there wouldn't be anything to show in the option item.
@@ -3955,7 +3979,8 @@ function shouldReportBeInOptionList({
!isUserCreatedPolicyRoom(report) &&
!isArchivedRoom(report) &&
!isMoneyRequestReport(report) &&
- !isTaskReport(report))
+ !isTaskReport(report) &&
+ !isSelfDM(report))
) {
return false;
}
@@ -4017,6 +4042,10 @@ function shouldReportBeInOptionList({
return false;
}
+ if (isSelfDM(report)) {
+ return includeSelfDM;
+ }
+
return true;
}
@@ -4244,7 +4273,7 @@ function hasIOUWaitingOnCurrentUserBankAccount(chatReport: OnyxEntry): b
*/
function canRequestMoney(report: OnyxEntry, policy: OnyxEntry, otherParticipants: number[]): boolean {
// User cannot request money in chat thread or in task report or in chat room
- if (isChatThread(report) || isTaskReport(report) || isChatRoom(report)) {
+ if (isChatThread(report) || isTaskReport(report) || isChatRoom(report) || isSelfDM(report)) {
return false;
}
@@ -4304,7 +4333,7 @@ function canRequestMoney(report: OnyxEntry, policy: OnyxEntry, o
*/
function getMoneyRequestOptions(report: OnyxEntry, policy: OnyxEntry, reportParticipants: number[]): Array> {
// In any thread or task report, we do not allow any new money requests yet
- if (isChatThread(report) || isTaskReport(report)) {
+ if (isChatThread(report) || isTaskReport(report) || isSelfDM(report)) {
return [];
}
@@ -4359,6 +4388,7 @@ function canLeaveRoom(report: OnyxEntry, isPolicyMember: boolean): boole
report?.chatType === CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE ||
report?.chatType === CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT ||
report?.chatType === CONST.REPORT.CHAT_TYPE.DOMAIN_ALL ||
+ report?.chatType === CONST.REPORT.CHAT_TYPE.SELF_DM ||
!report?.chatType
) {
// DM chats don't have a chatType
@@ -4493,19 +4523,6 @@ function getReportOfflinePendingActionAndErrors(report: OnyxEntry): Repo
return {reportPendingAction, reportErrors};
}
-function getPolicyExpenseChatReportIDByOwner(policyOwner: string): string | null {
- const policyWithOwner = Object.values(allPolicies ?? {}).find((policy) => policy?.owner === policyOwner);
- if (!policyWithOwner) {
- return null;
- }
-
- const expenseChat = Object.values(allReports ?? {}).find((report) => isPolicyExpenseChat(report) && report?.policyID === policyWithOwner.id);
- if (!expenseChat) {
- return null;
- }
- return expenseChat.reportID;
-}
-
/**
* Check if the report can create the request with type is iouType
*/
@@ -5187,8 +5204,8 @@ export {
getAddWorkspaceRoomOrChatReportErrors,
getReportOfflinePendingActionAndErrors,
isDM,
+ isSelfDM,
getPolicy,
- getPolicyExpenseChatReportIDByOwner,
getWorkspaceChats,
shouldDisableRename,
hasSingleParticipant,
diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts
index 35cf52a5ff99..4fa84d3b7c86 100644
--- a/src/libs/SidebarUtils.ts
+++ b/src/libs/SidebarUtils.ts
@@ -7,6 +7,7 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {PersonalDetails, PersonalDetailsList, TransactionViolation} from '@src/types/onyx';
import type Beta from '@src/types/onyx/Beta';
+import type * as OnyxCommon from '@src/types/onyx/OnyxCommon';
import type Policy from '@src/types/onyx/Policy';
import type Report from '@src/types/onyx/Report';
import type {ReportActions} from '@src/types/onyx/ReportAction';
@@ -23,7 +24,6 @@ import * as TaskUtils from './TaskUtils';
import * as UserUtils from './UserUtils';
const visibleReportActionItems: ReportActions = {};
-const lastReportActions: ReportActions = {};
Onyx.connect({
key: ONYXKEYS.COLLECTION.REPORT_ACTIONS,
@@ -34,7 +34,6 @@ Onyx.connect({
const reportID = CollectionUtils.extractCollectionItemID(key);
const actionsArray: ReportAction[] = ReportActionsUtils.getSortedReportActions(Object.values(actions));
- lastReportActions[reportID] = actionsArray[actionsArray.length - 1];
// The report is only visible if it is the last action not deleted that
// does not match a closed or created state.
@@ -59,6 +58,13 @@ function compareStringDates(a: string, b: string): 0 | 1 | -1 {
return 0;
}
+function filterDisplayName(displayName: string): string {
+ if (CONST.REGEX.INVALID_DISPLAY_NAME_ONLY_LHN.test(displayName)) {
+ return displayName;
+ }
+ return displayName.replace(CONST.REGEX.INVALID_DISPLAY_NAME_LHN, '').trim();
+}
+
/**
* @returns An array of reportIDs sorted in the proper order
*/
@@ -68,22 +74,27 @@ function getOrderedReportIDs(
betas: Beta[],
policies: Record,
priorityMode: ValueOf,
- allReportActions: OnyxCollection,
+ allReportActions: OnyxCollection,
transactionViolations: OnyxCollection,
currentPolicyID = '',
policyMemberAccountIDs: number[] = [],
+ reportIDsWithErrors: Record = {},
+ canUseViolations = false,
): string[] {
const isInGSDMode = priorityMode === CONST.PRIORITY_MODE.GSD;
const isInDefaultMode = !isInGSDMode;
const allReportsDictValues = Object.values(allReports);
+ const reportIDsWithViolations = new Set();
+
// Filter out all the reports that shouldn't be displayed
let reportsToDisplay = allReportsDictValues.filter((report) => {
const parentReportActionsKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.parentReportID}`;
- const parentReportActions = allReportActions?.[parentReportActionsKey];
- const parentReportAction = parentReportActions?.find((action) => action && report && action?.reportActionID === report?.parentReportActionID);
- const doesReportHaveViolations =
- betas.includes(CONST.BETAS.VIOLATIONS) && !!parentReportAction && ReportUtils.doesTransactionThreadHaveViolations(report, transactionViolations, parentReportAction);
+ const parentReportAction = allReportActions?.[parentReportActionsKey]?.[report.parentReportActionID ?? ''];
+ const doesReportHaveViolations = canUseViolations && !!parentReportAction && ReportUtils.doesTransactionThreadHaveViolations(report, transactionViolations, parentReportAction);
+ if (doesReportHaveViolations) {
+ reportIDsWithViolations.add(report.reportID);
+ }
return ReportUtils.shouldReportBeInOptionList({
report,
currentReportId: currentReportId ?? '',
@@ -92,6 +103,7 @@ function getOrderedReportIDs(
policies,
excludeEmptyChats: true,
doesReportHaveViolations,
+ includeSelfDM: true,
});
});
@@ -104,7 +116,7 @@ function getOrderedReportIDs(
}
// 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
+ // 1. Pinned/GBR/RBR - Always sorted by reportDisplayName
// 2. Drafts - Always sorted by reportDisplayName
// 3. Non-archived reports and settled IOUs
// - Sorted by lastVisibleActionCreated in default (most recent) view mode
@@ -112,13 +124,15 @@ function getOrderedReportIDs(
// 4. Archived reports
// - Sorted by lastVisibleActionCreated in default (most recent) view mode
// - Sorted by reportDisplayName in GSD (focus) view mode
- const pinnedAndGBRReports: Report[] = [];
+ const pinnedAndBrickRoadReports: Report[] = [];
const draftReports: Report[] = [];
const nonArchivedReports: Report[] = [];
const archivedReports: Report[] = [];
if (currentPolicyID || policyMemberAccountIDs.length > 0) {
- reportsToDisplay = reportsToDisplay.filter((report) => ReportUtils.doesReportBelongToWorkspace(report, policyMemberAccountIDs, currentPolicyID));
+ reportsToDisplay = reportsToDisplay.filter(
+ (report) => report.reportID === currentReportId || ReportUtils.doesReportBelongToWorkspace(report, policyMemberAccountIDs, currentPolicyID),
+ );
}
// There are a few properties that need to be calculated for the report which are used when sorting reports.
reportsToDisplay.forEach((report) => {
@@ -126,12 +140,14 @@ function getOrderedReportIDs(
// 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);
+ report.displayName = filterDisplayName(ReportUtils.getReportName(report));
+
+ const hasRBR = report.reportID in reportIDsWithErrors || reportIDsWithViolations.has(report.reportID);
const isPinned = report.isPinned ?? false;
const reportAction = ReportActionsUtils.getReportAction(report.parentReportID ?? '', report.parentReportActionID ?? '');
- if (isPinned || ReportUtils.requiresAttentionFromCurrentUser(report, reportAction)) {
- pinnedAndGBRReports.push(report);
+ if (isPinned || hasRBR || ReportUtils.requiresAttentionFromCurrentUser(report, reportAction)) {
+ pinnedAndBrickRoadReports.push(report);
} else if (report.hasDraft) {
draftReports.push(report);
} else if (ReportUtils.isArchivedRoom(report)) {
@@ -142,7 +158,7 @@ function getOrderedReportIDs(
});
// Sort each group of reports accordingly
- pinnedAndGBRReports.sort((a, b) => (a?.displayName && b?.displayName ? localeCompare(a.displayName, b.displayName) : 0));
+ pinnedAndBrickRoadReports.sort((a, b) => (a?.displayName && b?.displayName ? localeCompare(a.displayName, b.displayName) : 0));
draftReports.sort((a, b) => (a?.displayName && b?.displayName ? localeCompare(a.displayName, b.displayName) : 0));
if (isInDefaultMode) {
@@ -160,7 +176,7 @@ function getOrderedReportIDs(
// Now that we have all the reports grouped and sorted, they must be flattened into an array and only return the reportID.
// The order the arrays are concatenated in matters and will determine the order that the groups are displayed in the sidebar.
- const LHNReports = [...pinnedAndGBRReports, ...draftReports, ...nonArchivedReports, ...archivedReports].map((report) => report.reportID);
+ const LHNReports = [...pinnedAndBrickRoadReports, ...draftReports, ...nonArchivedReports, ...archivedReports].map((report) => report.reportID);
return LHNReports;
}
@@ -169,19 +185,19 @@ function getOrderedReportIDs(
*/
function getOptionData({
report,
- reportActions,
personalDetails,
preferredLocale,
policy,
parentReportAction,
+ reportErrors,
hasViolations,
}: {
report: OnyxEntry;
- reportActions: OnyxEntry;
personalDetails: OnyxEntry;
preferredLocale: DeepValueOf;
policy: OnyxEntry | undefined;
parentReportAction: OnyxEntry | undefined;
+ reportErrors: OnyxCommon.Errors | undefined;
hasViolations: boolean;
}): ReportUtils.OptionData | undefined {
// When a user signs out, Onyx is cleared. Due to the lazy rendering with a virtual list, it's possible for
@@ -194,7 +210,7 @@ function getOptionData({
const result: ReportUtils.OptionData = {
text: '',
alternateText: null,
- allReportErrors: OptionsListUtils.getAllReportErrors(report, reportActions),
+ allReportErrors: reportErrors,
brickRoadIndicator: null,
tooltipText: null,
subtitle: null,
@@ -221,7 +237,14 @@ function getOptionData({
isDeletedParentAction: false,
};
- const participantPersonalDetailList = Object.values(OptionsListUtils.getPersonalDetailsForAccountIDs(report.participantAccountIDs ?? [], personalDetails)) as PersonalDetails[];
+ let participantAccountIDs = report.participantAccountIDs ?? [];
+
+ // Currently, currentUser is not included in participantAccountIDs, so for selfDM we need to add the currentUser(report owner) as participants.
+ if (ReportUtils.isSelfDM(report)) {
+ participantAccountIDs = [report.ownerAccountID ?? 0];
+ }
+
+ const participantPersonalDetailList = Object.values(OptionsListUtils.getPersonalDetailsForAccountIDs(participantAccountIDs, personalDetails)) as PersonalDetails[];
const personalDetail = participantPersonalDetailList[0] ?? {};
const hasErrors = Object.keys(result.allReportErrors ?? {}).length !== 0;
@@ -258,6 +281,7 @@ function getOptionData({
result.isAllowedToComment = ReportUtils.canUserPerformWriteAction(report);
result.chatType = report.chatType;
result.isDeletedParentAction = report.isDeletedParentAction;
+ result.isSelfDM = ReportUtils.isSelfDM(report);
const hasMultipleParticipants = participantPersonalDetailList.length > 1 || result.isChatRoom || result.isPolicyExpenseChat || ReportUtils.isExpenseReport(report);
const subtitle = ReportUtils.getChatRoomSubtitle(report);
@@ -267,7 +291,12 @@ function getOptionData({
const formattedLogin = Str.isSMSLogin(login) ? LocalePhoneNumber.formatPhoneNumber(login) : login;
// We only create tooltips for the first 10 users or so since some reports have hundreds of users, causing performance to degrade.
- const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips((participantPersonalDetailList || []).slice(0, 10), hasMultipleParticipants);
+ const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(
+ (participantPersonalDetailList || []).slice(0, 10),
+ hasMultipleParticipants,
+ undefined,
+ ReportUtils.isSelfDM(report),
+ );
// If the last actor's details are not currently saved in Onyx Collection,
// then try to get that from the last report action if that action is valid
@@ -289,14 +318,12 @@ function getOptionData({
let lastMessageText = lastMessageTextFromReport;
- const reportAction = lastReportActions?.[report.reportID];
+ const lastAction = visibleReportActionItems[report.reportID];
const isThreadMessage =
- ReportUtils.isThread(report) && reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && reportAction?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE;
+ ReportUtils.isThread(report) && lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && lastAction?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE;
if ((result.isChatRoom || result.isPolicyExpenseChat || result.isThread || result.isTaskReport || isThreadMessage) && !result.isArchivedRoom) {
- const lastAction = visibleReportActionItems[report.reportID];
-
if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.RENAMED) {
const newName = lastAction?.originalMessage?.newName ?? '';
result.alternateText = Localize.translate(preferredLocale, 'newRoomPage.roomRenamedTo', {newName});
diff --git a/src/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts
index 669d10c4a1b8..3d5f23a84f74 100644
--- a/src/libs/ValidationUtils.ts
+++ b/src/libs/ValidationUtils.ts
@@ -82,7 +82,7 @@ function isValidPastDate(date: string | Date): boolean {
* Used to validate a value that is "required".
* @param value - field value
*/
-function isRequiredFulfilled(value?: FormValue): boolean {
+function isRequiredFulfilled(value?: FormValue | number[] | string[] | Record): boolean {
if (!value) {
return false;
}
diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts
index 5d089ed6e393..a14e752b1015 100644
--- a/src/libs/actions/User.ts
+++ b/src/libs/actions/User.ts
@@ -33,7 +33,7 @@ import playSoundExcludingMobile from '@libs/Sound/playSoundExcludingMobile';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
-import type {BlockedFromConcierge, FrequentlyUsedEmoji} from '@src/types/onyx';
+import type {BlockedFromConcierge, FrequentlyUsedEmoji, Policy} from '@src/types/onyx';
import type Login from '@src/types/onyx/Login';
import type {OnyxServerUpdate} from '@src/types/onyx/OnyxUpdatesFromServer';
import type OnyxPersonalDetails from '@src/types/onyx/PersonalDetails';
@@ -775,7 +775,7 @@ function generateStatementPDF(period: string) {
/**
* Sets a contact method / secondary login as the user's "Default" contact method.
*/
-function setContactMethodAsDefault(newDefaultContactMethod: string) {
+function setContactMethodAsDefault(newDefaultContactMethod: string, policies: OnyxCollection>) {
const oldDefaultContactMethod = currentEmail;
const optimisticData: OnyxUpdate[] = [
{
@@ -868,6 +868,25 @@ function setContactMethodAsDefault(newDefaultContactMethod: string) {
},
];
+ Object.values(policies ?? {}).forEach((policy) => {
+ if (policy?.ownerAccountID !== currentUserAccountID) {
+ return;
+ }
+ optimisticData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policy.id}`,
+ value: {
+ owner: newDefaultContactMethod,
+ },
+ });
+ failureData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policy.id}`,
+ value: {
+ owner: policy.owner,
+ },
+ });
+ });
const parameters: SetContactMethodAsDefaultParams = {
partnerUserID: newDefaultContactMethod,
};
diff --git a/src/pages/ProfilePage.js b/src/pages/ProfilePage.js
index 0b986adf1c6f..cf05b8e4ab28 100755
--- a/src/pages/ProfilePage.js
+++ b/src/pages/ProfilePage.js
@@ -95,9 +95,10 @@ const getPhoneNumber = (details) => {
function ProfilePage(props) {
const styles = useThemeStyles();
const accountID = Number(lodashGet(props.route.params, 'accountID', 0));
- const details = lodashGet(props.personalDetails, accountID, ValidationUtils.isValidAccountRoute(accountID) ? {} : {isloading: false});
+ const isCurrentUser = props.session.accountID === accountID;
- const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(details);
+ const details = lodashGet(props.personalDetails, accountID, ValidationUtils.isValidAccountRoute(accountID) ? {} : {isloading: false});
+ const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(details, undefined, undefined, isCurrentUser);
const avatar = lodashGet(details, 'avatar', UserUtils.getDefaultAvatar());
const fallbackIcon = lodashGet(details, 'fallbackIcon', '');
const login = lodashGet(details, 'login', '');
@@ -116,7 +117,6 @@ function ProfilePage(props) {
const phoneNumber = getPhoneNumber(details);
const phoneOrEmail = isSMSLogin ? getPhoneNumber(details) : login;
- const isCurrentUser = props.session.accountID === accountID;
const hasMinimumDetails = !_.isEmpty(details.avatar);
const isLoading = lodashGet(details, 'isLoading', false) || _.isEmpty(details);
@@ -130,7 +130,7 @@ function ProfilePage(props) {
const navigateBackTo = lodashGet(props.route, 'params.backTo');
- const shouldShowNotificationPreference = !_.isEmpty(props.report) && props.report.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN;
+ const shouldShowNotificationPreference = !_.isEmpty(props.report) && !isCurrentUser && props.report.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN;
const notificationPreference = shouldShowNotificationPreference ? props.translate(`notificationPreferencesPage.notificationPreferences.${props.report.notificationPreference}`) : '';
// eslint-disable-next-line rulesdir/prefer-early-return
@@ -234,7 +234,7 @@ function ProfilePage(props) {
shouldShowRightIcon
/>
)}
- {!_.isEmpty(props.report) && (
+ {!_.isEmpty(props.report) && !isCurrentUser && (