diff --git a/android/app/build.gradle b/android/app/build.gradle
index 33c70ddd1050..7125a88e1d70 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 1001032911
- versionName "1.3.29-11"
+ versionCode 1001033000
+ versionName "1.3.30-0"
}
splits {
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 1627d429f5b9..59b0cebc265f 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.3.29
+ 1.3.30
CFBundleSignature
????
CFBundleURLTypes
@@ -32,7 +32,7 @@
CFBundleVersion
- 1.3.29.11
+ 1.3.30.0
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 0d8e56366705..99593696afab 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 1.3.29
+ 1.3.30
CFBundleSignature
????
CFBundleVersion
- 1.3.29.11
+ 1.3.30.0
diff --git a/package-lock.json b/package-lock.json
index a59a6b919a3e..327a78c3dbcb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.3.29-11",
+ "version": "1.3.30-0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.3.29-11",
+ "version": "1.3.30-0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index 7df1b904ccdf..1868a6f75f83 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.3.29-11",
+ "version": "1.3.30-0",
"author": "Expensify, Inc.",
"homepage": "https://new.expensify.com",
"description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",
diff --git a/patches/@react-navigation+stack+6.3.16.patch b/patches/@react-navigation+stack+6.3.16.patch
deleted file mode 100644
index 7bfa8af945f6..000000000000
--- a/patches/@react-navigation+stack+6.3.16.patch
+++ /dev/null
@@ -1,85 +0,0 @@
-diff --git a/node_modules/@react-navigation/stack/src/views/Stack/CardContainer.tsx b/node_modules/@react-navigation/stack/src/views/Stack/CardContainer.tsx
-index 1e9ee0e..d85c7b4 100644
---- a/node_modules/@react-navigation/stack/src/views/Stack/CardContainer.tsx
-+++ b/node_modules/@react-navigation/stack/src/views/Stack/CardContainer.tsx
-@@ -105,14 +105,14 @@ function CardContainer({
- const handleOpen = () => {
- const { route } = scene.descriptor;
-
-- onTransitionEnd({ route }, false);
-+ onTransitionEnd({ route }, false, scene.descriptor.navigation.getState());
- onOpenRoute({ route });
- };
-
- const handleClose = () => {
- const { route } = scene.descriptor;
-
-- onTransitionEnd({ route }, true);
-+ onTransitionEnd({ route }, true, scene.descriptor.navigation.getState());
- onCloseRoute({ route });
- };
-
-@@ -120,7 +120,7 @@ function CardContainer({
- const { route } = scene.descriptor;
-
- onPageChangeStart();
-- onGestureStart({ route });
-+ onGestureStart({ route }, scene.descriptor.navigation.getState());
- };
-
- const handleGestureCanceled = () => {
-diff --git a/node_modules/@react-navigation/stack/src/views/Stack/StackView.tsx b/node_modules/@react-navigation/stack/src/views/Stack/StackView.tsx
-index 6bbce10..73594d3 100644
---- a/node_modules/@react-navigation/stack/src/views/Stack/StackView.tsx
-+++ b/node_modules/@react-navigation/stack/src/views/Stack/StackView.tsx
-@@ -385,19 +385,47 @@ export default class StackView extends React.Component {
-
- private handleTransitionEnd = (
- { route }: { route: Route },
-- closing: boolean
-- ) =>
-+ closing: boolean,
-+ state: StackNavigationState
-+ ) => {
- this.props.navigation.emit({
- type: 'transitionEnd',
- data: { closing },
- target: route.key,
- });
-+ // Patch introduced to pass information about events to screens lower in the stack, so they could be safely frozen
-+ if (state?.index > 1) {
-+ this.props.navigation.emit({
-+ type: 'transitionEnd',
-+ data: { closing: !closing },
-+ target: state.routes[state.index - 2].key,
-+ });
-+ }
-+ // We want the screen behind the closing screen to not be frozen
-+ if (state?.index > 0) {
-+ this.props.navigation.emit({
-+ type: 'transitionEnd',
-+ data: { closing: false },
-+ target: state.routes[state.index - 1].key,
-+ });
-+ }
-+ }
-
-- private handleGestureStart = ({ route }: { route: Route }) => {
-+ private handleGestureStart = (
-+ { route }: { route: Route },
-+ state: StackNavigationState
-+ ) => {
- this.props.navigation.emit({
- type: 'gestureStart',
- target: route.key,
- });
-+ // Patch introduced to pass information about events to screens lower in the stack, so they could be safely frozen
-+ if (state?.index > 1) {
-+ this.props.navigation.emit({
-+ type: 'gestureStart',
-+ target: state.routes[state.index - 2].key,
-+ });
-+ }
- };
-
- private handleGestureEnd = ({ route }: { route: Route }) => {
diff --git a/src/components/AddPlaidBankAccount.js b/src/components/AddPlaidBankAccount.js
index 4725d607ec64..4739e9ed1f12 100644
--- a/src/components/AddPlaidBankAccount.js
+++ b/src/components/AddPlaidBankAccount.js
@@ -197,7 +197,7 @@ class AddPlaidBankAccount extends React.Component {
// Plaid bank accounts view
return (
-
+
{!_.isEmpty(this.props.text) && {this.props.text}}
-
+
);
}
}
diff --git a/src/components/AttachmentCarousel/index.js b/src/components/AttachmentCarousel/index.js
index 77ab66400ae2..d4f7553d311c 100644
--- a/src/components/AttachmentCarousel/index.js
+++ b/src/components/AttachmentCarousel/index.js
@@ -176,6 +176,14 @@ class AttachmentCarousel extends React.Component {
_.forEach(actions, (action) => htmlParser.write(_.get(action, ['message', 0, 'html'])));
htmlParser.end();
+ // Inverting the list for touchscreen devices that can swipe or have an animation when scrolling
+ // promotes the natural feeling of swiping left/right to go to the next/previous image
+ // We don't want to invert the list for desktop/web because this interferes with mouse
+ // wheel or trackpad scrolling (in cases like document preview where you can scroll vertically)
+ if (this.canUseTouchScreen) {
+ attachments.reverse();
+ }
+
const page = _.findIndex(attachments, (a) => a.source === this.props.source);
if (page === -1) {
throw new Error('Attachment not found');
@@ -195,7 +203,12 @@ class AttachmentCarousel extends React.Component {
* @param {Number} deltaSlide
*/
cycleThroughAttachments(deltaSlide) {
- const nextIndex = this.state.page - deltaSlide;
+ let delta = deltaSlide;
+ if (this.canUseTouchScreen) {
+ delta = deltaSlide * -1;
+ }
+
+ const nextIndex = this.state.page - delta;
const nextItem = this.state.attachments[nextIndex];
if (!nextItem || !this.scrollRef.current) {
@@ -262,8 +275,13 @@ class AttachmentCarousel extends React.Component {
}
render() {
- const isForwardDisabled = this.state.page === 0;
- const isBackDisabled = this.state.page === _.size(this.state.attachments) - 1;
+ let isForwardDisabled = this.state.page === 0;
+ let isBackDisabled = this.state.page === _.size(this.state.attachments) - 1;
+
+ if (this.canUseTouchScreen) {
+ isForwardDisabled = isBackDisabled;
+ isBackDisabled = this.state.page === 0;
+ }
return (
{},
};
-class AttachmentModal extends PureComponent {
- constructor(props) {
- super(props);
-
- this.state = {
- isModalOpen: false,
- shouldLoadAttachment: false,
- isAttachmentInvalid: false,
- isAuthTokenRequired: props.isAuthTokenRequired,
- attachmentInvalidReasonTitle: null,
- attachmentInvalidReason: null,
- source: props.source,
- modalType: CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE,
- isConfirmButtonDisabled: false,
- confirmButtonFadeAnimation: new Animated.Value(1),
- file: props.originalFileName
- ? {
- name: props.originalFileName,
- }
- : undefined,
- };
-
- this.submitAndClose = this.submitAndClose.bind(this);
- this.closeConfirmModal = this.closeConfirmModal.bind(this);
- this.onNavigate = this.onNavigate.bind(this);
- this.downloadAttachment = this.downloadAttachment.bind(this);
- this.validateAndDisplayFileToUpload = this.validateAndDisplayFileToUpload.bind(this);
- this.updateConfirmButtonVisibility = this.updateConfirmButtonVisibility.bind(this);
- }
+function AttachmentModal(props) {
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const [shouldLoadAttachment, setShouldLoadAttachment] = useState(false);
+ const [isAttachmentInvalid, setIsAttachmentInvalid] = useState(false);
+ const [isAuthTokenRequired] = useState(props.isAuthTokenRequired);
+ const [attachmentInvalidReasonTitle, setAttachmentInvalidReasonTitle] = useState(null);
+ const [attachmentInvalidReason, setAttachmentInvalidReason] = useState(null);
+ const [source, setSource] = useState(props.source);
+ const [modalType, setModalType] = useState(CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE);
+ const [isConfirmButtonDisabled, setIsConfirmButtonDisabled] = useState(false);
+ const [confirmButtonFadeAnimation] = useState(new Animated.Value(1));
+ const [file, setFile] = useState(
+ props.originalFileName
+ ? {
+ name: props.originalFileName,
+ }
+ : undefined,
+ );
/**
* Keeps the attachment source in sync with the attachment displayed currently in the carousel.
* @param {{ source: String, isAuthTokenRequired: Boolean, file: { name: string } }} attachment
*/
- onNavigate(attachment) {
- this.setState(attachment);
- }
+ const onNavigate = useCallback((attachment) => {
+ setSource(attachment.source);
+ setFile(attachment.file);
+ }, []);
/**
* If our attachment is a PDF, return the unswipeable Modal type.
* @param {String} sourceURL
- * @param {Object} file
+ * @param {Object} _file
* @returns {String}
*/
- getModalType(sourceURL, file) {
- return sourceURL && (Str.isPDF(sourceURL) || (file && Str.isPDF(file.name || this.props.translate('attachmentView.unknownFilename'))))
- ? CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE
- : CONST.MODAL.MODAL_TYPE.CENTERED;
- }
-
+ const getModalType = useCallback(
+ (sourceURL, _file) =>
+ sourceURL && (Str.isPDF(sourceURL) || (_file && Str.isPDF(_file.name || props.translate('attachmentView.unknownFilename'))))
+ ? CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE
+ : CONST.MODAL.MODAL_TYPE.CENTERED,
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [props.translate],
+ );
/**
* Download the currently viewed attachment.
*/
- downloadAttachment() {
- let sourceURL = this.state.source;
- if (this.state.isAuthTokenRequired) {
+ const downloadAttachment = useCallback(() => {
+ let sourceURL = source;
+ if (isAuthTokenRequired) {
sourceURL = addEncryptedAuthTokenToURL(sourceURL);
}
- fileDownload(sourceURL, this.state.file.name);
+ fileDownload(sourceURL, file.name);
// At ios, if the keyboard is open while opening the attachment, then after downloading
// the attachment keyboard will show up. So, to fix it we need to dismiss the keyboard.
Keyboard.dismiss();
- }
+ }, [isAuthTokenRequired, source, file]);
/**
* Execute the onConfirm callback and close the modal.
*/
- submitAndClose() {
+ const submitAndClose = useCallback(() => {
// If the modal has already been closed or the confirm button is disabled
// do not submit.
- if (!this.state.isModalOpen || this.state.isConfirmButtonDisabled) {
+ if (!isModalOpen || isConfirmButtonDisabled) {
return;
}
- if (this.props.onConfirm) {
- this.props.onConfirm(lodashExtend(this.state.file, {source: this.state.source}));
+ if (props.onConfirm) {
+ props.onConfirm(lodashExtend(file, {source}));
}
- this.setState({isModalOpen: false});
- }
+ setIsModalOpen(false);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [isModalOpen, isConfirmButtonDisabled, props.onConfirm, file, source]);
/**
* Close the confirm modal.
*/
- closeConfirmModal() {
- this.setState({isAttachmentInvalid: false});
- }
-
+ const closeConfirmModal = useCallback(() => {
+ setIsAttachmentInvalid(false);
+ }, []);
/**
- * @param {Object} file
+ * @param {Object} _file
* @returns {Boolean}
*/
- isValidFile(file) {
- const {fileExtension} = FileUtils.splitExtensionFromFileName(lodashGet(file, 'name', ''));
- if (_.contains(CONST.API_ATTACHMENT_VALIDATIONS.UNALLOWED_EXTENSIONS, fileExtension.toLowerCase())) {
- const invalidReason = this.props.translate('attachmentPicker.notAllowedExtension');
- this.setState({
- isAttachmentInvalid: true,
- attachmentInvalidReasonTitle: this.props.translate('attachmentPicker.wrongFileType'),
- attachmentInvalidReason: invalidReason,
- });
- return false;
- }
-
- if (lodashGet(file, 'size', 0) > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) {
- this.setState({
- isAttachmentInvalid: true,
- attachmentInvalidReasonTitle: this.props.translate('attachmentPicker.attachmentTooLarge'),
- attachmentInvalidReason: this.props.translate('attachmentPicker.sizeExceeded'),
- });
- return false;
- }
-
- if (lodashGet(file, 'size', 0) < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) {
- this.setState({
- isAttachmentInvalid: true,
- attachmentInvalidReasonTitle: this.props.translate('attachmentPicker.attachmentTooSmall'),
- attachmentInvalidReason: this.props.translate('attachmentPicker.sizeNotMet'),
- });
- return false;
- }
-
- return true;
- }
-
+ const isValidFile = useCallback(
+ (_file) => {
+ const {fileExtension} = FileUtils.splitExtensionFromFileName(lodashGet(_file, 'name', ''));
+ if (_.contains(CONST.API_ATTACHMENT_VALIDATIONS.UNALLOWED_EXTENSIONS, fileExtension.toLowerCase())) {
+ const invalidReason = props.translate('attachmentPicker.notAllowedExtension');
+
+ setIsAttachmentInvalid(true);
+ setAttachmentInvalidReasonTitle(props.translate('attachmentPicker.wrongFileType'));
+ setAttachmentInvalidReason(invalidReason);
+ return false;
+ }
+
+ if (lodashGet(_file, 'size', 0) > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) {
+ setIsAttachmentInvalid(true);
+ setAttachmentInvalidReasonTitle(props.translate('attachmentPicker.attachmentTooLarge'));
+ setAttachmentInvalidReason(props.translate('attachmentPicker.sizeExceeded'));
+ return false;
+ }
+
+ if (lodashGet(_file, 'size', 0) < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) {
+ setIsAttachmentInvalid(true);
+ setAttachmentInvalidReasonTitle(props.translate('attachmentPicker.attachmentTooSmall'));
+ setAttachmentInvalidReason(props.translate('attachmentPicker.sizeNotMet'));
+ return false;
+ }
+
+ return true;
+ },
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [props.translate],
+ );
/**
- * @param {Object} file
+ * @param {Object} _file
*/
- validateAndDisplayFileToUpload(file) {
- if (!file) {
- return;
- }
-
- if (!this.isValidFile(file)) {
- return;
- }
-
- if (file instanceof File) {
- const source = URL.createObjectURL(file);
- const modalType = this.getModalType(source, file);
- this.setState({
- isModalOpen: true,
- source,
- file,
- modalType,
- });
- } else {
- const modalType = this.getModalType(file.uri, file);
- this.setState({
- isModalOpen: true,
- source: file.uri,
- file,
- modalType,
- });
- }
- }
+ const validateAndDisplayFileToUpload = useCallback(
+ (_file) => {
+ if (!_file) {
+ return;
+ }
+
+ if (!isValidFile(_file)) {
+ return;
+ }
+
+ if (_file instanceof File) {
+ const inputSource = URL.createObjectURL(_file);
+ const inputModalType = getModalType(inputSource, _file);
+ setIsModalOpen(true);
+ setSource(inputSource);
+ setFile(_file);
+ setModalType(inputModalType);
+ } else {
+ const inputModalType = getModalType(_file.uri, _file);
+ setIsModalOpen(true);
+ setSource(_file.uri);
+ setFile(_file);
+ setModalType(inputModalType);
+ }
+ },
+ [isValidFile, getModalType],
+ );
/**
* In order to gracefully hide/show the confirm button when the keyboard
@@ -245,110 +234,124 @@ class AttachmentModal extends PureComponent {
*
* @param {Boolean} shouldFadeOut If true, fade out confirm button. Otherwise fade in.
*/
- updateConfirmButtonVisibility(shouldFadeOut) {
- this.setState({isConfirmButtonDisabled: shouldFadeOut});
- const toValue = shouldFadeOut ? 0 : 1;
-
- Animated.timing(this.state.confirmButtonFadeAnimation, {
- toValue,
- duration: 100,
- useNativeDriver: true,
- }).start();
- }
-
- render() {
- const source = this.props.source || this.state.source;
- return (
- <>
- this.setState({isModalOpen: false})}
- isVisible={this.state.isModalOpen}
- backgroundColor={themeColors.componentBG}
- onModalShow={() => {
- this.props.onModalShow();
- this.setState({shouldLoadAttachment: true});
- }}
- onModalHide={(e) => {
- this.props.onModalHide(e);
- this.setState({shouldLoadAttachment: false});
- }}
- propagateSwipe
- >
- {this.props.isSmallScreenWidth && }
- this.setState({isModalOpen: false})}
- onCloseButtonPress={() => this.setState({isModalOpen: false})}
- />
-
- {!_.isEmpty(this.props.report) ? (
- {
+ setIsConfirmButtonDisabled(shouldFadeOut);
+ const toValue = shouldFadeOut ? 0 : 1;
+
+ Animated.timing(confirmButtonFadeAnimation, {
+ toValue,
+ duration: 100,
+ useNativeDriver: true,
+ }).start();
+ },
+ [confirmButtonFadeAnimation],
+ );
+
+ /**
+ * close the modal
+ */
+ const closeModal = useCallback(() => {
+ setIsModalOpen(false);
+ }, []);
+
+ /**
+ * open the modal
+ */
+ const openModal = useCallback(() => {
+ setIsModalOpen(true);
+ }, []);
+
+ const sourceForAttachmentView = props.source || source;
+ return (
+ <>
+ {
+ props.onModalShow();
+ setShouldLoadAttachment(true);
+ }}
+ onModalHide={(e) => {
+ props.onModalHide(e);
+ setShouldLoadAttachment(false);
+ }}
+ propagateSwipe
+ >
+ {props.isSmallScreenWidth && }
+ downloadAttachment(source)}
+ shouldShowCloseButton={!props.isSmallScreenWidth}
+ shouldShowBackButton={props.isSmallScreenWidth}
+ onBackButtonPress={closeModal}
+ onCloseButtonPress={closeModal}
+ />
+
+ {!_.isEmpty(props.report) ? (
+
+ ) : (
+ Boolean(sourceForAttachmentView) &&
+ shouldLoadAttachment && (
+
- ) : (
- Boolean(source) &&
- this.state.shouldLoadAttachment && (
-
+ {/* If we have an onConfirm method show a confirmation button */}
+ {Boolean(props.onConfirm) && (
+
+ {({safeAreaPaddingBottomStyle}) => (
+
+
- )
+
)}
-
- {/* If we have an onConfirm method show a confirmation button */}
- {Boolean(this.props.onConfirm) && (
-
- {({safeAreaPaddingBottomStyle}) => (
-
-
-
- )}
-
- )}
-
-
-
-
- {this.props.children({
- displayFileInModal: this.validateAndDisplayFileToUpload,
- show: () => {
- this.setState({isModalOpen: true});
- },
- })}
- >
- );
- }
+
+ )}
+
+
+
+
+ {props.children({
+ displayFileInModal: validateAndDisplayFileToUpload,
+ show: openModal,
+ })}
+ >
+ );
}
AttachmentModal.propTypes = propTypes;
AttachmentModal.defaultProps = defaultProps;
+AttachmentModal.displayName = 'AttachmentModal';
export default compose(withWindowDimensions, withLocalize)(AttachmentModal);
diff --git a/src/components/Checkbox.js b/src/components/Checkbox.js
index 5ce90473bbdd..0a023b7b4fb4 100644
--- a/src/components/Checkbox.js
+++ b/src/components/Checkbox.js
@@ -72,7 +72,7 @@ function Checkbox(props) {
onPress={firePressHandlerOnClick}
onMouseDown={props.onMouseDown}
ref={props.forwardedRef}
- style={props.style}
+ style={[props.style, styles.checkboxPressable]}
onKeyDown={handleSpaceKey}
accessibilityRole="checkbox"
accessibilityState={{checked: props.isChecked}}
@@ -89,8 +89,6 @@ function Checkbox(props) {
props.disabled && styles.cursorDisabled,
props.isChecked && styles.borderColorFocus,
]}
- // Used as CSS selector to customize focus-visible style
- dataSet={{checkbox: true}}
>
{props.isChecked && (
{};
this.state = {
+ reportAction: {},
isEmojiPickerVisible: false,
// The horizontal and vertical position (relative to the window) where the emoji popover will display.
@@ -100,6 +101,16 @@ class EmojiPicker extends React.Component {
this.setState({isEmojiPickerVisible: false});
}
+ /**
+ * Whether Context Menu is active for the Report Action.
+ *
+ * @param {Number|String} actionID
+ * @return {Boolean}
+ */
+ isActiveReportAction(actionID) {
+ return Boolean(actionID) && this.state.reportAction.reportActionID === actionID;
+ }
+
/**
* Show the emoji picker menu.
*
@@ -108,8 +119,9 @@ class EmojiPicker extends React.Component {
* @param {Element} emojiPopoverAnchor - Element to which Popover is anchored
* @param {Object} [anchorOrigin=DEFAULT_ANCHOR_ORIGIN] - Anchor origin for Popover
* @param {Function} [onWillShow=() => {}] - Run a callback when Popover will show
+ * @param {Object} reportAction - ReportAction for EmojiPicker
*/
- showEmojiPicker(onModalHide, onEmojiSelected, emojiPopoverAnchor, anchorOrigin, onWillShow = () => {}) {
+ showEmojiPicker(onModalHide, onEmojiSelected, emojiPopoverAnchor, anchorOrigin, onWillShow = () => {}, reportAction) {
this.onModalHide = onModalHide;
this.onEmojiSelected = onEmojiSelected;
this.emojiPopoverAnchor = emojiPopoverAnchor;
@@ -121,7 +133,7 @@ class EmojiPicker extends React.Component {
this.measureEmojiPopoverAnchorPosition().then((emojiPopoverAnchorPosition) => {
onWillShow();
- this.setState({isEmojiPickerVisible: true, emojiPopoverAnchorPosition, emojiPopoverAnchorOrigin: anchorOrigin || DEFAULT_ANCHOR_ORIGIN});
+ this.setState({reportAction, isEmojiPickerVisible: true, emojiPopoverAnchorPosition, emojiPopoverAnchorOrigin: anchorOrigin || DEFAULT_ANCHOR_ORIGIN});
});
}
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js
index 4cbc778e40b9..cd9c05344e82 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js
@@ -3,11 +3,14 @@ import htmlRendererPropTypes from './htmlRendererPropTypes';
import AttachmentModal from '../../AttachmentModal';
import styles from '../../../styles/styles';
import ThumbnailImage from '../../ThumbnailImage';
-import PressableWithoutFocus from '../../PressableWithoutFocus';
+import PressableWithoutFocus from '../../Pressable/PressableWithoutFocus';
import CONST from '../../../CONST';
import {ShowContextMenuContext, showContextMenuForReport} from '../../ShowContextMenuContext';
import tryResolveUrlFromApiRoot from '../../../libs/tryResolveUrlFromApiRoot';
import * as ReportUtils from '../../../libs/ReportUtils';
+import withLocalize, {withLocalizePropTypes} from '../../withLocalize';
+
+const propTypes = {...htmlRendererPropTypes, ...withLocalizePropTypes};
function ImageRenderer(props) {
const htmlAttribs = props.tnode.attributes;
@@ -60,9 +63,11 @@ function ImageRenderer(props) {
>
{({show}) => (
showContextMenuForReport(event, anchor, report.reportID, action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report))}
+ accessibilityRole="imagebutton"
+ accessibilityLabel={props.translate('accessibilityHints.viewAttachment')}
>
{
- if (!this.wrapperView.contains(el.relatedTarget)) {
+ // Check if the blur event occurred due to clicking outside the element
+ // and the wrapperView contains the element that caused the blur and reset isHovered
+ if (!this.wrapperView.contains(el.target) && !this.wrapperView.contains(el.relatedTarget)) {
this.setIsHovered(false);
}
diff --git a/src/components/MagicCodeInput.js b/src/components/MagicCodeInput.js
index 32c7ec21a920..4caa7328b92c 100644
--- a/src/components/MagicCodeInput.js
+++ b/src/components/MagicCodeInput.js
@@ -106,10 +106,18 @@ function MagicCodeInput(props) {
setFocusedIndex(undefined);
};
+ const focusMagicCodeInput = () => {
+ setFocusedIndex(0);
+ inputRefs.current[0].focus();
+ };
+
useImperativeHandle(props.innerRef, () => ({
focus() {
- setFocusedIndex(0);
- inputRefs.current[0].focus();
+ focusMagicCodeInput();
+ },
+ resetFocus() {
+ setInput('');
+ focusMagicCodeInput();
},
clear() {
setInput('');
diff --git a/src/components/Modal/index.web.js b/src/components/Modal/index.web.js
index 7d01efd652c2..6e304f0420c2 100644
--- a/src/components/Modal/index.web.js
+++ b/src/components/Modal/index.web.js
@@ -1,27 +1,36 @@
-import React from 'react';
+import React, {useState} from 'react';
+import {InteractionManager} from 'react-native';
import withWindowDimensions from '../withWindowDimensions';
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';
+import CONST from '../../CONST';
function Modal(props) {
+ const [previousStatusBarColor, setPreviousStatusBarColor] = useState();
+
const setStatusBarColor = (color = themeColors.appBG) => {
if (!props.fullscreen) {
return;
}
- StatusBar.setBackgroundColor(color);
+ InteractionManager.runAfterInteractions(() => StatusBar.setBackgroundColor(color));
};
const hideModal = () => {
- setStatusBarColor();
+ setStatusBarColor(previousStatusBarColor);
props.onModalHide();
};
const showModal = () => {
- setStatusBarColor(StyleUtils.getThemeBackgroundColor());
+ const statusBarColor = StatusBar.getBackgroundColor();
+ const isFullScreenModal =
+ props.type === CONST.MODAL.MODAL_TYPE.CENTERED || props.type === CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE || props.type === CONST.MODAL.MODAL_TYPE.RIGHT_DOCKED;
+ setPreviousStatusBarColor(statusBarColor);
+ // If it is a full screen modal then match it with appBG, otherwise we use the backdrop color
+ setStatusBarColor(isFullScreenModal ? themeColors.appBG : StyleUtils.getThemeBackgroundColor(statusBarColor));
props.onModalShow();
};
diff --git a/src/components/MoneyRequestHeader.js b/src/components/MoneyRequestHeader.js
index af79fd2b8132..0976818f9545 100644
--- a/src/components/MoneyRequestHeader.js
+++ b/src/components/MoneyRequestHeader.js
@@ -141,7 +141,7 @@ function MoneyRequestHeader(props) {
{!props.isSingleTransactionView && {formattedAmount}}
{!props.isSingleTransactionView && isSettled && (
-
+
- {props.children}
-
- );
+ return {props.children};
}
OpacityView.displayName = 'OpacityView';
diff --git a/src/components/OptionRow.js b/src/components/OptionRow.js
index 489c70c25dc5..14b16949de70 100644
--- a/src/components/OptionRow.js
+++ b/src/components/OptionRow.js
@@ -185,7 +185,6 @@ class OptionRow extends Component {
accessibilityRole="button"
hoverDimmingValue={1}
hoverStyle={this.props.hoverStyle}
- focusStyle={this.props.hoverStyle}
>
diff --git a/src/components/PinButton.js b/src/components/PinButton.js
index 6292123faa39..800e0a828d1d 100644
--- a/src/components/PinButton.js
+++ b/src/components/PinButton.js
@@ -1,5 +1,4 @@
import React from 'react';
-import {Pressable} from 'react-native';
import styles from '../styles/styles';
import themeColors from '../styles/themes/default';
import Icon from './Icon';
@@ -9,6 +8,7 @@ import reportPropTypes from '../pages/reportPropTypes';
import * as Report from '../libs/actions/Report';
import * as Expensicons from './Icon/Expensicons';
import * as Session from '../libs/actions/Session';
+import PressableWithFeedback from './Pressable/PressableWithFeedback';
const propTypes = {
/** Report to pin */
@@ -23,15 +23,18 @@ const defaultProps = {
function PinButton(props) {
return (
- Report.togglePinnedState(props.report.reportID, props.report.isPinned))}
style={[styles.touchableButtonImage]}
+ accessibilityState={{checked: props.report.isPinned}}
+ accessibilityLabel={props.report.isPinned ? props.translate('common.unPin') : props.translate('common.pin')}
+ accessibilityRole="button"
>
-
+
);
}
diff --git a/src/components/PopoverMenu/index.js b/src/components/PopoverMenu/index.js
index 4de4a2341283..1cafa9e12664 100644
--- a/src/components/PopoverMenu/index.js
+++ b/src/components/PopoverMenu/index.js
@@ -41,6 +41,7 @@ const defaultProps = {
function PopoverMenu(props) {
const {isSmallScreenWidth} = useWindowDimensions();
const [selectedItemIndex, setSelectedItemIndex] = useState(null);
+ const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({initialFocusedIndex: -1, maxIndex: props.menuItems.length - 1, isActive: props.isVisible});
const selectItem = (index) => {
const selectedItem = props.menuItems[index];
@@ -48,7 +49,6 @@ function PopoverMenu(props) {
setSelectedItemIndex(index);
};
- const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({initialFocusedIndex: -1, maxIndex: props.menuItems.length - 1});
useKeyboardShortcut(
CONST.KEYBOARD_SHORTCUTS.ENTER,
() => {
diff --git a/src/components/Pressable/PressableWithFeedback.js b/src/components/Pressable/PressableWithFeedback.js
index 94f10a380bb9..294968373455 100644
--- a/src/components/Pressable/PressableWithFeedback.js
+++ b/src/components/Pressable/PressableWithFeedback.js
@@ -6,12 +6,11 @@ import GenericPressable from './GenericPressable';
import GenericPressablePropTypes from './GenericPressable/PropTypes';
import OpacityView from '../OpacityView';
import variables from '../../styles/variables';
-import * as StyleUtils from '../../styles/StyleUtils';
-const omittedProps = ['style', 'pressStyle', 'hoverStyle', 'focusStyle', 'wrapperStyle'];
+const omittedProps = ['wrapperStyle', 'onHoverIn', 'onHoverOut', 'onPressIn', 'onPressOut'];
const PressableWithFeedbackPropTypes = {
- ..._.omit(GenericPressablePropTypes.pressablePropTypes, omittedProps),
+ ...GenericPressablePropTypes.pressablePropTypes,
/**
* Determines what opacity value should be applied to the underlaying view when Pressable is pressed.
* To disable dimming, pass 1 as pressDimmingValue
@@ -31,7 +30,7 @@ const PressableWithFeedbackPropTypes = {
};
const PressableWithFeedbackDefaultProps = {
- ..._.omit(GenericPressablePropTypes.defaultProps, omittedProps),
+ ...GenericPressablePropTypes.defaultProps,
pressDimmingValue: variables.pressDimValue,
hoverDimmingValue: variables.hoverDimValue,
nativeID: '',
@@ -39,49 +38,59 @@ const PressableWithFeedbackDefaultProps = {
};
const PressableWithFeedback = forwardRef((props, ref) => {
- const propsWithoutStyling = _.omit(props, omittedProps);
+ const rest = _.omit(props, omittedProps);
const [disabled, setDisabled] = useState(props.disabled);
+ const [isPressed, setIsPressed] = useState(false);
+ const [isHovered, setIsHovered] = useState(false);
useEffect(() => {
setDisabled(props.disabled);
}, [props.disabled]);
return (
- {
- setDisabled(true);
- const onPress = props.onPress(e);
- InteractionManager.runAfterInteractions(() => {
- if (!(onPress instanceof Promise)) {
- setDisabled(props.disabled);
- return;
- }
- onPress.finally(() => {
- setDisabled(props.disabled);
- });
- });
- }}
>
- {(state) => (
-
- {_.isFunction(props.children) ? props.children(state) : props.children}
-
- )}
-
+ {
+ setIsHovered(true);
+ if (props.onHoverIn) props.onHoverIn();
+ }}
+ onHoverOut={() => {
+ setIsHovered(false);
+ if (props.onHoverOut) props.onHoverOut();
+ }}
+ onPressIn={() => {
+ setIsPressed(true);
+ if (props.onPressIn) props.onPressIn();
+ }}
+ onPressOut={() => {
+ setIsPressed(false);
+ if (props.onPressOut) props.onPressOut();
+ }}
+ onPress={(e) => {
+ setDisabled(true);
+ const onPress = props.onPress(e);
+ InteractionManager.runAfterInteractions(() => {
+ if (!(onPress instanceof Promise)) {
+ setDisabled(props.disabled);
+ return;
+ }
+ onPress.finally(() => {
+ setDisabled(props.disabled);
+ });
+ });
+ }}
+ // eslint-disable-next-line react/jsx-props-no-spreading
+ {...rest}
+ >
+ {(state) => (_.isFunction(props.children) ? props.children(state) : props.children)}
+
+
);
});
diff --git a/src/components/PressableWithoutFocus.js b/src/components/Pressable/PressableWithoutFocus.js
similarity index 70%
rename from src/components/PressableWithoutFocus.js
rename to src/components/Pressable/PressableWithoutFocus.js
index 5b441f4ee9f4..39f0cd34abec 100644
--- a/src/components/PressableWithoutFocus.js
+++ b/src/components/Pressable/PressableWithoutFocus.js
@@ -1,6 +1,8 @@
import React from 'react';
-import {Pressable} from 'react-native';
+import _ from 'underscore';
import PropTypes from 'prop-types';
+import GenericPressable from './GenericPressable';
+import genericPressablePropTypes from './GenericPressable/PropTypes';
const propTypes = {
/** Element that should be clickable */
@@ -14,11 +16,14 @@ const propTypes = {
/** Styles that should be passed to touchable container */
// eslint-disable-next-line react/forbid-prop-types
- styles: PropTypes.arrayOf(PropTypes.object),
+ style: PropTypes.arrayOf(PropTypes.object),
+
+ /** Proptypes of pressable component used for implementation */
+ ...genericPressablePropTypes.pressablePropTypes,
};
const defaultProps = {
- styles: [],
+ style: [],
onLongPress: undefined,
};
@@ -41,15 +46,18 @@ class PressableWithoutFocus extends React.Component {
}
render() {
+ const restProps = _.omit(this.props, ['children', 'onPress', 'onLongPress', 'style']);
return (
- (this.pressableRef = el)}
- style={this.props.styles}
+ style={this.props.style}
+ // eslint-disable-next-line react/jsx-props-no-spreading
+ {...restProps}
>
{this.props.children}
-
+
);
}
}
diff --git a/src/components/QRCode/index.js b/src/components/QRCode/index.js
index 10a99fca5d99..f27cf28066ef 100644
--- a/src/components/QRCode/index.js
+++ b/src/components/QRCode/index.js
@@ -34,11 +34,6 @@ const propTypes = {
* The QRCode background color
*/
backgroundColor: PropTypes.string,
-
- /**
- * The QRCode logo background color
- */
- logoBackgroundColor: PropTypes.string,
/**
* Function to retrieve the internal component ref and be able to call it's
* methods
@@ -50,7 +45,6 @@ const defaultProps = {
logo: undefined,
size: 120,
color: defaultTheme.text,
- logoBackgroundColor: defaultTheme.icon,
backgroundColor: defaultTheme.highlightBG,
getRef: undefined,
logoRatio: CONST.QR.DEFAULT_LOGO_SIZE_RATIO,
@@ -64,7 +58,7 @@ function QRCode(props) {
value={props.url}
size={props.size}
logo={props.logo}
- logoBackgroundColor={props.logoBackgroundColor}
+ logoBackgroundColor={props.backgroundColor}
logoSize={props.size * props.logoRatio}
logoMargin={props.size * props.logoMarginRatio}
logoBorderRadius={props.size}
diff --git a/src/components/Reactions/AddReactionBubble.js b/src/components/Reactions/AddReactionBubble.js
index 7048bc9735cf..113968e8b9ad 100644
--- a/src/components/Reactions/AddReactionBubble.js
+++ b/src/components/Reactions/AddReactionBubble.js
@@ -35,6 +35,13 @@ const propTypes = {
*/
onSelectEmoji: PropTypes.func.isRequired,
+ /**
+ * ReportAction for EmojiPicker.
+ */
+ reportAction: PropTypes.shape({
+ reportActionID: PropTypes.string.isRequired,
+ }),
+
...withLocalizePropTypes,
};
@@ -42,6 +49,7 @@ const defaultProps = {
isContextMenu: false,
onWillShowPicker: () => {},
onPressOpenPicker: undefined,
+ reportAction: {},
};
function AddReactionBubble(props) {
@@ -57,6 +65,7 @@ function AddReactionBubble(props) {
refParam || ref.current,
anchorOrigin,
props.onWillShowPicker,
+ props.reportAction,
);
};
diff --git a/src/components/Reactions/MiniQuickEmojiReactions.js b/src/components/Reactions/MiniQuickEmojiReactions.js
index 91fa8817172c..5c77726d8070 100644
--- a/src/components/Reactions/MiniQuickEmojiReactions.js
+++ b/src/components/Reactions/MiniQuickEmojiReactions.js
@@ -28,6 +28,13 @@ const propTypes = {
*/
onEmojiPickerClosed: PropTypes.func,
+ /**
+ * ReportAction for EmojiPicker.
+ */
+ reportAction: PropTypes.shape({
+ reportActionID: PropTypes.string.isRequired,
+ }),
+
...withLocalizePropTypes,
preferredSkinTone: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
};
@@ -35,6 +42,7 @@ const propTypes = {
const defaultProps = {
onEmojiPickerClosed: () => {},
preferredSkinTone: CONST.EMOJI_DEFAULT_SKIN_TONE,
+ reportAction: {},
};
/**
@@ -56,6 +64,9 @@ function MiniQuickEmojiReactions(props) {
props.onEmojiSelected(emojiObject);
},
ref.current,
+ undefined,
+ () => {},
+ props.reportAction,
);
};
diff --git a/src/components/Reactions/QuickEmojiReactions/BaseQuickEmojiReactions.js b/src/components/Reactions/QuickEmojiReactions/BaseQuickEmojiReactions.js
index 7a0eb42a0ebd..e237083232d4 100644
--- a/src/components/Reactions/QuickEmojiReactions/BaseQuickEmojiReactions.js
+++ b/src/components/Reactions/QuickEmojiReactions/BaseQuickEmojiReactions.js
@@ -29,11 +29,17 @@ const baseQuickEmojiReactionsPropTypes = {
* to actually open the emoji picker.
*/
onPressOpenPicker: PropTypes.func,
+
+ /**
+ * ReportAction for EmojiPicker.
+ */
+ reportAction: PropTypes.object,
};
const baseQuickEmojiReactionsDefaultProps = {
onWillShowPicker: () => {},
onPressOpenPicker: () => {},
+ reportAction: {},
};
const propTypes = {
@@ -68,6 +74,7 @@ function BaseQuickEmojiReactions(props) {
onPressOpenPicker={props.onPressOpenPicker}
onWillShowPicker={props.onWillShowPicker}
onSelectEmoji={props.onEmojiSelected}
+ reportAction={props.reportAction}
/>
);
diff --git a/src/components/Reactions/ReportActionItemReactions.js b/src/components/Reactions/ReportActionItemReactions.js
index a770032d3049..8a818e1c7bff 100644
--- a/src/components/Reactions/ReportActionItemReactions.js
+++ b/src/components/Reactions/ReportActionItemReactions.js
@@ -98,7 +98,12 @@ function ReportActionItemReactions(props) {
);
})}
- {reactionsWithCount.length > 0 && }
+ {reactionsWithCount.length > 0 && (
+
+ )}
);
}
diff --git a/src/components/ReimbursementAccountLoadingIndicator.js b/src/components/ReimbursementAccountLoadingIndicator.js
index c8b12eb8592d..816eb7590088 100644
--- a/src/components/ReimbursementAccountLoadingIndicator.js
+++ b/src/components/ReimbursementAccountLoadingIndicator.js
@@ -24,7 +24,10 @@ const propTypes = {
function ReimbursementAccountLoadingIndicator(props) {
return (
-
+
{CurrencyUtils.convertToDisplayString(requestAmount, requestCurrency)}
{!props.iouReport.hasOutstandingIOU && !props.isBillSplit && (
-
+
{_.map(props.action.message, (message, index) => (
- {
Navigation.navigate(ROUTES.getReportRoute(props.iouReportID));
@@ -112,7 +112,8 @@ function ReportPreview(props) {
onPressOut={() => ControlSelection.unblock()}
onLongPress={(event) => showContextMenuForReport(event, props.contextMenuAnchor, props.chatReportID, props.action, props.checkIfContextMenuActive)}
style={[styles.flexRow, styles.justifyContentBetween]}
- focusable
+ accessibilityRole="button"
+ accessibilityLabel={props.translate('iou.viewDetails')}
>
{props.iouReport.hasOutstandingIOU ? (
@@ -124,15 +125,6 @@ function ReportPreview(props) {
{lodashGet(message, 'html', props.translate('iou.payerSettled', {amount: reportAmount}))}
- {!props.iouReport.hasOutstandingIOU && (
-
-
-
- )}
)}
@@ -140,7 +132,7 @@ function ReportPreview(props) {
src={Expensicons.ArrowRight}
fill={StyleUtils.getIconFillColor(getButtonState(props.isHovered))}
/>
-
+
))}
{isCurrentUserManager && !ReportUtils.isSettled(props.iouReport.reportID) && (
@${taskAssignee} ${taskTitle}` : `${taskTitle}`;
return (
- Navigation.navigate(ROUTES.getReportRoute(props.taskReportID))}
style={[styles.flexRow, styles.justifyContentBetween]}
+ accessibilityRole="button"
+ accessibilityLabel={props.translate('newTaskPage.task')}
>
-
+
-
+
);
}
diff --git a/src/components/ScreenWrapper/index.js b/src/components/ScreenWrapper/index.js
index 95677a49570d..9d2d8dfc0d65 100644
--- a/src/components/ScreenWrapper/index.js
+++ b/src/components/ScreenWrapper/index.js
@@ -1,19 +1,15 @@
import {Keyboard, View, PanResponder} from 'react-native';
import React from 'react';
import _ from 'underscore';
-import {withOnyx} from 'react-native-onyx';
import lodashGet from 'lodash/get';
import {PickerAvoidingView} from 'react-native-picker-select';
import KeyboardAvoidingView from '../KeyboardAvoidingView';
import CONST from '../../CONST';
-import KeyboardShortcut from '../../libs/KeyboardShortcut';
-import Navigation from '../../libs/Navigation/Navigation';
import styles from '../../styles/styles';
import HeaderGap from '../HeaderGap';
import OfflineIndicator from '../OfflineIndicator';
import compose from '../../libs/compose';
import withNavigation from '../withNavigation';
-import ONYXKEYS from '../../ONYXKEYS';
import {withNetwork} from '../OnyxProvider';
import {propTypes, defaultProps} from './propTypes';
import SafeAreaConsumer from '../SafeAreaConsumer';
@@ -39,22 +35,6 @@ class ScreenWrapper extends React.Component {
}
componentDidMount() {
- const shortcutConfig = CONST.KEYBOARD_SHORTCUTS.ESCAPE;
- this.unsubscribeEscapeKey = KeyboardShortcut.subscribe(
- shortcutConfig.shortcutKey,
- () => {
- if (this.props.modal.willAlertModalBecomeVisible) {
- return;
- }
-
- Navigation.dismissModal();
- },
- shortcutConfig.descriptionKey,
- shortcutConfig.modifiers,
- true,
- true,
- );
-
this.unsubscribeTransitionEnd = this.props.navigation.addListener('transitionEnd', (event) => {
// Prevent firing the prop callback when user is exiting the page.
if (lodashGet(event, 'data.closing')) {
@@ -89,9 +69,6 @@ class ScreenWrapper extends React.Component {
}
componentWillUnmount() {
- if (this.unsubscribeEscapeKey) {
- this.unsubscribeEscapeKey();
- }
if (this.unsubscribeTransitionEnd) {
this.unsubscribeTransitionEnd();
}
@@ -144,7 +121,7 @@ class ScreenWrapper extends React.Component {
})
: this.props.children
}
- {this.props.isSmallScreenWidth && }
+ {this.props.isSmallScreenWidth && this.props.shouldShowOfflineIndicator && }
@@ -158,15 +135,4 @@ class ScreenWrapper extends React.Component {
ScreenWrapper.propTypes = propTypes;
ScreenWrapper.defaultProps = defaultProps;
-export default compose(
- withNavigation,
- withEnvironment,
- withWindowDimensions,
- withKeyboardState,
- withOnyx({
- modal: {
- key: ONYXKEYS.MODAL,
- },
- }),
- withNetwork(),
-)(ScreenWrapper);
+export default compose(withNavigation, withEnvironment, withWindowDimensions, withKeyboardState, withNetwork())(ScreenWrapper);
diff --git a/src/components/ScreenWrapper/propTypes.js b/src/components/ScreenWrapper/propTypes.js
index a82fb3b9e507..b2e6c31af3f4 100644
--- a/src/components/ScreenWrapper/propTypes.js
+++ b/src/components/ScreenWrapper/propTypes.js
@@ -26,12 +26,6 @@ const propTypes = {
* scrollable form, gives a subtly better UX if disabled on non-scrollable screens with a submit button */
shouldEnablePickerAvoiding: PropTypes.bool,
- /** Details about any modals being used */
- modal: PropTypes.shape({
- /** Indicates when an Alert modal is about to be visible */
- willAlertModalBecomeVisible: PropTypes.bool,
- }),
-
/** Whether to dismiss keyboard before leaving a screen */
shouldDismissKeyboardBeforeClose: PropTypes.bool,
@@ -41,6 +35,9 @@ const propTypes = {
...windowDimensionsPropTypes,
...environmentPropTypes,
+
+ /** Whether to show offline indicator */
+ shouldShowOfflineIndicator: PropTypes.bool,
};
const defaultProps = {
@@ -49,10 +46,10 @@ const defaultProps = {
shouldDismissKeyboardBeforeClose: true,
includePaddingTop: true,
onEntryTransitionEnd: () => {},
- modal: {},
keyboardAvoidingViewBehavior: 'padding',
shouldEnableMaxHeight: false,
shouldEnablePickerAvoiding: true,
+ shouldShowOfflineIndicator: true,
};
export {propTypes, defaultProps};
diff --git a/src/components/TaskHeader.js b/src/components/TaskHeader.js
index 001ed2b10bdd..5ed4a4d0cba4 100644
--- a/src/components/TaskHeader.js
+++ b/src/components/TaskHeader.js
@@ -2,6 +2,7 @@ import React, {useEffect} from 'react';
import {View} from 'react-native';
import PropTypes from 'prop-types';
import lodashGet from 'lodash/get';
+import {withOnyx} from 'react-native-onyx';
import reportPropTypes from '../pages/reportPropTypes';
import withLocalize, {withLocalizePropTypes} from './withLocalize';
import * as ReportUtils from '../libs/ReportUtils';
@@ -22,6 +23,7 @@ import Button from './Button';
import * as TaskUtils from '../libs/actions/Task';
import * as UserUtils from '../libs/UserUtils';
import PressableWithFeedback from './Pressable/PressableWithFeedback';
+import ONYXKEYS from '../ONYXKEYS';
const propTypes = {
/** The report currently being looked at */
@@ -30,13 +32,25 @@ const propTypes = {
/** Personal details so we can get the ones for the report participants */
personalDetails: PropTypes.objectOf(participantPropTypes).isRequired,
+ /** Current user session */
+ session: PropTypes.shape({
+ accountID: PropTypes.number,
+ }),
+
...withLocalizePropTypes,
};
+const defaultProps = {
+ session: {
+ accountID: 0,
+ },
+};
+
function TaskHeader(props) {
const title = ReportUtils.getReportName(props.report);
- const assigneeName = ReportUtils.getDisplayNameForParticipant(props.report.managerID);
- const assigneeAvatar = UserUtils.getAvatar(lodashGet(props.personalDetails, [props.report.managerID, 'avatar']), props.report.managerID);
+ const assigneeAccountID = TaskUtils.getTaskAssigneeAccountID(props.report);
+ const assigneeName = ReportUtils.getDisplayNameForParticipant(assigneeAccountID);
+ const assigneeAvatar = UserUtils.getAvatar(lodashGet(props.personalDetails, [assigneeAccountID, 'avatar']), assigneeAccountID);
const isOpen = props.report.stateNum === CONST.REPORT.STATE_NUM.OPEN && props.report.statusNum === CONST.REPORT.STATUS.OPEN;
const isCompleted = ReportUtils.isTaskCompleted(props.report);
@@ -59,7 +73,7 @@ function TaskHeader(props) {
>
- {props.report.managerID && props.report.managerID > 0 && (
+ {assigneeAccountID && assigneeAccountID > 0 && (
<>
{props.translate('task.completed')}
-
+
TaskUtils.completeTask(props.report.reportID, title)}
@@ -123,6 +137,15 @@ function TaskHeader(props) {
}
TaskHeader.propTypes = propTypes;
+TaskHeader.defaultProps = defaultProps;
TaskHeader.displayName = 'TaskHeader';
-export default compose(withWindowDimensions, withLocalize)(TaskHeader);
+export default compose(
+ withWindowDimensions,
+ withLocalize,
+ withOnyx({
+ session: {
+ key: ONYXKEYS.SESSION,
+ },
+ }),
+)(TaskHeader);
diff --git a/src/components/withCurrentReportId.js b/src/components/withCurrentReportId.js
index d05ac2f1bce8..4611187ed404 100644
--- a/src/components/withCurrentReportId.js
+++ b/src/components/withCurrentReportId.js
@@ -1,4 +1,4 @@
-import React, {createContext, forwardRef} from 'react';
+import React, {createContext, forwardRef, useCallback, useState, useMemo} from 'react';
import PropTypes from 'prop-types';
import getComponentDisplayName from '../libs/getComponentDisplayName';
@@ -7,52 +7,55 @@ import Navigation from '../libs/Navigation/Navigation';
const CurrentReportIdContext = createContext(null);
const withCurrentReportIdPropTypes = {
- /** Actual content wrapped by this component */
- children: PropTypes.node.isRequired,
-};
+ /** Function to update the state */
+ updateCurrentReportId: PropTypes.func.isRequired,
-class CurrentReportIdContextProvider extends React.Component {
- constructor(props) {
- super(props);
+ /** The top most report id */
+ currentReportId: PropTypes.string,
+};
- this.state = {
- currentReportId: '',
- };
- }
+function CurrentReportIdContextProvider(props) {
+ const [currentReportId, setCurrentReportId] = useState('');
/**
- * The context this component exposes to child components
- * @returns {Object} currentReportId to share between central pane and LHN
+ * This function is used to update the currentReportId
+ * @param {Object} state root navigation state
*/
- getContextValue() {
- return {
- updateCurrentReportId: this.updateCurrentReportId.bind(this),
- currentReportId: this.state.currentReportId,
- };
- }
+ const updateCurrentReportId = useCallback(
+ (state) => {
+ setCurrentReportId(Navigation.getTopmostReportId(state));
+ },
+ [setCurrentReportId],
+ );
/**
- * @param {Object} state
- * @returns {String}
+ * The context this component exposes to child components
+ * @returns {Object} currentReportId to share between central pane and LHN
*/
- updateCurrentReportId(state) {
- return this.setState({currentReportId: Navigation.getTopmostReportId(state)});
- }
+ const contextValue = useMemo(
+ () => ({
+ updateCurrentReportId,
+ currentReportId,
+ }),
+ [updateCurrentReportId, currentReportId],
+ );
- render() {
- return {this.props.children};
- }
+ return {props.children};
}
-CurrentReportIdContextProvider.propTypes = withCurrentReportIdPropTypes;
+CurrentReportIdContextProvider.displayName = 'CurrentReportIdContextProvider';
+CurrentReportIdContextProvider.propTypes = {
+ /** Actual content wrapped by this component */
+ children: PropTypes.node.isRequired,
+};
export default function withCurrentReportId(WrappedComponent) {
const WithCurrentReportId = forwardRef((props, ref) => (
- {(translateUtils) => (
+ {(currentReportIdUtils) => (
{}, initialFocusedIndex = 0, disabledIndexes = [], shouldExcludeTextAreaNodes = true}) {
+export default function useArrowKeyFocusManager({
+ maxIndex,
+ onFocusedIndexChange = () => {},
+ initialFocusedIndex = 0,
+ disabledIndexes = EMPTY_ARRAY,
+ shouldExcludeTextAreaNodes = true,
+ isActive,
+}) {
const [focusedIndex, setFocusedIndex] = useState(initialFocusedIndex);
+ const arrowConfig = useMemo(
+ () => ({
+ excludedNodes: shouldExcludeTextAreaNodes ? ['TEXTAREA'] : [],
+ isActive,
+ }),
+ [isActive, shouldExcludeTextAreaNodes],
+ );
+
useEffect(() => onFocusedIndexChange(focusedIndex), [focusedIndex, onFocusedIndexChange]);
- useKeyboardShortcut(
- CONST.KEYBOARD_SHORTCUTS.ARROW_UP,
- () => {
- if (maxIndex < 0) {
- return;
- }
+ const arrowUpCallback = useCallback(() => {
+ if (maxIndex < 0) {
+ return;
+ }
- const currentFocusedIndex = focusedIndex > 0 ? focusedIndex - 1 : maxIndex;
+ setFocusedIndex((actualIndex) => {
+ const currentFocusedIndex = actualIndex > 0 ? actualIndex - 1 : maxIndex;
let newFocusedIndex = currentFocusedIndex;
while (disabledIndexes.includes(newFocusedIndex)) {
newFocusedIndex = newFocusedIndex > 0 ? newFocusedIndex - 1 : maxIndex;
if (newFocusedIndex === currentFocusedIndex) {
// all indexes are disabled
- return; // no-op
+ return actualIndex; // no-op
}
}
+ return newFocusedIndex;
+ });
+ }, [disabledIndexes, maxIndex]);
+ useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ARROW_UP, arrowUpCallback, arrowConfig);
- setFocusedIndex(newFocusedIndex);
- },
- {
- excludedNodes: shouldExcludeTextAreaNodes ? ['TEXTAREA'] : [],
- },
- );
-
- useKeyboardShortcut(
- CONST.KEYBOARD_SHORTCUTS.ARROW_DOWN,
- () => {
- if (maxIndex < 0) {
- return;
- }
+ const arrowDownCallback = useCallback(() => {
+ if (maxIndex < 0) {
+ return;
+ }
- const currentFocusedIndex = focusedIndex < maxIndex ? focusedIndex + 1 : 0;
+ setFocusedIndex((actualIndex) => {
+ const currentFocusedIndex = actualIndex < maxIndex ? actualIndex + 1 : 0;
let newFocusedIndex = currentFocusedIndex;
while (disabledIndexes.includes(newFocusedIndex)) {
newFocusedIndex = newFocusedIndex < maxIndex ? newFocusedIndex + 1 : 0;
if (newFocusedIndex === currentFocusedIndex) {
// all indexes are disabled
- return; // no-op
+ return actualIndex;
}
}
- setFocusedIndex(newFocusedIndex);
- },
- {
- excludedNodes: shouldExcludeTextAreaNodes ? ['TEXTAREA'] : [],
- },
- );
+ return newFocusedIndex;
+ });
+ }, [disabledIndexes, maxIndex]);
+ useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ARROW_DOWN, arrowDownCallback, arrowConfig);
// Note: you don't need to manually manage focusedIndex in the parent. setFocusedIndex is only exposed in case you want to reset focusedIndex or focus a specific item
return [focusedIndex, setFocusedIndex];
diff --git a/src/hooks/useKeyboardShortcut.js b/src/hooks/useKeyboardShortcut.js
index 434daeda1921..9b7244176864 100644
--- a/src/hooks/useKeyboardShortcut.js
+++ b/src/hooks/useKeyboardShortcut.js
@@ -1,20 +1,25 @@
-import {useEffect, useRef, useCallback} from 'react';
+import {useEffect} from 'react';
import KeyboardShortcut from '../libs/KeyboardShortcut';
+// Creating a default array this way because objects ({}) and arrays ([]) are not stable types.
+// The "excludedNodes" array needs to be stable to prevent the "useEffect" hook from being recreated unnecessarily.
+// Freezing the array ensures that it cannot be unintentionally modified.
+const EMPTY_ARRAY = Object.freeze([]);
+
/**
* Register a keyboard shortcut handler.
+ * Recommendation: To ensure stability, wrap the `callback` function with the useCallback hook before using it with this hook.
*
* @param {Object} shortcut
* @param {Function} callback
* @param {Object} [config]
*/
export default function useKeyboardShortcut(shortcut, callback, config = {}) {
- const {captureOnInputs = true, shouldBubble = false, priority = 0, shouldPreventDefault = true, excludedNodes = [], isActive = true} = config;
+ const {captureOnInputs = true, shouldBubble = false, priority = 0, shouldPreventDefault = true, excludedNodes = EMPTY_ARRAY, isActive = true} = config;
- const subscription = useRef(null);
- const subscribe = useCallback(
- () =>
- KeyboardShortcut.subscribe(
+ useEffect(() => {
+ if (isActive) {
+ return KeyboardShortcut.subscribe(
shortcut.shortcutKey,
callback,
shortcut.descriptionKey,
@@ -24,14 +29,8 @@ export default function useKeyboardShortcut(shortcut, callback, config = {}) {
priority,
shouldPreventDefault,
excludedNodes,
- ),
- [callback, captureOnInputs, excludedNodes, priority, shortcut.descriptionKey, shortcut.modifiers, shortcut.shortcutKey, shouldBubble, shouldPreventDefault],
- );
-
- useEffect(() => {
- const unsubscribe = subscription.current || (() => {});
- unsubscribe();
- subscription.current = isActive ? subscribe() : null;
- return isActive ? subscription.current : () => {};
- }, [isActive, subscribe]);
+ );
+ }
+ return () => {};
+ }, [isActive, callback, captureOnInputs, excludedNodes, priority, shortcut.descriptionKey, shortcut.modifiers, shortcut.shortcutKey, shouldBubble, shouldPreventDefault]);
}
diff --git a/src/languages/en.js b/src/languages/en.js
index 808a85375631..192d9954e5ff 100755
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -144,6 +144,7 @@ export default {
per: 'per',
mi: 'mile',
km: 'kilometer',
+ copied: 'Copied!',
someone: 'Someone',
},
anonymousReportFooter: {
@@ -818,6 +819,7 @@ export default {
'In order to finish setting up your bank account, you must validate your account. Please check your email to validate your account, and return here to finish up!',
hasPhoneLoginError: 'To add a verified bank account please ensure your primary login is a valid email and try again. You can add your phone number as a secondary login.',
hasBeenThrottledError: 'There was an error adding your bank account. Please wait a few minutes and try again.',
+ hasCurrencyError: 'Oops! It appears that your workspace currency is set to a different currency than USD. To proceed, please set it to USD and try again',
error: {
noBankAccountAvailable: 'Sorry, no bank account is available',
noBankAccountSelected: 'Please choose an account',
@@ -835,6 +837,7 @@ export default {
restrictedBusiness: 'Please confirm company is not on the list of restricted businesses',
routingNumber: 'Please enter a valid routing number',
accountNumber: 'Please enter a valid account number',
+ routingAndAccountNumberCannotBeSame: 'The routing number and account number cannot be the same',
companyType: 'Please select a valid company type',
tooManyAttempts: 'Due to a high number of login attempts, this option has been temporarily disabled for 24 hours. Please try again later or manually enter details instead.',
address: 'Please enter a valid address',
@@ -1072,8 +1075,6 @@ export default {
reconcileCards: 'Reconcile cards',
settlementFrequency: 'Settlement frequency',
deleteConfirmation: 'Are you sure you want to delete this workspace?',
- growlMessageOnDelete: 'Workspace deleted',
- 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}`,
@@ -1206,6 +1207,9 @@ export default {
bankAccountAnyTransactions: ' bank account. Any outstanding transactions for this account will still complete.',
clearProgress: 'Starting over will clear the progress you have made so far.',
areYouSure: 'Are you sure?',
+ workspaceCurrency: 'Workspace currency',
+ updateCurrencyPrompt: 'It looks like your Workspace is currently set to a different currency than USD. Please click the button below to update your currency to USD now.',
+ updateToUSD: 'Update to USD',
},
},
getAssistancePage: {
@@ -1400,6 +1404,7 @@ export default {
chatUserDisplayNames: 'Chat user display names',
scrollToNewestMessages: 'Scroll to newest messages',
prestyledText: 'Prestyled text',
+ viewAttachment: 'View attachment',
},
parentReportAction: {
deletedMessage: '[Deleted message]',
diff --git a/src/languages/es.js b/src/languages/es.js
index 75248bf87eb2..d54fa5110117 100644
--- a/src/languages/es.js
+++ b/src/languages/es.js
@@ -143,6 +143,7 @@ export default {
per: 'por',
mi: 'milla',
km: 'kilómetro',
+ copied: '¡Copiado!',
someone: 'Alguien',
},
anonymousReportFooter: {
@@ -820,6 +821,8 @@ export default {
hasPhoneLoginError:
'Para agregar una cuenta bancaria verificada, asegúrate de que tu nombre de usuario principal sea un correo electrónico válido y vuelve a intentarlo. Puedes agregar tu número de teléfono como nombre de usuario secundario.',
hasBeenThrottledError: 'Se produjo un error al intentar agregar tu cuenta bancaria. Por favor, espera unos minutos e inténtalo de nuevo.',
+ hasCurrencyError:
+ '¡Ups! Parece que la moneda de tu espacio de trabajo está configurada en una moneda diferente a USD. Para continuar, por favor configúrala en USD e inténtalo nuevamente.',
error: {
noBankAccountAvailable: 'Lo sentimos, no hay ninguna cuenta bancaria disponible',
noBankAccountSelected: 'Por favor, elige una cuenta bancaria',
@@ -837,6 +840,7 @@ export default {
restrictedBusiness: 'Por favor, confirma que la empresa no está en la lista de negocios restringidos',
routingNumber: 'Por favor, introduce un número de ruta válido',
accountNumber: 'Por favor, introduce un número de cuenta válido',
+ routingAndAccountNumberCannotBeSame: 'El número de ruta y el número de cuenta no pueden ser iguales',
companyType: 'Por favor, selecciona un tipo de compañÃa válido',
tooManyAttempts:
'Debido a la gran cantidad de intentos de inicio de sesión, esta opción se ha desactivado temporalmente durante 24 horas. Vuelve a intentarlo más tarde o introduce los detalles manualmente.',
@@ -1076,9 +1080,7 @@ export default {
issueAndManageCards: 'Emitir y gestionar tarjetas',
reconcileCards: 'Reconciliar tarjetas',
settlementFrequency: 'Frecuencia de liquidación',
- growlMessageOnDelete: 'Espacio de trabajo eliminado',
deleteConfirmation: '¿Estás seguro de que quieres eliminar este espacio de trabajo?',
- 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}`,
@@ -1212,6 +1214,10 @@ export default {
bankAccountAnyTransactions: '. Los reembolsos pendientes serán completados sin problemas.',
clearProgress: 'Empezar de nuevo descartará lo completado hasta ahora.',
areYouSure: '¿Estás seguro?',
+ workspaceCurrency: 'Moneda del espacio de trabajo',
+ updateCurrencyPrompt:
+ 'Parece que tu espacio de trabajo está configurado actualmente en una moneda diferente a USD. Por favor, haz clic en el botón de abajo para actualizar tu moneda a USD ahora.',
+ updateToUSD: 'Actualizar a USD',
},
},
getAssistancePage: {
@@ -1866,6 +1872,7 @@ export default {
chatUserDisplayNames: 'Nombres de los usuarios del chat',
scrollToNewestMessages: 'Desplázate a los mensajes más recientes',
prestyledText: 'texto preestilizado',
+ viewAttachment: 'Ver archivo adjunto',
},
parentReportAction: {
deletedMessage: '[Mensaje eliminado]',
diff --git a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator.js b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator.js
index 471be5c7209c..64eadcbe06c3 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator.js
+++ b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator.js
@@ -17,7 +17,7 @@ function CentralPaneNavigator() {
{
state.index = state.routes.length - 1;
};
-const CustomRouter = (options) => {
+function CustomRouter(options) {
const stackRouter = StackRouter(options);
return {
@@ -37,6 +37,6 @@ const CustomRouter = (options) => {
return state;
},
};
-};
+}
export default CustomRouter;
diff --git a/src/libs/Navigation/FreezeWrapper.js b/src/libs/Navigation/FreezeWrapper.js
index f4f0072e2ac8..07b05651a769 100644
--- a/src/libs/Navigation/FreezeWrapper.js
+++ b/src/libs/Navigation/FreezeWrapper.js
@@ -3,11 +3,12 @@ import lodashFindIndex from 'lodash/findIndex';
import PropTypes from 'prop-types';
import {useIsFocused, useNavigation, useRoute} from '@react-navigation/native';
import {Freeze} from 'react-freeze';
-import withWindowDimensions, {windowDimensionsPropTypes} from '../../components/withWindowDimensions';
const propTypes = {
- ...windowDimensionsPropTypes,
+ /** Prop to disable freeze */
keepVisible: PropTypes.bool,
+ /** Children to wrap in FreezeWrapper. */
+ children: PropTypes.node.isRequired,
};
const defaultProps = {
@@ -47,5 +48,6 @@ function FreezeWrapper(props) {
FreezeWrapper.propTypes = propTypes;
FreezeWrapper.defaultProps = defaultProps;
+FreezeWrapper.displayName = 'FreezeWrapper';
-export default withWindowDimensions(FreezeWrapper);
+export default FreezeWrapper;
diff --git a/src/libs/Navigation/NavigationRoot.js b/src/libs/Navigation/NavigationRoot.js
index f81a9fd2336d..4f32178ba5bd 100644
--- a/src/libs/Navigation/NavigationRoot.js
+++ b/src/libs/Navigation/NavigationRoot.js
@@ -8,7 +8,7 @@ import AppNavigator from './AppNavigator';
import themeColors from '../../styles/themes/default';
import withWindowDimensions, {windowDimensionsPropTypes} from '../../components/withWindowDimensions';
import Log from '../Log';
-import withCurrentReportId from '../../components/withCurrentReportId';
+import withCurrentReportId, {withCurrentReportIdPropTypes} from '../../components/withCurrentReportId';
import compose from '../compose';
// https://reactnavigation.org/docs/themes
@@ -28,6 +28,7 @@ const propTypes = {
/** Fired when react-navigation is ready */
onReady: PropTypes.func.isRequired,
+ ...withCurrentReportIdPropTypes,
};
/**
diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js
index b5cdefa115d0..f57084c9f0e0 100644
--- a/src/libs/ReportActionsUtils.js
+++ b/src/libs/ReportActionsUtils.js
@@ -65,7 +65,7 @@ function hasCommentThread(reportAction) {
}
/**
- * Returns the parentReportAction if the given report is a thread.
+ * Returns the parentReportAction if the given report is a thread/task.
*
* @param {Object} report
* @returns {Object}
diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js
index 996c97e18269..6f317cc08d29 100644
--- a/src/libs/ReportUtils.js
+++ b/src/libs/ReportUtils.js
@@ -470,22 +470,15 @@ function isArchivedRoom(report) {
* @returns {String}
*/
function getPolicyName(report) {
- // Public rooms send back the policy name with the reportSummary,
- // since they can also be accessed by people who aren't in the workspace
- if (report.policyName) {
- return report.policyName;
- }
-
- if (!allPolicies || _.size(allPolicies) === 0) {
+ if ((!allPolicies || _.size(allPolicies) === 0) && !report.policyName) {
return Localize.translateLocal('workspace.common.unavailable');
}
+ const policy = _.get(allPolicies, `${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`);
- const policy = allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`];
- if (!policy) {
- return report.oldPolicyName || Localize.translateLocal('workspace.common.unavailable');
- }
+ // // Public rooms send back the policy name with the reportSummary,
+ // // since they can also be accessed by people who aren't in the workspace
- return policy.name || report.oldPolicyName || Localize.translateLocal('workspace.common.unavailable');
+ return lodashGet(policy, 'name') || report.policyName || report.oldPolicyName || Localize.translateLocal('workspace.common.unavailable');
}
/**
diff --git a/src/libs/actions/EmojiPickerAction.js b/src/libs/actions/EmojiPickerAction.js
index 6aba8e43bd23..9d4ef0b2e98c 100644
--- a/src/libs/actions/EmojiPickerAction.js
+++ b/src/libs/actions/EmojiPickerAction.js
@@ -10,13 +10,39 @@ const emojiPickerRef = React.createRef();
* @param {Element} emojiPopoverAnchor - Element on which EmojiPicker is anchored
* @param {Object} [anchorOrigin] - Anchor origin for Popover
* @param {Function} [onWillShow=() => {}] - Run a callback when Popover will show
+ * @param {Object} reportAction - ReportAction for EmojiPicker
*/
-function showEmojiPicker(onModalHide = () => {}, onEmojiSelected = () => {}, emojiPopoverAnchor, anchorOrigin = undefined, onWillShow = () => {}) {
+function showEmojiPicker(onModalHide = () => {}, onEmojiSelected = () => {}, emojiPopoverAnchor, anchorOrigin = undefined, onWillShow = () => {}, reportAction = {}) {
if (!emojiPickerRef.current) {
return;
}
- emojiPickerRef.current.showEmojiPicker(onModalHide, onEmojiSelected, emojiPopoverAnchor, anchorOrigin, onWillShow);
+ emojiPickerRef.current.showEmojiPicker(onModalHide, onEmojiSelected, emojiPopoverAnchor, anchorOrigin, onWillShow, reportAction);
}
-export {emojiPickerRef, showEmojiPicker};
+/**
+ * Hide the Emoji Picker modal.
+ *
+ * @param {Boolean} isNavigating
+ */
+function hideEmojiPicker(isNavigating) {
+ if (!emojiPickerRef.current) {
+ return;
+ }
+ emojiPickerRef.current.hideEmojiPicker(isNavigating);
+}
+
+/**
+ * Whether Emoji Picker is active for the Report Action.
+ *
+ * @param {Number|String} actionID
+ * @return {Boolean}
+ */
+function isActiveReportAction(actionID) {
+ if (!emojiPickerRef.current) {
+ return;
+ }
+ return emojiPickerRef.current.isActiveReportAction(actionID);
+}
+
+export {emojiPickerRef, showEmojiPicker, hideEmojiPicker, isActiveReportAction};
diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js
index f9092f87c48b..8d253a36a771 100644
--- a/src/libs/actions/Policy.js
+++ b/src/libs/actions/Policy.js
@@ -531,9 +531,12 @@ function clearAvatarErrors(policyID) {
function updateGeneralSettings(policyID, name, currency) {
const optimisticData = [
{
- onyxMethod: Onyx.METHOD.MERGE,
+ // We use SET because it's faster than merge and avoids a race condition when setting the currency and navigating the user to the Bank account page in confirmCurrencyChangeAndHideModal
+ onyxMethod: Onyx.METHOD.SET,
key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
value: {
+ ...allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`],
+
pendingFields: {
generalSettings: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
},
diff --git a/src/libs/actions/Session/index.js b/src/libs/actions/Session/index.js
index f59cf5972c9e..2941c9ad831c 100644
--- a/src/libs/actions/Session/index.js
+++ b/src/libs/actions/Session/index.js
@@ -24,6 +24,7 @@ import ROUTES from '../../../ROUTES';
import * as ErrorUtils from '../../ErrorUtils';
import * as ReportUtils from '../../ReportUtils';
import * as Report from '../Report';
+import {hideContextMenu} from '../../../pages/home/report/ContextMenu/ReportActionContextMenu';
let authTokenType = '';
Onyx.connect({
@@ -89,6 +90,7 @@ function isAnonymousUser() {
}
function signOutAndRedirectToSignIn() {
+ hideContextMenu(false);
signOut();
redirectToSignIn();
Log.info('Redirecting to Sign In because signOut() was called');
diff --git a/src/libs/actions/Task.js b/src/libs/actions/Task.js
index acb47d34e3dd..5e27d7db764d 100644
--- a/src/libs/actions/Task.js
+++ b/src/libs/actions/Task.js
@@ -10,6 +10,8 @@ import ROUTES from '../../ROUTES';
import CONST from '../../CONST';
import DateUtils from '../DateUtils';
import * as UserUtils from '../UserUtils';
+import * as PersonalDetailsUtils from '../PersonalDetailsUtils';
+import * as ReportActionsUtils from '../ReportActionsUtils';
/**
* Clears out the task info from the store
@@ -629,6 +631,47 @@ function dismissModalAndClearOutTaskInfo() {
clearOutTaskInfo();
}
+/**
+ * Returns Task assignee accountID
+ *
+ * @param {Object} taskReport
+ * @returns {Number|null}
+ */
+function getTaskAssigneeAccountID(taskReport) {
+ if (!taskReport) {
+ return null;
+ }
+
+ if (taskReport.managerID) {
+ return taskReport.managerID;
+ }
+
+ const reportAction = ReportActionsUtils.getParentReportAction(taskReport);
+ const childManagerEmail = lodashGet(reportAction, 'childManagerEmail', '');
+ return PersonalDetailsUtils.getAccountIDsByLogins([childManagerEmail])[0];
+}
+
+/**
+ * Returns Task owner accountID
+ *
+ * @param {Object} taskReport
+ * @returns {Number|null}
+ */
+function getTaskOwnerAccountID(taskReport) {
+ return lodashGet(taskReport, 'ownerAccountID', null);
+}
+
+/**
+ * Check if current user is either task assignee or task owner
+ *
+ * @param {Object} taskReport
+ * @param {Number} sessionAccountID
+ * @returns {Boolean}
+ */
+function isTaskAssigneeOrTaskOwner(taskReport, sessionAccountID) {
+ return sessionAccountID === getTaskOwnerAccountID(taskReport) || sessionAccountID === getTaskAssigneeAccountID(taskReport);
+}
+
export {
createTaskAndNavigate,
editTaskAndNavigate,
@@ -648,4 +691,6 @@ export {
cancelTask,
isTaskCanceled,
dismissModalAndClearOutTaskInfo,
+ getTaskAssigneeAccountID,
+ isTaskAssigneeOrTaskOwner,
};
diff --git a/src/pages/AddPersonalBankAccountPage.js b/src/pages/AddPersonalBankAccountPage.js
index 8e9e067957c3..b23f706d4481 100644
--- a/src/pages/AddPersonalBankAccountPage.js
+++ b/src/pages/AddPersonalBankAccountPage.js
@@ -88,6 +88,7 @@ class AddPersonalBankAccountPage extends React.Component {
{({show}) => (
- {this.props.translate('bankAccount.hasPhoneLoginError')}
-
+ if (this.state.shouldShowContinueSetupButton) {
+ return (
+
);
}
+ let errorText;
+ const userHasPhonePrimaryEmail = Str.endsWith(this.props.session.email, CONST.SMS.DOMAIN);
const throttledDate = lodashGet(this.props.reimbursementAccount, 'throttledDate');
- if (throttledDate) {
- errorComponent = (
-
- {this.props.translate('bankAccount.hasBeenThrottledError')}
-
- );
+ const hasUnsupportedCurrency = lodashGet(this.props.policy, 'outputCurrency', '') !== CONST.CURRENCY.USD;
+
+ if (userHasPhonePrimaryEmail) {
+ errorText = this.props.translate('bankAccount.hasPhoneLoginError');
+ } else if (throttledDate) {
+ errorText = this.props.translate('bankAccount.hasBeenThrottledError');
+ } else if (hasUnsupportedCurrency) {
+ errorText = this.props.translate('bankAccount.hasCurrencyError');
}
- if (errorComponent) {
+ if (errorText) {
return (
Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)}
/>
- {errorComponent}
+
+ {errorText}
+
);
}
diff --git a/src/pages/ReimbursementAccount/RequestorOnfidoStep.js b/src/pages/ReimbursementAccount/RequestorOnfidoStep.js
index aad5def99518..5d91f5dae562 100644
--- a/src/pages/ReimbursementAccount/RequestorOnfidoStep.js
+++ b/src/pages/ReimbursementAccount/RequestorOnfidoStep.js
@@ -41,7 +41,10 @@ class RequestorOnfidoStep extends React.Component {
render() {
return (
-
+
`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID || report.reportID}`,
},
+ session: {
+ key: ONYXKEYS.SESSION,
+ },
}),
)(HeaderView);
diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.js b/src/pages/home/report/ContextMenu/ContextMenuActions.js
index 9f01db925abb..d7c7896946d3 100644
--- a/src/pages/home/report/ContextMenu/ContextMenuActions.js
+++ b/src/pages/home/report/ContextMenu/ContextMenuActions.js
@@ -69,6 +69,7 @@ export default [
onEmojiSelected={onEmojiSelected}
onPressOpenPicker={openContextMenu}
onEmojiPickerClosed={closeContextMenu}
+ reportAction={reportAction}
/>
);
}
@@ -78,6 +79,7 @@ export default [
key="BaseQuickEmojiReactions"
closeContextMenu={closeContextMenu}
onEmojiSelected={onEmojiSelected}
+ reportAction={reportAction}
/>
);
},
diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js
index d17a12cbc7d6..2303ee6c556f 100644
--- a/src/pages/home/report/ReportActionItem.js
+++ b/src/pages/home/report/ReportActionItem.js
@@ -27,6 +27,7 @@ import * as DeviceCapabilities from '../../../libs/DeviceCapabilities';
import MiniReportActionContextMenu from './ContextMenu/MiniReportActionContextMenu';
import * as ReportActionContextMenu from './ContextMenu/ReportActionContextMenu';
import * as ContextMenuActions from './ContextMenu/ContextMenuActions';
+import * as EmojiPickerAction from '../../../libs/actions/EmojiPickerAction';
import {withBlockedFromConcierge, withNetwork, withPersonalDetails, withReportActionsDrafts} from '../../../components/OnyxProvider';
import RenameAction from '../../../components/ReportActionItem/RenameAction';
import InlineSystemMessage from '../../../components/InlineSystemMessage';
@@ -110,6 +111,22 @@ function ReportActionItem(props) {
const popoverAnchorRef = useRef();
const downloadedPreviews = useRef([]);
+ useEffect(
+ () => () => {
+ // ReportActionContextMenu and EmojiPicker are global component,
+ // we use showContextMenu and showEmojiPicker to show them,
+ // so we should also hide them when the current component is destroyed
+ if (ReportActionContextMenu.isActiveReportAction(props.action.reportActionID)) {
+ ReportActionContextMenu.hideContextMenu();
+ ReportActionContextMenu.hideDeleteModal();
+ }
+ if (EmojiPickerAction.isActiveReportAction(props.action.reportActionID)) {
+ EmojiPickerAction.hideEmojiPicker(true);
+ }
+ },
+ [props.action.reportActionID],
+ );
+
const isDraftEmpty = !props.draftMessage;
useEffect(() => {
if (isDraftEmpty) {
@@ -400,7 +417,12 @@ function ReportActionItem(props) {
};
if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) {
- return ;
+ return (
+
+ );
}
if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.RENAMED) {
return ;
diff --git a/src/pages/home/report/ReportActionItemCreated.js b/src/pages/home/report/ReportActionItemCreated.js
index 680ef678a53d..e90e793dcfcb 100644
--- a/src/pages/home/report/ReportActionItemCreated.js
+++ b/src/pages/home/report/ReportActionItemCreated.js
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, {memo} from 'react';
import {View, Image} from 'react-native';
import lodashGet from 'lodash/get';
import {withOnyx} from 'react-native-onyx';
@@ -29,11 +29,21 @@ const propTypes = {
/** Personal details of all the users */
personalDetails: PropTypes.objectOf(participantPropTypes),
+ /** The policy object for the current route */
+ policy: PropTypes.shape({
+ /** The name of the policy */
+ name: PropTypes.string,
+
+ /** The URL for the policy avatar */
+ avatar: PropTypes.string,
+ }),
+
...windowDimensionsPropTypes,
};
const defaultProps = {
report: {},
personalDetails: {},
+ policy: {},
};
function ReportActionItemCreated(props) {
@@ -91,5 +101,18 @@ export default compose(
personalDetails: {
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
},
+ policy: {
+ key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ },
}),
-)(ReportActionItemCreated);
+)(
+ memo(
+ ReportActionItemCreated,
+ (prevProps, nextProps) =>
+ lodashGet(prevProps.props, 'policy.name') === lodashGet(nextProps, 'policy.name') &&
+ lodashGet(prevProps.props, 'policy.avatar') === lodashGet(nextProps, 'policy.avatar') &&
+ lodashGet(prevProps.props, 'report.lastReadTime') === lodashGet(nextProps, 'report.lastReadTime') &&
+ lodashGet(prevProps.props, 'report.statusNum') === lodashGet(nextProps, 'report.statusNum') &&
+ lodashGet(prevProps.props, 'report.stateNum') === lodashGet(nextProps, 'report.stateNum'),
+ ),
+);
diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js
index c5b092487d70..56b78a73ae50 100644
--- a/src/pages/home/report/ReportActionItemMessageEdit.js
+++ b/src/pages/home/report/ReportActionItemMessageEdit.js
@@ -208,14 +208,18 @@ function ReportActionItemMessageEdit(props) {
const trimmedNewDraft = draft.trim();
+ // If the reportActionID and parentReportActionID are the same then the user is editing the first message of a
+ // thread and we should pass the parentReportID instead of the reportID of the thread
+ const reportID = props.report.parentReportActionID === props.action.reportActionID ? props.report.parentReportID : props.reportID;
+
// When user tries to save the empty message, it will delete it. Prompt the user to confirm deleting.
if (!trimmedNewDraft) {
- ReportActionContextMenu.showDeleteModal(props.reportID, props.action, false, deleteDraft, () => InteractionManager.runAfterInteractions(() => textInputRef.current.focus()));
+ ReportActionContextMenu.showDeleteModal(reportID, props.action, false, deleteDraft, () => InteractionManager.runAfterInteractions(() => textInputRef.current.focus()));
return;
}
- Report.editReportComment(props.reportID, props.action, trimmedNewDraft);
+ Report.editReportComment(reportID, props.action, trimmedNewDraft);
deleteDraft();
- }, [props.action, debouncedSaveDraft, deleteDraft, draft, props.reportID]);
+ }, [props.action, debouncedSaveDraft, deleteDraft, draft, props.reportID, props.report]);
/**
* @param {String} emoji
diff --git a/src/pages/home/report/ReportActionItemSingle.js b/src/pages/home/report/ReportActionItemSingle.js
index fa299858c338..3e49dfe621c4 100644
--- a/src/pages/home/report/ReportActionItemSingle.js
+++ b/src/pages/home/report/ReportActionItemSingle.js
@@ -1,6 +1,6 @@
import lodashGet from 'lodash/get';
import React from 'react';
-import {View, Pressable} from 'react-native';
+import {View} from 'react-native';
import PropTypes from 'prop-types';
import _ from 'underscore';
import reportActionPropTypes from './reportActionPropTypes';
@@ -21,6 +21,7 @@ import CONST from '../../../CONST';
import SubscriptAvatar from '../../../components/SubscriptAvatar';
import reportPropTypes from '../../reportPropTypes';
import * as UserUtils from '../../../libs/UserUtils';
+import PressableWithoutFeedback from '../../../components/Pressable/PressableWithoutFeedback';
import UserDetailsTooltip from '../../../components/UserDetailsTooltip';
const propTypes = {
@@ -85,11 +86,13 @@ function ReportActionItemSingle(props) {
return (
- showUserDetails(actorAccountID)}
+ accessibilityLabel={actorEmail}
+ accessibilityRole="button"
>
{props.shouldShowSubscriptAvatar ? (
@@ -111,15 +114,17 @@ function ReportActionItemSingle(props) {
)}
-
+
{props.showHeader ? (
- showUserDetails(actorAccountID)}
+ accessibilityLabel={actorEmail}
+ accessibilityRole="button"
>
{_.map(personArray, (fragment, index) => (
))}
-
+
) : null}
diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js
index 57c41564230f..4573b7acb665 100644
--- a/src/pages/home/sidebar/SidebarLinks.js
+++ b/src/pages/home/sidebar/SidebarLinks.js
@@ -28,7 +28,7 @@ import SidebarUtils from '../../../libs/SidebarUtils';
import reportPropTypes from '../../reportPropTypes';
import OfflineWithFeedback from '../../../components/OfflineWithFeedback';
import withNavigationFocus from '../../../components/withNavigationFocus';
-import withCurrentReportId from '../../../components/withCurrentReportId';
+import withCurrentReportId, {withCurrentReportIdPropTypes} from '../../../components/withCurrentReportId';
import withNavigation, {withNavigationPropTypes} from '../../../components/withNavigation';
import Header from '../../../components/Header';
import defaultTheme from '../../../styles/themes/default';
@@ -39,6 +39,7 @@ import PressableWithoutFeedback from '../../../components/Pressable/PressableWit
import * as Session from '../../../libs/actions/Session';
import Button from '../../../components/Button';
import * as UserUtils from '../../../libs/UserUtils';
+import KeyboardShortcut from '../../../libs/KeyboardShortcut';
const propTypes = {
/** Toggles the navigation menu open and closed */
@@ -83,7 +84,14 @@ const propTypes = {
/** The chat priority mode */
priorityMode: PropTypes.string,
+ /** Details about any modals being used */
+ modal: PropTypes.shape({
+ /** Indicates when an Alert modal is about to be visible */
+ willAlertModalBecomeVisible: PropTypes.bool,
+ }),
+
...withLocalizePropTypes,
+ ...withCurrentReportIdPropTypes,
...withNavigationPropTypes,
};
@@ -96,6 +104,7 @@ const defaultProps = {
},
reportIDFromRoute: '',
priorityMode: CONST.PRIORITY_MODE.DEFAULT,
+ modal: {},
};
class SidebarLinks extends React.Component {
@@ -115,10 +124,29 @@ class SidebarLinks extends React.Component {
App.setSidebarLoaded();
SidebarUtils.setIsSidebarLoadedReady();
this.isSidebarLoaded = true;
+
+ const shortcutConfig = CONST.KEYBOARD_SHORTCUTS.ESCAPE;
+ this.unsubscribeEscapeKey = KeyboardShortcut.subscribe(
+ shortcutConfig.shortcutKey,
+ () => {
+ if (this.props.modal.willAlertModalBecomeVisible) {
+ return;
+ }
+
+ Navigation.dismissModal();
+ },
+ shortcutConfig.descriptionKey,
+ shortcutConfig.modifiers,
+ true,
+ true,
+ );
}
componentWillUnmount() {
SidebarUtils.resetIsSidebarLoadedReadyPromise();
+ if (this.unsubscribeEscapeKey) {
+ this.unsubscribeEscapeKey();
+ }
}
showSearchPage() {
@@ -344,5 +372,8 @@ export default compose(
preferredLocale: {
key: ONYXKEYS.NVP_PREFERRED_LOCALE,
},
+ modal: {
+ key: ONYXKEYS.MODAL,
+ },
}),
)(SidebarLinks);
diff --git a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js
index bbe4806deb76..462e0d88906f 100644
--- a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js
+++ b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js
@@ -10,6 +10,7 @@ import CONST from '../../../../CONST';
import Performance from '../../../../libs/Performance';
import withWindowDimensions, {windowDimensionsPropTypes} from '../../../../components/withWindowDimensions';
import sidebarPropTypes from './sidebarPropTypes';
+import * as Browser from '../../../../libs/Browser';
const propTypes = {
...sidebarPropTypes,
@@ -48,7 +49,7 @@ class BaseSidebarScreen extends Component {
return (
{({insets}) => (
<>
diff --git a/src/pages/home/sidebar/SidebarScreen/index.js b/src/pages/home/sidebar/SidebarScreen/index.js
index 20eb62d0ddd6..75c28c2f069e 100755
--- a/src/pages/home/sidebar/SidebarScreen/index.js
+++ b/src/pages/home/sidebar/SidebarScreen/index.js
@@ -1,13 +1,28 @@
-import React, {useRef} from 'react';
+import React, {useRef, useCallback} from 'react';
+import {InteractionManager} from 'react-native';
+import {useFocusEffect} from '@react-navigation/native';
import sidebarPropTypes from './sidebarPropTypes';
import BaseSidebarScreen from './BaseSidebarScreen';
import FloatingActionButtonAndPopover from './FloatingActionButtonAndPopover';
import FreezeWrapper from '../../../../libs/Navigation/FreezeWrapper';
import withWindowDimensions from '../../../../components/withWindowDimensions';
+import StatusBar from '../../../../libs/StatusBar';
+import themeColors from '../../../../styles/themes/default';
function SidebarScreen(props) {
const popoverModal = useRef(null);
+ useFocusEffect(
+ useCallback(() => {
+ const previousColor = StatusBar.getBackgroundColor();
+ InteractionManager.runAfterInteractions(() => StatusBar.setBackgroundColor(themeColors.sidebar));
+
+ return () => {
+ InteractionManager.runAfterInteractions(() => StatusBar.setBackgroundColor(previousColor));
+ };
+ }, []),
+ );
+
/**
* Method create event listener
*/
diff --git a/src/pages/iou/SplitBillDetailsPage.js b/src/pages/iou/SplitBillDetailsPage.js
index d3f1847b373a..f6b63be4df76 100644
--- a/src/pages/iou/SplitBillDetailsPage.js
+++ b/src/pages/iou/SplitBillDetailsPage.js
@@ -18,6 +18,7 @@ import withReportOrNotFound from '../home/report/withReportOrNotFound';
import FullPageNotFoundView from '../../components/BlockingViews/FullPageNotFoundView';
import CONST from '../../CONST';
import HeaderWithBackButton from '../../components/HeaderWithBackButton';
+import * as PersonalDetailsUtils from '../../libs/PersonalDetailsUtils';
const propTypes = {
/* Onyx Props */
@@ -64,8 +65,9 @@ function getReportID(route) {
function SplitBillDetailsPage(props) {
const reportAction = props.reportActions[`${props.route.params.reportActionID.toString()}`];
- const personalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(reportAction.originalMessage.participantAccountIDs, props.personalDetails);
- const participants = OptionsListUtils.getParticipantsOptions(reportAction.originalMessage, personalDetails);
+ const participantAccountIDs = reportAction.originalMessage.participantAccountIDs || PersonalDetailsUtils.getAccountIDsByLogins(reportAction.originalMessage.participants);
+ const personalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(participantAccountIDs, props.personalDetails);
+ const participants = OptionsListUtils.getParticipantsOptions({participantAccountIDs}, personalDetails);
const payeePersonalDetails = personalDetails[reportAction.actorAccountID];
const participantsExcludingPayee = _.filter(participants, (participant) => participant.accountID !== reportAction.actorAccountID);
const splitAmount = parseInt(lodashGet(reportAction, 'originalMessage.amount', 0), 10);
diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSplitSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSplitSelector.js
index 638cb1250f64..8d03f0189f9c 100755
--- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSplitSelector.js
+++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSplitSelector.js
@@ -28,7 +28,7 @@ const propTypes = {
/** Selected participants from MoneyRequestModal with login */
participants: PropTypes.arrayOf(
PropTypes.shape({
- login: PropTypes.string.isRequired,
+ login: PropTypes.string,
accountID: PropTypes.number.isRequired,
alternateText: PropTypes.string,
hasDraftComment: PropTypes.bool,
diff --git a/src/pages/settings/Report/ReportSettingsPage.js b/src/pages/settings/Report/ReportSettingsPage.js
index ad57de80aaaa..53f0b99910bb 100644
--- a/src/pages/settings/Report/ReportSettingsPage.js
+++ b/src/pages/settings/Report/ReportSettingsPage.js
@@ -88,9 +88,7 @@ class ReportSettingsPage extends Component {
* @returns {Boolean}
*/
shouldDisableWelcomeMessage(linkedWorkspace) {
- return (
- ReportUtils.isArchivedRoom(this.props.report) || !ReportUtils.isChatRoom(this.props.report) || (!_.isEmpty(linkedWorkspace) && linkedWorkspace.role !== CONST.POLICY.ROLE.ADMIN)
- );
+ return ReportUtils.isArchivedRoom(this.props.report) || !ReportUtils.isChatRoom(this.props.report) || _.isEmpty(linkedWorkspace) || linkedWorkspace.role !== CONST.POLICY.ROLE.ADMIN;
}
render() {
diff --git a/src/pages/settings/Security/TwoFactorAuth/CodesPage.js b/src/pages/settings/Security/TwoFactorAuth/CodesPage.js
index 02911676d430..ec0631370d02 100644
--- a/src/pages/settings/Security/TwoFactorAuth/CodesPage.js
+++ b/src/pages/settings/Security/TwoFactorAuth/CodesPage.js
@@ -25,6 +25,7 @@ import Clipboard from '../../../../libs/Clipboard';
import themeColors from '../../../../styles/themes/default';
import localFileDownload from '../../../../libs/localFileDownload';
import * as TwoFactorAuthActions from '../../../../libs/actions/TwoFactorAuthActions';
+import * as StyleUtils from '../../../../styles/StyleUtils';
const propTypes = {
...withLocalizePropTypes,
@@ -56,7 +57,7 @@ function CodesPage(props) {
}, []);
return (
-
+
Navigation.goBack(ROUTES.SETTINGS_SECURITY)}
/>
-
+
-
+
{
@@ -123,15 +125,15 @@ function CodesPage(props) {
)}
-
-
+
+
);
diff --git a/src/pages/settings/Security/TwoFactorAuth/DisablePage.js b/src/pages/settings/Security/TwoFactorAuth/DisablePage.js
index 5f996378c5f0..1369c4d13e65 100644
--- a/src/pages/settings/Security/TwoFactorAuth/DisablePage.js
+++ b/src/pages/settings/Security/TwoFactorAuth/DisablePage.js
@@ -25,7 +25,7 @@ function DisablePage(props) {
}, []);
return (
-
+
Navigation.goBack(ROUTES.SETTINGS_SECURITY)}
diff --git a/src/pages/settings/Security/TwoFactorAuth/SuccessPage.js b/src/pages/settings/Security/TwoFactorAuth/SuccessPage.js
index a44342ea27b0..e3608b43685e 100644
--- a/src/pages/settings/Security/TwoFactorAuth/SuccessPage.js
+++ b/src/pages/settings/Security/TwoFactorAuth/SuccessPage.js
@@ -12,7 +12,7 @@ const defaultProps = {};
function SuccessPage(props) {
return (
-
+
+
Navigation.goBack(ROUTES.SETTINGS_2FA_CODES)}
/>
-
+
{props.translate('twoFactorAuth.scanCode')}
@@ -125,6 +128,7 @@ function VerifyPage(props) {
{Boolean(props.account.twoFactorAuthSecretKey) && {splitSecretInChunks(props.account.twoFactorAuthSecretKey)}}
Clipboard.setString(props.account.twoFactorAuthSecretKey)}
@@ -138,7 +142,7 @@ function VerifyPage(props) {
-
+
diff --git a/src/pages/tasks/TaskShareDestinationSelectorModal.js b/src/pages/tasks/TaskShareDestinationSelectorModal.js
index cb2f500195a0..329f4a410ccf 100644
--- a/src/pages/tasks/TaskShareDestinationSelectorModal.js
+++ b/src/pages/tasks/TaskShareDestinationSelectorModal.js
@@ -1,5 +1,5 @@
/* eslint-disable es/no-optional-chaining */
-import React, {useState, useEffect, useCallback} from 'react';
+import React, {useState, useEffect} from 'react';
import {View} from 'react-native';
import PropTypes from 'prop-types';
import {withOnyx} from 'react-native-onyx';
@@ -10,13 +10,11 @@ import styles from '../../styles/styles';
import Navigation from '../../libs/Navigation/Navigation';
import HeaderWithBackButton from '../../components/HeaderWithBackButton';
import ScreenWrapper from '../../components/ScreenWrapper';
-import Timing from '../../libs/actions/Timing';
import CONST from '../../CONST';
import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize';
import compose from '../../libs/compose';
import personalDetailsPropType from '../personalDetailsPropType';
import reportPropTypes from '../reportPropTypes';
-import Performance from '../../libs/Performance';
import * as TaskUtils from '../../libs/actions/Task';
import ROUTES from '../../ROUTES';
@@ -56,7 +54,7 @@ function TaskShareDestinationSelectorModal(props) {
setFilteredPersonalDetails(results.personalDetails);
}, [props]);
- const updateOptions = useCallback(() => {
+ useEffect(() => {
const {recentReports, personalDetails, userToInvite} = OptionsListUtils.getShareDestinationOptions(
props.reports,
props.personalDetails,
@@ -74,21 +72,8 @@ function TaskShareDestinationSelectorModal(props) {
setFilteredPersonalDetails(personalDetails);
}, [props, searchValue]);
- useEffect(() => {
- Timing.start(CONST.TIMING.SEARCH_RENDER);
- Performance.markStart(CONST.TIMING.SEARCH_RENDER);
-
- updateOptions();
-
- return () => {
- Timing.end(CONST.TIMING.SEARCH_RENDER);
- Performance.markEnd(CONST.TIMING.SEARCH_RENDER);
- };
- }, [updateOptions]);
-
const onChangeText = (newSearchTerm = '') => {
setSearchValue(newSearchTerm);
- updateOptions();
};
const getSections = () => {
@@ -158,10 +143,6 @@ function TaskShareDestinationSelectorModal(props) {
showTitleTooltip
shouldShowOptions={didScreenTransitionEnd}
placeholderText={props.translate('optionsSelector.nameEmailOrPhoneNumber')}
- onLayout={() => {
- Timing.end(CONST.TIMING.SEARCH_RENDER);
- Performance.markEnd(CONST.TIMING.SEARCH_RENDER);
- }}
safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle}
/>
diff --git a/src/pages/wallet/WalletStatementPage.js b/src/pages/wallet/WalletStatementPage.js
index c9ef84711016..9b0828d1da57 100644
--- a/src/pages/wallet/WalletStatementPage.js
+++ b/src/pages/wallet/WalletStatementPage.js
@@ -93,7 +93,10 @@ class WalletStatementPage extends React.Component {
const url = `${CONFIG.EXPENSIFY.EXPENSIFY_URL}statement.php?period=${this.yearMonth}`;
return (
-
+
report && report.policyID === policy.id);
Policy.deleteWorkspace(policy.id, policyReports, policy.name);
setIsDeleteModalOpen(false);
+ // Pop the deleted workspace page before opening workspace settings.
+ Navigation.goBack();
Navigation.navigate(ROUTES.SETTINGS_WORKSPACES);
}, [props.reports, policy]);
+ /**
+ * Call update workspace currency and hide the modal
+ */
+ const confirmCurrencyChangeAndHideModal = useCallback(() => {
+ Policy.updateGeneralSettings(policy.id, policy.name, CONST.CURRENCY.USD);
+ setIsCurrencyModalOpen(false);
+ ReimbursementAccount.navigateToBankAccountRoute(policy.id);
+ }, [policy]);
+
/**
* Navigates to workspace rooms
* @param {String} chatType
@@ -137,7 +149,7 @@ function WorkspaceInitialPage(props) {
{
translationKey: 'workspace.common.bankAccount',
icon: Expensicons.Bank,
- action: () => ReimbursementAccount.navigateToBankAccountRoute(policy.id),
+ action: () => (policy.outputCurrency === CONST.CURRENCY.USD ? ReimbursementAccount.navigateToBankAccountRoute(policy.id) : setIsCurrencyModalOpen(true)),
brickRoadIndicator: !_.isEmpty(props.reimbursementAccount.errors) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : '',
},
];
@@ -242,6 +254,16 @@ function WorkspaceInitialPage(props) {
+ setIsCurrencyModalOpen(false)}
+ prompt={props.translate('workspace.bankAccount.updateCurrencyPrompt')}
+ confirmText={props.translate('workspace.bankAccount.updateToUSD')}
+ cancelText={props.translate('common.cancel')}
+ danger
+ />
diff --git a/src/pages/workspace/WorkspaceMembersPage.js b/src/pages/workspace/WorkspaceMembersPage.js
index f2488af5d5af..4c170deb9ec5 100644
--- a/src/pages/workspace/WorkspaceMembersPage.js
+++ b/src/pages/workspace/WorkspaceMembersPage.js
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, {useState, useEffect, useCallback} from 'react';
import _ from 'underscore';
import lodashGet from 'lodash/get';
import {View} from 'react-native';
@@ -35,6 +35,7 @@ import KeyboardDismissingFlatList from '../../components/KeyboardDismissingFlatL
import withCurrentUserPersonalDetails from '../../components/withCurrentUserPersonalDetails';
import * as PolicyUtils from '../../libs/PolicyUtils';
import PressableWithFeedback from '../../components/Pressable/PressableWithFeedback';
+import usePrevious from '../../hooks/usePrevious';
import Log from '../../libs/Log';
import * as PersonalDetailsUtils from '../../libs/PersonalDetailsUtils';
@@ -71,69 +72,73 @@ const defaultProps = {
...policyDefaultProps,
};
-class WorkspaceMembersPage extends React.Component {
- constructor(props) {
- super(props);
-
- this.state = {
- selectedEmployees: [],
- isRemoveMembersConfirmModalVisible: false,
- errors: {},
- searchValue: '',
- };
-
- this.renderItem = this.renderItem.bind(this);
- this.updateSearchValue = this.updateSearchValue.bind(this);
- this.inviteUser = this.inviteUser.bind(this);
- this.addUser = this.addUser.bind(this);
- this.removeUser = this.removeUser.bind(this);
- this.askForConfirmationToRemove = this.askForConfirmationToRemove.bind(this);
- this.hideConfirmModal = this.hideConfirmModal.bind(this);
- }
-
- componentDidMount() {
- this.getWorkspaceMembers();
- }
+function WorkspaceMembersPage(props) {
+ const [selectedEmployees, setSelectedEmployees] = useState([]);
+ const [removeMembersConfirmModalVisible, setRemoveMembersConfirmModalVisible] = useState(false);
+ const [errors, setErrors] = useState({});
+ const [searchValue, setSearchValue] = useState('');
+ const prevIsOffline = usePrevious(props.network.isOffline);
- componentDidUpdate(prevProps) {
- if (prevProps.preferredLocale !== this.props.preferredLocale) {
- this.validate();
- }
+ /**
+ * Get members for the current workspace
+ */
+ const getWorkspaceMembers = useCallback(() => {
+ Policy.openWorkspaceMembersPage(props.route.params.policyID, _.keys(PolicyUtils.getClientPolicyMemberEmailsToAccountIDs(props.policyMembers, props.personalDetails)));
+ }, [props.route.params.policyID, props.policyMembers, props.personalDetails]);
- if (prevProps.policyMembers !== this.props.policyMembers) {
- this.setState((prevState) => ({
- selectedEmployees: _.intersection(
- prevState.selectedEmployees,
- _.map(_.values(PolicyUtils.getClientPolicyMemberEmailsToAccountIDs(this.props.policyMembers, this.props.personalDetails)), (accountID) => Number(accountID)),
- ),
- }));
- }
+ /**
+ * Check if the current selection includes members that cannot be removed
+ */
+ const validateSelection = useCallback(() => {
+ const newErrors = {};
+ const ownerAccountID = _.first(PersonalDetailsUtils.getAccountIDsByLogins([props.policy.owner]));
+ _.each(selectedEmployees, (member) => {
+ if (member !== ownerAccountID && member !== props.session.accountID) {
+ return;
+ }
+ newErrors[member] = props.translate('workspace.people.error.cannotRemove');
+ });
+ setErrors(newErrors);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [selectedEmployees, props.policy.owner, props.session.accountID]);
+
+ useEffect(() => {
+ getWorkspaceMembers();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ useEffect(() => {
+ validateSelection();
+ }, [props.preferredLocale, validateSelection]);
+
+ useEffect(() => {
+ setSelectedEmployees((prevSelected) =>
+ _.intersection(
+ prevSelected,
+ _.map(_.values(PolicyUtils.getClientPolicyMemberEmailsToAccountIDs(props.policyMembers, props.personalDetails)), (accountID) => Number(accountID)),
+ ),
+ );
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [props.policyMembers]);
- const isReconnecting = prevProps.network.isOffline && !this.props.network.isOffline;
+ useEffect(() => {
+ const isReconnecting = prevIsOffline && !props.network.isOffline;
if (!isReconnecting) {
return;
}
-
- this.getWorkspaceMembers();
- }
-
- /**
- * Get members for the current workspace
- */
- getWorkspaceMembers() {
- Policy.openWorkspaceMembersPage(this.props.route.params.policyID, _.keys(PolicyUtils.getClientPolicyMemberEmailsToAccountIDs(this.props.policyMembers, this.props.personalDetails)));
- }
+ getWorkspaceMembers();
+ }, [props.network.isOffline, prevIsOffline, getWorkspaceMembers]);
/**
* This function will iterate through the details of each policy member to check if the
* search string matches with any detail and return that filter.
* @param {Array} policyMembersPersonalDetails - This is the list of policy members
- * @param {*} searchValue - This is the string that the user has entered
+ * @param {*} search - This is the string that the user has entered
* @returns {Array} - The list of policy members that have anything similar to the searchValue
*/
- getMemberOptions(policyMembersPersonalDetails, searchValue) {
+ const getMemberOptions = (policyMembersPersonalDetails, search) => {
// If no search value, we return all members.
- if (_.isEmpty(searchValue)) {
+ if (_.isEmpty(search)) {
return policyMembersPersonalDetails;
}
@@ -155,164 +160,129 @@ class WorkspaceMembersPage extends React.Component {
if (member.phoneNumber) {
memberDetails += ` ${member.phoneNumber.toLowerCase()}`;
}
- return OptionsListUtils.isSearchStringMatch(searchValue, memberDetails);
+ return OptionsListUtils.isSearchStringMatch(search, memberDetails);
});
- }
-
- /**
- * @param {String} searchValue
- */
- updateSearchValue(searchValue = '') {
- this.setState({searchValue});
- }
+ };
/**
* Open the modal to invite a user
*/
- inviteUser() {
- this.updateSearchValue('');
- Navigation.navigate(ROUTES.getWorkspaceInviteRoute(this.props.route.params.policyID));
- }
+ const inviteUser = () => {
+ setSearchValue('');
+ Navigation.navigate(ROUTES.getWorkspaceInviteRoute(props.route.params.policyID));
+ };
/**
* Remove selected users from the workspace
*/
- removeUsers() {
- if (!_.isEmpty(this.state.errors)) {
+ const removeUsers = () => {
+ if (!_.isEmpty(errors)) {
return;
}
// Remove the admin from the list
- const accountIDsToRemove = _.without(this.state.selectedEmployees, this.props.session.accountID);
+ const accountIDsToRemove = _.without(selectedEmployees, props.session.accountID);
- Policy.removeMembers(accountIDsToRemove, this.props.route.params.policyID);
- this.setState({
- selectedEmployees: [],
- isRemoveMembersConfirmModalVisible: false,
- });
- }
+ Policy.removeMembers(accountIDsToRemove, props.route.params.policyID);
+ setSelectedEmployees([]);
+ setRemoveMembersConfirmModalVisible(false);
+ };
/**
* Show the modal to confirm removal of the selected members
*/
- askForConfirmationToRemove() {
- if (!_.isEmpty(this.state.errors)) {
+ const askForConfirmationToRemove = () => {
+ if (!_.isEmpty(errors)) {
return;
}
-
- this.setState({isRemoveMembersConfirmModalVisible: true});
- }
-
- /**
- * Hide the confirmation modal
- */
- hideConfirmModal() {
- this.setState({isRemoveMembersConfirmModalVisible: false});
- }
+ setRemoveMembersConfirmModalVisible(true);
+ };
/**
* Add or remove all users passed from the selectedEmployees list
* @param {Object} memberList
*/
- toggleAllUsers(memberList) {
+ const toggleAllUsers = (memberList) => {
const accountIDList = _.map(_.keys(memberList), (memberAccountID) => Number(memberAccountID));
- this.setState(
- (prevState) => ({
- selectedEmployees: !_.every(accountIDList, (memberAccountID) => _.contains(prevState.selectedEmployees, memberAccountID)) ? accountIDList : [],
- }),
- () => this.validate(),
- );
- }
+ setSelectedEmployees((prevSelected) => (!_.every(accountIDList, (memberAccountID) => _.contains(prevSelected, memberAccountID)) ? accountIDList : []));
+ validateSelection();
+ };
/**
- * Toggle user from the selectedEmployees list
- *
- * @param {String} accountID
- * @param {String} pendingAction
+ * Add user from the selectedEmployees list
*
+ * @param {String} login
*/
- toggleUser(accountID, pendingAction) {
- if (pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
- return;
- }
-
- // Add or remove the user if the checkbox is enabled
- if (_.contains(this.state.selectedEmployees, Number(accountID))) {
- this.removeUser(Number(accountID));
- } else {
- this.addUser(Number(accountID));
- }
- }
+ const addUser = useCallback(
+ (accountID) => {
+ setSelectedEmployees((prevSelected) => [...prevSelected, accountID]);
+ validateSelection();
+ },
+ [validateSelection],
+ );
/**
- * Add user from the selectedEmployees list
+ * Remove user from the selectedEmployees list
*
- * @param {Number} accountID
+ * @param {String} login
*/
- addUser(accountID) {
- this.setState(
- (prevState) => ({
- selectedEmployees: [...prevState.selectedEmployees, accountID],
- }),
- () => this.validate(),
- );
- }
+ const removeUser = useCallback(
+ (accountID) => {
+ setSelectedEmployees((prevSelected) => _.without(prevSelected, accountID));
+ validateSelection();
+ },
+ [validateSelection],
+ );
/**
- * Remove user from the selectedEmployees list
+ * Toggle user from the selectedEmployees list
+ *
+ * @param {String} accountID
+ * @param {String} pendingAction
*
- * @param {Number} accountID
*/
- removeUser(accountID) {
- this.setState(
- (prevState) => ({
- selectedEmployees: _.without(prevState.selectedEmployees, accountID),
- }),
- () => this.validate(),
- );
- }
+ const toggleUser = useCallback(
+ (accountID, pendingAction) => {
+ if (pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
+ return;
+ }
+
+ // Add or remove the user if the checkbox is enabled
+ if (_.contains(selectedEmployees, Number(accountID))) {
+ removeUser(accountID);
+ } else {
+ addUser(accountID);
+ }
+ },
+ [selectedEmployees, addUser, removeUser],
+ );
/**
* Dismisses the errors on one item
*
* @param {Object} item
*/
- dismissError(item) {
- if (item.pendingAction === 'delete') {
- Policy.clearDeleteMemberError(this.props.route.params.policyID, item.accountID);
- } else {
- Policy.clearAddMemberError(this.props.route.params.policyID, item.accountID);
- }
- }
-
- validate() {
- const errors = {};
- const ownerAccountID = _.first(PersonalDetailsUtils.getAccountIDsByLogins([this.props.policy.owner]));
- _.each(this.state.selectedEmployees, (member) => {
- if (member !== ownerAccountID && member !== this.props.session.accountID) {
- return;
+ const dismissError = useCallback(
+ (item) => {
+ if (item.pendingAction === 'delete') {
+ Policy.clearDeleteMemberError(props.route.params.policyID, item.accountID);
+ } else {
+ Policy.clearAddMemberError(props.route.params.policyID, item.accountID);
}
-
- errors[member] = 'workspace.people.error.cannotRemove';
- });
-
- this.setState({errors});
- }
+ },
+ [props.route.params.policyID],
+ );
/**
* Check if the policy member is deleted from the workspace
+ *
* @param {Object} policyMember
* @returns {Boolean}
*/
- isDeletedPolicyMember(policyMember) {
- return !this.props.network.isOffline && policyMember.pendingAction === 'delete' && _.isEmpty(policyMember.errors);
- }
+ const isDeletedPolicyMember = (policyMember) => !props.network.isOffline && policyMember.pendingAction === 'delete' && _.isEmpty(policyMember.errors);
/**
- * Do not move this or make it an anonymous function it is a method
- * so it will not be recreated each time we render an item
- *
- * See: https://reactnative.dev/docs/optimizing-flatlist-configuration#avoid-anonymous-function-on-renderitem
+ * Render a workspace member component
*
* @param {Object} args
* @param {Object} args.item
@@ -320,196 +290,197 @@ class WorkspaceMembersPage extends React.Component {
*
* @returns {React.Component}
*/
- renderItem({item}) {
- const hasError = !_.isEmpty(item.errors) || this.state.errors[item.accountID];
- const isChecked = _.contains(this.state.selectedEmployees, Number(item.accountID));
- return (
- this.dismissError(item)}
- pendingAction={item.pendingAction}
- errors={item.errors}
- >
- this.toggleUser(item.accountID, item.pendingAction)}
- accessibilityRole="checkbox"
- accessibilityState={{
- checked: isChecked,
- }}
- accessibilityLabel={this.props.formatPhoneNumber(item.displayName)}
- // disable hover dimming
- hoverDimmingValue={1}
- pressDimmingValue={0.7}
+ const renderItem = useCallback(
+ ({item}) => {
+ const hasError = !_.isEmpty(item.errors) || errors[item.accountID];
+ const isChecked = _.contains(selectedEmployees, Number(item.accountID));
+ return (
+ dismissError(item)}
+ pendingAction={item.pendingAction}
+ errors={item.errors}
>
- this.toggleUser(item.accountID, item.pendingAction)}
- />
-
- this.toggleUser(item.accountID, item.pendingAction)}
+ toggleUser(item.accountID, item.pendingAction)}
+ accessibilityRole="checkbox"
+ accessibilityState={{
+ checked: isChecked,
+ }}
+ accessibilityLabel={props.formatPhoneNumber(item.displayName)}
+ // disable hover dimming
+ hoverDimmingValue={1}
+ pressDimmingValue={0.7}
+ >
+ toggleUser(item.accountID, item.pendingAction)}
/>
-
- {(this.props.session.email === item.login || item.role === 'admin') && (
-
- {this.props.translate('common.admin')}
+
+ toggleUser(item.accountID, item.pendingAction)}
+ />
+ {(props.session.email === item.login || item.role === 'admin') && (
+
+ {props.translate('common.admin')}
+
+ )}
+
+ {!_.isEmpty(errors[item.accountID]) && (
+
)}
-
- {!_.isEmpty(this.state.errors[item.accountID]) && (
-
- )}
-
- );
+
+ );
+ },
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [selectedEmployees, errors, props.session.email, dismissError, toggleUser],
+ );
+
+ const policyOwner = lodashGet(props.policy, 'owner');
+ const currentUserLogin = lodashGet(props.currentUserPersonalDetails, 'login');
+ const removableMembers = {};
+ let data = [];
+ _.each(props.policyMembers, (policyMember, accountID) => {
+ if (isDeletedPolicyMember(policyMember)) {
+ return;
+ }
+ const details = props.personalDetails[accountID];
+ if (!details) {
+ Log.hmmm(`[WorkspaceMembersPage] no personal details found for policy member with accountID: ${accountID}`);
+ return;
+ }
+ data.push({
+ ...policyMember,
+ ...details,
+ });
+ });
+ data = _.sortBy(data, (value) => value.displayName.toLowerCase());
+ data = getMemberOptions(data, searchValue.trim().toLowerCase());
+
+ // If this policy is owned by Expensify then show all support (expensify.com or team.expensify.com) emails
+ // We don't want to show guides as policy members unless the user is a guide. Some customers get confused when they
+ // see random people added to their policy, but guides having access to the policies help set them up.
+ if (policyOwner && currentUserLogin && !PolicyUtils.isExpensifyTeam(policyOwner) && !PolicyUtils.isExpensifyTeam(currentUserLogin)) {
+ data = _.reject(data, (member) => PolicyUtils.isExpensifyTeam(member.login || member.displayName));
}
- render() {
- const policyOwner = lodashGet(this.props.policy, 'owner');
- const currentUserLogin = lodashGet(this.props.currentUserPersonalDetails, 'login');
- const removableMembers = {};
- let data = [];
- _.each(this.props.policyMembers, (policyMember, accountID) => {
- if (this.isDeletedPolicyMember(policyMember)) {
- return;
- }
- const details = this.props.personalDetails[accountID];
- if (!details) {
- Log.hmmm(`[WorkspaceMembersPage] no personal details found for policy member with accountID: ${accountID}`);
- return;
- }
- data.push({
- ...policyMember,
- ...details,
- });
- });
- data = _.sortBy(data, (value) => value.displayName.toLowerCase());
- data = this.getMemberOptions(data, this.state.searchValue.trim().toLowerCase());
-
- // If this policy is owned by Expensify then show all support (expensify.com or team.expensify.com) emails
- // We don't want to show guides as policy members unless the user is a guide. Some customers get confused when they
- // see random people added to their policy, but guides having access to the policies help set them up.
- if (policyOwner && currentUserLogin && !PolicyUtils.isExpensifyTeam(policyOwner) && !PolicyUtils.isExpensifyTeam(currentUserLogin)) {
- data = _.reject(data, (member) => PolicyUtils.isExpensifyTeam(member.login || member.displayName));
+ _.each(data, (member) => {
+ if (member.login === props.session.email || member.login === props.policy.owner || member.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
+ return;
}
-
- _.each(data, (member) => {
- if (member.login === this.props.session.email || member.login === this.props.policy.owner || member.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
- return;
- }
- removableMembers[member.accountID] = member;
- });
- const policyID = lodashGet(this.props.route, 'params.policyID');
- const policyName = lodashGet(this.props.policy, 'name');
-
- return (
-
- {({safeAreaPaddingBottomStyle}) => (
- Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)}
- >
- {
- this.updateSearchValue('');
- Navigation.goBack(ROUTES.getWorkspaceInitialRoute(policyID));
- }}
- shouldShowGetAssistanceButton
- guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_MEMBERS}
- />
- this.removeUsers()}
- onCancel={this.hideConfirmModal}
- prompt={this.props.translate('workspace.people.removeMembersPrompt')}
- confirmText={this.props.translate('common.remove')}
- cancelText={this.props.translate('common.cancel')}
- />
-
-
-
-
- );
- }
+ )}
+
+
+ )}
+
+ );
}
WorkspaceMembersPage.propTypes = propTypes;
WorkspaceMembersPage.defaultProps = defaultProps;
+WorkspaceMembersPage.displayName = 'WorkspaceMembersPage';
export default compose(
withLocalize,
diff --git a/src/styles/StyleUtils.js b/src/styles/StyleUtils.js
index 2feed7b9b8d8..d39a5032eff7 100644
--- a/src/styles/StyleUtils.js
+++ b/src/styles/StyleUtils.js
@@ -692,12 +692,13 @@ function convertRGBToUnitValues(red, green, blue) {
* Determines the theme color for a modal based on the app's background color,
* the modal's backdrop, and the backdrop's opacity.
*
+ * @param {String} bgColor - theme background color
* @returns {String} The theme color as an RGB value.
*/
-function getThemeBackgroundColor() {
+function getThemeBackgroundColor(bgColor = themeColors.appBG) {
const backdropOpacity = variables.modalFullscreenBackdropOpacity;
- const [backgroundRed, backgroundGreen, backgroundBlue] = hexadecimalToRGBArray(themeColors.appBG);
+ const [backgroundRed, backgroundGreen, backgroundBlue] = hexadecimalToRGBArray(bgColor);
const [backdropRed, backdropGreen, backdropBlue] = hexadecimalToRGBArray(themeColors.modalBackdrop);
const normalizedBackdropRGB = convertRGBToUnitValues(backdropRed, backdropGreen, backdropBlue);
const normalizedBackgroundRGB = convertRGBToUnitValues(backgroundRed, backgroundGreen, backgroundBlue);
@@ -1184,6 +1185,17 @@ function getOuterModalStyle(windowHeight, viewportOffsetTop) {
return Browser.isMobile() ? {maxHeight: windowHeight, marginTop: viewportOffsetTop} : {};
}
+/**
+ * Returns style object for flexWrap depending on the screen size
+ * @param {Boolean} isExtraSmallScreenWidth
+ * @return {Object}
+ */
+function getWrappingStyle(isExtraSmallScreenWidth) {
+ return {
+ flexWrap: isExtraSmallScreenWidth ? 'wrap' : 'nowrap',
+ };
+}
+
export {
getAvatarSize,
getAvatarStyle,
@@ -1250,4 +1262,5 @@ export {
getMentionTextColor,
getHeightOfMagicCodeInput,
getOuterModalStyle,
+ getWrappingStyle,
};
diff --git a/src/styles/styles.js b/src/styles/styles.js
index c42c1a77c07f..b7d19603eb52 100644
--- a/src/styles/styles.js
+++ b/src/styles/styles.js
@@ -1246,7 +1246,7 @@ const styles = {
},
leftPanelContainer: {
- maxWidth: variables.leftPaneMaxWidth,
+ maxWidth: variables.sideBarWidth,
},
rightPanelContainer: {
@@ -2217,10 +2217,6 @@ const styles = {
minWidth: 100,
},
- twoFactorAuthFooter: {
- marginTop: 'auto',
- },
-
anonymousRoomFooter: {
flexDirection: 'row',
alignItems: 'center',
@@ -2453,13 +2449,20 @@ const styles = {
alignItems: 'center',
},
+ checkboxPressable: {
+ borderRadius: 6,
+ padding: 2,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+
checkboxContainer: {
backgroundColor: themeColors.componentBG,
- borderRadius: 2,
+ borderRadius: 4,
height: 20,
width: 20,
- borderColor: themeColors.icon,
- borderWidth: 1,
+ borderColor: themeColors.borderLighter,
+ borderWidth: 2,
justifyContent: 'center',
alignItems: 'center',
},
@@ -2546,8 +2549,8 @@ const styles = {
marginBottom: 0,
},
- iouPreviewBoxCheckmark: {
- marginLeft: 4,
+ defaultCheckmarkWrapper: {
+ marginLeft: 8,
alignSelf: 'center',
},
@@ -2680,10 +2683,6 @@ const styles = {
outline: 'none',
},
- cursorPointer: {
- cursor: 'pointer',
- },
-
cardStyleNavigator: {
overflow: 'hidden',
height: '100%',
@@ -3341,11 +3340,6 @@ const styles = {
lineHeight: variables.lineHeightXXLarge,
},
- moneyRequestHeaderCheckmark: {
- marginLeft: 5,
- alignSelf: 'center',
- },
-
loginHeroBody: {
fontFamily: fontFamily.EXP_NEUE,
fontSize: variables.fontSizeSignInHeroBody,
diff --git a/src/styles/utilities/spacing.js b/src/styles/utilities/spacing.js
index e1cbb7e42469..e6823e43e921 100644
--- a/src/styles/utilities/spacing.js
+++ b/src/styles/utilities/spacing.js
@@ -189,6 +189,10 @@ export default {
marginTop: 44,
},
+ mtAuto: {
+ marginTop: 'auto',
+ },
+
mb0: {
marginBottom: 0,
},
diff --git a/src/styles/variables.js b/src/styles/variables.js
index 89897b7c1817..e0a9b78e60cb 100644
--- a/src/styles/variables.js
+++ b/src/styles/variables.js
@@ -78,7 +78,6 @@ export default {
modalFullscreenBackdropOpacity: 0.5,
tabletResponsiveWidthBreakpoint: 1024,
safeInsertPercentage: 0.7,
- leftPaneMaxWidth: 375,
sideBarWidth: 375,
pdfPageMaxWidth: 992,
tooltipzIndex: 10050,
diff --git a/web/index.html b/web/index.html
index 3dfeed27a4d2..90011c87e7a9 100644
--- a/web/index.html
+++ b/web/index.html
@@ -64,9 +64,6 @@
select:focus-visible, select:focus[data-focusvisible-polyfill] {
box-shadow: none;
}
- div[role="checkbox"]:focus-visible > div[data-checkbox], div[role="checkbox"]:focus[data-focusvisible-polyfill] > div[data-checkbox] {
- border-color: #03D47C !important;
- }
::-ms-reveal {
display: none;
}