Skip to content

Commit

Permalink
Merge pull request Expensify#30094 from lukemorawski/29990-chrome_mwe…
Browse files Browse the repository at this point in the history
…b_scan_button_provides_no_feedback

Chrome mWeb - scan button provides no feedback
  • Loading branch information
tgolen authored Nov 15, 2023
2 parents 0151ae3 + 5da6f16 commit 0a4ca7b
Show file tree
Hide file tree
Showing 8 changed files with 110 additions and 163 deletions.
72 changes: 0 additions & 72 deletions src/components/withTabAnimation.js

This file was deleted.

79 changes: 79 additions & 0 deletions src/hooks/useTabNavigatorFocus/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import {useTabAnimation} from '@react-navigation/material-top-tabs';
import {useIsFocused} from '@react-navigation/native';
import {useEffect, useState} from 'react';
import DomUtils from '@libs/DomUtils';

/**
* Custom React hook to determine the focus status of a tab in a Material Top Tab Navigator.
* It evaluates whether the current tab is focused based on the tab's animation position and
* the screen's focus status within a React Navigation environment.
*
* This hook is designed for use with the Material Top Tabs provided by '@react-navigation/material-top-tabs'.
* It leverages the `useTabAnimation` hook from the same package to track the animated position of tabs
* and the `useIsFocused` hook from '@react-navigation/native' to ascertain if the current screen is in focus.
*
* Note: This hook contains a conditional invocation of another hook (`useTabAnimation`),
* which is typically an anti-pattern in React. This is done to account for scenarios where the hook
* might not be used within a Material Top Tabs Navigator context. Proper usage should ensure that
* this hook is only used where appropriate.
*
* @param {Object} params - The parameters object.
* @param {Number} params.tabIndex - The index of the tab for which focus status is being determined.
* @returns {Boolean} Returns `true` if the tab is both animation-focused and screen-focused, otherwise `false`.
*
* @example
* const isTabFocused = useTabNavigatorFocus({ tabIndex: 1 });
*/
function useTabNavigatorFocus({tabIndex}) {
let tabPositionAnimation = null;
try {
// Retrieve the animation value from the tab navigator, which ranges from 0 to the total number of pages displayed.
// Even a minimal scroll towards the camera page (e.g., a value of 0.001 at start) should activate the camera for immediate responsiveness.
// STOP!!!!!!! This is not a pattern to be followed! We are conditionally rendering this hook becase when used in the edit flow we'll never be inside a tab navigator.
// eslint-disable-next-line react-hooks/rules-of-hooks
tabPositionAnimation = useTabAnimation();
} catch (error) {
tabPositionAnimation = null;
}
const isPageFocused = useIsFocused();
// set to true if the hook is not used within the MaterialTopTabs context
// the hook will then return true if the screen is focused
const [isTabFocused, setIsTabFocused] = useState(!tabPositionAnimation);

useEffect(() => {
if (!tabPositionAnimation) {
return;
}
const index = Number(tabIndex);

const listenerId = tabPositionAnimation.addListener(({value}) => {
// Activate camera as soon the index is animating towards the `tabIndex`
DomUtils.requestAnimationFrame(() => {
setIsTabFocused(value > index - 1 && value < index + 1);
});
});

// We need to get the position animation value on component initialization to determine
// if the tab is focused or not. Since it's an Animated.Value the only synchronous way
// to retrieve the value is to use a private method.
// eslint-disable-next-line no-underscore-dangle
const initialTabPositionValue = tabPositionAnimation.__getValue();

if (typeof initialTabPositionValue === 'number') {
DomUtils.requestAnimationFrame(() => {
setIsTabFocused(initialTabPositionValue > index - 1 && initialTabPositionValue < index + 1);
});
}

return () => {
if (!tabPositionAnimation) {
return;
}
tabPositionAnimation.removeListener(listenerId);
};
}, [tabIndex, tabPositionAnimation]);

return isTabFocused && isPageFocused;
}

export default useTabNavigatorFocus;
9 changes: 9 additions & 0 deletions src/libs/DomUtils/index.native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@ import GetActiveElement from './types';

const getActiveElement: GetActiveElement = () => null;

const requestAnimationFrame = (callback: () => void) => {
if (!callback) {
return;
}

callback();
};

