diff --git a/.eslintrc.js b/.eslintrc.js
index ac352db40431..2f9a4167ae51 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -31,6 +31,11 @@ module.exports = {
importNames: ['TouchableOpacity', 'TouchableWithoutFeedback', 'TouchableNativeFeedback', 'TouchableHighlight'],
message: 'Please use PressableWithFeedback and/or PressableWithoutFeedback from src/components/Pressable instead',
},
+ {
+ name: 'react-native',
+ importNames: ['StatusBar'],
+ message: 'Please use StatusBar from src/libs/StatusBar instead',
+ },
],
},
],
diff --git a/.github/actions/composite/setupNode/action.yml b/.github/actions/composite/setupNode/action.yml
index 643c707da230..6bdf500912c0 100644
--- a/.github/actions/composite/setupNode/action.yml
+++ b/.github/actions/composite/setupNode/action.yml
@@ -8,10 +8,34 @@ runs:
with:
node-version-file: '.nvmrc'
cache: npm
+ cache-dependency-path: |
+ package-lock.json
+ desktop/package-lock.json
- - name: Install node packages
- uses: nick-invision/retry@0711ba3d7808574133d713a0d92d2941be03a350
+ - id: cache-node-modules
+ uses: actions/cache@v3
+ with:
+ path: node_modules
+ key: ${{ runner.os }}-node-modules-${{ hashFiles('package-lock.json') }}
+
+ - id: cache-desktop-node-modules
+ uses: actions/cache@v3
+ with:
+ path: desktop/node_modules
+ key: ${{ runner.os }}-desktop-node-modules-${{ hashFiles('desktop/package-lock.json') }}
+
+ - name: Install root project node packages
+ if: steps.cache-node-modules.outputs.cache-hit != 'true'
+ uses: nick-fields/retry@v2
with:
timeout_minutes: 30
max_attempts: 3
command: npm ci
+
+ - name: Install node packages for desktop submodule
+ if: steps.cache-desktop-node-modules.outputs.cache-hit != 'true'
+ uses: nick-fields/retry@v2
+ with:
+ timeout_minutes: 30
+ max_attempts: 3
+ command: cd desktop && npm ci
diff --git a/.github/workflows/platformDeploy.yml b/.github/workflows/platformDeploy.yml
index 48c8e38fe3e1..bac7ab34920d 100644
--- a/.github/workflows/platformDeploy.yml
+++ b/.github/workflows/platformDeploy.yml
@@ -158,13 +158,13 @@ jobs:
bundler-cache: true
- uses: actions/cache@v3
+ id: cache-pods
with:
path: ios/Pods
key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
- restore-keys: |
- ${{ runner.os }}-pods-
- name: Install cocoapods
+ if: steps.cache-pods.outputs.cache-hit != 'true'
uses: nick-invision/retry@0711ba3d7808574133d713a0d92d2941be03a350
with:
timeout_minutes: 10
diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml
index 9b4bf6d020d6..b75ee2a402e4 100644
--- a/.github/workflows/testBuild.yml
+++ b/.github/workflows/testBuild.yml
@@ -141,13 +141,13 @@ jobs:
bundler-cache: true
- uses: actions/cache@v3
+ id: cache-pods
with:
path: ios/Pods
key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
- restore-keys: |
- ${{ runner.os }}-pods-
- name: Install cocoapods
+ if: steps.cache-pods.outputs.cache-hit != 'true'
uses: nick-invision/retry@0711ba3d7808574133d713a0d92d2941be03a350
with:
timeout_minutes: 10
diff --git a/.well-known/apple-app-site-association b/.well-known/apple-app-site-association
index fcd7138da7e6..7568dbb55eb0 100644
--- a/.well-known/apple-app-site-association
+++ b/.well-known/apple-app-site-association
@@ -12,10 +12,6 @@
"/": "/settings/*",
"comment": "Profile and app settings"
},
- {
- "/": "/setpassword/*",
- "comment": "Passoword setup"
- },
{
"/": "/details/*",
"comment": "Details of another users"
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 0976b66e69bd..c29f64297d23 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -106,8 +106,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1001032507
- versionName "1.3.25-7"
+ versionCode 1001032601
+ versionName "1.3.26-1"
}
splits {
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 05302f43ef8d..e79b72f0e904 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -52,7 +52,6 @@
-
@@ -64,7 +63,6 @@
-
diff --git a/docs/articles/other/Everything-About-Chat.md b/docs/articles/other/Everything-About-Chat.md
index 8d4ad5f8740c..521bcd6eca25 100644
--- a/docs/articles/other/Everything-About-Chat.md
+++ b/docs/articles/other/Everything-About-Chat.md
@@ -23,6 +23,37 @@ In addition to 1:1 and group chat, members of a Workspace or Policy will have ac
All workspace members are added to the #announce room by default. The #announce room lets you share important company announcements and have conversations between workspace members.
All workspace admins can access the #admins room. Use the #admins room to collaborate with the other admins on your policy, and chat with your dedicated Expensify Onboarding Guide. If you have a subscription of 10 or more users, you're automatically assigned an Account Manager. You can ask for help and collaborate with your Account Manager in this same #admins room. Anytime someone on your team, your dedicated setup specialist, or your dedicated account manager makes any changes to your Workspace settings, that update is logged in the #admins room.
+## How to format text
+
+#### Italic
+###### To italicize your message, place an underscore on both sides of the text:
+*text*
+
+#### Bold
+###### To bold your message, place an asterisk on both sides of the text:
+**text**
+
+#### Strikethrough
+###### To strikethrough your message, place a tilde on both sides of the text:
+~text~
+
+#### Quote
+###### To turn your text into a blockquote, add a `>` symbol in front of the text:
+> your text
+
+#### Code
+###### To turn your message into code, place a backtick on both sides of the text:
+`text`
+
+#### Codeblock
+###### To turn your entire message into code block, place three backticks on both sides of the text:
+```
+text
+and even more text
+```
+#### Heading
+###### To turn your message into a heading, place the `#` symbol in front of the text:
+## Heading
# FAQs
## How do I add more than one person to a chat?
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 38c7628b753b..4a2406aaa1b8 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.3.25
+ 1.3.26
CFBundleSignature
????
CFBundleURLTypes
@@ -32,7 +32,7 @@
CFBundleVersion
- 1.3.25.7
+ 1.3.26.1
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index bf23732dceb4..591c2f2b999a 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 1.3.25
+ 1.3.26
CFBundleSignature
????
CFBundleVersion
- 1.3.25.7
+ 1.3.26.1
diff --git a/package-lock.json b/package-lock.json
index d0405cf47463..82d99eaa3ab6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.3.25-7",
+ "version": "1.3.26-1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.3.25-7",
+ "version": "1.3.26-1",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -40,7 +40,7 @@
"babel-polyfill": "^6.26.0",
"dom-serializer": "^0.2.2",
"domhandler": "^4.3.0",
- "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#925e62744f784a92c3ad8947aabfc21c93bec221",
+ "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#50aacd53fe54ef7131e5cb9c74ee4526b3bcfe16",
"fbjs": "^3.0.2",
"html-entities": "^1.3.1",
"htmlparser2": "^7.2.0",
@@ -24054,8 +24054,8 @@
},
"node_modules/expensify-common": {
"version": "1.0.0",
- "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#925e62744f784a92c3ad8947aabfc21c93bec221",
- "integrity": "sha512-c6Q2xPI5pzU1mlrW65HKZ6mn5BJvoCgC1wO/sf4Yw+p5XYNQbolj0WYvGLtSxIw/x9G4M+H8bQo7qRd1ckt+5w==",
+ "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#50aacd53fe54ef7131e5cb9c74ee4526b3bcfe16",
+ "integrity": "sha512-wnx6WiG2NVqIs2m3M0VjL/kw2I1rgWOD06mNp6FwRGeOVC+QIL6hsdJgqf9z9NjBSuhc6THfzbcrAOSGs3kTkw==",
"license": "MIT",
"dependencies": {
"classnames": "2.3.1",
@@ -59286,9 +59286,9 @@
}
},
"expensify-common": {
- "version": "git+ssh://git@github.com/Expensify/expensify-common.git#925e62744f784a92c3ad8947aabfc21c93bec221",
- "integrity": "sha512-c6Q2xPI5pzU1mlrW65HKZ6mn5BJvoCgC1wO/sf4Yw+p5XYNQbolj0WYvGLtSxIw/x9G4M+H8bQo7qRd1ckt+5w==",
- "from": "expensify-common@git+ssh://git@github.com/Expensify/expensify-common.git#925e62744f784a92c3ad8947aabfc21c93bec221",
+ "version": "git+ssh://git@github.com/Expensify/expensify-common.git#50aacd53fe54ef7131e5cb9c74ee4526b3bcfe16",
+ "integrity": "sha512-wnx6WiG2NVqIs2m3M0VjL/kw2I1rgWOD06mNp6FwRGeOVC+QIL6hsdJgqf9z9NjBSuhc6THfzbcrAOSGs3kTkw==",
+ "from": "expensify-common@git+ssh://git@github.com/Expensify/expensify-common.git#50aacd53fe54ef7131e5cb9c74ee4526b3bcfe16",
"requires": {
"classnames": "2.3.1",
"clipboard": "2.0.4",
diff --git a/package.json b/package.json
index c58561daea9e..14f859fe945a 100644
--- a/package.json
+++ b/package.json
@@ -1,13 +1,13 @@
{
"name": "new.expensify",
- "version": "1.3.25-7",
+ "version": "1.3.26-1",
"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.",
"license": "MIT",
"private": true,
"scripts": {
- "postinstall": "npx patch-package && cd desktop && npm install",
+ "postinstall": "scripts/postInstall.sh",
"clean": "npx react-native clean-project-auto",
"android": "scripts/set-pusher-suffix.sh && npx react-native run-android --port=8083",
"ios": "scripts/set-pusher-suffix.sh && npx react-native run-ios --port=8082",
@@ -76,7 +76,7 @@
"babel-polyfill": "^6.26.0",
"dom-serializer": "^0.2.2",
"domhandler": "^4.3.0",
- "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#925e62744f784a92c3ad8947aabfc21c93bec221",
+ "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#50aacd53fe54ef7131e5cb9c74ee4526b3bcfe16",
"fbjs": "^3.0.2",
"html-entities": "^1.3.1",
"htmlparser2": "^7.2.0",
diff --git a/scripts/postInstall.sh b/scripts/postInstall.sh
new file mode 100755
index 000000000000..02ec3caa5175
--- /dev/null
+++ b/scripts/postInstall.sh
@@ -0,0 +1,16 @@
+#!/bin/bash
+
+# Go to project root
+ROOT_DIR=$(dirname "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)")
+cd "$ROOT_DIR" || exit 1
+
+# Run patch-package
+npx patch-package
+
+# Install node_modules in subpackages, unless we're in a CI/CD environment,
+# where the node_modules for subpackages are cached separately.
+# See `.github/actions/composite/setupNode/action.yml` for more context.
+if [[ -n ${CI+x} ]]; then
+ cd desktop || exit 1
+ npm install
+fi
diff --git a/src/CONST.js b/src/CONST.js
index 156df68f4836..bcc1941fcb39 100755
--- a/src/CONST.js
+++ b/src/CONST.js
@@ -829,6 +829,7 @@ const CONST = {
WIDTH: 320,
HEIGHT: 416,
},
+ DESKTOP_HEADER_PADDING: 12,
CATEGORY_SHORTCUT_BAR_HEIGHT: 32,
SMALL_EMOJI_PICKER_SIZE: {
WIDTH: '100%',
@@ -1107,7 +1108,7 @@ const CONST = {
CARD_NUMBER: /^[0-9]{15,16}$/,
CARD_SECURITY_CODE: /^[0-9]{3,4}$/,
CARD_EXPIRATION_DATE: /^(0[1-9]|1[0-2])([^0-9])?([0-9]{4}|([0-9]{2}))$/,
- PAYPAL_ME_USERNAME: /^[a-zA-Z0-9]+$/,
+ PAYPAL_ME_USERNAME: /^[a-zA-Z0-9]{1,20}$/,
ROOM_NAME: /^#[a-z0-9-]{1,80}$/,
// eslint-disable-next-line max-len, no-misleading-character-class
@@ -2436,9 +2437,9 @@ const CONST = {
},
SPACE_CHARACTER_WIDTH: 4,
- // This ID is used in SelectionScraper.js to query the DOM for UnreadActionIndicator's
- // div and then remove it from copied contents in the getHTMLOfSelection() method.
- UNREAD_ACTION_INDICATOR_ID: 'no-copy-area-unread-action-indicator',
+ // The attribute used in the SelectionScraper.js helper to query all the DOM elements
+ // that should be removed from the copied contents in the getHTMLOfSelection() method
+ SELECTION_SCRAPER_HIDDEN_ELEMENT: 'selection-scrapper-hidden-element',
MODERATION: {
MODERATOR_DECISION_PENDING: 'pending',
MODERATOR_DECISION_PENDING_HIDE: 'pendingHide',
diff --git a/src/ONYXKEYS.js b/src/ONYXKEYS.js
index 2b4e81205590..082a1c7efbd5 100755
--- a/src/ONYXKEYS.js
+++ b/src/ONYXKEYS.js
@@ -45,6 +45,9 @@ export default {
// Contains all the personalDetails the user has access to
PERSONAL_DETAILS: 'personalDetails',
+ // Contains all the personalDetails the user has access to, keyed by accountID
+ PERSONAL_DETAILS_LIST: 'personalDetailsList',
+
// Contains all the private personal details of the user
PRIVATE_PERSONAL_DETAILS: 'private_personalDetails',
diff --git a/src/ROUTES.js b/src/ROUTES.js
index bd0ad304c2f5..966c3d0c5a1a 100644
--- a/src/ROUTES.js
+++ b/src/ROUTES.js
@@ -111,13 +111,14 @@ export default {
FLAG_COMMENT: `flag/:reportID/:reportActionID`,
getFlagCommentRoute: (reportID, reportActionID) => `flag/${reportID}/${reportActionID}`,
SEARCH: 'search',
- SET_PASSWORD_WITH_VALIDATE_CODE: 'setpassword/:accountID/:validateCode',
DETAILS: 'details',
getDetailsRoute: (login) => `details?login=${encodeURIComponent(login)}`,
+ PROFILE: 'a/:accountID',
+ getProfileRoute: (accountID) => `a/${accountID}`,
REPORT_PARTICIPANTS: 'r/:reportID/participants',
getReportParticipantsRoute: (reportID) => `r/${reportID}/participants`,
- REPORT_PARTICIPANT: 'r/:reportID/participants/details',
- getReportParticipantRoute: (reportID, login) => `r/${reportID}/participants/details?login=${encodeURIComponent(login)}`,
+ REPORT_PARTICIPANT: 'r/:reportID/participants/a/:accountID',
+ getReportParticipantRoute: (reportID, accountID) => `r/${reportID}/participants/a/${accountID}`,
REPORT_WITH_ID_DETAILS: 'r/:reportID/details',
getReportDetailsRoute: (reportID) => `r/${reportID}/details`,
REPORT_SETTINGS: 'r/:reportID/settings',
diff --git a/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.js b/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.js
index 21c1ea618841..1f95ead89a4c 100644
--- a/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.js
+++ b/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.js
@@ -1,6 +1,6 @@
import _ from 'underscore';
import React from 'react';
-import {View, StyleSheet} from 'react-native';
+import {StyleSheet} from 'react-native';
import PropTypes from 'prop-types';
import lodashGet from 'lodash/get';
import Str from 'expensify-common/lib/str';
@@ -11,6 +11,7 @@ import * as ContextMenuActions from '../../pages/home/report/ContextMenu/Context
import Tooltip from '../Tooltip';
import * as DeviceCapabilities from '../../libs/DeviceCapabilities';
import styles from '../../styles/styles';
+import * as StyleUtils from '../../styles/StyleUtils';
import withWindowDimensions, {windowDimensionsPropTypes} from '../withWindowDimensions';
import {propTypes as anchorForCommentsOnlyPropTypes, defaultProps as anchorForCommentsOnlyDefaultProps} from './anchorForCommentsOnlyPropTypes';
@@ -51,41 +52,40 @@ const BaseAnchorForCommentsOnly = (props) => {
const isEmail = Str.isValidEmailMarkdown(props.href.replace(/mailto:/i, ''));
return (
-
- {
- ReportActionContextMenu.showContextMenu(
- isEmail ? ContextMenuActions.CONTEXT_MENU_TYPES.EMAIL : ContextMenuActions.CONTEXT_MENU_TYPES.LINK,
- event,
- props.href,
- lodashGet(linkRef, 'current'),
- );
- }}
- onPress={linkProps.onPress}
- onPressIn={props.onPressIn}
- onPressOut={props.onPressOut}
- >
-
- (linkRef = el)}
- style={StyleSheet.flatten([props.style, defaultTextStyle])}
- accessibilityRole="link"
- hrefAttrs={{
- rel: props.rel,
- target: isEmail ? '_self' : props.target,
- }}
- href={linkProps.href}
- // Add testID so it gets selected as an anchor tag by SelectionScraper
- testID="a"
- // eslint-disable-next-line react/jsx-props-no-spreading
- {...rest}
- >
- {props.children}
-
-
-
-
+ {
+ ReportActionContextMenu.showContextMenu(
+ isEmail ? ContextMenuActions.CONTEXT_MENU_TYPES.EMAIL : ContextMenuActions.CONTEXT_MENU_TYPES.LINK,
+ event,
+ props.href,
+ lodashGet(linkRef, 'current'),
+ );
+ }}
+ onPress={linkProps.onPress}
+ onPressIn={props.onPressIn}
+ onPressOut={props.onPressOut}
+ >
+
+ (linkRef = el)}
+ style={StyleSheet.flatten([props.style, defaultTextStyle])}
+ accessibilityRole="link"
+ hrefAttrs={{
+ rel: props.rel,
+ target: isEmail ? '_self' : props.target,
+ }}
+ href={linkProps.href}
+ // Add testID so it gets selected as an anchor tag by SelectionScraper
+ testID="a"
+ // eslint-disable-next-line react/jsx-props-no-spreading
+ {...rest}
+ >
+ {props.children}
+
+
+
);
};
diff --git a/src/components/AnchorForCommentsOnly/index.js b/src/components/AnchorForCommentsOnly/index.js
index fd670809e2cf..1ea73d5a648d 100644
--- a/src/components/AnchorForCommentsOnly/index.js
+++ b/src/components/AnchorForCommentsOnly/index.js
@@ -3,7 +3,6 @@ import * as anchorForCommentsOnlyPropTypes from './anchorForCommentsOnlyPropType
import BaseAnchorForCommentsOnly from './BaseAnchorForCommentsOnly';
import * as DeviceCapabilities from '../../libs/DeviceCapabilities';
import ControlSelection from '../../libs/ControlSelection';
-import styles from '../../styles/styles';
const AnchorForCommentsOnly = (props) => (
(
{...props}
onPressIn={() => DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()}
onPressOut={() => ControlSelection.unblock()}
- // HTML renderer root view display is flex. Using flex will force all child elements
- // to be block elements even when they have display inline added to them.
- // This will affect elements like which are inline by default.
- // Setting display block to the container view will solve that.
- containerStyles={[styles.dBlock]}
/>
);
diff --git a/src/components/AttachmentCarousel/CarouselActions/index.native.js b/src/components/AttachmentCarousel/CarouselActions.js
similarity index 69%
rename from src/components/AttachmentCarousel/CarouselActions/index.native.js
rename to src/components/AttachmentCarousel/CarouselActions.js
index d12cd6bfbb60..3946a613ee16 100644
--- a/src/components/AttachmentCarousel/CarouselActions/index.native.js
+++ b/src/components/AttachmentCarousel/CarouselActions.js
@@ -1,7 +1,8 @@
import {useEffect} from 'react';
+import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
-import KeyboardShortcut from '../../../libs/KeyboardShortcut';
-import CONST from '../../../CONST';
+import KeyboardShortcut from '../../libs/KeyboardShortcut';
+import CONST from '../../CONST';
const propTypes = {
/** Callback to cycle through attachments */
@@ -13,7 +14,12 @@ const Carousel = (props) => {
const shortcutLeftConfig = CONST.KEYBOARD_SHORTCUTS.ARROW_LEFT;
const unsubscribeLeftKey = KeyboardShortcut.subscribe(
shortcutLeftConfig.shortcutKey,
- () => {
+ (e) => {
+ if (lodashGet(e, 'target.blur')) {
+ // prevents focus from highlighting around the modal
+ e.target.blur();
+ }
+
props.onCycleThroughAttachments(-1);
},
shortcutLeftConfig.descriptionKey,
@@ -23,7 +29,12 @@ const Carousel = (props) => {
const shortcutRightConfig = CONST.KEYBOARD_SHORTCUTS.ARROW_RIGHT;
const unsubscribeRightKey = KeyboardShortcut.subscribe(
shortcutRightConfig.shortcutKey,
- () => {
+ (e) => {
+ if (lodashGet(e, 'target.blur')) {
+ // prevents focus from highlighting around the modal
+ e.target.blur();
+ }
+
props.onCycleThroughAttachments(1);
},
shortcutRightConfig.descriptionKey,
diff --git a/src/components/AttachmentCarousel/CarouselActions/index.js b/src/components/AttachmentCarousel/CarouselActions/index.js
deleted file mode 100644
index 4ec551daa252..000000000000
--- a/src/components/AttachmentCarousel/CarouselActions/index.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import {useCallback, useEffect} from 'react';
-import PropTypes from 'prop-types';
-
-const propTypes = {
- /** Callback to cycle through attachments */
- onCycleThroughAttachments: PropTypes.func.isRequired,
-};
-
-const Carousel = (props) => {
- /**
- * Listens for keyboard shortcuts and applies the action
- *
- * @param {Object} e
- */
- const handleKeyPress = useCallback((e) => {
- // prevents focus from highlighting around the modal
- e.target.blur();
-
- if (e.key === 'ArrowLeft') {
- props.onCycleThroughAttachments(-1);
- }
- if (e.key === 'ArrowRight') {
- props.onCycleThroughAttachments(1);
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- useEffect(() => {
- document.addEventListener('keydown', handleKeyPress);
-
- return () => {
- document.removeEventListener('keydown', handleKeyPress);
- };
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- return null;
-};
-
-Carousel.propTypes = propTypes;
-
-export default Carousel;
diff --git a/src/components/AttachmentCarousel/index.js b/src/components/AttachmentCarousel/index.js
index 778044586172..77ab66400ae2 100644
--- a/src/components/AttachmentCarousel/index.js
+++ b/src/components/AttachmentCarousel/index.js
@@ -20,6 +20,7 @@ import Tooltip from '../Tooltip';
import withLocalize, {withLocalizePropTypes} from '../withLocalize';
import compose from '../../libs/compose';
import withWindowDimensions from '../withWindowDimensions';
+import reportPropTypes from '../../pages/reportPropTypes';
const propTypes = {
/** source is used to determine the starting index in the array of attachments */
@@ -31,6 +32,9 @@ const propTypes = {
/** Object of report actions for this report */
reportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)),
+ /** The report currently being looked at */
+ report: reportPropTypes.isRequired,
+
...withLocalizePropTypes,
};
@@ -148,7 +152,7 @@ class AttachmentCarousel extends React.Component {
* @returns {{page: Number, attachments: Array, shouldShowArrow: Boolean, containerWidth: Number, isZoomed: Boolean}}
*/
createInitialState() {
- const actions = ReportActionsUtils.getSortedReportActions(_.values(this.props.reportActions));
+ const actions = [ReportActionsUtils.getParentReportAction(this.props.report), ...ReportActionsUtils.getSortedReportActions(_.values(this.props.reportActions))];
const attachments = [];
const htmlParser = new HtmlParser({
@@ -359,7 +363,7 @@ AttachmentCarousel.defaultProps = defaultProps;
export default compose(
withOnyx({
reportActions: {
- key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
+ key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`,
canEvict: false,
},
}),
diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js
index b29c910e83e4..393a0085176c 100755
--- a/src/components/AttachmentModal.js
+++ b/src/components/AttachmentModal.js
@@ -23,6 +23,7 @@ import ConfirmModal from './ConfirmModal';
import HeaderGap from './HeaderGap';
import SafeAreaConsumer from './SafeAreaConsumer';
import addEncryptedAuthTokenToURL from '../libs/addEncryptedAuthTokenToURL';
+import reportPropTypes from '../pages/reportPropTypes';
/**
* Modal render prop component that exposes modal launching triggers that can be used
@@ -57,8 +58,8 @@ const propTypes = {
/** Title shown in the header of the modal */
headerTitle: PropTypes.string,
- /** The ID of the report that has this attachment */
- reportID: PropTypes.string,
+ /** The report that has this attachment */
+ report: reportPropTypes,
...withLocalizePropTypes,
@@ -72,7 +73,7 @@ const defaultProps = {
isAuthTokenRequired: false,
allowDownload: false,
headerTitle: null,
- reportID: '',
+ report: {},
onModalShow: () => {},
onModalHide: () => {},
};
@@ -287,9 +288,9 @@ class AttachmentModal extends PureComponent {
onCloseButtonPress={() => this.setState({isModalOpen: false})}
/>
- {this.props.reportID ? (
+ {!_.isEmpty(this.props.report) ? (
{
* @returns {JSX.Element}
*/
const renderSuggestionMenuItem = ({item, index}) => (
- StyleUtils.getAutoCompleteSuggestionItemStyle(props.highlightedSuggestionIndex, CONST.AUTO_COMPLETE_SUGGESTER.ITEM_HEIGHT, hovered, index)}
+ hoverDimmingValue={1}
onMouseDown={(e) => e.preventDefault()}
onPress={() => props.onSelect(index)}
onLongPress={() => {}}
+ accessibilityLabel={props.accessibilityLabelExtractor(item, index)}
>
{props.renderSuggestionMenuItem(item, index)}
-
+
);
const innerHeight = CONST.AUTO_COMPLETE_SUGGESTER.ITEM_HEIGHT * props.suggestions.length;
diff --git a/src/components/AutoCompleteSuggestions/autoCompleteSuggestionsPropTypes.js b/src/components/AutoCompleteSuggestions/autoCompleteSuggestionsPropTypes.js
index eed9368e534d..6ff330d839c6 100644
--- a/src/components/AutoCompleteSuggestions/autoCompleteSuggestionsPropTypes.js
+++ b/src/components/AutoCompleteSuggestions/autoCompleteSuggestionsPropTypes.js
@@ -24,6 +24,9 @@ const propTypes = {
/** Show that we should include ReportRecipientLocalTime view height */
shouldIncludeReportRecipientLocalTimeHeight: PropTypes.bool.isRequired,
+
+ /** create accessibility label for each item */
+ accessibilityLabelExtractor: PropTypes.func.isRequired,
};
const defaultProps = {};
diff --git a/src/components/AutoEmailLink.js b/src/components/AutoEmailLink.js
new file mode 100644
index 000000000000..f99c8ea76f3c
--- /dev/null
+++ b/src/components/AutoEmailLink.js
@@ -0,0 +1,54 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import _ from 'underscore';
+import {CONST} from 'expensify-common/lib/CONST';
+import Text from './Text';
+import TextLink from './TextLink';
+import styles from '../styles/styles';
+
+const propTypes = {
+ text: PropTypes.string.isRequired,
+ style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]),
+};
+
+const defaultProps = {
+ style: [],
+};
+
+/*
+ * This is a "utility component", that does this:
+ * - Checks if a text contains any email. If it does, render it as a mailto: link
+ * - Else just render it inside `Text` component
+ */
+
+const AutoEmailLink = (props) => (
+
+ {_.map(props.text.split(CONST.REG_EXP.EXTRACT_EMAIL), (str, index) => {
+ if (CONST.REG_EXP.EMAIL.test(str)) {
+ return (
+
+ {str}
+
+ );
+ }
+
+ return (
+
+ {str}
+
+ );
+ })}
+
+);
+
+AutoEmailLink.displayName = 'AutoEmailLink';
+AutoEmailLink.propTypes = propTypes;
+AutoEmailLink.defaultProps = defaultProps;
+export default AutoEmailLink;
diff --git a/src/components/AvatarWithImagePicker.js b/src/components/AvatarWithImagePicker.js
index 843b131571ac..45e75aaeec57 100644
--- a/src/components/AvatarWithImagePicker.js
+++ b/src/components/AvatarWithImagePicker.js
@@ -12,6 +12,7 @@ import themeColors from '../styles/themes/default';
import AttachmentPicker from './AttachmentPicker';
import ConfirmModal from './ConfirmModal';
import AvatarCropModal from './AvatarCropModal/AvatarCropModal';
+import OfflineWithFeedback from './OfflineWithFeedback';
import withLocalize, {withLocalizePropTypes} from './withLocalize';
import variables from '../styles/variables';
import CONST from '../CONST';
@@ -64,6 +65,19 @@ const propTypes = {
/** Image crop vector mask */
editorMaskImage: PropTypes.func,
+ /** Additional style object for the error row */
+ errorRowStyles: stylePropTypes,
+
+ /** A function to run when the X button next to the error is clicked */
+ onErrorClose: PropTypes.func,
+
+ /** The type of action that's pending */
+ pendingAction: PropTypes.oneOf(['add', 'update', 'delete']),
+
+ /** The errors to display */
+ // eslint-disable-next-line react/forbid-prop-types
+ errors: PropTypes.object,
+
...withLocalizePropTypes,
};
@@ -79,6 +93,10 @@ const defaultProps = {
fallbackIcon: Expensicons.FallbackAvatar,
type: CONST.ICON_TYPE_AVATAR,
editorMaskImage: undefined,
+ errorRowStyles: [],
+ onErrorClose: () => {},
+ pendingAction: null,
+ errors: null,
};
class AvatarWithImagePicker extends React.Component {
@@ -263,22 +281,30 @@ class AvatarWithImagePicker extends React.Component {
accessibilityLabel={this.props.translate('avatarWithImagePicker.editImage')}
>
-
-
- {this.props.source ? (
-
- ) : (
-
- )}
-
-
+
+
+
+ {this.props.source ? (
+
+ ) : (
+
+ )}
+
+
+
+
{({openPicker}) => (
<>
diff --git a/src/components/BlockingViews/BlockingView.js b/src/components/BlockingViews/BlockingView.js
index d3a552d83824..0ac4ec4a9e13 100644
--- a/src/components/BlockingViews/BlockingView.js
+++ b/src/components/BlockingViews/BlockingView.js
@@ -8,6 +8,7 @@ import Text from '../Text';
import themeColors from '../../styles/themes/default';
import TextLink from '../TextLink';
import Navigation from '../../libs/Navigation/Navigation';
+import AutoEmailLink from '../AutoEmailLink';
const propTypes = {
/** Expensicon for the page */
@@ -20,7 +21,7 @@ const propTypes = {
title: PropTypes.string.isRequired,
/** Subtitle message below the title */
- subtitle: PropTypes.string.isRequired,
+ subtitle: PropTypes.string,
/** Link message below the subtitle */
link: PropTypes.string,
@@ -40,6 +41,7 @@ const propTypes = {
const defaultProps = {
iconColor: themeColors.offline,
+ subtitle: '',
shouldShowLink: false,
link: 'notFound.goBackHome',
iconWidth: variables.iconSizeSuperLarge,
@@ -56,7 +58,10 @@ const BlockingView = (props) => (
height={props.iconHeight}
/>
{props.title}
- {props.subtitle}
+
{props.shouldShowLink ? (
{
StatusBar.setBarStyle('light-content', true);
- }
-
- render() {
- return ;
- }
+ StatusBar.setBackgroundColor(themeColors.appBG);
+ }, []);
+ return ;
}
+
+CustomStatusBar.displayName = 'CustomStatusBar';
+
+export default CustomStatusBar;
diff --git a/src/components/CustomStatusBar/index.web.js b/src/components/CustomStatusBar/index.web.js
deleted file mode 100644
index 4f9b8d2c044d..000000000000
--- a/src/components/CustomStatusBar/index.web.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import React from 'react';
-import {StatusBar} from 'react-native';
-import themeColors from '../../styles/themes/default';
-
-export default class CustomStatusBar extends React.Component {
- componentDidMount() {
- StatusBar.setBarStyle('light-content', true);
-
- // For mobile web browsers, match the default status bar color to the app's background color
- const element = document.querySelector('meta[name=theme-color]');
- if (element) {
- element.content = themeColors.appBG;
- }
- }
-
- render() {
- return ;
- }
-}
diff --git a/src/components/EmojiSuggestions.js b/src/components/EmojiSuggestions.js
index c098cc988b81..2af9dfc99368 100644
--- a/src/components/EmojiSuggestions.js
+++ b/src/components/EmojiSuggestions.js
@@ -94,6 +94,7 @@ const EmojiSuggestions = (props) => {
onSelect={props.onSelect}
isSuggestionPickerLarge={props.isEmojiPickerLarge}
shouldIncludeReportRecipientLocalTimeHeight={props.shouldIncludeReportRecipientLocalTimeHeight}
+ accessibilityLabelExtractor={keyExtractor}
/>
);
};
diff --git a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js
index 3cc30322b5d4..dd8a06927a07 100755
--- a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js
+++ b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js
@@ -61,7 +61,7 @@ const BaseHTMLEngineProvider = (props) => {
// We need to pass multiple system-specific fonts for emojis but
// we can't apply multiple fonts at once so we need to pass fallback fonts.
- const fallbackFonts = {'ExpensifyNeue-Regular': fontFamily.EMOJI_TEXT_FONT};
+ const fallbackFonts = {'ExpensifyNeue-Regular': fontFamily.EXP_NEUE};
return (
{
{({anchor, report, action, checkIfContextMenuActive}) => (
{({show}) => (
showContextMenuForReport(event, anchor, report.reportID, action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report))}
>
diff --git a/src/components/HeaderWithBackButton.js b/src/components/HeaderWithBackButton.js
index dab5f024a9e6..e82251542987 100755
--- a/src/components/HeaderWithBackButton.js
+++ b/src/components/HeaderWithBackButton.js
@@ -1,6 +1,7 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {View, Keyboard} from 'react-native';
+import _ from 'underscore';
import styles from '../styles/styles';
import Header from './Header';
import Navigation from '../libs/Navigation/Navigation';
@@ -18,6 +19,7 @@ import withKeyboardState, {keyboardStatePropTypes} from './withKeyboardState';
import AvatarWithDisplayName from './AvatarWithDisplayName';
import iouReportPropTypes from '../pages/iouReportPropTypes';
import participantPropTypes from './participantPropTypes';
+import CONST from '../CONST';
import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback';
import PinButton from './PinButton';
@@ -66,6 +68,12 @@ const propTypes = {
left: PropTypes.number,
}),
+ /** The anchor alignment of the menu */
+ threeDotsAnchorAlignment: PropTypes.shape({
+ horizontal: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL)),
+ vertical: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_VERTICAL)),
+ }),
+
/** Whether we should show a close button */
shouldShowCloseButton: PropTypes.bool,
@@ -135,6 +143,10 @@ const defaultProps = {
vertical: 0,
horizontal: 0,
},
+ threeDotsAnchorAlignment: {
+ horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT,
+ vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP,
+ },
};
class HeaderWithBackButton extends Component {
@@ -233,6 +245,7 @@ class HeaderWithBackButton extends Component {
menuItems={this.props.threeDotsMenuItems}
onIconPress={this.props.onThreeDotsButtonPress}
anchorPosition={this.props.threeDotsAnchorPosition}
+ anchorAlignment={this.props.threeDotsAnchorAlignment}
/>
)}
diff --git a/src/components/KeyboardSpacer/index.android.js b/src/components/KeyboardSpacer/index.android.js
index 8c549e056fe7..217fdbd9b18c 100644
--- a/src/components/KeyboardSpacer/index.android.js
+++ b/src/components/KeyboardSpacer/index.android.js
@@ -3,7 +3,7 @@
* view up with the keyboard allowing the user to see what they are typing.
*/
import React from 'react';
-import {StatusBar} from 'react-native';
+import StatusBar from '../../libs/StatusBar';
import BaseKeyboardSpacer from './BaseKeyboardSpacer';
import withWindowDimensions, {windowDimensionsPropTypes} from '../withWindowDimensions';
diff --git a/src/components/LHNOptionsList/OptionRowLHN.js b/src/components/LHNOptionsList/OptionRowLHN.js
index 67a26a519801..75a6949c573f 100644
--- a/src/components/LHNOptionsList/OptionRowLHN.js
+++ b/src/components/LHNOptionsList/OptionRowLHN.js
@@ -22,6 +22,7 @@ import OfflineWithFeedback from '../OfflineWithFeedback';
import PressableWithSecondaryInteraction from '../PressableWithSecondaryInteraction';
import * as ReportActionContextMenu from '../../pages/home/report/ContextMenu/ReportActionContextMenu';
import * as ContextMenuActions from '../../pages/home/report/ContextMenu/ContextMenuActions';
+import * as OptionsListUtils from '../../libs/OptionsListUtils';
const propTypes = {
/** Style for hovered state */
@@ -55,12 +56,6 @@ const defaultProps = {
const OptionRowLHN = (props) => {
const optionItem = SidebarUtils.getOptionData(props.reportID);
- const isPinned = _.get(optionItem, 'isPinned', false);
- const isUnread = _.get(optionItem, 'isUnread', false);
-
- React.useEffect(() => {
- ReportActionContextMenu.hideContextMenu(false);
- }, [isPinned, isUnread]);
if (!optionItem) {
return null;
@@ -169,7 +164,7 @@ const OptionRowLHN = (props) => {
props.isFocused ? StyleUtils.getBackgroundAndBorderStyle(focusedBackgroundColor) : undefined,
hovered && !props.isFocused ? StyleUtils.getBackgroundAndBorderStyle(hoveredBackgroundColor) : undefined,
]}
- shouldShowTooltip={!optionItem.isChatRoom && !optionItem.isArchivedRoom}
+ shouldShowTooltip={OptionsListUtils.shouldOptionShowTooltip(optionItem)}
/>
))}
diff --git a/src/components/MentionSuggestions.js b/src/components/MentionSuggestions.js
index 4d03a0b2d584..48e1520d736a 100644
--- a/src/components/MentionSuggestions.js
+++ b/src/components/MentionSuggestions.js
@@ -117,6 +117,7 @@ const MentionSuggestions = (props) => {
onSelect={props.onSelect}
isSuggestionPickerLarge={props.isMentionPickerLarge}
shouldIncludeReportRecipientLocalTimeHeight={props.shouldIncludeReportRecipientLocalTimeHeight}
+ accessibilityLabelExtractor={keyExtractor}
/>
);
};
diff --git a/src/components/MenuItem.js b/src/components/MenuItem.js
index e672c68b6133..77b4d559701d 100644
--- a/src/components/MenuItem.js
+++ b/src/components/MenuItem.js
@@ -76,7 +76,7 @@ const MenuItem = (props) => {
[
styles.flexShrink1,
styles.popoverMenuText,
- props.icon ? styles.ml3 : undefined,
+ props.icon && !_.isArray(props.icon) ? styles.ml3 : undefined,
props.shouldShowBasicTitle ? undefined : styles.textStrong,
props.interactive && props.disabled ? {...styles.disabledText, ...styles.userSelectNone} : undefined,
styles.pre,
@@ -88,7 +88,7 @@ const MenuItem = (props) => {
);
const descriptionTextStyle = StyleUtils.combineStyles([
styles.textLabelSupporting,
- props.icon ? styles.ml3 : undefined,
+ props.icon && !_.isArray(props.icon) ? styles.ml3 : undefined,
styles.lineHeightNormal,
props.title ? descriptionVerticalMargin : undefined,
props.descriptionTextStyle,
@@ -125,90 +125,110 @@ const MenuItem = (props) => {
>
{({hovered, pressed}) => (
<>
-
- {Boolean(props.icon) && (
-
- {props.iconType === CONST.ICON_TYPE_ICON && (
-
- )}
- {props.iconType === CONST.ICON_TYPE_WORKSPACE && (
-
- )}
- {props.iconType === CONST.ICON_TYPE_AVATAR && (
-
- )}
+
+ {Boolean(props.label) && (
+
+ {props.label}
)}
-
- {Boolean(props.description) && props.shouldShowDescriptionOnTop && (
-
- {props.description}
-
+
+ {Boolean(props.icon) && _.isArray(props.icon) && (
+
)}
-
- {Boolean(props.title) && (
+ {Boolean(props.icon) && !_.isArray(props.icon) && (
+
+ {props.iconType === CONST.ICON_TYPE_ICON && (
+
+ )}
+ {props.iconType === CONST.ICON_TYPE_WORKSPACE && (
+
+ )}
+ {props.iconType === CONST.ICON_TYPE_AVATAR && (
+
+ )}
+
+ )}
+
+ {Boolean(props.description) && props.shouldShowDescriptionOnTop && (
- {convertToLTR(props.title)}
+ {props.description}
)}
- {Boolean(props.shouldShowTitleIcon) && (
-
+
+ {Boolean(props.title) && (
+
+ {convertToLTR(props.title)}
+
+ )}
+ {Boolean(props.shouldShowTitleIcon) && (
+
+
+
+ )}
+
+ {Boolean(props.description) && !props.shouldShowDescriptionOnTop && (
+
+ {props.description}
+
+ )}
+ {Boolean(props.furtherDetails) && (
+
+
+ {props.furtherDetails}
+
)}
- {Boolean(props.description) && !props.shouldShowDescriptionOnTop && (
-
- {props.description}
-
- )}
- {Boolean(props.furtherDetails) && (
-
-
-
- {props.furtherDetails}
-
-
- )}
diff --git a/src/components/Modal/index.web.js b/src/components/Modal/index.web.js
index a4c396e5efdb..00a208db4f85 100644
--- a/src/components/Modal/index.web.js
+++ b/src/components/Modal/index.web.js
@@ -4,6 +4,7 @@ import BaseModal from './BaseModal';
import {propTypes, defaultProps} from './modalPropTypes';
import * as StyleUtils from '../../styles/StyleUtils';
import themeColors from '../../styles/themes/default';
+import StatusBar from '../../libs/StatusBar';
const Modal = (props) => {
const setStatusBarColor = (color = themeColors.appBG) => {
@@ -11,11 +12,7 @@ const Modal = (props) => {
return;
}
- // Change the color of the status bar to align with the modal's backdrop (refer to https://github.com/Expensify/App/issues/12156).
- const element = document.querySelector('meta[name=theme-color]');
- if (element) {
- element.content = color;
- }
+ StatusBar.setBackgroundColor(color);
};
const hideModal = () => {
diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js
index 3fca7f714be7..f8006788bbac 100755
--- a/src/components/MoneyRequestConfirmationList.js
+++ b/src/components/MoneyRequestConfirmationList.js
@@ -166,7 +166,6 @@ function MoneyRequestConfirmationList(props) {
data: [formattedPayeePersonalDetails],
shouldShow: true,
indexOffset: 0,
- isDisabled: true,
},
{
title: translate('moneyRequestConfirmationList.whoWasThere'),
@@ -296,7 +295,6 @@ function MoneyRequestConfirmationList(props) {
selectedOptions={selectedOptions}
canSelectMultipleOptions={canModifyParticipants}
disableArrowKeysActions={!canModifyParticipants}
- isDisabled={!canModifyParticipants}
boldStyle
shouldTextInputAppearBelowOptions
shouldShowTextInput={false}
diff --git a/src/components/Onfido/index.css b/src/components/Onfido/index.css
index ebc2089d9cd5..07cc81e1306c 100644
--- a/src/components/Onfido/index.css
+++ b/src/components/Onfido/index.css
@@ -10,6 +10,12 @@
position: relative;
}
+/* This is needed to dsiable the blue outline that shows up once the payment page is opened through keyboard */
+#onfido-mount [tabindex="-1"]:focus-visible,
+#onfido-mount [tabindex="-1"]:focus[data-focusvisible-polyfill] {
+ box-shadow: none !important;
+}
+
@media only screen and (max-width: 600px) {
.onfido-sdk-ui-Modal-inner {
/* This keeps the bottom of the Onfido window from being cut off on mobile web because the height was being
diff --git a/src/components/OnyxProvider.js b/src/components/OnyxProvider.js
index 6cee7e5b7a62..76cda71da2b2 100644
--- a/src/components/OnyxProvider.js
+++ b/src/components/OnyxProvider.js
@@ -11,7 +11,7 @@ const [withPersonalDetails, PersonalDetailsProvider] = createOnyxContext(ONYXKEY
const [withCurrentDate, CurrentDateProvider] = createOnyxContext(ONYXKEYS.CURRENT_DATE);
const [withReportActionsDrafts, ReportActionsDraftsProvider] = createOnyxContext(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS);
const [withBlockedFromConcierge, BlockedFromConciergeProvider] = createOnyxContext(ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE);
-const [withBetas, BetasProvider] = createOnyxContext(ONYXKEYS.BETAS);
+const [withBetas, BetasProvider, BetasContext] = createOnyxContext(ONYXKEYS.BETAS);
const propTypes = {
/** Rendered child component */
@@ -29,4 +29,4 @@ OnyxProvider.propTypes = propTypes;
export default OnyxProvider;
-export {withNetwork, withPersonalDetails, withReportActionsDrafts, withCurrentDate, withBlockedFromConcierge, withBetas, NetworkContext};
+export {withNetwork, withPersonalDetails, withReportActionsDrafts, withCurrentDate, withBlockedFromConcierge, withBetas, NetworkContext, BetasContext};
diff --git a/src/components/OptionRow.js b/src/components/OptionRow.js
index 453e28a6271e..28ee50ccf8a1 100644
--- a/src/components/OptionRow.js
+++ b/src/components/OptionRow.js
@@ -20,6 +20,7 @@ import OfflineWithFeedback from './OfflineWithFeedback';
import CONST from '../CONST';
import * as ReportUtils from '../libs/ReportUtils';
import PressableWithFeedback from './Pressable/PressableWithFeedback';
+import * as OptionsListUtils from '../libs/OptionsListUtils';
const propTypes = {
/** Style for hovered state */
@@ -67,7 +68,7 @@ const defaultProps = {
isSelected: false,
boldStyle: false,
showTitleTooltip: false,
- onSelectRow: () => {},
+ onSelectRow: undefined,
isDisabled: false,
optionIsFocused: false,
style: null,
@@ -150,6 +151,10 @@ class OptionRow extends Component {
(pressableRef = el)}
onPress={(e) => {
+ if (!this.props.onSelectRow) {
+ return;
+ }
+
this.setState({isDisabled: true});
if (e) {
e.preventDefault();
@@ -171,6 +176,7 @@ class OptionRow extends Component {
this.props.shouldDisableRowInnerPadding ? null : styles.sidebarLinkInner,
this.props.optionIsFocused ? styles.sidebarLinkActive : null,
this.props.shouldHaveOptionSeparator && styles.borderTop,
+ !this.props.onSelectRow && !this.props.isDisabled ? styles.cursorDefault : null,
]}
accessibilityLabel={this.props.option.text}
accessibilityRole="button"
@@ -198,7 +204,7 @@ class OptionRow extends Component {
this.props.optionIsFocused ? StyleUtils.getBackgroundAndBorderStyle(focusedBackgroundColor) : undefined,
hovered && !this.props.optionIsFocused ? StyleUtils.getBackgroundAndBorderStyle(hoveredBackgroundColor) : undefined,
]}
- shouldShowTooltip={this.props.showTitleTooltip && !this.props.option.isChatRoom && !this.props.option.isArchivedRoom}
+ shouldShowTooltip={this.props.showTitleTooltip && OptionsListUtils.shouldOptionShowTooltip(this.props.option)}
/>
))}
diff --git a/src/components/OptionsList/optionsListPropTypes.js b/src/components/OptionsList/optionsListPropTypes.js
index 915d84e058c3..e6e507f4da09 100644
--- a/src/components/OptionsList/optionsListPropTypes.js
+++ b/src/components/OptionsList/optionsListPropTypes.js
@@ -88,7 +88,7 @@ const defaultProps = {
hideSectionHeaders: false,
disableFocusOptions: false,
boldStyle: false,
- onSelectRow: () => {},
+ onSelectRow: undefined,
headerMessage: '',
innerRef: null,
showTitleTooltip: false,
diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js
index df15ad71febc..337674012106 100755
--- a/src/components/OptionsSelector/BaseOptionsSelector.js
+++ b/src/components/OptionsSelector/BaseOptionsSelector.js
@@ -305,7 +305,7 @@ class BaseOptionsSelector extends Component {
(this.list = el)}
optionHoveredStyle={this.props.optionHoveredStyle}
- onSelectRow={this.selectRow}
+ onSelectRow={this.props.onSelectRow ? this.selectRow : undefined}
sections={this.props.sections}
focusedIndex={this.state.focusedIndex}
selectedOptions={this.props.selectedOptions}
diff --git a/src/components/OptionsSelector/optionsSelectorPropTypes.js b/src/components/OptionsSelector/optionsSelectorPropTypes.js
index bc7436e3dba8..02b807bf66c1 100644
--- a/src/components/OptionsSelector/optionsSelectorPropTypes.js
+++ b/src/components/OptionsSelector/optionsSelectorPropTypes.js
@@ -109,7 +109,7 @@ const propTypes = {
};
const defaultProps = {
- onSelectRow: () => {},
+ onSelectRow: undefined,
textInputLabel: '',
placeholderText: '',
keyboardType: 'default',
diff --git a/src/components/PopoverMenu/index.js b/src/components/PopoverMenu/index.js
index 17982c0d5684..330a6a8b61f5 100644
--- a/src/components/PopoverMenu/index.js
+++ b/src/components/PopoverMenu/index.js
@@ -64,7 +64,7 @@ const PopoverMenu = (props) => {
return (
{
diff --git a/src/components/Pressable/GenericPressable/BaseGenericPressable.js b/src/components/Pressable/GenericPressable/BaseGenericPressable.js
index 6cdb83bbb81a..0b15378de8bc 100644
--- a/src/components/Pressable/GenericPressable/BaseGenericPressable.js
+++ b/src/components/Pressable/GenericPressable/BaseGenericPressable.js
@@ -135,7 +135,7 @@ const GenericPressable = forwardRef((props, ref) => {
return (
(this.pressableRef = el)}
// eslint-disable-next-line react/jsx-props-no-spreading
{...defaultPressableProps}
+ style={(state) => [StyleUtils.parseStyleFromFunction(this.props.style, state), ...[this.props.inline && styles.dInline]]}
>
{this.props.children}
diff --git a/src/components/QRShare/index.js b/src/components/QRShare/index.js
index 0a942c951d9d..5dc2c53dc8d3 100644
--- a/src/components/QRShare/index.js
+++ b/src/components/QRShare/index.js
@@ -1,5 +1,6 @@
import React, {Component} from 'react';
import {View} from 'react-native';
+import _ from 'underscore';
import withLocalize, {withLocalizePropTypes} from '../withLocalize';
import defaultTheme from '../../styles/themes/default';
import styles from '../../styles/styles';
@@ -73,7 +74,7 @@ class QRShare extends Component {
{this.props.title}
- {this.props.subtitle && (
+ {!_.isEmpty(this.props.subtitle) && (
{
const isChatRoom = ReportUtils.isChatRoom(props.report);
const isDefault = !(isChatRoom || isPolicyExpenseChat);
const participants = lodashGet(props.report, 'participants', []);
+ const participantAccountIDs = lodashGet(props.report, 'participantAccountIDs', []);
const isMultipleParticipant = participants.length > 1;
const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(OptionsListUtils.getPersonalDetailsForLogins(participants, props.personalDetails), isMultipleParticipant);
const roomWelcomeMessage = ReportUtils.getRoomWelcomeMessage(props.report);
@@ -97,7 +98,7 @@ const ReportWelcomeText = (props) => {
Navigation.navigate(ROUTES.getDetailsRoute(participants[index]))}
+ onPress={() => Navigation.navigate(ROUTES.getProfileRoute(participantAccountIDs[index]))}
>
{displayName}
diff --git a/src/components/TaskSelectorLink.js b/src/components/TaskSelectorLink.js
deleted file mode 100644
index f495049a74fd..000000000000
--- a/src/components/TaskSelectorLink.js
+++ /dev/null
@@ -1,115 +0,0 @@
-import React from 'react';
-// eslint-disable-next-line no-restricted-imports
-import {View, TouchableOpacity} from 'react-native';
-import PropTypes from 'prop-types';
-import styles from '../styles/styles';
-import Icon from './Icon';
-import * as Expensicons from './Icon/Expensicons';
-import themeColors from '../styles/themes/default';
-import variables from '../styles/variables';
-import Text from './Text';
-import withLocalize, {withLocalizePropTypes} from './withLocalize';
-import * as StyleUtils from '../styles/StyleUtils';
-import DisplayNames from './DisplayNames';
-import MultipleAvatars from './MultipleAvatars';
-import CONST from '../CONST';
-import avatarPropTypes from './avatarPropTypes';
-
-const propTypes = {
- /** Array of avatar URLs or icons */
- icons: PropTypes.arrayOf(avatarPropTypes),
-
- /** The title to display */
- text: PropTypes.string,
-
- /** The description to display */
- alternateText: PropTypes.string,
-
- /** The function to call when the link is pressed */
- onPress: PropTypes.func.isRequired,
-
- /** Label for the Link */
- label: PropTypes.string.isRequired,
-
- /** Whether it is a share location */
- isShareDestination: PropTypes.bool,
-
- /** Whether the Touchable should be disabled */
- disabled: PropTypes.bool,
-
- /** Whether we're creating a new task or editing */
- isNewTask: PropTypes.bool,
-
- ...withLocalizePropTypes,
-};
-
-const defaultProps = {
- icons: [],
- text: '',
- alternateText: '',
- isShareDestination: false,
- disabled: false,
- isNewTask: true,
-};
-
-const TaskSelectorLink = (props) => {
- const displayNameStyle = StyleUtils.combineStyles(styles.optionDisplayName, styles.pre);
- const alternateTextStyle = StyleUtils.combineStyles(styles.sidebarLinkText, styles.optionAlternateText, styles.textLabelSupporting, styles.pre);
- return (
-
-
- {props.icons.length !== 0 || props.text !== '' ? (
-
- {props.translate(props.label)}
-
-
-
-
- {props.alternateText ? (
-
- {props.alternateText}
-
- ) : null}
-
-
-
- ) : (
- {props.translate(props.label)}
- )}
- {props.disabled || !props.isNewTask ? null : (
-
- )}
-
-
- );
-};
-
-TaskSelectorLink.defaultProps = defaultProps;
-TaskSelectorLink.propTypes = propTypes;
-TaskSelectorLink.displayName = 'TaskSelectorLink';
-
-export default withLocalize(TaskSelectorLink);
diff --git a/src/components/ThreeDotsMenu/index.js b/src/components/ThreeDotsMenu/index.js
index b395d4dcb388..5217ca8441a1 100644
--- a/src/components/ThreeDotsMenu/index.js
+++ b/src/components/ThreeDotsMenu/index.js
@@ -1,6 +1,7 @@
import React, {Component} from 'react';
import {View} from 'react-native';
import PropTypes from 'prop-types';
+import _ from 'underscore';
import Icon from '../Icon';
import PopoverMenu from '../PopoverMenu';
import styles from '../../styles/styles';
@@ -8,6 +9,7 @@ import withLocalize, {withLocalizePropTypes} from '../withLocalize';
import Tooltip from '../Tooltip';
import * as Expensicons from '../Icon/Expensicons';
import ThreeDotsMenuItemPropTypes from './ThreeDotsMenuItemPropTypes';
+import CONST from '../../CONST';
import PressableWithoutFeedback from '../Pressable/PressableWithoutFeedback';
const propTypes = {
@@ -39,6 +41,12 @@ const propTypes = {
bottom: PropTypes.number,
left: PropTypes.number,
}).isRequired,
+
+ /** The anchor alignment of the menu */
+ anchorAlignment: PropTypes.shape({
+ horizontal: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL)),
+ vertical: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_VERTICAL)),
+ }),
};
const defaultProps = {
@@ -47,6 +55,10 @@ const defaultProps = {
iconStyles: [],
icon: Expensicons.ThreeDots,
onIconPress: () => {},
+ anchorAlignment: {
+ horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT,
+ vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM,
+ },
};
class ThreeDotsMenu extends Component {
@@ -97,6 +109,7 @@ class ThreeDotsMenu extends Component {
onClose={this.hidePopoverMenu}
isVisible={this.state.isPopupMenuVisible}
anchorPosition={this.props.anchorPosition}
+ anchorAlignment={this.props.anchorAlignment}
onItemSelected={this.hidePopoverMenu}
menuItems={this.props.menuItems}
/>
diff --git a/src/components/UnreadActionIndicator.js b/src/components/UnreadActionIndicator.js
index 81cf266bef28..0d45c90aa441 100755
--- a/src/components/UnreadActionIndicator.js
+++ b/src/components/UnreadActionIndicator.js
@@ -10,7 +10,7 @@ const UnreadActionIndicator = (props) => (
accessibilityLabel={props.translate('accessibilityHints.newMessageLineIndicator')}
data-action-id={props.reportActionID}
style={[styles.unreadIndicatorContainer, styles.userSelectNone]}
- nativeID={CONST.UNREAD_ACTION_INDICATOR_ID}
+ dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}}
>
{props.translate('common.new')}
diff --git a/src/components/ValidateCode/AbracadabraModal.js b/src/components/ValidateCode/AbracadabraModal.js
deleted file mode 100644
index 8debd439bf64..000000000000
--- a/src/components/ValidateCode/AbracadabraModal.js
+++ /dev/null
@@ -1,47 +0,0 @@
-import React, {PureComponent} from 'react';
-import {View} from 'react-native';
-import colors from '../../styles/colors';
-import styles from '../../styles/styles';
-import Icon from '../Icon';
-import withLocalize, {withLocalizePropTypes} from '../withLocalize';
-import Text from '../Text';
-import * as Expensicons from '../Icon/Expensicons';
-import * as Illustrations from '../Icon/Illustrations';
-import variables from '../../styles/variables';
-
-const propTypes = {
- ...withLocalizePropTypes,
-};
-
-class AbracadabraModal extends PureComponent {
- render() {
- return (
-
-
-
-
-
- {this.props.translate('validateCodeModal.successfulSignInTitle')}
-
- {this.props.translate('validateCodeModal.successfulSignInDescription')}
-
-
-
-
-
-
- );
- }
-}
-
-AbracadabraModal.propTypes = propTypes;
-export default withLocalize(AbracadabraModal);
diff --git a/src/components/ValidateCode/JustSignedInModal.js b/src/components/ValidateCode/JustSignedInModal.js
new file mode 100644
index 000000000000..40b25e4a19e0
--- /dev/null
+++ b/src/components/ValidateCode/JustSignedInModal.js
@@ -0,0 +1,53 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {View} from 'react-native';
+import colors from '../../styles/colors';
+import styles from '../../styles/styles';
+import Icon from '../Icon';
+import withLocalize, {withLocalizePropTypes} from '../withLocalize';
+import Text from '../Text';
+import * as Expensicons from '../Icon/Expensicons';
+import * as Illustrations from '../Icon/Illustrations';
+import variables from '../../styles/variables';
+
+const propTypes = {
+ ...withLocalizePropTypes,
+
+ /** Whether the 2FA is needed to get fully authenticated. */
+ is2FARequired: PropTypes.bool.isRequired,
+};
+
+function JustSignedInModal(props) {
+ return (
+
+
+
+
+
+
+ {props.translate(props.is2FARequired ? 'validateCodeModal.tfaRequiredTitle' : 'validateCodeModal.successfulSignInTitle')}
+
+
+
+ {props.translate(props.is2FARequired ? 'validateCodeModal.tfaRequiredDescription' : 'validateCodeModal.successfulSignInDescription')}
+
+
+
+
+
+
+
+ );
+}
+
+JustSignedInModal.propTypes = propTypes;
+export default withLocalize(JustSignedInModal);
diff --git a/src/components/ValidateCode/TfaRequiredModal.js b/src/components/ValidateCode/TfaRequiredModal.js
deleted file mode 100644
index a4afb79c84b2..000000000000
--- a/src/components/ValidateCode/TfaRequiredModal.js
+++ /dev/null
@@ -1,47 +0,0 @@
-import React, {PureComponent} from 'react';
-import {View} from 'react-native';
-import colors from '../../styles/colors';
-import styles from '../../styles/styles';
-import Icon from '../Icon';
-import withLocalize, {withLocalizePropTypes} from '../withLocalize';
-import Text from '../Text';
-import * as Expensicons from '../Icon/Expensicons';
-import * as Illustrations from '../Icon/Illustrations';
-import variables from '../../styles/variables';
-
-const propTypes = {
- ...withLocalizePropTypes,
-};
-
-class TfaRequiredModal extends PureComponent {
- render() {
- return (
-
-
-
-
-
- {this.props.translate('validateCodeModal.tfaRequiredTitle')}
-
- {this.props.translate('validateCodeModal.tfaRequiredDescription')}
-
-
-
-
-
-
- );
- }
-}
-
-TfaRequiredModal.propTypes = propTypes;
-export default withLocalize(TfaRequiredModal);
diff --git a/src/components/menuItemPropTypes.js b/src/components/menuItemPropTypes.js
index 57266831377f..3909f446c907 100644
--- a/src/components/menuItemPropTypes.js
+++ b/src/components/menuItemPropTypes.js
@@ -22,7 +22,7 @@ const propTypes = {
onPress: PropTypes.func,
/** Icon to display on the left side of component */
- icon: PropTypes.oneOfType([PropTypes.elementType, PropTypes.string]),
+ icon: PropTypes.oneOfType([PropTypes.elementType, PropTypes.string, PropTypes.arrayOf(avatarPropTypes)]),
/** Icon Width */
iconWidth: PropTypes.number,
@@ -33,6 +33,9 @@ const propTypes = {
/** Text to display for the item */
title: PropTypes.string.isRequired,
+ /** Text that appears above the title */
+ label: PropTypes.string,
+
/** Boolean whether to display the title right icon */
shouldShowTitleIcon: PropTypes.bool,
diff --git a/src/components/withLocalize.js b/src/components/withLocalize.js
index 4cbdda876231..def7110c1b40 100755
--- a/src/components/withLocalize.js
+++ b/src/components/withLocalize.js
@@ -179,4 +179,4 @@ export default function withLocalize(WrappedComponent) {
return WithLocalize;
}
-export {withLocalizePropTypes, Provider as LocaleContextProvider};
+export {withLocalizePropTypes, Provider as LocaleContextProvider, LocaleContext};
diff --git a/src/hooks/useLocalize.js b/src/hooks/useLocalize.js
new file mode 100644
index 000000000000..9ad5048729bd
--- /dev/null
+++ b/src/hooks/useLocalize.js
@@ -0,0 +1,6 @@
+import {useContext} from 'react';
+import {LocaleContext} from '../components/withLocalize';
+
+export default function useLocalize() {
+ return useContext(LocaleContext);
+}
diff --git a/src/hooks/usePermissions.js b/src/hooks/usePermissions.js
new file mode 100644
index 000000000000..4ab581231489
--- /dev/null
+++ b/src/hooks/usePermissions.js
@@ -0,0 +1,21 @@
+import _ from 'underscore';
+import {useContext, useMemo} from 'react';
+import Permissions from '../libs/Permissions';
+import {BetasContext} from '../components/OnyxProvider';
+
+export default function usePermissions() {
+ const betas = useContext(BetasContext);
+ return useMemo(
+ () =>
+ _.reduce(
+ Permissions,
+ (memo, checkerFunction, beta) => {
+ // eslint-disable-next-line no-param-reassign
+ memo[beta] = checkerFunction(betas);
+ return memo;
+ },
+ {},
+ ),
+ [betas],
+ );
+}
diff --git a/src/languages/en.js b/src/languages/en.js
index 8526b4aa4ec3..a2c372393cc9 100755
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -44,6 +44,7 @@ export default {
and: 'and',
details: 'Details',
privacy: 'Privacy',
+ hidden: 'Hidden',
delete: 'Delete',
archived: 'archived',
contacts: 'Contacts',
@@ -1074,6 +1075,7 @@ export default {
growlMessageOnDeleteError: 'This workspace cannot be deleted right now because reports are actively being processed',
unavailable: 'Unavailable workspace',
memberNotFound: 'Member not found. To invite a new member to the workspace, please use the Invite button above.',
+ notAuthorized: `You do not have access to this page. Are you trying to join the workspace? Please reach out to the owner of this workspace so they can add you as a member! Something else? Reach out to ${CONST.EMAIL.CONCIERGE}`,
goToRoom: ({roomName}) => `Go to ${roomName} room`,
},
emptyWorkspace: {
diff --git a/src/languages/es.js b/src/languages/es.js
index e47d54cc7756..3ba86d87ec0c 100644
--- a/src/languages/es.js
+++ b/src/languages/es.js
@@ -43,6 +43,7 @@ export default {
and: 'y',
details: 'Detalles',
privacy: 'Privacidad',
+ hidden: 'Oculto',
delete: 'Eliminar',
archived: 'archivado',
contacts: 'Contactos',
@@ -1079,6 +1080,7 @@ export default {
growlMessageOnDeleteError: 'No se puede eliminar el espacio de trabajo porque tiene informes que están siendo procesados',
unavailable: 'Espacio de trabajo no disponible',
memberNotFound: 'Miembro no encontrado. Para invitar a un nuevo miembro al espacio de trabajo, por favor, utiliza el botón Invitar que está arriba.',
+ notAuthorized: `No tienes acceso a esta página. ¿Estás tratando de unirte al espacio de trabajo? ComunÃcate con el propietario de este espacio de trabajo para que pueda agregarte como miembro. ¿Necesitas algo más? ComunÃcate con ${CONST.EMAIL.CONCIERGE}`,
goToRoom: ({roomName}) => `Ir a la sala ${roomName}`,
},
emptyWorkspace: {
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js
index ab4753a67c09..b983ffd14968 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js
@@ -131,6 +131,16 @@ const DetailsModalStackNavigator = createModalStackNavigator([
},
]);
+const ProfileModalStackNavigator = createModalStackNavigator([
+ {
+ getComponent: () => {
+ const ProfilePage = require('../../../pages/ProfilePage').default;
+ return ProfilePage;
+ },
+ name: 'Profile_Root',
+ },
+]);
+
const ReportDetailsModalStackNavigator = createModalStackNavigator([
{
getComponent: () => {
@@ -223,8 +233,8 @@ const ReportParticipantsModalStackNavigator = createModalStackNavigator([
},
{
getComponent: () => {
- const DetailsPage = require('../../../pages/DetailsPage').default;
- return DetailsPage;
+ const ProfilePage = require('../../../pages/ProfilePage').default;
+ return ProfilePage;
},
name: 'ReportParticipants_Details',
},
@@ -725,6 +735,7 @@ export {
IOUSendModalStackNavigator,
SplitDetailsModalStackNavigator,
DetailsModalStackNavigator,
+ ProfileModalStackNavigator,
ReportDetailsModalStackNavigator,
TaskModalStackNavigator,
ReportSettingsModalStackNavigator,
diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.js b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.js
index 2f5ac9267a50..cb82795936c2 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.js
+++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.js
@@ -34,6 +34,11 @@ function RigthModalNavigator() {
options={defaultModalScreenOptions}
component={ModalStackNavigators.DetailsModalStackNavigator}
/>
+
(
options={defaultScreenOptions}
component={UnlinkLoginPage}
/>
-
);
diff --git a/src/libs/Navigation/linkingConfig.js b/src/libs/Navigation/linkingConfig.js
index 666506ca1ab8..37506c0460ce 100644
--- a/src/libs/Navigation/linkingConfig.js
+++ b/src/libs/Navigation/linkingConfig.js
@@ -9,7 +9,6 @@ export default {
initialRouteName: SCREENS.HOME,
screens: {
// Main Routes
- SetPassword: ROUTES.SET_PASSWORD_WITH_VALIDATE_CODE,
ValidateLogin: ROUTES.VALIDATE_LOGIN,
UnlinkLogin: ROUTES.UNLINK_LOGIN,
[SCREENS.TRANSITION_FROM_OLD_DOT]: ROUTES.TRANSITION_FROM_OLD_DOT,
@@ -278,6 +277,11 @@ export default {
Details_Root: ROUTES.DETAILS,
},
},
+ Profile: {
+ screens: {
+ Profile_Root: ROUTES.PROFILE,
+ },
+ },
Participants: {
screens: {
ReportParticipants_Root: ROUTES.REPORT_PARTICIPANTS,
diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js
index ced088836927..7d1dd8592e67 100644
--- a/src/libs/OptionsListUtils.js
+++ b/src/libs/OptionsListUtils.js
@@ -419,7 +419,7 @@ function createOption(logins, personalDetails, report, reportActions = {}, {show
result.isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report);
result.isThread = ReportUtils.isThread(report);
result.isTaskReport = ReportUtils.isTaskReport(report);
- result.shouldShowSubscript = result.isPolicyExpenseChat && !report.isOwnPolicyExpenseChat && !result.isArchivedRoom;
+ result.shouldShowSubscript = ReportUtils.shouldReportShowSubscript(report);
result.allReportErrors = getAllReportErrors(report, reportActions);
result.brickRoadIndicator = !_.isEmpty(result.allReportErrors) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : '';
result.pendingAction = report.pendingFields ? report.pendingFields.addWorkspaceRoom || report.pendingFields.createChat : null;
@@ -950,6 +950,15 @@ function getHeaderMessage(hasSelectableOptions, hasUserToInvite, searchValue, ma
return '';
}
+/**
+ * Helper method to check whether an option can show tooltip or not
+ * @param {Object} option
+ * @returns {Boolean}
+ */
+function shouldOptionShowTooltip(option) {
+ return (!option.isChatRoom || option.isThread) && !option.isArchivedRoom;
+}
+
export {
addSMSDomainIfPhoneNumber,
getAvatarsForLogins,
@@ -968,4 +977,5 @@ export {
getPolicyExpenseReportOptions,
getParticipantsOptions,
isSearchStringMatch,
+ shouldOptionShowTooltip,
};
diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js
index d27ee3610813..fa8b93741aa3 100644
--- a/src/libs/ReportUtils.js
+++ b/src/libs/ReportUtils.js
@@ -1057,14 +1057,14 @@ function getReport(reportID) {
* @param {Object} report
*/
function navigateToDetailsPage(report) {
- const participants = lodashGet(report, 'participants', []);
+ const participantAccountIDs = lodashGet(report, 'participantAccountIDs', []);
if (isChatRoom(report) || isPolicyExpenseChat(report) || isThread(report)) {
Navigation.navigate(ROUTES.getReportDetailsRoute(report.reportID));
return;
}
- if (participants.length === 1) {
- Navigation.navigate(ROUTES.getDetailsRoute(participants[0]));
+ if (participantAccountIDs.length === 1) {
+ Navigation.navigate(ROUTES.getProfileRoute(participantAccountIDs[0]));
return;
}
Navigation.navigate(ROUTES.getReportParticipantsRoute(report.reportID));
@@ -2124,7 +2124,7 @@ function shouldReportShowSubscript(report) {
return false;
}
- if (isPolicyExpenseChat(report) && !report.isOwnPolicyExpenseChat) {
+ if (isPolicyExpenseChat(report) && !isThread(report) && !isTaskReport(report) && !report.isOwnPolicyExpenseChat) {
return true;
}
diff --git a/src/libs/SelectionScraper/index.js b/src/libs/SelectionScraper/index.js
index aab5ca78e4ed..44b87deba796 100644
--- a/src/libs/SelectionScraper/index.js
+++ b/src/libs/SelectionScraper/index.js
@@ -83,10 +83,12 @@ const getHTMLOfSelection = () => {
// Find and remove the div housing the UnreadActionIndicator because we don't want
// the 'New/Nuevo' text inside it being copied.
- const newMessageLineIndicatorDiv = div.querySelector(`#${CONST.UNREAD_ACTION_INDICATOR_ID}`);
+ const divsToRemove = div.querySelectorAll(`[data-${CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT}=true]`);
- if (newMessageLineIndicatorDiv) {
- newMessageLineIndicatorDiv.remove();
+ if (divsToRemove && divsToRemove.length > 0) {
+ divsToRemove.forEach((element) => {
+ element.remove();
+ });
}
return div.innerHTML;
diff --git a/src/libs/StatusBar/index.android.js b/src/libs/StatusBar/index.android.js
index 0ea4480dc928..45b6d9626c9f 100644
--- a/src/libs/StatusBar/index.android.js
+++ b/src/libs/StatusBar/index.android.js
@@ -1,4 +1,8 @@
+// eslint-disable-next-line no-restricted-imports
import {StatusBar} from 'react-native';
+// Only has custom web implementation
+StatusBar.getBackgroundColor = () => null;
+
// Just export StatusBar – no changes.
export default StatusBar;
diff --git a/src/libs/StatusBar/index.js b/src/libs/StatusBar/index.js
index 4aea200cab00..ef15d597f93e 100644
--- a/src/libs/StatusBar/index.js
+++ b/src/libs/StatusBar/index.js
@@ -1,6 +1,11 @@
+// eslint-disable-next-line no-restricted-imports
import {StatusBar} from 'react-native';
-// Overwrite setTranslucent to suppress a warning on iOS
+// Only has custom web implementation
+StatusBar.getBackgroundColor = () => null;
+
+// Overwrite setTranslucent and setBackgroundColor suppress warnings on iOS
StatusBar.setTranslucent = () => {};
+StatusBar.setBackgroundColor = () => {};
export default StatusBar;
diff --git a/src/libs/StatusBar/index.web.js b/src/libs/StatusBar/index.web.js
new file mode 100644
index 000000000000..dfa1226c33a8
--- /dev/null
+++ b/src/libs/StatusBar/index.web.js
@@ -0,0 +1,20 @@
+// eslint-disable-next-line no-restricted-imports
+import {StatusBar} from 'react-native';
+
+StatusBar.getBackgroundColor = () => {
+ const element = document.querySelector('meta[name=theme-color]');
+ if (!element || !element.content) {
+ return null;
+ }
+ return element.content;
+};
+
+StatusBar.setBackgroundColor = (backgroundColor) => {
+ const element = document.querySelector('meta[name=theme-color]');
+ if (!element) {
+ return;
+ }
+ element.content = backgroundColor;
+};
+
+export default StatusBar;
diff --git a/src/libs/actions/PersonalDetails.js b/src/libs/actions/PersonalDetails.js
index fc47ca5142f6..b362436def5c 100644
--- a/src/libs/actions/PersonalDetails.js
+++ b/src/libs/actions/PersonalDetails.js
@@ -310,6 +310,49 @@ function openPersonalDetailsPage() {
API.read('OpenPersonalDetailsPage');
}
+/**
+ * Fetches public profile info about a given user.
+ * The API will only return the accountID, displayName, and avatar for the user
+ * but the profile page will use other info (e.g. contact methods and pronouns) if they are already available in Onyx
+ * @param {Number} accountID
+ */
+function openPublicProfilePage(accountID) {
+ const optimisticData = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.PERSONAL_DETAILS_LIST,
+ value: {
+ [accountID]: {
+ isLoading: true,
+ },
+ },
+ },
+ ];
+ const successData = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.PERSONAL_DETAILS_LIST,
+ value: {
+ [accountID]: {
+ isLoading: false,
+ },
+ },
+ },
+ ];
+ const failureData = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.PERSONAL_DETAILS_LIST,
+ value: {
+ [accountID]: {
+ isLoading: false,
+ },
+ },
+ },
+ ];
+ API.read('OpenPublicProfilePage', {accountID}, {optimisticData, successData, failureData});
+}
+
/**
* Updates the user's avatar image
*
@@ -427,6 +470,7 @@ export {
deleteAvatar,
openMoneyRequestModalPage,
openPersonalDetailsPage,
+ openPublicProfilePage,
extractFirstAndLastNameFromAvailableDetails,
updateDisplayName,
updateLegalName,
diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js
index c1ec6266bb18..a88cccebeaaa 100644
--- a/src/libs/actions/Report.js
+++ b/src/libs/actions/Report.js
@@ -383,10 +383,6 @@ function openReport(reportID, participantList = [], newReportObject = {}, parent
// and we need data to be available when we navigate to the chat page
if (_.isEmpty(ReportUtils.getReport(reportID))) {
optimisticReportData.onyxMethod = Onyx.METHOD.SET;
- optimisticReportData.value = {
- ...optimisticReportData.value,
- reportID: reportID.toString(),
- };
}
// If we are creating a new report, we need to add the optimistic report data and a report action
diff --git a/src/libs/actions/Session/index.js b/src/libs/actions/Session/index.js
index 9cb3c127b287..b53d7811ef8d 100644
--- a/src/libs/actions/Session/index.js
+++ b/src/libs/actions/Session/index.js
@@ -197,45 +197,6 @@ function resendValidateCode(login = credentials.login) {
API.write('RequestNewValidateCode', {email: login}, {optimisticData, successData, failureData});
}
-/**
- * Request a new validate / magic code for user to sign in automatically with the link
- *
- * @param {String} [login]
- */
-function resendLinkWithValidateCode(login = credentials.login) {
- const optimisticData = [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: ONYXKEYS.ACCOUNT,
- value: {
- isLoading: true,
- message: null,
- },
- },
- ];
- const successData = [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: ONYXKEYS.ACCOUNT,
- value: {
- isLoading: false,
- message: Localize.translateLocal('validateCodeModal.successfulNewCodeRequest'),
- },
- },
- ];
- const failureData = [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: ONYXKEYS.ACCOUNT,
- value: {
- isLoading: false,
- message: null,
- },
- },
- ];
- API.write('RequestNewValidateCode', {email: login}, {optimisticData, successData, failureData});
-}
-
/**
* Checks the API to see if an account exists for the given login
*
@@ -768,7 +729,7 @@ function requestUnlinkValidationLink() {
key: ONYXKEYS.ACCOUNT,
value: {
isLoading: false,
- message: Localize.translateLocal('unlinkLoginForm.linkSent'),
+ message: 'unlinkLoginForm.linkSent',
loadingForm: null,
},
},
@@ -926,7 +887,6 @@ export {
signOutAndRedirectToSignIn,
resendValidationLink,
resendValidateCode,
- resendLinkWithValidateCode,
resetPassword,
resendResetPassword,
requestUnlinkValidationLink,
diff --git a/src/libs/actions/Task.js b/src/libs/actions/Task.js
index ec150a332155..59ea33e0d722 100644
--- a/src/libs/actions/Task.js
+++ b/src/libs/actions/Task.js
@@ -297,7 +297,9 @@ function editTaskAndNavigate(report, ownerEmail, title, description, assignee) {
let assigneeChatReportID;
if (assignee && assignee !== report.managerEmail) {
assigneeChatReportID = ReportUtils.getChatByParticipants([assignee]).reportID;
- optimisticAssigneeAddComment = ReportUtils.buildOptimisticTaskCommentReportAction(report.reportID, reportName, assignee, `Assigned a task to you: ${reportName}`);
+ if (assigneeChatReportID !== report.parentReportID.toString()) {
+ optimisticAssigneeAddComment = ReportUtils.buildOptimisticTaskCommentReportAction(report.reportID, reportName, assignee, `Assigned a task to you: ${reportName}`);
+ }
}
const optimisticData = [
diff --git a/src/pages/ProfilePage.js b/src/pages/ProfilePage.js
new file mode 100755
index 000000000000..932f489be5f4
--- /dev/null
+++ b/src/pages/ProfilePage.js
@@ -0,0 +1,240 @@
+import {View, ScrollView} from 'react-native';
+import React, {useEffect} from 'react';
+import PropTypes from 'prop-types';
+import _ from 'underscore';
+import {withOnyx} from 'react-native-onyx';
+import Str from 'expensify-common/lib/str';
+import lodashGet from 'lodash/get';
+import {parsePhoneNumber} from 'awesome-phonenumber';
+import styles from '../styles/styles';
+import Text from '../components/Text';
+import ONYXKEYS from '../ONYXKEYS';
+import Avatar from '../components/Avatar';
+import HeaderWithBackButton from '../components/HeaderWithBackButton';
+import Navigation from '../libs/Navigation/Navigation';
+import ScreenWrapper from '../components/ScreenWrapper';
+import personalDetailsPropType from './personalDetailsPropType';
+import withLocalize, {withLocalizePropTypes} from '../components/withLocalize';
+import compose from '../libs/compose';
+import CommunicationsLink from '../components/CommunicationsLink';
+import Tooltip from '../components/Tooltip';
+import CONST from '../CONST';
+import * as ReportUtils from '../libs/ReportUtils';
+import * as Expensicons from '../components/Icon/Expensicons';
+import MenuItem from '../components/MenuItem';
+import AttachmentModal from '../components/AttachmentModal';
+import PressableWithoutFocus from '../components/PressableWithoutFocus';
+import * as Report from '../libs/actions/Report';
+import OfflineWithFeedback from '../components/OfflineWithFeedback';
+import AutoUpdateTime from '../components/AutoUpdateTime';
+import * as UserUtils from '../libs/UserUtils';
+import * as PersonalDetails from '../libs/actions/PersonalDetails';
+import FullScreenLoadingIndicator from '../components/FullscreenLoadingIndicator';
+import BlockingView from '../components/BlockingViews/BlockingView';
+import * as Illustrations from '../components/Icon/Illustrations';
+import variables from '../styles/variables';
+import ROUTES from '../ROUTES';
+
+const matchType = PropTypes.shape({
+ params: PropTypes.shape({
+ /** accountID passed via route /a/:accountID */
+ accountID: PropTypes.string,
+
+ /** report ID passed */
+ reportID: PropTypes.string,
+ }),
+});
+
+const propTypes = {
+ /* Onyx Props */
+
+ /** The personal details of all users */
+ personalDetails: PropTypes.objectOf(personalDetailsPropType),
+
+ /** Route params */
+ route: matchType.isRequired,
+
+ /** Login list for the user that is signed in */
+ loginList: PropTypes.shape({
+ /** Phone/Email associated with user */
+ partnerUserID: PropTypes.string,
+ }),
+
+ ...withLocalizePropTypes,
+};
+
+const defaultProps = {
+ // When opening someone else's profile (via deep link) before login, this is empty
+ personalDetails: {},
+ loginList: {},
+};
+
+/**
+ * Gets the phone number to display for SMS logins
+ *
+ * @param {Object} details
+ * @param {String} details.login
+ * @param {String} details.displayName
+ * @returns {String}
+ */
+const getPhoneNumber = (details) => {
+ // If the user hasn't set a displayName, it is set to their phone number, so use that
+ const displayName = lodashGet(details, 'displayName', '');
+ const parsedPhoneNumber = parsePhoneNumber(displayName);
+ if (parsedPhoneNumber.possible) {
+ return parsedPhoneNumber.number.e164;
+ }
+
+ // If the user has set a displayName, get the phone number from the SMS login
+ return details.login ? Str.removeSMSDomain(details.login) : '';
+};
+
+function ProfilePage(props) {
+ const accountID = lodashGet(props.route.params, 'accountID', 0);
+
+ // eslint-disable-next-line rulesdir/prefer-early-return
+ useEffect(() => {
+ if (accountID > 0) {
+ PersonalDetails.openPublicProfilePage(accountID);
+ }
+ }, [accountID]);
+
+ const details = lodashGet(props.personalDetails, accountID, {});
+ const displayName = details.displayName ? details.displayName : props.translate('common.hidden');
+ const avatar = lodashGet(details, 'avatar', UserUtils.getDefaultAvatar());
+ const originalFileName = lodashGet(details, 'originalFileName', '');
+ const login = lodashGet(details, 'login', '');
+ const timezone = lodashGet(details, 'timezone', {});
+
+ // If we have a reportID param this means that we
+ // arrived here via the ParticipantsPage and should be allowed to navigate back to it
+ const shouldShowLocalTime = !ReportUtils.hasAutomatedExpensifyEmails([login]) && !_.isEmpty(timezone);
+
+ let pronouns = lodashGet(details, 'pronouns', '');
+ if (pronouns && pronouns.startsWith(CONST.PRONOUNS.PREFIX)) {
+ const localeKey = pronouns.replace(CONST.PRONOUNS.PREFIX, '');
+ pronouns = props.translate(`pronouns.${localeKey}`);
+ }
+
+ const isSMSLogin = Str.isSMSLogin(login);
+ const phoneNumber = getPhoneNumber(details);
+ const phoneOrEmail = isSMSLogin ? getPhoneNumber(details) : login;
+
+ const isCurrentUser = _.keys(props.loginList).includes(login);
+ const hasMinimumDetails = !_.isEmpty(details.avatar);
+ const isLoading = lodashGet(details, 'isLoading', false) || _.isEmpty(details);
+
+ // If the API returns an error for some reason there won't be any details and isLoading will get set to false, so we want to show a blocking screen
+ const shouldShowBlockingView = !hasMinimumDetails && !isLoading;
+
+ return (
+
+ Navigation.goBack(ROUTES.HOME)}
+ />
+
+ {hasMinimumDetails && (
+
+
+
+ {({show}) => (
+
+
+
+
+
+ )}
+
+ {Boolean(displayName) && (
+
+ {displayName}
+
+ )}
+ {login ? (
+
+
+ {props.translate(isSMSLogin ? 'common.phoneNumber' : 'common.email')}
+
+
+
+ {isSMSLogin ? props.formatPhoneNumber(phoneNumber) : login}
+
+
+
+ ) : null}
+ {pronouns ? (
+
+
+ {props.translate('profilePage.preferredPronouns')}
+
+ {pronouns}
+
+ ) : null}
+ {shouldShowLocalTime && }
+
+ {!isCurrentUser && Boolean(login) && (
+
+ )}
+ {!hasMinimumDetails && isLoading && }
+ {shouldShowBlockingView && (
+
+ )}
+
+
+ );
+}
+
+ProfilePage.propTypes = propTypes;
+ProfilePage.defaultProps = defaultProps;
+ProfilePage.displayName = 'ProfilePage';
+
+export default compose(
+ withLocalize,
+ withOnyx({
+ personalDetails: {
+ key: ONYXKEYS.PERSONAL_DETAILS_LIST,
+ },
+ loginList: {
+ key: ONYXKEYS.LOGIN_LIST,
+ },
+ }),
+)(ProfilePage);
diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.js b/src/pages/ReimbursementAccount/ReimbursementAccountPage.js
index a7cf6f4d4a1b..7e18a7edbdc2 100644
--- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.js
+++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.js
@@ -343,16 +343,6 @@ class ReimbursementAccountPage extends React.Component {
);
}
- if (this.state.shouldShowContinueSetupButton) {
- return (
-
- );
- }
-
let errorComponent;
const userHasPhonePrimaryEmail = Str.endsWith(this.props.session.email, CONST.SMS.DOMAIN);
@@ -386,6 +376,16 @@ class ReimbursementAccountPage extends React.Component {
);
}
+ if (this.state.shouldShowContinueSetupButton) {
+ return (
+
+ );
+ }
+
if (currentStep === CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT) {
return (
{
return {
alternateText: userLogin,
displayName: userPersonalDetail.displayName,
+ accountID: userPersonalDetail.accountID,
icons: [
{
source: UserUtils.getAvatar(userPersonalDetail.avatar, login),
@@ -109,7 +110,7 @@ const ReportParticipantsPage = (props) => {
},
]}
onSelectRow={(option) => {
- Navigation.navigate(ROUTES.getReportParticipantRoute(props.route.params.reportID, option.login));
+ Navigation.navigate(ROUTES.getReportParticipantRoute(props.route.params.reportID, option.accountID));
}}
hideSectionHeaders
showTitleTooltip
diff --git a/src/pages/ReportWelcomeMessagePage.js b/src/pages/ReportWelcomeMessagePage.js
index 185eeab446d5..6d7a69f8de92 100644
--- a/src/pages/ReportWelcomeMessagePage.js
+++ b/src/pages/ReportWelcomeMessagePage.js
@@ -43,7 +43,7 @@ function ReportWelcomeMessagePage(props) {
}, []);
const submitForm = useCallback(() => {
- Report.updateWelcomeMessage(props.report.reportID, props.report.welcomeMessage, welcomeMessage);
+ Report.updateWelcomeMessage(props.report.reportID, props.report.welcomeMessage, welcomeMessage.trim());
}, [props.report.reportID, props.report.welcomeMessage, welcomeMessage]);
return (
diff --git a/src/pages/SetPasswordPage.js b/src/pages/SetPasswordPage.js
deleted file mode 100755
index f00a7095e463..000000000000
--- a/src/pages/SetPasswordPage.js
+++ /dev/null
@@ -1,137 +0,0 @@
-import React, {Component} from 'react';
-import {View} from 'react-native';
-import {SafeAreaView} from 'react-native-safe-area-context';
-import PropTypes from 'prop-types';
-import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
-import lodashGet from 'lodash/get';
-import {propTypes as validateLinkPropTypes, defaultProps as validateLinkDefaultProps} from './ValidateLoginPage/validateLinkPropTypes';
-import styles from '../styles/styles';
-import * as Session from '../libs/actions/Session';
-import ONYXKEYS from '../ONYXKEYS';
-import SignInPageLayout from './signin/SignInPageLayout';
-import withLocalize, {withLocalizePropTypes} from '../components/withLocalize';
-import compose from '../libs/compose';
-import NewPasswordForm from './settings/NewPasswordForm';
-import FormAlertWithSubmitButton from '../components/FormAlertWithSubmitButton';
-import FormSubmit from '../components/FormSubmit';
-import * as ErrorUtils from '../libs/ErrorUtils';
-
-const propTypes = {
- /* Onyx Props */
-
- /** The details about the account that the user is signing in with */
- account: PropTypes.shape({
- /** An error message to display to the user */
- errors: PropTypes.objectOf(PropTypes.string),
-
- /** Whether a sign on form is loading (being submitted) */
- isLoading: PropTypes.bool,
- }),
-
- /** The credentials of the logged in person */
- credentials: PropTypes.shape({
- /** The email the user logged in with */
- login: PropTypes.string,
-
- /** The password used to log in the user */
- password: PropTypes.string,
- }),
-
- /** Session object */
- session: PropTypes.shape({
- /** An error message to display to the user */
- errors: PropTypes.objectOf(PropTypes.string),
- }),
-
- /** The accountID and validateCode are passed via the URL */
- route: validateLinkPropTypes,
-
- ...withLocalizePropTypes,
-};
-
-const defaultProps = {
- account: {},
- credentials: {},
- route: validateLinkDefaultProps,
- session: {
- errors: null,
- authToken: '',
- },
-};
-
-class SetPasswordPage extends Component {
- constructor(props) {
- super(props);
-
- this.validateAndSubmitForm = this.validateAndSubmitForm.bind(this);
-
- this.state = {
- password: '',
- isFormValid: false,
- };
- }
-
- componentWillUnmount() {
- Session.clearAccountMessages();
- }
-
- validateAndSubmitForm() {
- if (!this.state.isFormValid) {
- return;
- }
- const accountID = lodashGet(this.props.route.params, 'accountID', '');
- const validateCode = lodashGet(this.props.route.params, 'validateCode', '');
- Session.updatePasswordAndSignin(accountID, validateCode, this.state.password);
- }
-
- render() {
- const buttonText = this.props.translate('setPasswordPage.setPassword');
- const error = ErrorUtils.getLatestErrorMessage(this.props.account) || ErrorUtils.getLatestErrorMessage(this.props.session);
- return (
-
-
-
-
- this.setState({password})}
- updateIsFormValid={(isValid) => this.setState({isFormValid: isValid})}
- />
-
-
-
-
-
-
-
- );
- }
-}
-
-SetPasswordPage.propTypes = propTypes;
-SetPasswordPage.defaultProps = defaultProps;
-
-export default compose(
- withLocalize,
- withOnyx({
- credentials: {key: ONYXKEYS.CREDENTIALS},
- account: {key: ONYXKEYS.ACCOUNT},
- session: {
- key: ONYXKEYS.SESSION,
- initWithStoredValues: false,
- },
- }),
-)(SetPasswordPage);
diff --git a/src/pages/ValidateLoginPage/index.website.js b/src/pages/ValidateLoginPage/index.website.js
index 6dfc5d8d87b7..26bfdf902a22 100644
--- a/src/pages/ValidateLoginPage/index.website.js
+++ b/src/pages/ValidateLoginPage/index.website.js
@@ -11,12 +11,11 @@ import ONYXKEYS from '../../ONYXKEYS';
import * as Session from '../../libs/actions/Session';
import Permissions from '../../libs/Permissions';
import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize';
-import AbracadabraModal from '../../components/ValidateCode/AbracadabraModal';
import ExpiredValidateCodeModal from '../../components/ValidateCode/ExpiredValidateCodeModal';
import Navigation from '../../libs/Navigation/Navigation';
import ROUTES from '../../ROUTES';
import CONST from '../../CONST';
-import TfaRequiredModal from '../../components/ValidateCode/TfaRequiredModal';
+import JustSignedInModal from '../../components/ValidateCode/JustSignedInModal';
const propTypes = {
/** The accountID and validateCode are passed via the URL */
@@ -124,8 +123,8 @@ class ValidateLoginPage extends Component {
return (
<>
{currentAuthState === CONST.AUTO_AUTH_STATE.FAILED && }
- {currentAuthState === CONST.AUTO_AUTH_STATE.JUST_SIGNED_IN && is2FARequired && !isSignedIn && }
- {currentAuthState === CONST.AUTO_AUTH_STATE.JUST_SIGNED_IN && isSignedIn && }
+ {currentAuthState === CONST.AUTO_AUTH_STATE.JUST_SIGNED_IN && is2FARequired && !isSignedIn && }
+ {currentAuthState === CONST.AUTO_AUTH_STATE.JUST_SIGNED_IN && isSignedIn && }
{currentAuthState === CONST.AUTO_AUTH_STATE.NOT_STARTED && !isSignedIn && (
{
}
const shouldShowThreeDotsButton = !!threeDotMenuItems.length;
- const shouldShowSubscript = isPolicyExpenseChat && !props.report.isOwnPolicyExpenseChat && !ReportUtils.isArchivedRoom(props.report) && !isTaskReport;
+ const shouldShowSubscript = ReportUtils.shouldReportShowSubscript(props.report);
const icons = ReportUtils.getIcons(reportHeaderData, props.personalDetails);
const brickRoadIndicator = ReportUtils.hasReportNameError(props.report) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : '';
return (
@@ -158,7 +158,7 @@ const HeaderView = (props) => {
) : (
)}
diff --git a/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/index.js b/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/index.js
index f2883acc3b95..2c0f2ff65561 100644
--- a/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/index.js
+++ b/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/index.js
@@ -5,6 +5,7 @@ import PropTypes from 'prop-types';
import {propTypes as genericReportActionContextMenuPropTypes, defaultProps as GenericReportActionContextMenuDefaultProps} from '../genericReportActionContextMenuPropTypes';
import * as StyleUtils from '../../../../../styles/StyleUtils';
import BaseReportActionContextMenu from '../BaseReportActionContextMenu';
+import CONST from '../../../../../CONST';
const propTypes = {
..._.omit(genericReportActionContextMenuPropTypes, ['isMini']),
@@ -20,7 +21,10 @@ const defaultProps = {
};
const MiniReportActionContextMenu = (props) => (
-
+
{
hoverStyle={styles.hoveredComponentBG}
onSelectRow={() => {
props.onClose();
- Navigation.navigate(ROUTES.getDetailsRoute(item.login));
+ Navigation.navigate(ROUTES.getProfileRoute(item.accountID));
}}
option={{
text: Str.removeSMSDomain(item.displayName),
diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js
index a7542d69d213..30ab83916f38 100644
--- a/src/pages/home/report/ReportActionCompose.js
+++ b/src/pages/home/report/ReportActionCompose.js
@@ -85,6 +85,9 @@ const propTypes = {
/** Array of report actions for this report */
reportActions: PropTypes.arrayOf(PropTypes.shape(reportActionPropTypes)),
+ /** The actions from the parent report */
+ parentReportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)),
+
/** Is the window width narrow, like on a mobile device */
isSmallScreenWidth: PropTypes.bool.isRequired,
@@ -125,6 +128,7 @@ const defaultProps = {
modal: {},
report: {},
reportActions: [],
+ parentReportActions: {},
blockedFromConcierge: {},
personalDetails: {},
preferredSkinTone: CONST.EMOJI_DEFAULT_SKIN_TONE,
@@ -829,7 +833,9 @@ class ReportActionCompose extends React.Component {
) {
e.preventDefault();
- const lastReportAction = _.find(this.props.reportActions, (action) => ReportUtils.canEditReportAction(action));
+ const parentReportActionID = lodashGet(this.props.report, 'parentReportActionID', '');
+ const parentReportAction = lodashGet(this.props.parentReportActions, [parentReportActionID], {});
+ const lastReportAction = _.find([...this.props.reportActions, parentReportAction], (action) => ReportUtils.canEditReportAction(action));
if (lastReportAction !== -1 && lastReportAction) {
Report.saveReportActionDraft(this.props.reportID, lastReportAction.reportActionID, _.last(lastReportAction.message).html);
@@ -1257,5 +1263,9 @@ export default compose(
shouldShowComposeInput: {
key: ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT,
},
+ parentReportActions: {
+ key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`,
+ canEvict: false,
+ },
}),
)(ReportActionCompose);
diff --git a/src/pages/home/report/ReportActionItemFragment.js b/src/pages/home/report/ReportActionItemFragment.js
index d1aa0cf7ddac..e606b0c65f69 100644
--- a/src/pages/home/report/ReportActionItemFragment.js
+++ b/src/pages/home/report/ReportActionItemFragment.js
@@ -122,7 +122,6 @@ const ReportActionItemFragment = (props) => {
return (
diff --git a/src/pages/home/report/ReportActionItemSingle.js b/src/pages/home/report/ReportActionItemSingle.js
index 96fd6be6ad26..eb5f2fa07434 100644
--- a/src/pages/home/report/ReportActionItemSingle.js
+++ b/src/pages/home/report/ReportActionItemSingle.js
@@ -61,13 +61,13 @@ const defaultProps = {
report: undefined,
};
-const showUserDetails = (email) => {
- Navigation.navigate(ROUTES.getDetailsRoute(email));
+const showUserDetails = (accountID) => {
+ Navigation.navigate(ROUTES.getProfileRoute(accountID));
};
const ReportActionItemSingle = (props) => {
const actorEmail = props.action.actorEmail.replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, '');
- const {avatar, displayName, pendingFields} = props.personalDetails[actorEmail] || {};
+ const {accountID, avatar, displayName, pendingFields} = props.personalDetails[actorEmail] || {};
const avatarSource = UserUtils.getAvatar(avatar, actorEmail);
// Since the display name for a report action message is delivered with the report history as an array of fragments
@@ -88,7 +88,7 @@ const ReportActionItemSingle = (props) => {
style={[styles.alignSelfStart, styles.mr3]}
onPressIn={ControlSelection.block}
onPressOut={ControlSelection.unblock}
- onPress={() => showUserDetails(actorEmail)}
+ onPress={() => showUserDetails(accountID)}
>
{props.shouldShowSubscriptAvatar ? (
@@ -118,7 +118,7 @@ const ReportActionItemSingle = (props) => {
style={[styles.flexShrink1, styles.mr1]}
onPressIn={ControlSelection.block}
onPressOut={ControlSelection.unblock}
- onPress={() => showUserDetails(actorEmail)}
+ onPress={() => showUserDetails(accountID)}
>
{_.map(personArray, (fragment, index) => (
{
+function MoneyRequestParticipantsPage(props) {
if (props.iou.loading) {
return (
@@ -67,7 +67,7 @@ const MoneyRequestParticipantsPage = (props) => {
iouType={props.iouType}
/>
);
-};
+}
MoneyRequestParticipantsPage.displayName = 'IOUParticipantsPage';
MoneyRequestParticipantsPage.propTypes = propTypes;
diff --git a/src/pages/settings/Payments/AddPayPalMePage.js b/src/pages/settings/Payments/AddPayPalMePage.js
index 69a89ceb5f79..cbb437ea00e6 100644
--- a/src/pages/settings/Payments/AddPayPalMePage.js
+++ b/src/pages/settings/Payments/AddPayPalMePage.js
@@ -1,6 +1,5 @@
import React, {useRef, useState, useCallback} from 'react';
-// eslint-disable-next-line no-restricted-imports
-import {View, TouchableWithoutFeedback, Linking} from 'react-native';
+import {View, Linking} from 'react-native';
import _ from 'underscore';
import CONST from '../../../CONST';
import ROUTES from '../../../ROUTES';
@@ -20,6 +19,7 @@ import * as User from '../../../libs/actions/User';
import Icon from '../../../components/Icon';
import * as Expensicons from '../../../components/Icon/Expensicons';
import variables from '../../../styles/variables';
+import PressableWithoutFeedback from '../../../components/Pressable/PressableWithoutFeedback';
const AddPayPalMePage = (props) => {
const [payPalMeUsername, setPayPalMeUsername] = useState('');
@@ -69,8 +69,10 @@ const AddPayPalMePage = (props) => {
/>
{props.translate('addPayPalMePage.checkListOf')}
- Linking.openURL('https://developer.paypal.com/docs/reports/reference/paypal-supported-currencies')}
>
@@ -89,7 +91,7 @@ const AddPayPalMePage = (props) => {
/>
-
+
diff --git a/src/pages/settings/Payments/PaymentMethodList.js b/src/pages/settings/Payments/PaymentMethodList.js
index 4d9c3e62b33c..b6ffbf1e0f9d 100644
--- a/src/pages/settings/Payments/PaymentMethodList.js
+++ b/src/pages/settings/Payments/PaymentMethodList.js
@@ -1,5 +1,5 @@
import _ from 'underscore';
-import React, {Component} from 'react';
+import React, {useCallback, useMemo} from 'react';
import PropTypes from 'prop-types';
import {FlatList} from 'react-native';
import lodashGet from 'lodash/get';
@@ -87,47 +87,66 @@ const defaultProps = {
listHeaderComponent: null,
};
-class PaymentMethodList extends Component {
- constructor(props) {
- super(props);
+/**
+ * Dismisses the error on the payment method
+ * @param {Object} item
+ */
+function dismissError(item) {
+ const paymentList = item.accountType === CONST.PAYMENT_METHODS.BANK_ACCOUNT ? ONYXKEYS.BANK_ACCOUNT_LIST : ONYXKEYS.CARD_LIST;
+ const paymentID = item.accountType === CONST.PAYMENT_METHODS.BANK_ACCOUNT ? lodashGet(item, ['accountData', 'bankAccountID'], '') : lodashGet(item, ['accountData', 'fundID'], '');
+
+ if (!paymentID) {
+ Log.info('Unable to clear payment method error: ', item);
+ return;
+ }
- this.renderItem = this.renderItem.bind(this);
+ if (item.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
+ PaymentMethods.clearDeletePaymentMethodError(paymentList, paymentID);
+ } else {
+ PaymentMethods.clearAddPaymentMethodError(paymentList, paymentID);
}
+}
- /**
- * @param {Boolean} isDefault
- * @returns {*}
- */
- getDefaultBadgeText(isDefault = false) {
- if (!isDefault) {
- return null;
- }
+/**
+ * @param {Array} filteredPaymentMethods
+ * @param {Boolean} isDefault
+ * @returns {Boolean}
+ */
+function shouldShowDefaultBadge(filteredPaymentMethods, isDefault = false) {
+ if (!isDefault) {
+ return false;
+ }
- const defaultablePaymentMethodCount = _.reduce(
- this.getFilteredPaymentMethods(),
- (count, method) => (method.accountType === CONST.PAYMENT_METHODS.BANK_ACCOUNT || method.accountType === CONST.PAYMENT_METHODS.DEBIT_CARD ? count + 1 : count),
- 0,
- );
- if (defaultablePaymentMethodCount <= 1) {
- return null;
- }
+ const defaultablePaymentMethodCount = _.filter(
+ filteredPaymentMethods,
+ (method) => method.accountType === CONST.PAYMENT_METHODS.BANK_ACCOUNT || method.accountType === CONST.PAYMENT_METHODS.DEBIT_CARD,
+ ).length;
+ return defaultablePaymentMethodCount > 1;
+}
- return this.props.translate('paymentMethodList.defaultPaymentMethod');
- }
+/**
+ * @param {String} actionPaymentMethodType
+ * @param {String|Number} activePaymentMethodID
+ * @param {String} paymentMethod
+ * @return {Boolean}
+ */
+function isPaymentMethodActive(actionPaymentMethodType, activePaymentMethodID, paymentMethod) {
+ return paymentMethod.accountType === actionPaymentMethodType && paymentMethod.methodID === activePaymentMethodID;
+}
+function PaymentMethodList(props) {
+ const {actionPaymentMethodType, activePaymentMethodID, bankAccountList, cardList, filterType, network, onPress, payPalMeData, shouldShowSelectedState, selectedMethodID, translate} =
+ props;
- /**
- * @returns {Array}
- */
- getFilteredPaymentMethods() {
+ const filteredPaymentMethods = useMemo(() => {
// Hide any billing cards that are not P2P debit cards for now because you cannot make them your default method, or delete them
- const filteredCardList = _.filter(this.props.cardList, (card) => card.accountData.additionalData.isP2PDebitCard);
- let combinedPaymentMethods = PaymentUtils.formatPaymentMethods(this.props.bankAccountList, filteredCardList, this.props.payPalMeData);
+ const filteredCardList = _.filter(cardList, (card) => card.accountData.additionalData.isP2PDebitCard);
+ let combinedPaymentMethods = PaymentUtils.formatPaymentMethods(bankAccountList, filteredCardList, payPalMeData);
- if (!_.isEmpty(this.props.filterType)) {
- combinedPaymentMethods = _.filter(combinedPaymentMethods, (paymentMethod) => paymentMethod.accountType === this.props.filterType);
+ if (!_.isEmpty(filterType)) {
+ combinedPaymentMethods = _.filter(combinedPaymentMethods, (paymentMethod) => paymentMethod.accountType === filterType);
}
- if (!this.props.network.isOffline) {
+ if (!network.isOffline) {
combinedPaymentMethods = _.filter(
combinedPaymentMethods,
(paymentMethod) => paymentMethod.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || !_.isEmpty(paymentMethod.errors),
@@ -136,44 +155,23 @@ class PaymentMethodList extends Component {
combinedPaymentMethods = _.map(combinedPaymentMethods, (paymentMethod) => ({
...paymentMethod,
- onPress: (e) => this.props.onPress(e, paymentMethod.accountType, paymentMethod.accountData, paymentMethod.isDefault, paymentMethod.methodID),
- iconFill: this.isPaymentMethodActive(paymentMethod) ? StyleUtils.getIconFillColor(CONST.BUTTON_STATES.PRESSED) : null,
- wrapperStyle: this.isPaymentMethodActive(paymentMethod) ? [StyleUtils.getButtonBackgroundColorStyle(CONST.BUTTON_STATES.PRESSED)] : null,
+ onPress: (e) => onPress(e, paymentMethod.accountType, paymentMethod.accountData, paymentMethod.isDefault, paymentMethod.methodID),
+ iconFill: isPaymentMethodActive(actionPaymentMethodType, activePaymentMethodID, paymentMethod) ? StyleUtils.getIconFillColor(CONST.BUTTON_STATES.PRESSED) : null,
+ wrapperStyle: isPaymentMethodActive(actionPaymentMethodType, activePaymentMethodID, paymentMethod)
+ ? [StyleUtils.getButtonBackgroundColorStyle(CONST.BUTTON_STATES.PRESSED)]
+ : null,
disabled: paymentMethod.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE,
}));
return combinedPaymentMethods;
- }
-
- /**
- * Dismisses the error on the payment method
- * @param {Object} item
- */
- dismissError(item) {
- const paymentList = item.accountType === CONST.PAYMENT_METHODS.BANK_ACCOUNT ? ONYXKEYS.BANK_ACCOUNT_LIST : ONYXKEYS.CARD_LIST;
- const paymentID = item.accountType === CONST.PAYMENT_METHODS.BANK_ACCOUNT ? lodashGet(item, ['accountData', 'bankAccountID'], '') : lodashGet(item, ['accountData', 'fundID'], '');
-
- if (!paymentID) {
- Log.info('Unable to clear payment method error: ', item);
- return;
- }
-
- if (item.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
- PaymentMethods.clearDeletePaymentMethodError(paymentList, paymentID);
- } else {
- PaymentMethods.clearAddPaymentMethodError(paymentList, paymentID);
- }
- }
+ }, [actionPaymentMethodType, activePaymentMethodID, bankAccountList, cardList, filterType, network, onPress, payPalMeData]);
/**
- * @param {Object} paymentMethod
- * @param {String|Number} paymentMethod.methodID
- * @param {String} paymentMethod.accountType
- * @return {Boolean}
+ * Render placeholder when there are no payments methods
+ *
+ * @return {React.Component}
*/
- isPaymentMethodActive(paymentMethod) {
- return paymentMethod.accountType === this.props.actionPaymentMethodType && paymentMethod.methodID === this.props.activePaymentMethodID;
- }
+ const renderListEmptyComponent = useCallback(() => {translate('paymentMethodList.addFirstPaymentMethod')}, [translate]);
/**
* Create a menuItem for each passed paymentMethod
@@ -183,10 +181,10 @@ class PaymentMethodList extends Component {
*
* @return {React.Component}
*/
- renderItem({item}) {
- return (
+ const renderItem = useCallback(
+ ({item}) => (
this.dismissError(item)}
+ onClose={() => dismissError(item)}
pendingAction={item.pendingAction}
errors={item.errors}
errorRowStyles={styles.ph6}
@@ -200,59 +198,50 @@ class PaymentMethodList extends Component {
iconFill={item.iconFill}
iconHeight={item.iconSize}
iconWidth={item.iconSize}
- badgeText={this.getDefaultBadgeText(item.isDefault)}
+ badgeText={shouldShowDefaultBadge(filteredPaymentMethods, item.isDefault) ? translate('paymentMethodList.defaultPaymentMethod') : null}
wrapperStyle={item.wrapperStyle}
- shouldShowSelectedState={this.props.shouldShowSelectedState}
- isSelected={this.props.selectedMethodID === item.methodID}
+ shouldShowSelectedState={shouldShowSelectedState}
+ isSelected={selectedMethodID === item.methodID}
/>
- );
- }
-
- /**
- * Show add first payment copy when payment methods are
- *
- * @return {React.Component}
- */
- renderListEmptyComponent() {
- return {this.props.translate('paymentMethodList.addFirstPaymentMethod')};
- }
-
- render() {
- return (
- <>
- item.key}
- ListEmptyComponent={this.renderListEmptyComponent()}
- ListHeaderComponent={this.props.listHeaderComponent}
- />
- {this.props.shouldShowAddPaymentMethodButton && (
-
- {(isOffline) => (
-
- )}
- >
- );
- }
+ ),
+ [shouldShowSelectedState, selectedMethodID, filteredPaymentMethods, translate],
+ );
+
+ return (
+ <>
+ item.key}
+ ListEmptyComponent={renderListEmptyComponent(translate)}
+ ListHeaderComponent={props.listHeaderComponent}
+ />
+ {props.shouldShowAddPaymentMethodButton && (
+
+ {(isOffline) => (
+
+ )}
+
+ )}
+ >
+ );
}
PaymentMethodList.propTypes = propTypes;
PaymentMethodList.defaultProps = defaultProps;
+PaymentMethodList.displayName = 'PaymentMethodList';
export default compose(
withLocalize,
diff --git a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js
index ac91b2bd402e..8c97c2706ebd 100644
--- a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js
+++ b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js
@@ -215,6 +215,7 @@ class ContactMethodDetailsPage extends Component {
onCancel={() => this.toggleDeleteModal(false)}
prompt={this.props.translate('contacts.removeAreYouSure')}
confirmText={this.props.translate('common.yesContinue')}
+ cancelText={this.props.translate('common.cancel')}
isVisible={this.state.isDeleteModalOpen}
danger
/>
diff --git a/src/pages/settings/Profile/ProfilePage.js b/src/pages/settings/Profile/ProfilePage.js
index e908aef0b338..9d0a83855be0 100755
--- a/src/pages/settings/Profile/ProfilePage.js
+++ b/src/pages/settings/Profile/ProfilePage.js
@@ -9,7 +9,6 @@ import AvatarWithImagePicker from '../../../components/AvatarWithImagePicker';
import HeaderWithBackButton from '../../../components/HeaderWithBackButton';
import MenuItem from '../../../components/MenuItem';
import MenuItemWithTopDescription from '../../../components/MenuItemWithTopDescription';
-import OfflineWithFeedback from '../../../components/OfflineWithFeedback';
import ScreenWrapper from '../../../components/ScreenWrapper';
import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '../../../components/withCurrentUserPersonalDetails';
import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize';
@@ -88,22 +87,19 @@ const ProfilePage = (props) => {
onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS)}
/>
-
-
-
+ onErrorClose={PersonalDetails.clearAvatarErrors}
+ />
{_.map(profileSettingsOptions, (detail, index) => (
{this.props.translate(`newRoomPage.visibilityOptions.${this.props.report.visibility}`)}
-
- {this.props.report.visibility === CONST.REPORT.VISIBILITY.RESTRICTED
- ? this.props.translate('newRoomPage.restrictedDescription')
- : this.props.translate('newRoomPage.privateDescription')}
-
+ {this.props.translate(`newRoomPage.${this.props.report.visibility}Description`)}
)}
diff --git a/src/pages/settings/Security/CloseAccountPage.js b/src/pages/settings/Security/CloseAccountPage.js
index 913a30d4d624..c5189727a3b7 100644
--- a/src/pages/settings/Security/CloseAccountPage.js
+++ b/src/pages/settings/Security/CloseAccountPage.js
@@ -126,9 +126,10 @@ class CloseAccountPage extends Component {
onCancel={this.hideConfirmModal}
isVisible={this.state.isConfirmModalVisible}
prompt={this.props.translate('closeAccountPage.closeAccountPermanentlyDeleteData')}
- confirmText={this.props.translate('common.yes')}
+ confirmText={this.props.translate('common.yesContinue')}
cancelText={this.props.translate('common.cancel')}
shouldShowCancelButton
+ danger
/>
diff --git a/src/pages/signin/SignInPage.js b/src/pages/signin/SignInPage.js
index a875c25359b0..dcfc7b67a56a 100644
--- a/src/pages/signin/SignInPage.js
+++ b/src/pages/signin/SignInPage.js
@@ -1,30 +1,27 @@
-import React, {Component} from 'react';
+import React, {useEffect} from 'react';
+import {View} from 'react-native';
import PropTypes from 'prop-types';
import {withOnyx} from 'react-native-onyx';
-import {View} from 'react-native';
-import lodashGet from 'lodash/get';
import Str from 'expensify-common/lib/str';
-import {withSafeAreaInsets} from 'react-native-safe-area-context';
+import {useSafeAreaInsets} from 'react-native-safe-area-context';
import ONYXKEYS from '../../ONYXKEYS';
import styles from '../../styles/styles';
-import compose from '../../libs/compose';
import SignInPageLayout from './SignInPageLayout';
import LoginForm from './LoginForm';
import PasswordForm from './PasswordForm';
import ValidateCodeForm from './ValidateCodeForm';
import ResendValidationForm from './ResendValidationForm';
-import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize';
import Performance from '../../libs/Performance';
import * as App from '../../libs/actions/App';
-import Permissions from '../../libs/Permissions';
import UnlinkLoginForm from './UnlinkLoginForm';
-import withWindowDimensions, {windowDimensionsPropTypes} from '../../components/withWindowDimensions';
import * as Localize from '../../libs/Localize';
+import useLocalize from '../../hooks/useLocalize';
+import usePermissions from '../../hooks/usePermissions';
+import useWindowDimensions from '../../hooks/useWindowDimensions';
+import Log from '../../libs/Log';
import * as StyleUtils from '../../styles/StyleUtils';
const propTypes = {
- /* Onyx Props */
-
/** The details about the account that the user is signing in with */
account: PropTypes.shape({
/** Error to display when there is an account error returned */
@@ -35,153 +32,149 @@ const propTypes = {
/** The primaryLogin associated with the account */
primaryLogin: PropTypes.string,
- }),
- /** List of betas available to current user */
- betas: PropTypes.arrayOf(PropTypes.string),
+ /** Has the user pressed the forgot password button? */
+ forgotPassword: PropTypes.bool,
+
+ /** Does this account require 2FA? */
+ requiresTwoFactorAuth: PropTypes.bool,
+ }),
/** The credentials of the person signing in */
credentials: PropTypes.shape({
login: PropTypes.string,
password: PropTypes.string,
twoFactorAuthCode: PropTypes.string,
+ validateCode: PropTypes.string,
}),
-
- ...withLocalizePropTypes,
-
- ...windowDimensionsPropTypes,
};
const defaultProps = {
account: {},
- betas: [],
credentials: {},
};
-class SignInPage extends Component {
- componentDidMount() {
- Performance.measureTTI();
+/**
+ * @param {Boolean} hasLogin
+ * @param {Boolean} hasPassword
+ * @param {Boolean} hasValidateCode
+ * @param {Boolean} isPrimaryLogin
+ * @param {Boolean} isAccountValidated
+ * @param {Boolean} didForgetPassword
+ * @param {Boolean} canUsePasswordlessLogins
+ * @returns {Object}
+ */
+function getRenderOptions({hasLogin, hasPassword, hasValidateCode, isPrimaryLogin, isAccountValidated, didForgetPassword, canUsePasswordlessLogins}) {
+ const shouldShowLoginForm = !hasLogin && !hasValidateCode;
+ const isUnvalidatedSecondaryLogin = hasLogin && !isPrimaryLogin && !isAccountValidated;
+ const shouldShowPasswordForm = hasLogin && isAccountValidated && !hasPassword && !didForgetPassword && !isUnvalidatedSecondaryLogin && !canUsePasswordlessLogins;
+ const shouldShowValidateCodeForm = (hasLogin || hasValidateCode) && !isUnvalidatedSecondaryLogin && canUsePasswordlessLogins;
+ const shouldShowResendValidationForm = hasLogin && (!isAccountValidated || didForgetPassword) && !isUnvalidatedSecondaryLogin && !canUsePasswordlessLogins;
+ const shouldShowWelcomeHeader = shouldShowLoginForm || shouldShowPasswordForm || shouldShowValidateCodeForm || isUnvalidatedSecondaryLogin;
+ const shouldShowWelcomeText = shouldShowLoginForm || shouldShowPasswordForm || shouldShowValidateCodeForm;
+ return {
+ shouldShowLoginForm,
+ shouldShowUnlinkLoginForm: isUnvalidatedSecondaryLogin,
+ shouldShowPasswordForm,
+ shouldShowValidateCodeForm,
+ shouldShowResendValidationForm,
+ shouldShowWelcomeHeader,
+ shouldShowWelcomeText,
+ };
+}
- App.setLocale(Localize.getDevicePreferredLocale());
- }
+function SignInPage({account, credentials}) {
+ const {translate, formatPhoneNumber} = useLocalize();
+ const {canUsePasswordlessLogins} = usePermissions();
+ const {isSmallScreenWidth} = useWindowDimensions();
+ const safeAreaInsets = useSafeAreaInsets();
- render() {
- // Show the login form if
- // - A login has not been entered yet
- // - AND a validateCode has not been cached with sign in link
- const showLoginForm = !this.props.credentials.login && !this.props.credentials.validateCode;
-
- // Show the unlink form if
- // - A login has been entered
- // - AND the login is not the primary login
- // - AND the login is not validated
- const showUnlinkLoginForm =
- Boolean(this.props.credentials.login && this.props.account.primaryLogin) && this.props.account.primaryLogin !== this.props.credentials.login && !this.props.account.validated;
-
- // Show the old password form if
- // - A login has been entered
- // - AND an account exists and is validated for this login
- // - AND a password hasn't been entered yet
- // - AND haven't forgotten password
- // - AND the login isn't an unvalidated secondary login
- // - AND the user is NOT on the passwordless beta
- const showPasswordForm =
- Boolean(this.props.credentials.login) &&
- this.props.account.validated &&
- !this.props.credentials.password &&
- !this.props.account.forgotPassword &&
- !showUnlinkLoginForm &&
- !Permissions.canUsePasswordlessLogins(this.props.betas);
-
- // Show the new magic code / validate code form if
- // - A login has been entered or a validateCode has been cached from sign in link
- // - AND the login isn't an unvalidated secondary login
- // - AND the user is on the 'passwordless' beta
- const showValidateCodeForm =
- Boolean(this.props.credentials.login || this.props.credentials.validateCode) && !showUnlinkLoginForm && Permissions.canUsePasswordlessLogins(this.props.betas);
-
- // Show the resend validation link form if
- // - A login has been entered
- // - AND is not validated or password is forgotten
- // - AND the login isn't an unvalidated secondary login
- // - AND user is not on 'passwordless' beta
- const showResendValidationForm =
- Boolean(this.props.credentials.login) &&
- (!this.props.account.validated || this.props.account.forgotPassword) &&
- !showUnlinkLoginForm &&
- !Permissions.canUsePasswordlessLogins(this.props.betas);
-
- let welcomeHeader = '';
- let welcomeText = '';
- if (showValidateCodeForm) {
- if (this.props.account.requiresTwoFactorAuth) {
- // We will only know this after a user signs in successfully, without their 2FA code
- welcomeHeader = this.props.isSmallScreenWidth ? '' : this.props.translate('welcomeText.welcomeBack');
- welcomeText = this.props.translate('validateCodeForm.enterAuthenticatorCode');
+ useEffect(() => Performance.measureTTI(), []);
+ useEffect(() => {
+ App.setLocale(Localize.getDevicePreferredLocale());
+ }, []);
+
+ const {
+ shouldShowLoginForm,
+ shouldShowUnlinkLoginForm,
+ shouldShowPasswordForm,
+ shouldShowValidateCodeForm,
+ shouldShowResendValidationForm,
+ shouldShowWelcomeHeader,
+ shouldShowWelcomeText,
+ } = getRenderOptions({
+ hasLogin: Boolean(credentials.login),
+ hasPassword: Boolean(credentials.password),
+ hasValidateCode: Boolean(credentials.validateCode),
+ isPrimaryLogin: account.primaryLogin && account.primaryLogin === credentials.login,
+ isAccountValidated: Boolean(account.validated),
+ didForgetPassword: Boolean(account.forgotPassword),
+ canUsePasswordlessLogins,
+ });
+
+ let welcomeHeader;
+ let welcomeText;
+ if (shouldShowValidateCodeForm) {
+ if (account.requiresTwoFactorAuth) {
+ // We will only know this after a user signs in successfully, without their 2FA code
+ welcomeHeader = isSmallScreenWidth ? '' : translate('welcomeText.welcomeBack');
+ welcomeText = translate('validateCodeForm.enterAuthenticatorCode');
+ } else {
+ const userLogin = Str.removeSMSDomain(credentials.login || '');
+
+ // replacing spaces with "hard spaces" to prevent breaking the number
+ const userLoginToDisplay = Str.isSMSLogin(userLogin) ? formatPhoneNumber(userLogin).replace(/ /g, '\u00A0') : userLogin;
+ if (account.validated) {
+ welcomeHeader = isSmallScreenWidth ? '' : translate('welcomeText.welcomeBack');
+ welcomeText = isSmallScreenWidth
+ ? `${translate('welcomeText.welcomeBack')} ${translate('welcomeText.welcomeEnterMagicCode', {login: userLoginToDisplay})}`
+ : translate('welcomeText.welcomeEnterMagicCode', {login: userLoginToDisplay});
} else {
- const userLogin = Str.removeSMSDomain(lodashGet(this.props, 'credentials.login', ''));
-
- // replacing spaces with "hard spaces" to prevent breaking the number
- const userLoginToDisplay = Str.isSMSLogin(userLogin) ? this.props.formatPhoneNumber(userLogin).replace(/ /g, '\u00A0') : userLogin;
- if (this.props.account.validated) {
- welcomeHeader = this.props.isSmallScreenWidth ? '' : this.props.translate('welcomeText.welcomeBack');
- welcomeText = this.props.isSmallScreenWidth
- ? `${this.props.translate('welcomeText.welcomeBack')} ${this.props.translate('welcomeText.welcomeEnterMagicCode', {login: userLoginToDisplay})}`
- : this.props.translate('welcomeText.welcomeEnterMagicCode', {login: userLoginToDisplay});
- } else {
- welcomeHeader = this.props.isSmallScreenWidth ? '' : this.props.translate('welcomeText.welcome');
- welcomeText = this.props.isSmallScreenWidth
- ? `${this.props.translate('welcomeText.welcome')} ${this.props.translate('welcomeText.newFaceEnterMagicCode', {login: userLoginToDisplay})}`
- : this.props.translate('welcomeText.newFaceEnterMagicCode', {login: userLoginToDisplay});
- }
+ welcomeHeader = isSmallScreenWidth ? '' : translate('welcomeText.welcome');
+ welcomeText = isSmallScreenWidth
+ ? `${translate('welcomeText.welcome')} ${translate('welcomeText.newFaceEnterMagicCode', {login: userLoginToDisplay})}`
+ : translate('welcomeText.newFaceEnterMagicCode', {login: userLoginToDisplay});
}
- } else if (showPasswordForm) {
- welcomeHeader = this.props.isSmallScreenWidth ? '' : this.props.translate('welcomeText.welcomeBack');
- welcomeText = this.props.isSmallScreenWidth
- ? `${this.props.translate('welcomeText.welcomeBack')} ${this.props.translate('welcomeText.enterPassword')}`
- : this.props.translate('welcomeText.enterPassword');
- } else if (showUnlinkLoginForm) {
- welcomeHeader = this.props.isSmallScreenWidth ? this.props.translate('login.hero.header') : this.props.translate('welcomeText.welcomeBack');
- } else if (!showResendValidationForm) {
- welcomeHeader = this.props.isSmallScreenWidth ? this.props.translate('login.hero.header') : this.props.translate('welcomeText.getStarted');
- welcomeText = this.props.isSmallScreenWidth ? this.props.translate('welcomeText.getStarted') : '';
}
+ } else if (shouldShowPasswordForm) {
+ welcomeHeader = isSmallScreenWidth ? '' : translate('welcomeText.welcomeBack');
+ welcomeText = isSmallScreenWidth ? `${translate('welcomeText.welcomeBack')} ${translate('welcomeText.enterPassword')}` : translate('welcomeText.enterPassword');
+ } else if (shouldShowUnlinkLoginForm) {
+ welcomeHeader = isSmallScreenWidth ? translate('login.hero.header') : translate('welcomeText.welcomeBack');
+ } else if (!shouldShowResendValidationForm) {
+ welcomeHeader = isSmallScreenWidth ? translate('login.hero.header') : translate('welcomeText.getStarted');
+ welcomeText = isSmallScreenWidth ? translate('welcomeText.getStarted') : '';
+ } else {
+ Log.warn('SignInPage in unexpected state!');
+ }
- return (
- // There is an issue SafeAreaView on Android where wrong insets flicker on app start.
- // Can be removed once https://github.com/th3rdwave/react-native-safe-area-context/issues/364 is resolved.
-
-
- {/* LoginForm and PasswordForm must use the isVisible prop. This keeps them mounted, but visually hidden
+ return (
+
+
+ {/* LoginForm and PasswordForm must use the isVisible prop. This keeps them mounted, but visually hidden
so that password managers can access the values. Conditionally rendering these components will break this feature. */}
-
- {showValidateCodeForm ? : }
- {showResendValidationForm && }
- {showUnlinkLoginForm && }
-
-
- );
- }
+
+ {shouldShowValidateCodeForm ? : }
+ {shouldShowResendValidationForm && }
+ {shouldShowUnlinkLoginForm && }
+
+
+ );
}
SignInPage.propTypes = propTypes;
SignInPage.defaultProps = defaultProps;
+SignInPage.displayName = 'SignInPage';
-export default compose(
- withSafeAreaInsets,
- withLocalize,
- withWindowDimensions,
- withOnyx({
- account: {key: ONYXKEYS.ACCOUNT},
- betas: {key: ONYXKEYS.BETAS},
- credentials: {key: ONYXKEYS.CREDENTIALS},
- }),
-)(SignInPage);
+export default withOnyx({
+ account: {key: ONYXKEYS.ACCOUNT},
+ credentials: {key: ONYXKEYS.CREDENTIALS},
+})(SignInPage);
diff --git a/src/pages/signin/UnlinkLoginForm.js b/src/pages/signin/UnlinkLoginForm.js
index 884b4246db51..adf44aeb1d8b 100644
--- a/src/pages/signin/UnlinkLoginForm.js
+++ b/src/pages/signin/UnlinkLoginForm.js
@@ -51,8 +51,8 @@ const defaultProps = {
};
const UnlinkLoginForm = (props) => {
- const primaryLogin = Str.isSMSLogin(props.account.primaryLogin) ? Str.removeSMSDomain(props.account.primaryLogin) : props.account.primaryLogin;
- const secondaryLogin = Str.isSMSLogin(props.credentials.login) ? Str.removeSMSDomain(props.credentials.login) : props.credentials.login;
+ const primaryLogin = Str.isSMSLogin(props.account.primaryLogin || '') ? Str.removeSMSDomain(props.account.primaryLogin || '') : props.account.primaryLogin;
+ const secondaryLogin = Str.isSMSLogin(props.credentials.login || '') ? Str.removeSMSDomain(props.credentials.login || '') : props.credentials.login;
return (
<>
@@ -67,7 +67,7 @@ const UnlinkLoginForm = (props) => {
)}
{!_.isEmpty(props.account.errors) && (
diff --git a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js
index 1d44e4357e0c..7338ce3525e1 100755
--- a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js
+++ b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js
@@ -201,6 +201,7 @@ class BaseValidateCodeForm extends React.Component {
render() {
const hasError = Boolean(this.props.account) && !_.isEmpty(this.props.account.errors);
+ const resendButtonStyle = this.props.network.isOffline ? styles.buttonOpacityDisabled : {};
return (
<>
{/* At this point, if we know the account requires 2FA we already successfully authenticated */}
@@ -239,9 +240,10 @@ class BaseValidateCodeForm extends React.Component {
{this.props.account.message ? this.props.translate(this.props.account.message) : ''}
) : (
{
shouldShowBackButton
onBackButtonPress={() => Navigation.goBack(ROUTES.NEW_TASK_DETAILS)}
/>
-
-
-
- Navigation.navigate(ROUTES.NEW_TASK_TITLE)}
- label="newTaskPage.title"
- />
-
-
- Navigation.navigate(ROUTES.NEW_TASK_DESCRIPTION)}
- label="newTaskPage.description"
- />
-
-
- Navigation.navigate(ROUTES.NEW_TASK_ASSIGNEE)}
- label="newTaskPage.assignee"
- />
-
-
- Navigation.navigate(ROUTES.NEW_TASK_SHARE_DESTINATION)}
- label="newTaskPage.shareSomewhere"
- isShareDestination
- disabled={Boolean(props.task.parentReportID)}
- />
-
+
+
+ Navigation.navigate(ROUTES.NEW_TASK_TITLE)}
+ shouldShowRightIcon
+ />
+ Navigation.navigate(ROUTES.NEW_TASK_DESCRIPTION)}
+ shouldShowRightIcon
+ />
+
{
onSubmit={() => onSubmit()}
enabledWhenOffline
buttonText={props.translate('newTaskPage.confirmTask')}
- containerStyles={[styles.mh0, styles.mt5, styles.flex1]}
+ containerStyles={[styles.mh0, styles.mt5, styles.flex1, styles.ph5]}
/>
diff --git a/src/pages/workspace/WorkspaceInitialPage.js b/src/pages/workspace/WorkspaceInitialPage.js
index 92d8e0fe0f3d..e08ee256ca3c 100644
--- a/src/pages/workspace/WorkspaceInitialPage.js
+++ b/src/pages/workspace/WorkspaceInitialPage.js
@@ -1,7 +1,7 @@
import _ from 'underscore';
import lodashGet from 'lodash/get';
import React, {useCallback, useState} from 'react';
-import {View, ScrollView, Pressable} from 'react-native';
+import {View, ScrollView} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import PropTypes from 'prop-types';
import Navigation from '../../libs/Navigation/Navigation';
@@ -30,6 +30,7 @@ import OfflineWithFeedback from '../../components/OfflineWithFeedback';
import * as ReimbursementAccountProps from '../ReimbursementAccount/reimbursementAccountPropTypes';
import * as ReportUtils from '../../libs/ReportUtils';
import withWindowDimensions from '../../components/withWindowDimensions';
+import PressableWithoutFeedback from '../../components/Pressable/PressableWithoutFeedback';
const propTypes = {
...policyPropTypes,
@@ -163,8 +164,9 @@ const WorkspaceInitialPage = (props) => {
{({safeAreaPaddingBottomStyle}) => (
Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)}
+ shouldShow={_.isEmpty(props.policy) || !Policy.isPolicyOwner(props.policy)}
+ subtitleKey={_.isEmpty(props.policy) ? undefined : 'workspace.common.notAuthorized'}
>
{
- openEditor(policy.id)}
+ accessibilityLabel={props.translate('workspace.common.settings')}
+ accessibilityRole="button"
>
{
name={policyName}
type={CONST.ICON_TYPE_WORKSPACE}
/>
-
+
{!_.isEmpty(policy.name) && (
- openEditor(policy.id)}
+ accessibilityLabel={props.translate('workspace.common.settings')}
+ accessibilityRole="button"
>
{
>
{policy.name}
-
+
)}
diff --git a/src/pages/workspace/WorkspaceMembersPage.js b/src/pages/workspace/WorkspaceMembersPage.js
index 19db6d28bb70..4db6f45e7e4c 100644
--- a/src/pages/workspace/WorkspaceMembersPage.js
+++ b/src/pages/workspace/WorkspaceMembersPage.js
@@ -345,7 +345,6 @@ class WorkspaceMembersPage extends React.Component {
/>
this.toggleUser(item.login, item.pendingAction)}
/>
{(this.props.session.email === item.login || item.role === 'admin') && (
diff --git a/src/pages/workspace/WorkspacePageWithSections.js b/src/pages/workspace/WorkspacePageWithSections.js
index 97f8fd6795e0..7878d649ea56 100644
--- a/src/pages/workspace/WorkspacePageWithSections.js
+++ b/src/pages/workspace/WorkspacePageWithSections.js
@@ -6,6 +6,7 @@ import lodashGet from 'lodash/get';
import _ from 'underscore';
import styles from '../../styles/styles';
import Navigation from '../../libs/Navigation/Navigation';
+import * as Policy from '../../libs/actions/Policy';
import compose from '../../libs/compose';
import ROUTES from '../../ROUTES';
import HeaderWithBackButton from '../../components/HeaderWithBackButton';
@@ -115,8 +116,9 @@ class WorkspacePageWithSections extends React.Component {
shouldEnablePickerAvoiding={false}
>
Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)}
+ shouldShow={_.isEmpty(this.props.policy) || !Policy.isPolicyOwner(this.props.policy)}
+ subtitleKey={_.isEmpty(this.props.policy) ? undefined : 'workspace.common.notAuthorized'}
>
- (
+
+ )}
+ type={CONST.ICON_TYPE_WORKSPACE}
+ fallbackIcon={Expensicons.FallbackWorkspaceAvatar}
+ style={[styles.mb3]}
+ anchorPosition={styles.createMenuPositionProfile(props.windowWidth)}
+ anchorAlignment={{horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP}}
+ isUsingDefaultAvatar={!lodashGet(props.policy, 'avatar', null)}
+ onImageSelected={(file) => Policy.updateWorkspaceAvatar(lodashGet(props.policy, 'id', ''), file)}
+ onImageRemoved={() => Policy.deleteWorkspaceAvatar(lodashGet(props.policy, 'id', ''))}
+ editorMaskImage={Expensicons.ImageCropSquareMask}
pendingAction={lodashGet(props.policy, 'pendingFields.avatar', null)}
errors={lodashGet(props.policy, 'errorFields.avatar', null)}
- onClose={() => Policy.clearAvatarErrors(props.policy.id)}
- >
- (
-
- )}
- type={CONST.ICON_TYPE_WORKSPACE}
- fallbackIcon={Expensicons.FallbackWorkspaceAvatar}
- style={[styles.mb3]}
- anchorPosition={styles.createMenuPositionProfile(props.windowWidth)}
- anchorAlignment={{horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.CENTER, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP}}
- isUsingDefaultAvatar={!lodashGet(props.policy, 'avatar', null)}
- onImageSelected={(file) => Policy.updateWorkspaceAvatar(lodashGet(props.policy, 'id', ''), file)}
- onImageRemoved={() => Policy.deleteWorkspaceAvatar(lodashGet(props.policy, 'id', ''))}
- editorMaskImage={Expensicons.ImageCropSquareMask}
- />
-
+ onErrorClose={() => Policy.clearAvatarErrors(props.policy.id)}
+ />
({
+ // We add CONST.DESKTOP_HEADER_GAP on desktop which we
+ // need to add to vertical offset to have proper vertical
+ // offset on desktop
+ vertical: vertical + CONST.DESKTOP_HEADER_PADDING,
+});
diff --git a/src/styles/getPopOverVerticalOffset/index.js b/src/styles/getPopOverVerticalOffset/index.js
new file mode 100644
index 000000000000..2e7c4fbc07ff
--- /dev/null
+++ b/src/styles/getPopOverVerticalOffset/index.js
@@ -0,0 +1 @@
+export default (vertical) => ({vertical});
diff --git a/src/styles/styles.js b/src/styles/styles.js
index 70cfac3de847..1144e27412a8 100644
--- a/src/styles/styles.js
+++ b/src/styles/styles.js
@@ -18,10 +18,12 @@ import writingDirection from './utilities/writingDirection';
import optionAlternateTextPlatformStyles from './optionAlternateTextPlatformStyles';
import pointerEventsNone from './pointerEventsNone';
import pointerEventsAuto from './pointerEventsAuto';
+import getPopOverVerticalOffset from './getPopOverVerticalOffset';
import overflowXHidden from './overflowXHidden';
import CONST from '../CONST';
import cursor from './utilities/cursor';
import userSelect from './utilities/userSelect';
+import textUnderline from './utilities/textUnderline';
const picker = {
backgroundColor: themeColors.transparent,
@@ -162,6 +164,7 @@ const styles = {
...cursor,
...userSelect,
...themeColors,
+ ...textUnderline,
rateCol: {
margin: 0,
@@ -188,13 +191,11 @@ const styles = {
},
emojiSuggestionsEmoji: {
- fontFamily: fontFamily.EMOJI_TEXT_FONT,
fontSize: variables.fontSizeMedium,
width: 51,
textAlign: 'center',
},
emojiSuggestionsText: {
- fontFamily: fontFamily.EMOJI_TEXT_FONT,
fontSize: variables.fontSizeMedium,
},
@@ -275,14 +276,6 @@ const styles = {
textDecorationLine: 'underline',
},
- textUnderlinePositionUnder: {
- textUnderlinePosition: 'under',
- },
-
- textDecorationSkipInkNone: {
- textDecorationSkipInk: 'none',
- },
-
label: {
fontSize: variables.fontSizeLabel,
lineHeight: variables.lineHeightLarge,
@@ -768,7 +761,7 @@ const styles = {
},
headerGap: {
- height: 12,
+ height: CONST.DESKTOP_HEADER_PADDING,
},
pushTextRight: {
@@ -1272,7 +1265,7 @@ const styles = {
createMenuPositionProfile: (windowWidth) => ({
horizontal: windowWidth - 355,
- vertical: 250,
+ ...getPopOverVerticalOffset(162),
}),
createMenuPositionReportActionCompose: (windowHeight) => ({
@@ -1592,7 +1585,7 @@ const styles = {
backgroundColor: themeColors.componentBG,
borderColor: themeColors.border,
color: themeColors.text,
- fontFamily: fontFamily.EMOJI_TEXT_FONT,
+ fontFamily: fontFamily.EXP_NEUE,
fontSize: variables.fontSizeNormal,
borderWidth: 0,
height: 'auto',
@@ -1668,7 +1661,6 @@ const styles = {
// Emoji Picker Styles
emojiText: {
- fontFamily: fontFamily.EMOJI_TEXT_FONT,
textAlign: 'center',
fontSize: variables.emojiSize,
...spacing.pv0,
@@ -1733,13 +1725,6 @@ const styles = {
backgroundColor: themeColors.transparent,
},
- taskSelectorLink: {
- alignSelf: 'center',
- width: '100%',
- padding: 6,
- backgroundColor: themeColors.transparent,
- },
-
chatItemAttachmentPlaceholder: {
backgroundColor: themeColors.sidebar,
borderColor: themeColors.border,
@@ -2979,12 +2964,12 @@ const styles = {
},
threeDotsPopoverOffset: (windowWidth) => ({
- vertical: 50,
+ ...getPopOverVerticalOffset(60),
horizontal: windowWidth - 60,
}),
threeDotsPopoverOffsetNoCloseButton: (windowWidth) => ({
- vertical: 50,
+ ...getPopOverVerticalOffset(60),
horizontal: windowWidth - 10,
}),
diff --git a/src/styles/utilities/textUnderline/index.js b/src/styles/utilities/textUnderline/index.js
new file mode 100644
index 000000000000..3ec9ce507765
--- /dev/null
+++ b/src/styles/utilities/textUnderline/index.js
@@ -0,0 +1,9 @@
+const textUnderlinePositionUnder = {
+ textUnderlinePosition: 'under',
+};
+
+const textDecorationSkipInkNone = {
+ textDecorationSkipInk: 'none',
+};
+
+export default {textUnderlinePositionUnder, textDecorationSkipInkNone};
diff --git a/src/styles/utilities/textUnderline/index.native.js b/src/styles/utilities/textUnderline/index.native.js
new file mode 100644
index 000000000000..ce2e2e7d6c20
--- /dev/null
+++ b/src/styles/utilities/textUnderline/index.native.js
@@ -0,0 +1,4 @@
+// following styles are not supported
+const textUnderlinePositionUnder = {};
+const textDecorationSkipInkNone = {};
+export default {textUnderlinePositionUnder, textDecorationSkipInkNone};