Skip to content

Commit

Permalink
Merge branch 'main' of github.com:Expensify/App into paulogasparsv-ex…
Browse files Browse the repository at this point in the history
…clude-empty-chats-from-lnh
  • Loading branch information
Paulo Vale committed Aug 10, 2023
2 parents d059564 + 774b78c commit ef0b220
Show file tree
Hide file tree
Showing 19 changed files with 200 additions and 278 deletions.
2 changes: 1 addition & 1 deletion contributingGuides/FORMS.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ Form inputs will NOT store draft values by default. This is to avoid accidentall

### Validate on Blur, on Change and Submit

Each individual form field that requires validation will have its own validate test defined. When the form field loses focus (blur) we will run that validate test and show feedback. A blur on one field will not cause other fields to validate or show errors unless they have already been blurred. To prevent server errors from being cleared inadvertently, we only run validation on blur if any form data has changed since the last validation/submit.
Each individual form field that requires validation will have its own validate test defined. When the form field loses focus (blur) we will run that validate test and show feedback. A blur on one field will not cause other fields to validate or show errors unless they have already been blurred.

Once a user has “touched” an input, i.e. blurred the input, we will also start validating that input on change when the user goes back to editing it.

Expand Down
14 changes: 1 addition & 13 deletions src/components/Form.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,6 @@ function Form(props) {
const inputRefs = useRef({});
const touchedInputs = useRef({});
const isFirstRender = useRef(true);
const lastValidatedValues = useRef({...props.draftValues});

const {validate, onSubmit, children} = props;

Expand Down Expand Up @@ -148,11 +147,6 @@ function Form(props) {
setErrors(touchedInputErrors);
}

const isAtLeastOneInputTouched = _.keys(touchedInputs.current).length > 0;
if (isAtLeastOneInputTouched) {
lastValidatedValues.current = values;
}

return touchedInputErrors;
},
[errors, touchedInputs, props.formID, validate],
Expand Down Expand Up @@ -312,12 +306,7 @@ function Form(props) {
// web and mobile web platforms.
setTimeout(() => {
setTouchedInput(inputID);

// To prevent server errors from being cleared inadvertently, we only run validation on blur if any form values have changed since the last validation/submit
const shouldValidate = !_.isEqual(inputValues, lastValidatedValues.current);
if (shouldValidate) {
onValidate(inputValues);
}
onValidate(inputValues);
}, 200);
}