export default {
getActiveElement,
requestAnimationFrame,
};
1 change: 1 addition & 0 deletions src/libs/DomUtils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ const getActiveElement: GetActiveElement = () => document.activeElement;

export default {
getActiveElement,
requestAnimationFrame: window.requestAnimationFrame.bind(window),
};
19 changes: 12 additions & 7 deletions src/pages/iou/ReceiptSelector/NavigationAwareCamera/index.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import {useIsFocused} from '@react-navigation/native';
import PropTypes from 'prop-types';
import React, {useEffect, useRef} from 'react';
import {View} from 'react-native';
import Webcam from 'react-webcam';
import useTabNavigatorFocus from '@hooks/useTabNavigatorFocus';

const propTypes = {
/* Flag to turn on/off the torch/flashlight - if available */
/** Flag to turn on/off the torch/flashlight - if available */
torchOn: PropTypes.bool,

/* Callback function when media stream becomes available - user granted camera permissions and camera starts to work */
/** The index of the tab that contains this camera */
cameraTabIndex: PropTypes.number.isRequired,

/** Callback function when media stream becomes available - user granted camera permissions and camera starts to work */
onUserMedia: PropTypes.func,

/* Callback function passing torch/flashlight capability as bool param of the browser */
/** Callback function passing torch/flashlight capability as bool param of the browser */
onTorchAvailability: PropTypes.func,
};

Expand All @@ -22,9 +25,11 @@ const defaultProps = {
};

