diff --git a/android/app/build.gradle b/android/app/build.gradle
index b24050727c04..74f52a25b7fc 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 1001032805
- versionName "1.3.28-5"
+ versionCode 1001032903
+ versionName "1.3.29-3"
}
splits {
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 7615c1afdbf2..36db5a1a7925 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.3.28
+ 1.3.29
CFBundleSignature
????
CFBundleURLTypes
@@ -32,7 +32,7 @@
CFBundleVersion
- 1.3.28.5
+ 1.3.29.3
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index a614839d7a4b..85da6b831671 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 1.3.28
+ 1.3.29
CFBundleSignature
????
CFBundleVersion
- 1.3.28.5
+ 1.3.29.3
diff --git a/package-lock.json b/package-lock.json
index 13838e053b58..91467cada5d7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.3.28-5",
+ "version": "1.3.29-3",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.3.28-5",
+ "version": "1.3.29-3",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index 9d202ca32b8d..d0b6acc87d94 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.3.28-5",
+ "version": "1.3.29-3",
"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/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 (
{({show}) => (
showContextMenuForReport(event, anchor, report.reportID, action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report))}
+ accessibilityRole="imagebutton"
+ accessibilityLabel={props.translate('accessibilityHints.viewAttachment')}
>
{
- if (!option.login) {
+ if (!option.accountID) {
return;
}
- Navigation.navigate(ROUTES.getProfileRoute(ReportUtils.getAccountIDForLogin(option.login)));
+ Navigation.navigate(ROUTES.getProfileRoute(option.accountID));
};
/**
diff --git a/src/components/MultipleAvatars.js b/src/components/MultipleAvatars.js
index 83211ee1dafc..46535a259210 100644
--- a/src/components/MultipleAvatars.js
+++ b/src/components/MultipleAvatars.js
@@ -78,7 +78,7 @@ function MultipleAvatars(props) {
if (props.icons.length === 1 && !props.shouldStackHorizontally) {
return (
(
) : (
-
+
{/* View is necessary for tooltip to show for multiple avatars in LHN */}
{props.icons.length === 2 ? (
-
+
e === null);
+ const hasErrorMessages = !_.isEmpty(errorMessages);
const isOfflinePendingAction = props.network.isOffline && props.pendingAction;
const isUpdateOrDeleteError = hasErrors && (props.pendingAction === 'delete' || props.pendingAction === 'update');
const isAddError = hasErrors && props.pendingAction === 'add';
@@ -111,11 +115,11 @@ function OfflineWithFeedback(props) {
{children}
)}
- {props.shouldShowErrorMessages && hasErrors && (
+ {props.shouldShowErrorMessages && hasErrorMessages && (
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..cb86fd3d9e98 100644
--- a/src/components/Pressable/PressableWithFeedback.js
+++ b/src/components/Pressable/PressableWithFeedback.js
@@ -11,7 +11,7 @@ import * as StyleUtils from '../../styles/StyleUtils';
const omittedProps = ['style', 'pressStyle', 'hoverStyle', 'focusStyle', 'wrapperStyle'];
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 +31,7 @@ const PressableWithFeedbackPropTypes = {
};
const PressableWithFeedbackDefaultProps = {
- ..._.omit(GenericPressablePropTypes.defaultProps, omittedProps),
+ ...GenericPressablePropTypes.defaultProps,
pressDimmingValue: variables.pressDimValue,
hoverDimmingValue: variables.hoverDimValue,
nativeID: '',
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/ReportActionItem/ReportPreview.js b/src/components/ReportActionItem/ReportPreview.js
index 585e91c3f559..7fa9ebcd6745 100644
--- a/src/components/ReportActionItem/ReportPreview.js
+++ b/src/components/ReportActionItem/ReportPreview.js
@@ -1,5 +1,5 @@
import React from 'react';
-import {View, Pressable} from 'react-native';
+import {View} from 'react-native';
import PropTypes from 'prop-types';
import {withOnyx} from 'react-native-onyx';
import lodashGet from 'lodash/get';
@@ -25,6 +25,7 @@ import themeColors from '../../styles/themes/default';
import getButtonState from '../../libs/getButtonState';
import * as IOU from '../../libs/actions/IOU';
import refPropTypes from '../refPropTypes';
+import PressableWithoutFeedback from '../Pressable/PressableWithoutFeedback';
const propTypes = {
/** All the data of the action */
@@ -103,7 +104,7 @@ function ReportPreview(props) {
return (
{_.map(props.action.message, (message, index) => (
- {
Navigation.navigate(ROUTES.getReportRoute(props.iouReportID));
@@ -112,7 +113,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 ? (
@@ -140,7 +142,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/ReportWelcomeText.js b/src/components/ReportWelcomeText.js
index c000d88f4c8c..7c1b0252bee4 100644
--- a/src/components/ReportWelcomeText.js
+++ b/src/components/ReportWelcomeText.js
@@ -19,7 +19,7 @@ import CONST from '../CONST';
const personalDetailsPropTypes = PropTypes.shape({
/** The login of the person (either email or phone number) */
- login: PropTypes.string.isRequired,
+ login: PropTypes.string,
/** The URL of the person's avatar (there should already be a default avatar if
the person doesn't have their own avatar uploaded yet, except for anon users) */
diff --git a/src/components/TaskHeader.js b/src/components/TaskHeader.js
index 001ed2b10bdd..c9852a8cfcbf 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 && (
<>
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/UserDetailsTooltip/index.js b/src/components/UserDetailsTooltip/index.js
index 5dd2fa1c5785..786531801837 100644
--- a/src/components/UserDetailsTooltip/index.js
+++ b/src/components/UserDetailsTooltip/index.js
@@ -19,7 +19,7 @@ function UserDetailsTooltip(props) {
@@ -28,11 +28,11 @@ function UserDetailsTooltip(props) {
- {String(userDetails.login).trim() && !_.isEqual(userDetails.login, userDetails.displayName) ? Str.removeSMSDomain(userDetails.login) : ''}
+ {String(userDetails.login || '').trim() && !_.isEqual(userDetails.login, userDetails.displayName) ? Str.removeSMSDomain(userDetails.login) : ''}
),
- [userDetails.avatar, userDetails.displayName, userDetails.login],
+ [userDetails.avatar, userDetails.displayName, userDetails.login, userDetails.accountID],
);
if (!userDetails.displayName && !userDetails.login) {
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 151ae3c03e6a..daeb79db268b 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!',
},
anonymousReportFooter: {
logoTagline: 'Join in on the discussion.',
@@ -817,6 +818,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',
@@ -1071,8 +1073,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}`,
@@ -1205,6 +1205,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: {
@@ -1399,6 +1402,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 18c94ef3c23c..f16ef5b441e6 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!',
},
anonymousReportFooter: {
logoTagline: 'Únete a la discussion.',
@@ -819,6 +820,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',
@@ -1075,9 +1078,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}`,
@@ -1211,6 +1212,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: {
@@ -1865,6 +1870,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/OptionsListUtils.js b/src/libs/OptionsListUtils.js
index 8669eea4703a..9934880eb905 100644
--- a/src/libs/OptionsListUtils.js
+++ b/src/libs/OptionsListUtils.js
@@ -159,6 +159,7 @@ function getAvatarsForAccountIDs(accountIDs, personalDetails) {
return _.map(accountIDs, (accountID) => {
const userPersonalDetail = lodashGet(personalDetails, accountID, {login: '', accountID, avatar: ''});
return {
+ id: accountID,
source: UserUtils.getAvatar(userPersonalDetail.avatar, userPersonalDetail.accountID),
type: CONST.ICON_TYPE_AVATAR,
name: userPersonalDetail.login,
@@ -859,7 +860,7 @@ function getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetail, amount
alternateText: personalDetail.login,
icons: [
{
- source: UserUtils.getAvatar(personalDetail.avatar, personalDetail.login),
+ source: UserUtils.getAvatar(personalDetail.avatar, personalDetail.accountID),
name: personalDetail.login,
type: CONST.ICON_TYPE_AVATAR,
},
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 02c3308898f8..996c97e18269 100644
--- a/src/libs/ReportUtils.js
+++ b/src/libs/ReportUtils.js
@@ -683,19 +683,25 @@ function getIconsForParticipants(participants, personalDetails) {
for (let i = 0; i < participantsList.length; i++) {
const accountID = participantsList[i];
const avatarSource = UserUtils.getAvatar(lodashGet(personalDetails, [accountID, 'avatar'], ''), accountID);
- participantDetails.push([lodashGet(personalDetails, [accountID, 'login'], ''), lodashGet(personalDetails, [accountID, 'firstName'], ''), avatarSource]);
+ participantDetails.push([
+ accountID,
+ lodashGet(personalDetails, [accountID, 'login'], lodashGet(personalDetails, [accountID, 'displayName'], '')),
+ lodashGet(personalDetails, [accountID, 'firstName'], ''),
+ avatarSource,
+ ]);
}
// Sort all logins by first name (which is the second element in the array)
- const sortedParticipantDetails = participantDetails.sort((a, b) => a[1] - b[1]);
+ const sortedParticipantDetails = participantDetails.sort((a, b) => a[2] - b[2]);
// Now that things are sorted, gather only the avatars (third element in the array) and return those
const avatars = [];
for (let i = 0; i < sortedParticipantDetails.length; i++) {
const userIcon = {
- source: sortedParticipantDetails[i][2],
+ id: sortedParticipantDetails[i][0],
+ source: sortedParticipantDetails[i][3],
type: CONST.ICON_TYPE_AVATAR,
- name: sortedParticipantDetails[i][0],
+ name: sortedParticipantDetails[i][1],
};
avatars.push(userIcon);
}
@@ -724,11 +730,6 @@ function getIcons(report, personalDetails, defaultIcon = null, isPayer = false)
result.source = defaultIcon || Expensicons.FallbackAvatar;
return [result];
}
- if (isConciergeChatReport(report)) {
- result.source = CONST.CONCIERGE_ICON_URL;
- result.name = CONST.EMAIL.CONCIERGE;
- return [result];
- }
if (isArchivedRoom(report)) {
result.source = Expensicons.DeletedRoomAvatar;
return [result];
@@ -739,6 +740,7 @@ function getIcons(report, personalDetails, defaultIcon = null, isPayer = false)
const actorEmail = lodashGet(parentReportAction, 'actorEmail', '');
const actorAccountID = lodashGet(parentReportAction, 'actorAccountID', '');
const actorIcon = {
+ id: actorAccountID,
source: UserUtils.getAvatar(lodashGet(personalDetails, [actorAccountID, 'avatar']), actorAccountID),
name: actorEmail,
type: CONST.ICON_TYPE_AVATAR,
@@ -749,6 +751,7 @@ function getIcons(report, personalDetails, defaultIcon = null, isPayer = false)
if (isTaskReport(report)) {
const ownerEmail = report.ownerEmail || '';
const ownerIcon = {
+ id: report.ownerAccountID,
source: UserUtils.getAvatar(lodashGet(personalDetails, [report.ownerAccountID, 'avatar']), report.ownerAccountID),
name: ownerEmail,
type: CONST.ICON_TYPE_AVATAR,
@@ -786,6 +789,7 @@ function getIcons(report, personalDetails, defaultIcon = null, isPayer = false)
}
const adminIcon = {
+ id: report.ownerAccountID,
source: UserUtils.getAvatar(lodashGet(personalDetails, [report.ownerAccountID, 'avatar']), report.ownerAccountID),
name: report.ownerEmail,
type: CONST.ICON_TYPE_AVATAR,
@@ -806,6 +810,7 @@ function getIcons(report, personalDetails, defaultIcon = null, isPayer = false)
const accountID = isPayer ? report.managerID : report.ownerAccountID;
return [
{
+ id: accountID,
source: UserUtils.getAvatar(lodashGet(personalDetails, [accountID, 'avatar']), accountID),
name: email,
type: CONST.ICON_TYPE_AVATAR,
@@ -841,16 +846,6 @@ function getPersonalDetailsForAccountID(accountID) {
);
}
-/**
- * Gets the accountID for a login by looking in the ONYXKEYS.PERSONAL_DETAILS Onyx key (stored in the local variable, allPersonalDetails). If it doesn't exist in Onyx,
- * then an empty string is returned.
- * @param {String} login
- * @returns {String}
- */
-function getAccountIDForLogin(login) {
- return lodashGet(allPersonalDetails, [login, 'accountID'], '');
-}
-
/**
* Get the displayName for a single report participant.
*
@@ -2248,7 +2243,6 @@ function getParentReport(report) {
}
export {
- getAccountIDForLogin,
getReportParticipantsTitle,
isReportMessageAttachment,
findLastAccessedReport,
diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js
index e0e4f46ca486..eecd26069724 100644
--- a/src/libs/actions/IOU.js
+++ b/src/libs/actions/IOU.js
@@ -231,11 +231,15 @@ function buildOnyxDataForMoneyRequest(
[chatCreatedAction.reportActionID]: {
errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'),
},
+ [reportPreviewAction.reportActionID]: {
+ errors: ErrorUtils.getMicroSecondOnyxError(null),
+ },
}
- : {}),
- [reportPreviewAction.reportActionID]: {
- created: reportPreviewAction.created,
- },
+ : {
+ [reportPreviewAction.reportActionID]: {
+ created: reportPreviewAction.created,
+ },
+ }),
},
},
{
@@ -247,6 +251,9 @@ function buildOnyxDataForMoneyRequest(
[iouCreatedAction.reportActionID]: {
errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'),
},
+ [iouAction.reportActionID]: {
+ errors: ErrorUtils.getMicroSecondOnyxError(null),
+ },
}
: {
[iouAction.reportActionID]: {
@@ -494,15 +501,6 @@ function createSplitsAndOnyxData(participants, currentUserLogin, currentUserAcco
}
const failureData = [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${groupChatReport.reportID}`,
- value: {
- [groupIOUReportAction.reportActionID]: {
- errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'),
- },
- },
- },
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${groupTransaction.transactionID}`,
@@ -512,16 +510,37 @@ function createSplitsAndOnyxData(participants, currentUserLogin, currentUserAcco
},
];
- if (!existingGroupChatReport) {
+ if (existingGroupChatReport) {
failureData.push({
onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT}${groupChatReport.reportID}`,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${groupChatReport.reportID}`,
value: {
- errorFields: {
- createChat: ErrorUtils.getMicroSecondOnyxError('report.genericCreateReportFailureMessage'),
+ [groupIOUReportAction.reportActionID]: {
+ errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'),
},
},
});
+ } else {
+ failureData.push(
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${groupChatReport.reportID}`,
+ value: {
+ errorFields: {
+ createChat: ErrorUtils.getMicroSecondOnyxError('report.genericCreateReportFailureMessage'),
+ },
+ },
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${groupChatReport.reportID}`,
+ value: {
+ [groupIOUReportAction.reportActionID]: {
+ errors: ErrorUtils.getMicroSecondOnyxError(null),
+ },
+ },
+ },
+ );
}
// Loop through participants creating individual chats, iouReports and reportActionIDs as needed
@@ -951,15 +970,6 @@ function getSendMoneyParams(report, amount, currency, comment, paymentMethodType
];
const failureData = [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticIOUReport.reportID}`,
- value: {
- [optimisticIOUReportAction.reportActionID]: {
- errors: ErrorUtils.getMicroSecondOnyxError('iou.error.other'),
- },
- },
- },
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${optimisticTransaction.transactionID}`,
@@ -986,15 +996,26 @@ function getSendMoneyParams(report, amount, currency, comment, paymentMethodType
key: optimisticChatReportData.key,
value: {pendingFields: null},
});
- failureData.push({
- onyxMethod: Onyx.METHOD.MERGE,
- key: optimisticChatReportData.key,
- value: {
- errorFields: {
- createChat: ErrorUtils.getMicroSecondOnyxError('report.genericCreateReportFailureMessage'),
+ failureData.push(
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: optimisticChatReportData.key,
+ value: {
+ errorFields: {
+ createChat: ErrorUtils.getMicroSecondOnyxError('report.genericCreateReportFailureMessage'),
+ },
},
},
- });
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticIOUReport.reportID}`,
+ value: {
+ [optimisticIOUReportAction.reportActionID]: {
+ errors: ErrorUtils.getMicroSecondOnyxError(null),
+ },
+ },
+ },
+ );
// Add optimistic personal details for recipient
optimisticPersonalDetailListData = {
@@ -1012,9 +1033,16 @@ function getSendMoneyParams(report, amount, currency, comment, paymentMethodType
// Add an optimistic created action to the optimistic reportActions data
optimisticReportActionsData.value[optimisticCreatedAction.reportActionID] = optimisticCreatedAction;
-
- // If we're going to fail to create the report itself, let's not have redundant error messages for the IOU
- failureData[0].value[optimisticIOUReportAction.reportActionID] = {pendingAction: null};
+ } else {
+ failureData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticIOUReport.reportID}`,
+ value: {
+ [optimisticIOUReportAction.reportActionID]: {
+ errors: ErrorUtils.getMicroSecondOnyxError('iou.error.other'),
+ },
+ },
+ });
}
const optimisticData = [optimisticChatReportData, optimisticIOUReportData, optimisticReportActionsData, optimisticTransactionData];
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/Report.js b/src/libs/actions/Report.js
index 8c62046dea5a..5f24bcf7c409 100644
--- a/src/libs/actions/Report.js
+++ b/src/libs/actions/Report.js
@@ -67,6 +67,14 @@ Onyx.connect({
callback: (val) => (isNetworkOffline = lodashGet(val, 'isOffline', false)),
});
+let allPersonalDetails;
+Onyx.connect({
+ key: ONYXKEYS.PERSONAL_DETAILS_LIST,
+ callback: (val) => {
+ allPersonalDetails = val || {};
+ },
+});
+
const allReports = {};
let conciergeChatReportID;
const typingWatchTimers = {};
@@ -420,7 +428,7 @@ function openReport(reportID, participantLoginList = [], newReportObject = {}, p
const optimisticPersonalDetails = {};
_.map(participantLoginList, (login, index) => {
const accountID = newReportObject.participantAccountIDs[index];
- optimisticPersonalDetails[accountID] = {
+ optimisticPersonalDetails[accountID] = allPersonalDetails[accountID] || {
login,
accountID,
avatar: UserUtils.getDefaultAvatarURL(accountID),
diff --git a/src/libs/actions/Task.js b/src/libs/actions/Task.js
index 4003ba8002f0..c31f1617349d 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
@@ -627,6 +629,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,
@@ -646,4 +689,6 @@ export {
cancelTask,
isTaskCanceled,
dismissModalAndClearOutTaskInfo,
+ getTaskAssigneeAccountID,
+ isTaskAssigneeOrTaskOwner,
};
diff --git a/src/pages/DetailsPage.js b/src/pages/DetailsPage.js
index 2b5be4121760..ac42017d3c7c 100755
--- a/src/pages/DetailsPage.js
+++ b/src/pages/DetailsPage.js
@@ -22,7 +22,7 @@ import * as ReportUtils from '../libs/ReportUtils';
import * as Expensicons from '../components/Icon/Expensicons';
import MenuItem from '../components/MenuItem';
import AttachmentModal from '../components/AttachmentModal';
-import PressableWithoutFocus from '../components/PressableWithoutFocus';
+import PressableWithoutFocus from '../components/Pressable/PressableWithoutFocus';
import * as Report from '../libs/actions/Report';
import OfflineWithFeedback from '../components/OfflineWithFeedback';
import AutoUpdateTime from '../components/AutoUpdateTime';
diff --git a/src/pages/NewChatPage.js b/src/pages/NewChatPage.js
index a0244f72fae7..76a817acc334 100755
--- a/src/pages/NewChatPage.js
+++ b/src/pages/NewChatPage.js
@@ -14,6 +14,7 @@ import withWindowDimensions, {windowDimensionsPropTypes} from '../components/wit
import HeaderWithBackButton from '../components/HeaderWithBackButton';
import ScreenWrapper from '../components/ScreenWrapper';
import withLocalize, {withLocalizePropTypes} from '../components/withLocalize';
+import * as Browser from '../libs/Browser';
import compose from '../libs/compose';
import personalDetailsPropType from './personalDetailsPropType';
import reportPropTypes from './reportPropTypes';
@@ -242,7 +243,7 @@ class NewChatPage extends Component {
onChangeText={this.updateOptionsWithSearchTerm}
headerMessage={headerMessage}
boldStyle
- shouldFocusOnSelectRow={this.props.isGroupChat}
+ shouldFocusOnSelectRow={this.props.isGroupChat && !Browser.isMobile()}
shouldShowConfirmButton={this.props.isGroupChat}
shouldShowOptions={didScreenTransitionEnd && isOptionsDataReady}
confirmButtonText={this.props.translate('newChatPage.createGroup')}
diff --git a/src/pages/ProfilePage.js b/src/pages/ProfilePage.js
index ac0d1db4d14b..41a5d153f0cc 100755
--- a/src/pages/ProfilePage.js
+++ b/src/pages/ProfilePage.js
@@ -23,7 +23,7 @@ import * as ReportUtils from '../libs/ReportUtils';
import * as Expensicons from '../components/Icon/Expensicons';
import MenuItem from '../components/MenuItem';
import AttachmentModal from '../components/AttachmentModal';
-import PressableWithoutFocus from '../components/PressableWithoutFocus';
+import PressableWithoutFocus from '../components/Pressable/PressableWithoutFocus';
import * as Report from '../libs/actions/Report';
import OfflineWithFeedback from '../components/OfflineWithFeedback';
import AutoUpdateTime from '../components/AutoUpdateTime';
@@ -148,8 +148,10 @@ function ProfilePage(props) {
>
{({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/ReportParticipantsPage.js b/src/pages/ReportParticipantsPage.js
index da065dadcb77..cdcaabcfe34d 100755
--- a/src/pages/ReportParticipantsPage.js
+++ b/src/pages/ReportParticipantsPage.js
@@ -67,6 +67,7 @@ const getAllParticipants = (report, personalDetails) => {
accountID: userPersonalDetail.accountID,
icons: [
{
+ id: accountID,
source: UserUtils.getAvatar(userPersonalDetail.avatar, accountID),
name: userLogin,
type: CONST.ICON_TYPE_AVATAR,
diff --git a/src/pages/home/HeaderView.js b/src/pages/home/HeaderView.js
index b12733f64ef8..15ae1d2c64ec 100644
--- a/src/pages/home/HeaderView.js
+++ b/src/pages/home/HeaderView.js
@@ -51,6 +51,11 @@ const propTypes = {
guideCalendarLink: PropTypes.string,
}),
+ /** Current user session */
+ session: PropTypes.shape({
+ accountID: PropTypes.number,
+ }),
+
/** The report actions from the parent report */
// TO DO: Replace with HOC https://github.com/Expensify/App/issues/18769.
// eslint-disable-next-line react/no-unused-prop-types
@@ -68,6 +73,9 @@ const defaultProps = {
guideCalendarLink: null,
},
parentReport: {},
+ session: {
+ accountID: 0,
+ },
};
function HeaderView(props) {
@@ -91,7 +99,8 @@ function HeaderView(props) {
const shouldShowCallButton = (isConcierge && guideCalendarLink) || (!isAutomatedExpensifyAccount && !isTaskReport);
const threeDotMenuItems = [];
if (isTaskReport) {
- if (props.report.stateNum === CONST.REPORT.STATE_NUM.OPEN && props.report.statusNum === CONST.REPORT.STATUS.OPEN) {
+ const isTaskAssigneeOrTaskOwner = Task.isTaskAssigneeOrTaskOwner(props.report, props.session.accountID);
+ if (props.report.stateNum === CONST.REPORT.STATE_NUM.OPEN && props.report.statusNum === CONST.REPORT.STATUS.OPEN && isTaskAssigneeOrTaskOwner) {
threeDotMenuItems.push({
icon: Expensicons.Checkmark,
text: props.translate('newTaskPage.markAsDone'),
@@ -100,7 +109,7 @@ function HeaderView(props) {
}
// Task is marked as completed
- if (props.report.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED && props.report.statusNum === CONST.REPORT.STATUS.APPROVED) {
+ if (props.report.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED && props.report.statusNum === CONST.REPORT.STATUS.APPROVED && isTaskAssigneeOrTaskOwner) {
threeDotMenuItems.push({
icon: Expensicons.Checkmark,
text: props.translate('newTaskPage.markAsIncomplete'),
@@ -109,7 +118,7 @@ function HeaderView(props) {
}
// Task is not closed
- if (props.report.stateNum !== CONST.REPORT.STATE_NUM.SUBMITTED && props.report.statusNum !== CONST.REPORT.STATUS.CLOSED) {
+ if (props.report.stateNum !== CONST.REPORT.STATE_NUM.SUBMITTED && props.report.statusNum !== CONST.REPORT.STATUS.CLOSED && isTaskAssigneeOrTaskOwner) {
threeDotMenuItems.push({
icon: Expensicons.Trashcan,
text: props.translate('common.cancel'),
@@ -258,5 +267,8 @@ export default compose(
parentReport: {
key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID || report.reportID}`,
},
+ session: {
+ key: ONYXKEYS.SESSION,
+ },
}),
)(HeaderView);
diff --git a/src/pages/home/report/ReactionList/BaseReactionList.js b/src/pages/home/report/ReactionList/BaseReactionList.js
index 56ea9b23708e..87d68780e836 100755
--- a/src/pages/home/report/ReactionList/BaseReactionList.js
+++ b/src/pages/home/report/ReactionList/BaseReactionList.js
@@ -83,16 +83,16 @@ function BaseReactionList(props) {
}}
option={{
text: Str.removeSMSDomain(item.displayName),
- alternateText: Str.removeSMSDomain(item.login),
+ alternateText: Str.removeSMSDomain(item.login || ''),
participantsList: [item],
icons: [
{
- source: UserUtils.getAvatar(item.avatar, item.login),
+ source: UserUtils.getAvatar(item.avatar, item.accountID),
name: item.login,
type: CONST.ICON_TYPE_AVATAR,
},
],
- keyForList: item.login,
+ keyForList: item.login || String(item.accountID),
}}
/>
);
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..fdd2567c2c90 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';
@@ -84,6 +84,7 @@ const propTypes = {
priorityMode: PropTypes.string,
...withLocalizePropTypes,
+ ...withCurrentReportIdPropTypes,
...withNavigationPropTypes,
};
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 57057c17cff5..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,
@@ -67,7 +68,7 @@ function CodesPage(props) {
onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_SECURITY)}
/>
-
+
-
+
{
@@ -123,15 +125,15 @@ function CodesPage(props) {
)}
-
-
+
+
);
diff --git a/src/pages/settings/Security/TwoFactorAuth/VerifyPage.js b/src/pages/settings/Security/TwoFactorAuth/VerifyPage.js
index 644ff14f6ea8..7ad0721dfb10 100644
--- a/src/pages/settings/Security/TwoFactorAuth/VerifyPage.js
+++ b/src/pages/settings/Security/TwoFactorAuth/VerifyPage.js
@@ -102,7 +102,10 @@ function VerifyPage(props) {
onBackButtonPress={() => 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/workspace/WorkspaceInitialPage.js b/src/pages/workspace/WorkspaceInitialPage.js
index 0ceb52066cfe..73f659574e71 100644
--- a/src/pages/workspace/WorkspaceInitialPage.js
+++ b/src/pages/workspace/WorkspaceInitialPage.js
@@ -67,6 +67,7 @@ function dismissError(policyID) {
function WorkspaceInitialPage(props) {
const policy = props.policy;
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
+ const [isCurrencyModalOpen, setIsCurrencyModalOpen] = useState(false);
const hasPolicyCreationError = Boolean(policy.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && policy.errors);
/**
@@ -76,9 +77,20 @@ function WorkspaceInitialPage(props) {
const policyReports = _.filter(props.reports, (report) => 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 4e90c16c0077..509bc439c0e2 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,7 +35,9 @@ 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';
const propTypes = {
/** The personal details of the person who is logged in */
@@ -70,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;
}
@@ -154,163 +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 = {};
- _.each(this.state.selectedEmployees, (member) => {
- if (member !== this.props.policy.owner && member !== this.props.session.email) {
- 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
@@ -318,196 +290,197 @@ class WorkspaceMembersPage extends React.Component {
*
* @returns {React.Component}
*/
- renderItem({item}) {
- const hasError = !_.isEmpty(item.errors) || this.state.errors[item.login];
- 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.login]) && (
-
- )}
-
- );
+
+ );
+ },
+ // 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));
}
- 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));
+ _.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..a7455535240d 100644
--- a/src/styles/StyleUtils.js
+++ b/src/styles/StyleUtils.js
@@ -1184,6 +1184,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 +1261,5 @@ export {
getMentionTextColor,
getHeightOfMagicCodeInput,
getOuterModalStyle,
+ getWrappingStyle,
};
diff --git a/src/styles/styles.js b/src/styles/styles.js
index c42c1a77c07f..fcf3b9cc5dbb 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',
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,