Expand Down Expand Up @@ -429,7 +418,6 @@ function Form(props) {

delete inputRefs.current[inputID];
delete touchedInputs.current[inputID];
delete lastValidatedValues.current[inputID];

setInputValues((prevState) => {
const copyPrevState = _.clone(prevState);
Expand Down
82 changes: 35 additions & 47 deletions src/components/Image/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, {useEffect, useMemo} from 'react';
import {Image as RNImage} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import lodashGet from 'lodash/get';
Expand All @@ -7,76 +7,64 @@ import ONYXKEYS from '../../ONYXKEYS';
import {defaultProps, imagePropTypes} from './imagePropTypes';
import RESIZE_MODES from './resizeModes';

class Image extends React.Component {
componentDidMount() {
this.configureOnLoad();
}

componentDidUpdate(prevProps) {
if (prevProps.source === this.props.source) {
return;
}
this.configureOnLoad();
}

function Image(props) {
const {source: propsSource, isAuthTokenRequired, onLoad, session} = props;
/**
* Check if the image source is a URL - if so the `encryptedAuthToken` is appended
* to the source.
* @returns {Object} - the configured image source
*/
getImageSource() {
const source = this.props.source;
let imageSource = source;
if (this.props.isAuthTokenRequired) {
const source = useMemo(() => {
if (isAuthTokenRequired) {
// There is currently a `react-native-web` bug preventing the authToken being passed
// in the headers of the image request so the authToken is added as a query param.
// On native the authToken IS passed in the image request headers
const authToken = lodashGet(this.props, 'session.encryptedAuthToken', null);
imageSource = {uri: `${source.uri}?encryptedAuthToken=${encodeURIComponent(authToken)}`};
const authToken = lodashGet(session, 'encryptedAuthToken', null);
return {uri: `${propsSource.uri}?encryptedAuthToken=${encodeURIComponent(authToken)}`};
}

return imageSource;
}
return propsSource;
}, [propsSource, isAuthTokenRequired, session]);

/**
* The natural image dimensions are retrieved using the updated source
* and as a result the `onLoad` event needs to be manually invoked to return these dimensions
*/
configureOnLoad() {
useEffect(() => {
// If an onLoad callback was specified then manually call it and pass
// the natural image dimensions to match the native API
if (this.props.onLoad == null) {
if (onLoad == null) {
return;
}

const imageSource = this.getImageSource();
RNImage.getSize(imageSource.uri, (width, height) => {
this.props.onLoad({nativeEvent: {width, height}});
RNImage.getSize(source.uri, (width, height) => {
onLoad({nativeEvent: {width, height}});
});
}
}, [onLoad, source]);

render() {
// Omit the props which the underlying RNImage won't use
const forwardedProps = _.omit(this.props, ['source', 'onLoad', 'session', 'isAuthTokenRequired']);
const source = this.getImageSource();
// Omit the props which the underlying RNImage won't use
const forwardedProps = _.omit(props, ['source', 'onLoad', 'session', 'isAuthTokenRequired']);

return (
<RNImage
// eslint-disable-next-line react/jsx-props-no-spreading
{...forwardedProps}
source={source}
/>
);
}

return (
<RNImage
// eslint-disable-next-line react/jsx-props-no-spreading
{...forwardedProps}
source={source}
/>
);
}
function imagePropsAreEqual(prevProps, nextProps) {
return prevProps.source === nextProps.source;
}

Image.propTypes = imagePropTypes;
Image.defaultProps = defaultProps;

const ImageWithOnyx = withOnyx({
session: {
key: ONYXKEYS.SESSION,
},
})(Image);
const ImageWithOnyx = React.memo(
withOnyx({
session: {
key: ONYXKEYS.SESSION,
},
})(Image),
imagePropsAreEqual,
);
ImageWithOnyx.resizeMode = RESIZE_MODES;
export default ImageWithOnyx;
67 changes: 41 additions & 26 deletions src/components/MoneyRequestHeader.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
import React, {useCallback} from 'react';
import React, {useState, useCallback} from 'react';
import {withOnyx} from 'react-native-onyx';
import {View} from 'react-native';
import PropTypes from 'prop-types';
import lodashGet from 'lodash/get';
import HeaderWithBackButton from './HeaderWithBackButton';
import iouReportPropTypes from '../pages/iouReportPropTypes';
import withLocalize, {withLocalizePropTypes} from './withLocalize';
import * as ReportUtils from '../libs/ReportUtils';
import * as Expensicons from './Icon/Expensicons';
import participantPropTypes from './participantPropTypes';
import styles from '../styles/styles';
import withWindowDimensions from './withWindowDimensions';
import withWindowDimensions, {windowDimensionsPropTypes} from './withWindowDimensions';
import compose from '../libs/compose';
import Navigation from '../libs/Navigation/Navigation';
import ROUTES from '../ROUTES';
import * as Policy from '../libs/actions/Policy';
import ONYXKEYS from '../ONYXKEYS';
import * as IOU from '../libs/actions/IOU';
import * as ReportActionsUtils from '../libs/ReportActionsUtils';
import ConfirmModal from './ConfirmModal';
import useLocalize from '../hooks/useLocalize';

const propTypes = {
/** The report currently being looked at */
Expand All @@ -41,7 +42,7 @@ const propTypes = {
email: PropTypes.string,
}),

...withLocalizePropTypes,
...windowDimensionsPropTypes,
};

const defaultProps = {
Expand All @@ -52,6 +53,8 @@ const defaultProps = {
};

function MoneyRequestHeader(props) {
const {translate} = useLocalize();
const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false);
const moneyRequestReport = props.parentReport;
const isSettled = ReportUtils.isSettled(moneyRequestReport.reportID);
const policy = props.policies[`${ONYXKEYS.COLLECTION.POLICY}${props.report.policyID}`];
Expand All @@ -64,30 +67,43 @@ function MoneyRequestHeader(props) {

const deleteTransaction = useCallback(() => {
IOU.deleteMoneyRequest(parentReportAction.originalMessage.IOUTransactionID, parentReportAction, true);
}, [parentReportAction]);
setIsDeleteModalVisible(false);
}, [parentReportAction, setIsDeleteModalVisible]);

return (
<View style={[styles.pl0]}>
<HeaderWithBackButton
shouldShowAvatarWithDisplay
shouldShowPinButton={false}
shouldShowThreeDotsButton={!isPayer && !isSettled}
threeDotsMenuItems={[
{
icon: Expensicons.Trashcan,
text: props.translate('reportActionContextMenu.deleteAction', {action: parentReportAction}),
onSelected: deleteTransaction,
},
]}
threeDotsAnchorPosition={styles.threeDotsPopoverOffsetNoCloseButton(props.windowWidth)}
report={report}
policies={props.policies}
personalDetails={props.personalDetails}
shouldShowBackButton={props.isSmallScreenWidth}
onBackButtonPress={() => Navigation.goBack(ROUTES.HOME, false, true)}
shouldShowBorderBottom
<>
<View style={[styles.pl0]}>
<HeaderWithBackButton
shouldShowAvatarWithDisplay
shouldShowPinButton={false}
shouldShowThreeDotsButton={!isPayer && !isSettled}
threeDotsMenuItems={[
{
icon: Expensicons.Trashcan,
text: translate('reportActionContextMenu.deleteAction', {action: parentReportAction}),
onSelected: () => setIsDeleteModalVisible(true),
},
]}
threeDotsAnchorPosition={styles.threeDotsPopoverOffsetNoCloseButton(props.windowWidth)}
report={report}
policies={props.policies}
personalDetails={props.personalDetails}
shouldShowBackButton={props.isSmallScreenWidth}
onBackButtonPress={() => Navigation.goBack(ROUTES.HOME, false, true)}
shouldShowBorderBottom
/>
</View>
<ConfirmModal
title={translate('iou.deleteRequest')}
isVisible={isDeleteModalVisible}
onConfirm={deleteTransaction}
onCancel={() => setIsDeleteModalVisible(false)}
prompt={translate('iou.deleteConfirmation')}
confirmText={translate('common.delete')}
cancelText={translate('common.cancel')}
danger
/>
</View>
</>
);
}

Expand All @@ -97,7 +113,6 @@ MoneyRequestHeader.defaultProps = defaultProps;

export default compose(
withWindowDimensions,
withLocalize,
withOnyx({
session: {
key: ONYXKEYS.SESSION,
Expand Down
17 changes: 4 additions & 13 deletions src/components/MultipleAvatars.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {memo, useEffect, useState} from 'react';
import React, {memo, useMemo} from 'react';
import PropTypes from 'prop-types';
import {View} from 'react-native';
import _ from 'underscore';
Expand Down Expand Up @@ -91,17 +91,15 @@ function getContainerStyles(size, isInReportAction) {
return containerStyles;
}
function MultipleAvatars(props) {
const [avatarRows, setAvatarRows] = useState([props.icons]);
let avatarContainerStyles = getContainerStyles(props.size, props.isInReportAction);
const singleAvatarStyle = props.size === CONST.AVATAR_SIZE.SMALL ? styles.singleAvatarSmall : styles.singleAvatar;
const secondAvatarStyles = [props.size === CONST.AVATAR_SIZE.SMALL ? styles.secondAvatarSmall : styles.secondAvatar, ...props.secondAvatarStyle];
const tooltipTexts = props.shouldShowTooltip ? _.pluck(props.icons, 'name') : [''];

const calculateAvatarRows = () => {
const avatarRows = useMemo(() => {
// If we're not displaying avatars in rows or the number of icons is less than or equal to the max avatars in a row, return a single row
if (!props.shouldDisplayAvatarsInRows || props.icons.length <= props.maxAvatarsInRow) {
setAvatarRows([props.icons]);
return;
return [props.icons];
}

// Calculate the size of each row
Expand All @@ -112,14 +110,7 @@ function MultipleAvatars(props) {
const secondRow = props.icons.slice(0, rowSize);

// Update the state with the two rows as an array
setAvatarRows([firstRow, secondRow]);
};

useEffect(() => {
calculateAvatarRows();

// The only dependencies of the effect are based on props, so we can safely disable the exhaustive-deps rule
// eslint-disable-next-line react-hooks/exhaustive-deps
return [firstRow, secondRow];
}, [props.icons, props.maxAvatarsInRow, props.shouldDisplayAvatarsInRows]);

if (!props.icons.length) {
Expand Down
16 changes: 14 additions & 2 deletions src/components/TextInputWithCurrencySymbol.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, {useState, useEffect} from 'react';
import PropTypes from 'prop-types';
import AmountTextInput from './AmountTextInput';
import CurrencySymbolButton from './CurrencySymbolButton';
Expand Down Expand Up @@ -45,6 +45,12 @@ function TextInputWithCurrencySymbol(props) {
const currencySymbol = CurrencyUtils.getLocalizedCurrencySymbol(props.selectedCurrencyCode);
const isCurrencySymbolLTR = CurrencyUtils.isCurrencySymbolLTR(props.selectedCurrencyCode);

const [skipNextSelectionChange, setSkipNextSelectionChange] = useState(false);

useEffect(() => {
setSkipNextSelectionChange(true);
}, [props.formattedAmount]);

const currencySymbolButton = (
<CurrencySymbolButton
currencySymbol={currencySymbol}
Expand All @@ -59,7 +65,13 @@ function TextInputWithCurrencySymbol(props) {
placeholder={props.placeholder}
ref={props.forwardedRef}
selection={props.selection}
onSelectionChange={props.onSelectionChange}
onSelectionChange={(e) => {
if (skipNextSelectionChange) {
setSkipNextSelectionChange(false);
return;
}
props.onSelectionChange(e);
}}
/>
);

Expand Down
2 changes: 2 additions & 0 deletions src/languages/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,8 @@ export default {
pay: 'Pay',
viewDetails: 'View details',
pending: 'Pending',
deleteRequest: 'Delete request',
deleteConfirmation: 'Are you sure that you want to delete this request?',
settledExpensify: 'Paid',
settledElsewhere: 'Paid elsewhere',
settledPaypalMe: 'Paid using Paypal.me',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/es.js
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,8 @@ export default {
pay: 'Pagar',
viewDetails: 'Ver detalles',
pending: 'Pendiente',
deleteRequest: 'Eliminar pedido',
deleteConfirmation: '¿Estás seguro de que quieres eliminar este pedido?',
settledExpensify: 'Pagado',
settledElsewhere: 'Pagado de otra forma',
settledPaypalMe: 'Pagado con PayPal.me',
Expand Down
Loading

0 comments on commit ef0b220

Please sign in to comment.