Skip to content

Commit

Permalink
Merge pull request #24173 from tienifr/fix/23867-app-opens-all-presse…
Browse files Browse the repository at this point in the history
…d-option

Fix: Quickly press different menu items opens them one by one
  • Loading branch information
youssef-lr authored Aug 26, 2023
2 parents 253db32 + d105f13 commit 3ec41ac
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 29 deletions.
16 changes: 3 additions & 13 deletions src/components/Pressable/PressableWithFeedback.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React, {forwardRef, useState} from 'react';
import _ from 'underscore';
import propTypes from 'prop-types';
import {InteractionManager} from 'react-native';
import GenericPressable from './GenericPressable';
import GenericPressablePropTypes from './GenericPressable/PropTypes';
import OpacityView from '../OpacityView';
import variables from '../../styles/variables';
import useSingleExecution from '../../hooks/useSingleExecution';

const omittedProps = ['wrapperStyle'];

Expand Down Expand Up @@ -39,7 +39,7 @@ const PressableWithFeedbackDefaultProps = {

const PressableWithFeedback = forwardRef((props, ref) => {
const propsWithoutWrapperStyles = _.omit(props, omittedProps);
const [isExecuting, setIsExecuting] = useState(false);
const {isExecuting, singleExecution} = useSingleExecution();
const [isPressed, setIsPressed] = useState(false);
const [isHovered, setIsHovered] = useState(false);
const isDisabled = props.disabled || isExecuting;
Expand Down Expand Up @@ -73,17 +73,7 @@ const PressableWithFeedback = forwardRef((props, ref) => {
if (props.onPressOut) props.onPressOut();
}}
onPress={(e) => {
setIsExecuting(true);
const onPress = props.onPress(e);
InteractionManager.runAfterInteractions(() => {
if (!(onPress instanceof Promise)) {
setIsExecuting(false);
return;
}
onPress.finally(() => {
setIsExecuting(false);
});
});
singleExecution(() => props.onPress(e))();
}}
>
{(state) => (_.isFunction(props.children) ? props.children(state) : props.children)}
Expand Down
35 changes: 35 additions & 0 deletions src/hooks/useSingleExecution.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {InteractionManager} from 'react-native';
import {useCallback, useState} from 'react';

/**
* With any action passed in, it will only allow 1 such action to occur at a time.
*
* @returns {Object}
*/
export default function useSingleExecution() {
const [isExecuting, setIsExecuting] = useState(false);

const singleExecution = useCallback(
(action) => () => {
if (isExecuting) {
return;
}

setIsExecuting(true);

const execution = action();
InteractionManager.runAfterInteractions(() => {
if (!(execution instanceof Promise)) {
setIsExecuting(false);
return;
}
execution.finally(() => {
setIsExecuting(false);
});
});
},
[isExecuting],
);

return {isExecuting, singleExecution};
}
33 changes: 33 additions & 0 deletions src/hooks/useWaitForNavigation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {useEffect, useRef} from 'react';
import {useNavigation} from '@react-navigation/native';

/**
* Returns a promise that resolves when navigation finishes.
* Only use when navigating by react-navigation
*
* @returns {function}
*/
export default function useWaitForNavigation() {
const navigation = useNavigation();
const resolvePromises = useRef([]);

useEffect(() => {
const unsubscribeBlur = navigation.addListener('blur', () => {
resolvePromises.current.forEach((resolve) => {
resolve();
});
resolvePromises.current = [];
});

return () => {
unsubscribeBlur();
};
}, [navigation]);

return (navigate) => () => {
navigate();
return new Promise((resolve) => {
resolvePromises.current.push(resolve);
});
};
}
38 changes: 22 additions & 16 deletions src/pages/settings/InitialSettingsPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ import {CONTEXT_MENU_TYPES} from '../home/report/ContextMenu/ContextMenuActions'
import * as CurrencyUtils from '../../libs/CurrencyUtils';
import PressableWithoutFeedback from '../../components/Pressable/PressableWithoutFeedback';
import useLocalize from '../../hooks/useLocalize';
import useSingleExecution from '../../hooks/useSingleExecution';
import useWaitForNavigation from '../../hooks/useWaitForNavigation';

const propTypes = {
/* Onyx Props */
Expand Down Expand Up @@ -125,6 +127,8 @@ const defaultProps = {
};

function InitialSettingsPage(props) {
const {isExecuting, singleExecution} = useSingleExecution();
const waitForNavigate = useWaitForNavigation();
const popoverAnchor = useRef(null);
const {translate} = useLocalize();

Expand Down Expand Up @@ -186,16 +190,16 @@ function InitialSettingsPage(props) {
{
translationKey: 'common.shareCode',
icon: Expensicons.QrCode,
action: () => {
action: waitForNavigate(() => {
Navigation.navigate(ROUTES.SETTINGS_SHARE_CODE);
},
}),
},
{
translationKey: 'common.workspaces',
icon: Expensicons.Building,
action: () => {
action: waitForNavigate(() => {
Navigation.navigate(ROUTES.SETTINGS_WORKSPACES);
},
}),
floatRightAvatars: policiesAvatars,
shouldStackHorizontally: true,
avatarSize: CONST.AVATAR_SIZE.SMALLER,
Expand All @@ -204,31 +208,31 @@ function InitialSettingsPage(props) {
{
translationKey: 'common.profile',
icon: Expensicons.Profile,
action: () => {
action: waitForNavigate(() => {
Navigation.navigate(ROUTES.SETTINGS_PROFILE);
},
}),
brickRoadIndicator: profileBrickRoadIndicator,
},
{
translationKey: 'common.preferences',
icon: Expensicons.Gear,
action: () => {
action: waitForNavigate(() => {
Navigation.navigate(ROUTES.SETTINGS_PREFERENCES);
},
}),
},
{
translationKey: 'initialSettingsPage.security',
icon: Expensicons.Lock,
action: () => {
action: waitForNavigate(() => {
Navigation.navigate(ROUTES.SETTINGS_SECURITY);
},
}),
},
{
translationKey: 'common.wallet',
icon: Expensicons.Wallet,
action: () => {
action: waitForNavigate(() => {
Navigation.navigate(ROUTES.SETTINGS_WALLET);
},
}),
brickRoadIndicator:
PaymentMethods.hasPaymentMethodError(props.bankAccountList, paymentCardList) || !_.isEmpty(props.userWallet.errors) || !_.isEmpty(props.walletTerms.errors)
? 'error'
Expand All @@ -247,9 +251,9 @@ function InitialSettingsPage(props) {
{
translationKey: 'initialSettingsPage.about',
icon: Expensicons.Info,
action: () => {
action: waitForNavigate(() => {
Navigation.navigate(ROUTES.SETTINGS_ABOUT);
},
}),
},
{
translationKey: 'initialSettingsPage.signOut',
Expand All @@ -270,6 +274,7 @@ function InitialSettingsPage(props) {
props.userWallet.errors,
props.walletTerms.errors,
signOut,
waitForNavigate,
]);

const getMenuItems = useMemo(() => {
Expand All @@ -292,7 +297,8 @@ function InitialSettingsPage(props) {
title={keyTitle}
icon={item.icon}
iconType={item.iconType}
onPress={item.action}
disabled={isExecuting}
onPress={singleExecution(item.action)}
iconStyles={item.iconStyles}
shouldShowRightIcon
iconRight={item.iconRight}
Expand All @@ -312,7 +318,7 @@ function InitialSettingsPage(props) {
})}
</>
);
}, [getDefaultMenuItems, props.betas, props.userWallet.currentBalance, translate]);
}, [getDefaultMenuItems, props.betas, props.userWallet.currentBalance, translate, isExecuting, singleExecution]);

// On the very first sign in or after clearing storage these
// details will not be present on the first render so we'll just
Expand Down

0 comments on commit 3ec41ac

Please sign in to comment.