// Wraps a camera that will only be active when the tab is focused or as soon as it starts to become focused.
const NavigationAwareCamera = React.forwardRef(({torchOn, onTorchAvailability, ...props}, ref) => {
const NavigationAwareCamera = React.forwardRef(({torchOn, onTorchAvailability, cameraTabIndex, ...props}, ref) => {
const trackRef = useRef(null);
const isCameraActive = useIsFocused();
const shouldShowCamera = useTabNavigatorFocus({
tabIndex: cameraTabIndex,
});

const handleOnUserMedia = (stream) => {
if (props.onUserMedia) {
Expand All @@ -51,7 +56,7 @@ const NavigationAwareCamera = React.forwardRef(({torchOn, onTorchAvailability, .
});
}, [torchOn]);

if (!isCameraActive) {
if (!shouldShowCamera) {
return null;
}
return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,77 +1,16 @@
import {useNavigation} from '@react-navigation/native';
import PropTypes from 'prop-types';
import React, {useEffect, useState} from 'react';
import React from 'react';
import {Camera} from 'react-native-vision-camera';
import withTabAnimation from '@components/withTabAnimation';
import CONST from '@src/CONST';
import useTabNavigatorFocus from '@hooks/useTabNavigatorFocus';

const propTypes = {
/* The index of the tab that contains this camera */
cameraTabIndex: PropTypes.number.isRequired,

/* Whether we're in a tab navigator */
isInTabNavigator: PropTypes.bool.isRequired,

/** Name of the selected receipt tab */
selectedTab: PropTypes.string.isRequired,

/** The tab animation from hook */
tabAnimation: PropTypes.shape({
addListener: PropTypes.func,
removeListener: PropTypes.func,
}),
};

const defaultProps = {
tabAnimation: undefined,
};

// Wraps a camera that will only be active when the tab is focused or as soon as it starts to become focused.
const NavigationAwareCamera = React.forwardRef(({cameraTabIndex, isInTabNavigator, selectedTab, tabAnimation, ...props}, ref) => {
// Get navigation to get initial isFocused value (only needed once during init!)
const navigation = useNavigation();
const [isCameraActive, setIsCameraActive] = useState(() => navigation.isFocused());

// Retrieve the animation value from the tab navigator, which ranges from 0 to the total number of pages displayed.
// Even a minimal scroll towards the camera page (e.g., a value of 0.001 at start) should activate the camera for immediate responsiveness.

useEffect(() => {
if (!isInTabNavigator) {
return;
}

const listenerId = tabAnimation.addListener(({value}) => {
if (selectedTab !== CONST.TAB.SCAN) {
return;
}
// Activate camera as soon the index is animating towards the `cameraTabIndex`
setIsCameraActive(value > cameraTabIndex - 1 && value < cameraTabIndex + 1);
});

return () => {
tabAnimation.removeListener(listenerId);
};
}, [cameraTabIndex, tabAnimation, isInTabNavigator, selectedTab]);

// Note: The useEffect can be removed once VisionCamera V3 is used.
// Its only needed for android, because there is a native cameraX android bug. With out this flow would break the camera:
// 1. Open camera tab
// 2. Take a picture
// 3. Go back from the opened screen
// 4. The camera is not working anymore
useEffect(() => {
const removeBlurListener = navigation.addListener('blur', () => {
setIsCameraActive(false);
});
const removeFocusListener = navigation.addListener('focus', () => {
setIsCameraActive(true);
});

return () => {
removeBlurListener();
removeFocusListener();
};
}, [navigation]);
const NavigationAwareCamera = React.forwardRef(({cameraTabIndex, ...props}, ref) => {
const isCameraActive = useTabNavigatorFocus({tabIndex: cameraTabIndex});

return (
<Camera
Expand All @@ -84,7 +23,6 @@ const NavigationAwareCamera = React.forwardRef(({cameraTabIndex, isInTabNavigato
});

NavigationAwareCamera.propTypes = propTypes;
NavigationAwareCamera.defaultProps = defaultProps;
NavigationAwareCamera.displayName = 'NavigationAwareCamera';

export default withTabAnimation(NavigationAwareCamera);
export default NavigationAwareCamera;
9 changes: 3 additions & 6 deletions src/pages/iou/ReceiptSelector/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,21 +53,17 @@ const propTypes = {

/** The id of the transaction we're editing */
transactionID: PropTypes.string,

/** Whether or not the receipt selector is in a tab navigator for tab animations */
// eslint-disable-next-line react/no-unused-prop-types
isInTabNavigator: PropTypes.bool,
};

const defaultProps = {
report: {},
iou: iouDefaultProps,
transactionID: '',
isInTabNavigator: true,
};

function ReceiptSelector({route, transactionID, iou, report}) {
const iouType = lodashGet(route, 'params.iouType', '');
const pageIndex = lodashGet(route, 'params.pageIndex', 1);

// Grouping related states
const [isAttachmentInvalid, setIsAttachmentInvalid] = useState(false);
Expand All @@ -81,7 +77,7 @@ function ReceiptSelector({route, transactionID, iou, report}) {

const [cameraPermissionState, setCameraPermissionState] = useState('prompt');
const [isFlashLightOn, toggleFlashlight] = useReducer((state) => !state, false);
const [isTorchAvailable, setIsTorchAvailable] = useState(true);
const [isTorchAvailable, setIsTorchAvailable] = useState(false);
const cameraRef = useRef(null);

const hideReciptModal = () => {
Expand Down Expand Up @@ -200,6 +196,7 @@ function ReceiptSelector({route, transactionID, iou, report}) {
torchOn={isFlashLightOn}
onTorchAvailability={setIsTorchAvailable}
forceScreenshotSourceSize
cameraTabIndex={pageIndex}
/>
</View>

Expand Down
12 changes: 1 addition & 11 deletions src/pages/iou/ReceiptSelector/index.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,23 +50,15 @@ const propTypes = {

/** The id of the transaction we're editing */
transactionID: PropTypes.string,

/** Whether or not the receipt selector is in a tab navigator for tab animations */
isInTabNavigator: PropTypes.bool,

/** Name of the selected receipt tab */
selectedTab: PropTypes.string,
};

const defaultProps = {
report: {},
iou: iouDefaultProps,
transactionID: '',
isInTabNavigator: true,
selectedTab: '',
};

function ReceiptSelector({route, report, iou, transactionID, isInTabNavigator, selectedTab}) {
function ReceiptSelector({route, report, iou, transactionID}) {
const devices = useCameraDevices('wide-angle-camera');
const device = devices.back;

Expand Down Expand Up @@ -218,8 +210,6 @@ function ReceiptSelector({route, report, iou, transactionID, isInTabNavigator, s
zoom={device.neutralZoom}
photo
cameraTabIndex={pageIndex}
isInTabNavigator={isInTabNavigator}
selectedTab={selectedTab}
/>
)}
<View style={[styles.flexRow, styles.justifyContentAround, styles.alignItemsCenter, styles.pv3]}>
Expand Down

0 comments on commit 0a4ca7b

Please sign in to comment.