From 2eda49d79f8a8df6134dbc27e66ff776dd8f4b02 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Mon, 6 Mar 2023 20:19:28 +0500 Subject: [PATCH 0001/1075] feat: refactored popover component to work without overlay --- src/components/PopoverWithMeasuredContent.js | 2 +- src/components/PopoverWithoutOverlay/index.js | 96 +++++++++++++++++++ .../PopoverWithoutOverlay/popoverPropTypes.js | 36 +++++++ 3 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 src/components/PopoverWithoutOverlay/index.js create mode 100644 src/components/PopoverWithoutOverlay/popoverPropTypes.js diff --git a/src/components/PopoverWithMeasuredContent.js b/src/components/PopoverWithMeasuredContent.js index 2285f857968a..5cf045842551 100644 --- a/src/components/PopoverWithMeasuredContent.js +++ b/src/components/PopoverWithMeasuredContent.js @@ -3,7 +3,7 @@ import React, {Component} from 'react'; import PropTypes from 'prop-types'; import {View} from 'react-native'; import lodashGet from 'lodash/get'; -import Popover from './Popover'; +import Popover from './PopoverWithoutOverlay'; import {propTypes as popoverPropTypes, defaultProps as defaultPopoverProps} from './Popover/popoverPropTypes'; import withWindowDimensions, {windowDimensionsPropTypes} from './withWindowDimensions'; import CONST from '../CONST'; diff --git a/src/components/PopoverWithoutOverlay/index.js b/src/components/PopoverWithoutOverlay/index.js new file mode 100644 index 000000000000..8f3c9868b968 --- /dev/null +++ b/src/components/PopoverWithoutOverlay/index.js @@ -0,0 +1,96 @@ +import React from 'react'; +import {Pressable, View} from 'react-native'; +import {SafeAreaInsetsContext} from 'react-native-safe-area-context'; +import {propTypes, defaultProps} from './popoverPropTypes'; +import styles from '../../styles/styles'; +import * as StyleUtils from '../../styles/StyleUtils'; +import getModalStyles from '../../styles/getModalStyles'; +import withWindowDimensions from '../withWindowDimensions'; + +/* + * This is a convenience wrapper around the Modal component for a responsive Popover. + * On small screen widths, it uses BottomDocked modal type, and a Popover type on wide screen widths. + */ +const Popover = (props) => { + const ref = React.useRef(null); + + const { + modalStyle, + modalContainerStyle, + shouldAddTopSafeAreaMargin, + shouldAddBottomSafeAreaMargin, + shouldAddTopSafeAreaPadding, + shouldAddBottomSafeAreaPadding, + } = getModalStyles( + 'popover', + { + windowWidth: props.windowWidth, + windowHeight: props.windowHeight, + isSmallScreenWidth: props.isSmallScreenWidth, + }, + props.anchorPosition, + props.innerContainerStyle, + props.outerStyle, + ); + + React.useEffect(() => { + const listener = (e) => { + if (!ref.current || ref.current.contains(e.target)) { + return; + } + props.onClose(); + }; + document.addEventListener('click', listener, true); + return () => { + document.removeEventListener('click', listener, true); + }; + }, []); + + return props.isVisible ? ( + + + {(insets) => { + const { + paddingTop: safeAreaPaddingTop, + paddingBottom: safeAreaPaddingBottom, + paddingLeft: safeAreaPaddingLeft, + paddingRight: safeAreaPaddingRight, + } = StyleUtils.getSafeAreaPadding(insets); + + const modalPaddingStyles = StyleUtils.getModalPaddingStyles({ + safeAreaPaddingTop, + safeAreaPaddingBottom, + safeAreaPaddingLeft, + safeAreaPaddingRight, + shouldAddBottomSafeAreaMargin, + shouldAddTopSafeAreaMargin, + shouldAddBottomSafeAreaPadding, + shouldAddTopSafeAreaPadding, + modalContainerStyleMarginTop: modalContainerStyle.marginTop, + modalContainerStyleMarginBottom: modalContainerStyle.marginBottom, + modalContainerStylePaddingTop: modalContainerStyle.paddingTop, + modalContainerStylePaddingBottom: modalContainerStyle.paddingBottom, + }); + return ( + + {props.children} + + ); + }} + + + ) : null; +}; + +Popover.propTypes = propTypes; +Popover.defaultProps = defaultProps; +Popover.displayName = 'Popover'; + +export default withWindowDimensions(Popover); diff --git a/src/components/PopoverWithoutOverlay/popoverPropTypes.js b/src/components/PopoverWithoutOverlay/popoverPropTypes.js new file mode 100644 index 000000000000..839ab68204d0 --- /dev/null +++ b/src/components/PopoverWithoutOverlay/popoverPropTypes.js @@ -0,0 +1,36 @@ +import _ from 'underscore'; +import PropTypes from 'prop-types'; +import {propTypes as modalPropTypes, defaultProps as defaultModalProps} from '../Modal/modalPropTypes'; +import CONST from '../../CONST'; + +const propTypes = { + ...(_.omit(modalPropTypes, ['type', 'popoverAnchorPosition'])), + + /** The anchor position of the popover */ + anchorPosition: PropTypes.shape({ + top: PropTypes.number, + right: PropTypes.number, + bottom: PropTypes.number, + left: PropTypes.number, + }), + + /** A react-native-animatable animation timing for the modal display animation. */ + animationInTiming: PropTypes.number, + + /** Whether disable the animations */ + disableAnimation: PropTypes.bool, +}; + +const defaultProps = { + ...(_.omit(defaultModalProps, ['type', 'popoverAnchorPosition'])), + + animationIn: 'fadeIn', + animationOut: 'fadeOut', + animationInTiming: CONST.ANIMATED_TRANSITION, + + // Anchor position is optional only because it is not relevant on mobile + anchorPosition: {}, + disableAnimation: true, +}; + +export {propTypes, defaultProps}; From 6c39e703385b85f6556f199e1263f871bd14cf4b Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Tue, 7 Mar 2023 01:37:43 +0500 Subject: [PATCH 0002/1075] fix: added onShowModal and onHideModal callbacks --- src/components/PopoverWithMeasuredContent.js | 10 +++++++--- src/components/PopoverWithoutOverlay/index.js | 19 +++++++++++++++++++ .../PopoverReportActionContextMenu.js | 1 + 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/components/PopoverWithMeasuredContent.js b/src/components/PopoverWithMeasuredContent.js index 5cf045842551..2eed88fbeb44 100644 --- a/src/components/PopoverWithMeasuredContent.js +++ b/src/components/PopoverWithMeasuredContent.js @@ -3,7 +3,8 @@ import React, {Component} from 'react'; import PropTypes from 'prop-types'; import {View} from 'react-native'; import lodashGet from 'lodash/get'; -import Popover from './PopoverWithoutOverlay'; +import PopoverWithoutOverlay from './PopoverWithoutOverlay'; +import Popover from './Popover'; import {propTypes as popoverPropTypes, defaultProps as defaultPopoverProps} from './Popover/popoverPropTypes'; import withWindowDimensions, {windowDimensionsPropTypes} from './withWindowDimensions'; import CONST from '../CONST'; @@ -40,6 +41,7 @@ const propTypes = { }), ...windowDimensionsPropTypes, + withoutOverlay: PropTypes.bool, }; const defaultProps = { @@ -54,6 +56,7 @@ const defaultProps = { height: 0, width: 0, }, + withoutOverlay: false, }; /** @@ -181,15 +184,16 @@ class PopoverWithMeasuredContent extends Component { left: adjustedAnchorPosition.left + horizontalShift, top: adjustedAnchorPosition.top + verticalShift, }; + const PopoverComponentToUse = this.props.withoutOverlay ? PopoverWithoutOverlay : Popover; return this.state.isContentMeasured ? ( - {this.props.measureContent()} - + ) : ( /* diff --git a/src/components/PopoverWithoutOverlay/index.js b/src/components/PopoverWithoutOverlay/index.js index 8f3c9868b968..019be3085ca9 100644 --- a/src/components/PopoverWithoutOverlay/index.js +++ b/src/components/PopoverWithoutOverlay/index.js @@ -1,6 +1,7 @@ import React from 'react'; import {Pressable, View} from 'react-native'; import {SafeAreaInsetsContext} from 'react-native-safe-area-context'; +import * as Modal from '../../libs/actions/Modal'; import {propTypes, defaultProps} from './popoverPropTypes'; import styles from '../../styles/styles'; import * as StyleUtils from '../../styles/StyleUtils'; @@ -46,6 +47,24 @@ const Popover = (props) => { }; }, []); + React.useEffect(() => { + Modal.setCloseModal(props.onClose); + + return () => { + Modal.setCloseModal(null); + }; + }, []); + + React.useEffect(() => { + if (props.isVisible) { + props.onModalShow(); + } else { + props.onModalHide(); + } + Modal.willAlertModalBecomeVisible(props.isVisible); + Modal.setCloseModal(props.isVisible ? props.onClose : null); + }, [props.isVisible]); + return props.isVisible ? ( diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js index dd637193b0e9..64b2e2162572 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js @@ -304,6 +304,7 @@ class PopoverReportActionContextMenu extends React.Component { measureContent={this.measureContent} shouldSetModalVisibility={false} fullscreen + withoutOverlay > Date: Fri, 10 Mar 2023 13:41:16 +0500 Subject: [PATCH 0003/1075] feat: added more listeners --- src/components/PopoverWithoutOverlay/index.js | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/components/PopoverWithoutOverlay/index.js b/src/components/PopoverWithoutOverlay/index.js index 019be3085ca9..31deb7308e23 100644 --- a/src/components/PopoverWithoutOverlay/index.js +++ b/src/components/PopoverWithoutOverlay/index.js @@ -47,6 +47,42 @@ const Popover = (props) => { }; }, []); + React.useEffect(() => { + const listener = (e) => { + if (e.key !== 'Escape') { + return; + } + props.onClose(); + }; + document.addEventListener('keydown', listener); + return () => { + document.removeEventListener('keydown', listener); + }; + }, []); + + React.useEffect(() => { + const listener = () => { + if (document.hasFocus()) { + return; + } + props.onClose(); + }; + document.addEventListener('visibilitychange', listener); + return () => { + document.removeEventListener('visibilitychange', listener); + }; + }, []); + + React.useEffect(() => { + const listener = () => { + props.onClose(); + }; + document.addEventListener('contextmenu', listener); + return () => { + document.removeEventListener('contextmenu', listener); + }; + }, []); + React.useEffect(() => { Modal.setCloseModal(props.onClose); From 41226e3349d2298a058ddc3d65b88ccaf2f4d684 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Thu, 13 Apr 2023 12:37:05 +0500 Subject: [PATCH 0004/1075] feat: updated popover without overlay implementation --- src/App.js | 2 + src/components/PopoverProvider.js | 100 ++++++++++++++++++ src/components/PopoverWithoutOverlay/index.js | 63 ++--------- 3 files changed, 108 insertions(+), 57 deletions(-) create mode 100644 src/components/PopoverProvider.js diff --git a/src/App.js b/src/App.js index 581012838973..949b866dca21 100644 --- a/src/App.js +++ b/src/App.js @@ -11,6 +11,7 @@ import Expensify from './Expensify'; import {LocaleContextProvider} from './components/withLocalize'; import OnyxProvider from './components/OnyxProvider'; import HTMLEngineProvider from './components/HTMLEngineProvider'; +import PopoverContextProvider from './components/PopoverProvider'; import ComposeProviders from './components/ComposeProviders'; import SafeArea from './components/SafeArea'; import * as Environment from './libs/Environment/Environment'; @@ -43,6 +44,7 @@ const App = () => ( HTMLEngineProvider, WindowDimensionsProvider, KeyboardStateProvider, + PopoverContextProvider, ]} > diff --git a/src/components/PopoverProvider.js b/src/components/PopoverProvider.js new file mode 100644 index 000000000000..c6b0ebb4a4a1 --- /dev/null +++ b/src/components/PopoverProvider.js @@ -0,0 +1,100 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const propTypes = { + // eslint-disable-next-line + children: PropTypes.any, +}; + +const defaultProps = {}; + +const PopoverContext = React.createContext({ + onOpen: () => {}, + popover: {}, +}); + +const PopoverContextProvider = (props) => { + const [isOpen, setIsOpen] = React.useState(false); + const activePopoverRef = React.useRef(null); + + const closePopover = () => { + if (!activePopoverRef.current) { + return; + } + activePopoverRef.current.close(); + activePopoverRef.current = null; + setIsOpen(false); + }; + + React.useEffect(() => { + const listener = (e) => { + if ( + !activePopoverRef.current + || !activePopoverRef.current.ref + || activePopoverRef.current.ref.current.contains(e.target) + ) { + return; + } + closePopover(); + }; + document.addEventListener('click', listener, true); + return () => { + document.removeEventListener('click', listener, true); + }; + }, []); + + React.useEffect(() => { + const listener = (e) => { + if (e.key !== 'Escape') { + return; + } + closePopover(); + }; + document.addEventListener('keydown', listener); + return () => { + document.removeEventListener('keydown', listener); + }; + }, []); + + React.useEffect(() => { + const listener = () => { + if (document.hasFocus()) { + return; + } + closePopover(); + }; + document.addEventListener('visibilitychange', listener); + return () => { + document.removeEventListener('visibilitychange', listener); + }; + }, []); + + const onOpen = (popoverParams) => { + if (activePopoverRef.current) { + closePopover(); + } + activePopoverRef.current = popoverParams; + setIsOpen(true); + }; + return ( + + {props.children} + + ); +}; + +PopoverContextProvider.defaultProps = defaultProps; +PopoverContextProvider.propTypes = propTypes; +PopoverContextProvider.displayName = 'PopoverContextProvider'; + +export default PopoverContextProvider; + +export { + PopoverContext, +}; diff --git a/src/components/PopoverWithoutOverlay/index.js b/src/components/PopoverWithoutOverlay/index.js index 31deb7308e23..ed32edff380c 100644 --- a/src/components/PopoverWithoutOverlay/index.js +++ b/src/components/PopoverWithoutOverlay/index.js @@ -1,6 +1,7 @@ import React from 'react'; import {Pressable, View} from 'react-native'; import {SafeAreaInsetsContext} from 'react-native-safe-area-context'; +import {PopoverContext} from '../PopoverProvider'; import * as Modal from '../../libs/actions/Modal'; import {propTypes, defaultProps} from './popoverPropTypes'; import styles from '../../styles/styles'; @@ -14,6 +15,7 @@ import withWindowDimensions from '../withWindowDimensions'; */ const Popover = (props) => { const ref = React.useRef(null); + const {onOpen} = React.useContext(PopoverContext); const { modalStyle, @@ -34,66 +36,13 @@ const Popover = (props) => { props.outerStyle, ); - React.useEffect(() => { - const listener = (e) => { - if (!ref.current || ref.current.contains(e.target)) { - return; - } - props.onClose(); - }; - document.addEventListener('click', listener, true); - return () => { - document.removeEventListener('click', listener, true); - }; - }, []); - - React.useEffect(() => { - const listener = (e) => { - if (e.key !== 'Escape') { - return; - } - props.onClose(); - }; - document.addEventListener('keydown', listener); - return () => { - document.removeEventListener('keydown', listener); - }; - }, []); - - React.useEffect(() => { - const listener = () => { - if (document.hasFocus()) { - return; - } - props.onClose(); - }; - document.addEventListener('visibilitychange', listener); - return () => { - document.removeEventListener('visibilitychange', listener); - }; - }, []); - - React.useEffect(() => { - const listener = () => { - props.onClose(); - }; - document.addEventListener('contextmenu', listener); - return () => { - document.removeEventListener('contextmenu', listener); - }; - }, []); - - React.useEffect(() => { - Modal.setCloseModal(props.onClose); - - return () => { - Modal.setCloseModal(null); - }; - }, []); - React.useEffect(() => { if (props.isVisible) { props.onModalShow(); + onOpen({ + ref, + close: props.onClose, + }); } else { props.onModalHide(); } From 872ad78e0aba4f4431481cadc0b4af6c965c29d2 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Tue, 2 May 2023 02:06:46 +0500 Subject: [PATCH 0005/1075] fix: comments --- src/CONST.js | 1 + src/components/AddPaymentMethodMenu.js | 1 + src/components/AvatarWithImagePicker.js | 1 + src/components/ButtonWithMenu.js | 1 + src/components/Popover/index.js | 7 ++++ src/components/PopoverMenu/index.js | 1 + .../index.js} | 11 ++++++ .../PopoverProvider/index.native.js | 39 +++++++++++++++++++ src/components/PopoverWithMeasuredContent.js | 6 +-- src/components/PopoverWithoutOverlay/index.js | 15 ++++--- src/components/ThreeDotsMenu/index.js | 1 + .../BaseVideoChatButtonAndMenu.js | 1 + src/pages/home/report/ReportActionCompose.js | 1 + .../FloatingActionButtonAndPopover.js | 1 + .../Payments/PaymentsPage/BasePaymentsPage.js | 1 + .../getModalStyles/getBaseModalStyles.js | 24 ++++++++++++ 16 files changed, 100 insertions(+), 12 deletions(-) rename src/components/{PopoverProvider.js => PopoverProvider/index.js} (88%) create mode 100644 src/components/PopoverProvider/index.native.js diff --git a/src/CONST.js b/src/CONST.js index b021c0e85b8f..697ba3812972 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -535,6 +535,7 @@ const CONST = { CENTERED_SMALL: 'centered_small', BOTTOM_DOCKED: 'bottom_docked', POPOVER: 'popover', + POPOVER_WITHOUT_OVERLAY: 'popover_without_overlay', RIGHT_DOCKED: 'right_docked', }, ANCHOR_ORIGIN_VERTICAL: { diff --git a/src/components/AddPaymentMethodMenu.js b/src/components/AddPaymentMethodMenu.js index 0a2117d1ca7c..eaf7be388e7b 100644 --- a/src/components/AddPaymentMethodMenu.js +++ b/src/components/AddPaymentMethodMenu.js @@ -65,6 +65,7 @@ const AddPaymentMethodMenu = props => ( }, ] : []), ]} + withoutOverlay /> ); diff --git a/src/components/AvatarWithImagePicker.js b/src/components/AvatarWithImagePicker.js index df19b6c1be44..2f01f4ea85bb 100644 --- a/src/components/AvatarWithImagePicker.js +++ b/src/components/AvatarWithImagePicker.js @@ -293,6 +293,7 @@ class AvatarWithImagePicker extends React.Component { onItemSelected={() => this.setState({isMenuVisible: false})} menuItems={this.createMenuItems(openPicker)} anchorPosition={this.props.anchorPosition} + withoutOverlay /> )} diff --git a/src/components/ButtonWithMenu.js b/src/components/ButtonWithMenu.js index eea6b5ffb835..2d2990e83557 100644 --- a/src/components/ButtonWithMenu.js +++ b/src/components/ButtonWithMenu.js @@ -90,6 +90,7 @@ class ButtonWithMenu extends PureComponent { this.setState({selectedItemIndex: index}); }, }))} + withoutOverlay /> )} diff --git a/src/components/Popover/index.js b/src/components/Popover/index.js index 031d9703d2c4..5858947e2c8e 100644 --- a/src/components/Popover/index.js +++ b/src/components/Popover/index.js @@ -4,6 +4,7 @@ import {propTypes, defaultProps} from './popoverPropTypes'; import CONST from '../../CONST'; import Modal from '../Modal'; import withWindowDimensions from '../withWindowDimensions'; +import PopoverWithoutOverlay from '../PopoverWithoutOverlay'; /* * This is a convenience wrapper around the Modal component for a responsive Popover. @@ -24,6 +25,12 @@ const Popover = (props) => { document.body, ); } + + if (props.withoutOverlay && !props.isSmallScreenWidth) { + // eslint-disable-next-line react/jsx-props-no-spreading + return ; + } + return ( {!_.isEmpty(this.props.headerText) && ( diff --git a/src/components/PopoverProvider.js b/src/components/PopoverProvider/index.js similarity index 88% rename from src/components/PopoverProvider.js rename to src/components/PopoverProvider/index.js index c6b0ebb4a4a1..b30ac752c6c2 100644 --- a/src/components/PopoverProvider.js +++ b/src/components/PopoverProvider/index.js @@ -11,6 +11,8 @@ const defaultProps = {}; const PopoverContext = React.createContext({ onOpen: () => {}, popover: {}, + close: () => {}, + isOpen: false, }); const PopoverContextProvider = (props) => { @@ -31,6 +33,7 @@ const PopoverContextProvider = (props) => { if ( !activePopoverRef.current || !activePopoverRef.current.ref + || !activePopoverRef.current.ref.current || activePopoverRef.current.ref.current.contains(e.target) ) { return; @@ -69,6 +72,13 @@ const PopoverContextProvider = (props) => { }; }, []); + React.useEffect(() => { + document.addEventListener('scroll', closePopover, true); + return () => { + document.removeEventListener('scroll', closePopover, true); + }; + }, []); + const onOpen = (popoverParams) => { if (activePopoverRef.current) { closePopover(); @@ -80,6 +90,7 @@ const PopoverContextProvider = (props) => { {}, + popover: {}, + close: () => {}, + isOpen: false, +}); + +const PopoverContextProvider = ({children}) => ( + {}, + close: () => {}, + popover: {}, + isOpen: false, + }} + > + {children} + +); + +PopoverContextProvider.defaultProps = defaultProps; +PopoverContextProvider.propTypes = propTypes; +PopoverContextProvider.displayName = 'PopoverContextProvider'; + +export default PopoverContextProvider; + +export { + PopoverContext, +}; diff --git a/src/components/PopoverWithMeasuredContent.js b/src/components/PopoverWithMeasuredContent.js index 2eed88fbeb44..85da80d3dbdf 100644 --- a/src/components/PopoverWithMeasuredContent.js +++ b/src/components/PopoverWithMeasuredContent.js @@ -3,7 +3,6 @@ import React, {Component} from 'react'; import PropTypes from 'prop-types'; import {View} from 'react-native'; import lodashGet from 'lodash/get'; -import PopoverWithoutOverlay from './PopoverWithoutOverlay'; import Popover from './Popover'; import {propTypes as popoverPropTypes, defaultProps as defaultPopoverProps} from './Popover/popoverPropTypes'; import withWindowDimensions, {windowDimensionsPropTypes} from './withWindowDimensions'; @@ -184,16 +183,15 @@ class PopoverWithMeasuredContent extends Component { left: adjustedAnchorPosition.left + horizontalShift, top: adjustedAnchorPosition.top + verticalShift, }; - const PopoverComponentToUse = this.props.withoutOverlay ? PopoverWithoutOverlay : Popover; return this.state.isContentMeasured ? ( - {this.props.measureContent()} - + ) : ( /* diff --git a/src/components/PopoverWithoutOverlay/index.js b/src/components/PopoverWithoutOverlay/index.js index ed32edff380c..ae8a513748b0 100644 --- a/src/components/PopoverWithoutOverlay/index.js +++ b/src/components/PopoverWithoutOverlay/index.js @@ -9,14 +9,9 @@ import * as StyleUtils from '../../styles/StyleUtils'; import getModalStyles from '../../styles/getModalStyles'; import withWindowDimensions from '../withWindowDimensions'; -/* - * This is a convenience wrapper around the Modal component for a responsive Popover. - * On small screen widths, it uses BottomDocked modal type, and a Popover type on wide screen widths. - */ const Popover = (props) => { const ref = React.useRef(null); - const {onOpen} = React.useContext(PopoverContext); - + const {onOpen, isOpen, close} = React.useContext(PopoverContext); const { modalStyle, modalContainerStyle, @@ -25,11 +20,11 @@ const Popover = (props) => { shouldAddTopSafeAreaPadding, shouldAddBottomSafeAreaPadding, } = getModalStyles( - 'popover', + 'popover_without_overlay', { windowWidth: props.windowWidth, windowHeight: props.windowHeight, - isSmallScreenWidth: props.isSmallScreenWidth, + isSmallScreenWidth: false, }, props.anchorPosition, props.innerContainerStyle, @@ -45,6 +40,9 @@ const Popover = (props) => { }); } else { props.onModalHide(); + if (isOpen) { + close(); + } } Modal.willAlertModalBecomeVisible(props.isVisible); Modal.setCloseModal(props.isVisible ? props.onClose : null); @@ -74,6 +72,7 @@ const Popover = (props) => { modalContainerStyleMarginBottom: modalContainerStyle.marginBottom, modalContainerStylePaddingTop: modalContainerStyle.paddingTop, modalContainerStylePaddingBottom: modalContainerStyle.paddingBottom, + insets, }); return ( ); diff --git a/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.js b/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.js index 3d56924125e9..0c78d59a28ee 100755 --- a/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.js +++ b/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.js @@ -130,6 +130,7 @@ class BaseVideoChatButtonAndMenu extends Component { left: this.state.videoChatIconPosition.x - 150, top: this.state.videoChatIconPosition.y + 40, }} + withoutOverlay > {_.map(this.menuItemData, ({icon, text, onPress}) => ( diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index e4c88999000b..cb7382f641eb 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -858,6 +858,7 @@ class ReportActionCompose extends React.Component { }, }, ]} + withoutOverlay /> )} diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js index ebff5f4e6a51..41bfc90c9b97 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js @@ -229,6 +229,7 @@ class FloatingActionButtonAndPopover extends React.Component { }, ] : []), ]} + withoutOverlay /> {!this.state.showConfirmDeleteContent ? ( Date: Fri, 5 May 2023 21:06:43 +0100 Subject: [PATCH 0006/1075] Initial changes to use Expensify's 0.72.0-rc.1-alpha.0 RN version --- .gitignore | 5 +- .ruby-version | 1 - .watchmanconfig | 1 + Gemfile | 5 +- android/app/build.gradle | 47 +- .../java/com/expensify/chat/MainActivity.java | 5 +- .../res/drawable/rn_edit_text_material.xml | 2 +- android/build.gradle | 2 +- android/gradle.properties | 2 +- android/gradle/wrapper/gradle-wrapper.jar | Bin 59536 -> 61574 bytes .../gradle/wrapper/gradle-wrapper.properties | 3 +- android/gradlew | 20 +- android/gradlew.bat | 37 +- android/settings.gradle | 2 +- ios/NewExpensify/AppDelegate.mm | 13 - ios/Podfile | 14 +- metro.config.js | 4 +- package-lock.json | 12224 +++++----------- package.json | 10 +- 19 files changed, 4012 insertions(+), 8385 deletions(-) delete mode 100644 .ruby-version create mode 100644 .watchmanconfig diff --git a/.gitignore b/.gitignore index 8265d5fd272b..3aad6c44ae84 100644 --- a/.gitignore +++ b/.gitignore @@ -33,7 +33,7 @@ build/ .gradle local.properties *.iml -android/*.hprof +*.hprof android/app/src/main/java/com/expensify/chat/generated/ .cxx/ @@ -94,3 +94,6 @@ storybook-static # E2E test reports tests/e2e/results/ + +# Temporary files created by Metro to check the health of the file watcher +.metro-health-check* diff --git a/.ruby-version b/.ruby-version deleted file mode 100644 index a603bb50a29e..000000000000 --- a/.ruby-version +++ /dev/null @@ -1 +0,0 @@ -2.7.5 diff --git a/.watchmanconfig b/.watchmanconfig new file mode 100644 index 000000000000..9e26dfeeb6e6 --- /dev/null +++ b/.watchmanconfig @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/Gemfile b/Gemfile index d8b9c135680d..33f0cd9333db 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,9 @@ source "https://rubygems.org" -gem "cocoapods", "~> 1.11.3" +# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version +ruby ">= 2.6.10" + +gem "cocoapods", "~> 1.12" gem "fastlane", "~> 2" gem "xcpretty", "~> 0" diff --git a/android/app/build.gradle b/android/app/build.gradle index 315a85ed7c90..d1bb2577c571 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -3,8 +3,6 @@ apply plugin: "com.facebook.react" apply plugin: "com.google.firebase.firebase-perf" apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.gradle" -import com.android.build.OutputFile - /** * This is the configuration block to customize your React Native Android app. * By default you don't need to apply any configuration, just uncomment the lines you need. @@ -15,8 +13,8 @@ react { // root = file("../") // The folder where the react-native NPM package is. Default is ../node_modules/react-native // reactNativeDir = file("../node_modules/react-native") - // The folder where the react-native Codegen package is. Default is ../node_modules/react-native-codegen - // codegenDir = file("../node_modules/react-native-codegen") + // The folder where the react-native Codegen package is. Default is ../node_modules/@react-native/codegen + // codegenDir = file("../node_modules/@react-native/codegen") // The cli.js file which is the React Native CLI entrypoint. Default is ../node_modules/react-native/cli.js // cliFile = file("../node_modules/react-native/cli.js") @@ -59,14 +57,6 @@ project.ext.envConfigFiles = [ release: ".env.production", ] -/** - * Set this to true to create four separate APKs instead of one, - * one for each native architecture. This is useful if you don't - * use App Bundles (https://developer.android.com/guide/app-bundle/) - * and want to have separate APKs to upload to the Play Store. - */ -def enableSeparateBuildPerCPUArchitecture = false - /** * Set this to true to Run Proguard on Release builds to minify the Java bytecode. */ @@ -85,16 +75,6 @@ def enableProguardInReleaseBuilds = false */ def jscFlavor = 'org.webkit:android-jsc:+' -/** - * Private function to get the list of Native Architectures you want to build. - * This reads the value from reactNativeArchitectures in your gradle.properties - * file and works together with the --active-arch-only flag of react-native run-android. - */ -def reactNativeArchitectures() { - def value = project.getProperties().get("reactNativeArchitectures") - return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"] -} - android { ndkVersion rootProject.ext.ndkVersion @@ -110,14 +90,6 @@ android { versionName "1.3.9-19" } - splits { - abi { - reset() - enable enableSeparateBuildPerCPUArchitecture - universalApk false // If true, also generate a universal APK - include (*reactNativeArchitectures()) - } - } signingConfigs { release { if (project.hasProperty('MYAPP_UPLOAD_STORE_FILE')) { @@ -155,21 +127,6 @@ android { signingConfig signingConfigs.debug } } - - // applicationVariants are e.g. debug, release - applicationVariants.all { variant -> - variant.outputs.each { output -> - // For each separate APK per architecture, set a unique version code as described here: - // https://developer.android.com/studio/build/configure-apk-splits.html - def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4] - def abi = output.getFilter(OutputFile.ABI) - if (abi != null) { // null for the universal-debug, universal-release variants - output.versionCodeOverride = - versionCodes.get(abi) * 1048576 + defaultConfig.versionCode - } - - } - } } dependencies { diff --git a/android/app/src/main/java/com/expensify/chat/MainActivity.java b/android/app/src/main/java/com/expensify/chat/MainActivity.java index b4eb483f8de6..14830b5934b9 100644 --- a/android/app/src/main/java/com/expensify/chat/MainActivity.java +++ b/android/app/src/main/java/com/expensify/chat/MainActivity.java @@ -32,10 +32,7 @@ protected ReactActivityDelegate createReactActivityDelegate() { this, getMainComponentName(), // If you opted-in for the New Architecture, we enable the Fabric Renderer. - DefaultNewArchitectureEntryPoint.getFabricEnabled(), // fabricEnabled - // If you opted-in for the New Architecture, we enable Concurrent React (i.e. React 18). - DefaultNewArchitectureEntryPoint.getConcurrentReactEnabled() // concurrentRootEnabled - ); + DefaultNewArchitectureEntryPoint.getFabricEnabled()); } @Override diff --git a/android/app/src/main/res/drawable/rn_edit_text_material.xml b/android/app/src/main/res/drawable/rn_edit_text_material.xml index f35d9962026a..73b37e4d9963 100644 --- a/android/app/src/main/res/drawable/rn_edit_text_material.xml +++ b/android/app/src/main/res/drawable/rn_edit_text_material.xml @@ -20,7 +20,7 @@ android:insetBottom="@dimen/abc_edit_text_inset_bottom_material"> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/TabSelectorItem.js b/src/components/TabSelectorItem.js index 35454b98431b..b164ba561802 100644 --- a/src/components/TabSelectorItem.js +++ b/src/components/TabSelectorItem.js @@ -6,6 +6,7 @@ import Icon from './Icon'; import Colors from '../styles/colors'; import Styles from '../styles/styles'; import PressableWithFeedback from './Pressable/PressableWithFeedback'; +import fontFamily from '../styles/fontFamily'; const propTypes = { /** Function to call when onPress */ @@ -29,7 +30,9 @@ const defaultProps = { }; function TabSelectorItem(props) { - const textStyle = props.selected ? [Styles.textStrong, Styles.mt2, Styles.textWhite] : [Styles.mt2, Styles.colorMuted]; + const textStyle = props.selected + ? [Styles.textStrong, Styles.mt2, Styles.textWhite, {fontFamily: fontFamily.EXP_NEUE}] + : [Styles.mt2, Styles.colorMuted, {fontFamily: fontFamily.EXP_NEUE}]; return ( Upload receipt Drag a receipt onto this page, forward a receipt to{' '} - {' '} + + + {' '} or choose a file to upload below. diff --git a/src/pages/iou/ReceiptSelector/index.native.js b/src/pages/iou/ReceiptSelector/index.native.js index 7ecbd0cd0631..e5af42a2c851 100644 --- a/src/pages/iou/ReceiptSelector/index.native.js +++ b/src/pages/iou/ReceiptSelector/index.native.js @@ -258,7 +258,7 @@ function ReceiptSelector(props) { flex: 1, overflow: 'hidden', padding: 10, - borderRadius: 16, + borderRadius: 28, borderStyle: 'solid', borderWidth: 8, borderColor: Colors.greenAppBackground, @@ -322,7 +322,7 @@ function ReceiptSelector(props) { flex: 1, overflow: 'hidden', padding: 10, - borderRadius: 16, + borderRadius: 28, borderStyle: 'solid', borderWidth: 8, backgroundColor: Colors.greenHighlightBackground, diff --git a/src/styles/styles.js b/src/styles/styles.js index dcfc0d5ea198..22714669bdcd 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -762,7 +762,7 @@ const styles = { borderRadius: variables.componentBorderRadiusLarge, borderWidth: 2, borderColor: themeColors.borderFocus, - borderStyle: 'dashed', + borderStyle: 'dotted', }, headerAnonymousFooter: { @@ -1069,6 +1069,7 @@ const styles = { }, subTextReceiptUpload: { + fontFamily: fontFamily.EXP_NEUE, lineHeight: variables.lineHeightLarge, textAlign: 'center', color: themeColors.textLight, From 1621dd0d3ae055d207ad86efce6ae2d2a1f95a25 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 18 Jul 2023 17:02:27 +0200 Subject: [PATCH 0245/1075] move autoHideArrow logic to native --- .../AttachmentCarouselView/index.js | 4 +- .../AttachmentCarouselView/index.native.js | 29 ++++++++-- .../AttachmentCarouselView/propTypes.js | 2 +- src/components/AttachmentCarousel/index.js | 56 +++---------------- 4 files changed, 34 insertions(+), 57 deletions(-) diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.js index 5a3e54fa88fd..9d9b9910dcf7 100644 --- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.js +++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.js @@ -111,8 +111,8 @@ function AttachmentCarouselView(props) { return ( props.toggleArrowsVisibility(true)} - onMouseLeave={() => props.toggleArrowsVisibility(false)} + onMouseEnter={() => props.setArrowsVisibility(true)} + onMouseLeave={() => props.setArrowsVisibility(false)} style={[styles.flex1, styles.attachmentCarouselButtonsContainer]} > props.carouselState.attachments.reverse(), [props.carouselState.attachments]); + const processedItems = useMemo(() => _.map(reversedAttachments, (item) => ({key: item.source, url: addEncryptedAuthTokenToURL(item.source)})), [reversedAttachments]); + const reversePage = useCallback( (page) => Math.max(0, Math.min(props.carouselState.attachments.length - page - 1, props.carouselState.attachments.length)), [props.carouselState.attachments.length], @@ -50,7 +53,23 @@ function AttachmentCarouselView(props) { [reversedPage, updatePage], ); - const processedItems = useMemo(() => _.map(reversedAttachments, (item) => ({key: item.source, url: addEncryptedAuthTokenToURL(item.source)})), [reversedAttachments]); + const autoHideArrowTimeout = useRef(null); + + /** + * Cancels the automatic hiding of the arrows. + */ + const cancelAutoHideArrow = useCallback(() => clearTimeout(autoHideArrowTimeout.current), []); + + /** + * On a touch screen device, automatically hide the arrows + * if there is no interaction for 3 seconds. + */ + const autoHideArrow = useCallback(() => { + cancelAutoHideArrow(); + autoHideArrowTimeout.current = setTimeout(() => { + props.setArrowsVisibility(false); + }, CONST.ARROW_HIDE_DELAY); + }, [cancelAutoHideArrow, props]); return ( @@ -58,11 +77,11 @@ function AttachmentCarouselView(props) { carouselState={props.carouselState} onBack={() => { cycleThroughAttachments(-1); - props.autoHideArrow(); + autoHideArrow(); }} onForward={() => { cycleThroughAttachments(1); - props.autoHideArrow(); + autoHideArrow(); }} autoHideArrow={props.autoHideArrow} cancelAutoHideArrow={props.cancelAutoHideArrow} @@ -76,8 +95,8 @@ function AttachmentCarouselView(props) { console.log('page updated'); updatePage(newPage); }} - onTap={() => props.toggleArrowsVisibility(!props.carouselState.shouldShowArrow)} - onPinchGestureChange={(isPinchGestureRunning) => props.toggleArrowsVisibility(!isPinchGestureRunning)} + onTap={() => props.setArrowsVisibility(!props.carouselState.shouldShowArrow)} + onPinchGestureChange={(isPinchGestureRunning) => props.setArrowsVisibility(!isPinchGestureRunning)} onSwipeDown={props.onClose} containerWidth={props.carouselState.containerWidth} containerHeight={props.carouselState.containerHeight} diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/propTypes.js b/src/components/AttachmentCarousel/AttachmentCarouselView/propTypes.js index 6a7d401dbaa4..fda059acaf1c 100644 --- a/src/components/AttachmentCarousel/AttachmentCarouselView/propTypes.js +++ b/src/components/AttachmentCarousel/AttachmentCarouselView/propTypes.js @@ -28,7 +28,7 @@ const propTypes = { /** * A callback for toggling the visibility of the arrows */ - toggleArrowsVisibility: PropTypes.func.isRequired, + setArrowsVisibility: PropTypes.func.isRequired, /** * Trigger "auto-hiding" of the arrow buttons in the carousel diff --git a/src/components/AttachmentCarousel/index.js b/src/components/AttachmentCarousel/index.js index 94fda0478ec6..9cc2306b0f1b 100644 --- a/src/components/AttachmentCarousel/index.js +++ b/src/components/AttachmentCarousel/index.js @@ -49,63 +49,23 @@ class AttachmentCarousel extends React.Component { this.canUseTouchScreen = DeviceCapabilities.canUseTouchScreen(); - this.autoHideArrow = this.autoHideArrow.bind(this); - this.cancelAutoHideArrow = this.cancelAutoHideArrow.bind(this); this.updatePage = this.updatePage.bind(this); - this.toggleArrowsVisibility = this.toggleArrowsVisibility.bind(this); + this.setArrowsVisibility = this.setArrowsVisibility.bind(this); this.createInitialState = this.createInitialState.bind(this); this.state = this.createInitialState(); } - componentDidMount() { - this.autoHideArrow(); - } - - /** - * On a touch screen device, automatically hide the arrows - * if there is no interaction for 3 seconds. - */ - autoHideArrow() { - if (!this.canUseTouchScreen) { - return; - } - this.cancelAutoHideArrow(); - this.autoHideArrowTimeout = setTimeout(() => { - this.toggleArrowsVisibility(false); - }, CONST.ARROW_HIDE_DELAY); - } - - /** - * Cancels the automatic hiding of the arrows. - */ - cancelAutoHideArrow() { - clearTimeout(this.autoHideArrowTimeout); - } - /** * Toggles the visibility of the arrows * @param {Boolean} shouldShowArrow * @param {Boolean} isGestureInUse */ - toggleArrowsVisibility(shouldShowArrow, isGestureInUse = false) { - // Don't toggle arrows in a zoomed state - if (isGestureInUse) { - return; - } - this.setState( - (current) => { - const newShouldShowArrow = _.isBoolean(shouldShowArrow) ? shouldShowArrow : !current.shouldShowArrow; - return {shouldShowArrow: newShouldShowArrow}; - }, - () => { - if (this.state.shouldShowArrow) { - this.autoHideArrow(); - } else { - this.cancelAutoHideArrow(); - } - }, - ); + setArrowsVisibility(shouldShowArrow) { + this.setState((current) => { + const newShouldShowArrow = _.isBoolean(shouldShowArrow) ? shouldShowArrow : !current.shouldShowArrow; + return {shouldShowArrow: newShouldShowArrow}; + }); } /** @@ -192,9 +152,7 @@ class AttachmentCarousel extends React.Component { From 00136796ef73426cec074b77fc13c0f6f2b2a574 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 18 Jul 2023 17:06:40 +0200 Subject: [PATCH 0246/1075] move initial arrow showing logic to native implementation --- .../AttachmentCarouselView/index.native.js | 4 +++- src/components/AttachmentCarousel/index.js | 5 +---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js index c15012096da7..e17db4f6ef24 100644 --- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js +++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js @@ -1,4 +1,4 @@ -import React, {useCallback, useMemo, useRef} from 'react'; +import React, {useCallback, useMemo, useRef, useEffect} from 'react'; import {View} from 'react-native'; import _ from 'underscore'; import addEncryptedAuthTokenToURL from '../../../libs/addEncryptedAuthTokenToURL'; @@ -71,6 +71,8 @@ function AttachmentCarouselView(props) { }, CONST.ARROW_HIDE_DELAY); }, [cancelAutoHideArrow, props]); + useEffect(() => props.setArrowsVisibility(true), [props]); + return ( Date: Tue, 18 Jul 2023 17:41:31 +0200 Subject: [PATCH 0247/1075] simplify Lightbox and add propTypes to ImageTransformer --- src/components/AttachmentCarousel/Lightbox.js | 162 +++++++++--------- 1 file changed, 81 insertions(+), 81 deletions(-) diff --git a/src/components/AttachmentCarousel/Lightbox.js b/src/components/AttachmentCarousel/Lightbox.js index 14ac48a9d759..bb123206d33a 100644 --- a/src/components/AttachmentCarousel/Lightbox.js +++ b/src/components/AttachmentCarousel/Lightbox.js @@ -53,16 +53,20 @@ function ImageWrapper({children}) { ); } -// eslint-disable-next-line react/prop-types -function ImageTransformer({canvasWidth, canvasHeight, imageWidth = 0, imageHeight = 0, imageScale = 1, isActive, onSwipe, onSwipeSuccess, onTap, children}) { - const {pagerRef, shouldPagerScroll, isScrolling, onPinchGestureChange} = useContext(Context); +const imageTransformerPropTypes = { + imageWidth: PropTypes.number.isRequired, + imageHeight: PropTypes.number.isRequired, + imageScale: PropTypes.number.isRequired, + scaledImageWidth: PropTypes.number.isRequired, + scaledImageHeight: PropTypes.number.isRequired, + isActive: PropTypes.bool.isRequired, + children: PropTypes.node.isRequired, +}; - const [imageDimensions, setImageDimensions] = useState({width: imageWidth, height: imageHeight}); +function ImageTransformer({imageWidth = 0, imageHeight = 0, imageScale = 1, scaledImageWidth = 0, scaledImageHeight = 0, isActive, children}) { + const {canvasWidth, canvasHeight, onTap, onSwipe, onSwipeSuccess, pagerRef, shouldPagerScroll, isScrolling, onPinchGestureChange} = useContext(Context); - const targetDimensions = { - width: useSharedValue(0), - height: useSharedValue(0), - }; + const [imageDimensions, setImageDimensions] = useState({width: imageWidth, height: imageHeight}); const canvas = { width: useSharedValue(canvasWidth), @@ -90,10 +94,7 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth = 0, imageHeigh if (imageWidth === 0 || imageHeight === 0) return; setImageDimensions({width: imageWidth, height: imageHeight}); - - targetDimensions.width.value = imageWidth; - targetDimensions.height.value = imageHeight; - }, [imageDimensions.height, imageDimensions.width, imageHeight, imageWidth, targetDimensions.height, targetDimensions.width]); + }, [imageDimensions.height, imageDimensions.width, imageHeight, imageWidth]); // used for pan gesture const translateY = useSharedValue(0); @@ -120,17 +121,12 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth = 0, imageHeigh const scaleOffset = useSharedValue(1); // disable pan vertically when image is smaller than screen - const canPanVertically = useDerivedValue(() => canvas.height.value < targetDimensions.height.value * totalScale.value); + const canPanVertically = useDerivedValue(() => canvas.height.value < imageDimensions.height * totalScale.value, [imageDimensions.height]); // calculates bounds of the scaled image // can we pan left/right/up/down // can be used to limit gesture or implementing tension effect const getBounds = useWorkletCallback(() => { - const finalScale = zoomScale.value + canvasFitScale.value - 1; - - const scaledImageWidth = targetDimensions.width.value * finalScale; - const scaledImageHeight = targetDimensions.height.value * finalScale; - const rightBoundary = Math.abs(canvas.width.value - scaledImageWidth) / 2; let topBoundary = 0; @@ -230,57 +226,60 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth = 0, imageHeigh cancelAnimation(offsetY); }); - const zoomToCoordinates = useWorkletCallback((x, y) => { - 'worklet'; - - stopAnimation(); - - const usableImage = { - x: targetDimensions.width.value * canvasFitScale.value, - y: targetDimensions.height.value * canvasFitScale.value, - }; - - const targetImageSize = { - x: usableImage.x * DOUBLE_TAP_SCALE, - y: usableImage.y * DOUBLE_TAP_SCALE, - }; - - const CENTER = { - x: canvas.width.value / 2, - y: canvas.height.value / 2, - }; - - const imageCenter = { - x: usableImage.x / 2, - y: usableImage.y / 2, - }; - - const focal = {x, y}; - - const currentOrigin = { - x: (targetImageSize.x / 2 - CENTER.x) * -1, - y: (targetImageSize.y / 2 - CENTER.y) * -1, - }; - - const koef = { - x: (1 / imageCenter.x) * focal.x - 1, - y: (1 / imageCenter.y) * focal.y - 1, - }; + const zoomToCoordinates = useWorkletCallback( + (x, y) => { + 'worklet'; - const target = { - x: currentOrigin.x * koef.x, - y: currentOrigin.y * koef.y, - }; + stopAnimation(); - if (targetImageSize.y < canvas.height.value) { - target.y = 0; - } + const usableImage = { + x: imageDimensions.width * canvasFitScale.value, + y: imageDimensions.height * canvasFitScale.value, + }; + + const targetImageSize = { + x: usableImage.x * DOUBLE_TAP_SCALE, + y: usableImage.y * DOUBLE_TAP_SCALE, + }; + + const CENTER = { + x: canvas.width.value / 2, + y: canvas.height.value / 2, + }; + + const imageCenter = { + x: usableImage.x / 2, + y: usableImage.y / 2, + }; + + const focal = {x, y}; + + const currentOrigin = { + x: (targetImageSize.x / 2 - CENTER.x) * -1, + y: (targetImageSize.y / 2 - CENTER.y) * -1, + }; + + const koef = { + x: (1 / imageCenter.x) * focal.x - 1, + y: (1 / imageCenter.y) * focal.y - 1, + }; + + const target = { + x: currentOrigin.x * koef.x, + y: currentOrigin.y * koef.y, + }; + + if (targetImageSize.y < canvas.height.value) { + target.y = 0; + } - offsetX.value = withSpring(target.x, SPRING_CONFIG); - offsetY.value = withSpring(target.y, SPRING_CONFIG); - zoomScale.value = withSpring(DOUBLE_TAP_SCALE, SPRING_CONFIG); - scaleOffset.value = DOUBLE_TAP_SCALE; - }); + offsetX.value = withSpring(target.x, SPRING_CONFIG); + offsetY.value = withSpring(target.y, SPRING_CONFIG); + zoomScale.value = withSpring(DOUBLE_TAP_SCALE, SPRING_CONFIG); + scaleOffset.value = DOUBLE_TAP_SCALE; + }, + [imageDimensions], + ); const reset = useWorkletCallback((animated) => { scaleOffset.value = 1; @@ -411,7 +410,7 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth = 0, imageHeigh }; offsetY.value = withSpring( - maybeInvert(targetDimensions.height.value * 2), + maybeInvert(imageDimensions.height * 2), { stiffness: 50, damping: 30, @@ -464,7 +463,9 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth = 0, imageHeigh origin.y.value = adjustFocal.y; }) .onChange((evt) => { - zoomScale.value = clamp(scaleOffset.value * evt.scale, MIN_SCALE, MAX_SCALE); + const newZoomScale = clamp(scaleOffset.value * evt.scale, MIN_SCALE, MAX_SCALE); + + zoomScale.value = newZoomScale; if (zoomScale.value > MIN_SCALE && zoomScale.value < MAX_SCALE) { gestureScale.value = evt.scale; @@ -593,6 +594,7 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth = 0, imageHeigh ); } +ImageTransformer.propTypes = imageTransformerPropTypes; function getCanvasFitScale({canvasWidth, canvasHeight, imageWidth, imageHeight}) { const scaleFactorX = canvasWidth / imageWidth; @@ -610,7 +612,9 @@ const pagePropTypes = { }; // eslint-disable-next-line react/prop-types -function Page({isActive, item, onSwipe, onSwipeSuccess, onSwipeDown, canvasWidth, canvasHeight, onTap}) { +function Page({isActive, item}) { + const {canvasWidth, canvasHeight} = useContext(Context); + const dimensions = cachedDimensions.get(item.url); const areImageDimensionsSet = dimensions?.imageWidth !== 0 && dimensions?.imageHeight !== 0; @@ -627,16 +631,12 @@ function Page({isActive, item, onSwipe, onSwipeSuccess, onSwipeDown, canvasWidth {isActive && ( ))} From 80e5bee8042fdf5680e05d92854a5d57640fcc7e Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 18 Jul 2023 17:41:58 +0200 Subject: [PATCH 0248/1075] extract more logic from AttachmentCarousel and improve propTypes and memoizing --- .../AttachmentCarouselView/index.js | 48 ++++++++----------- .../AttachmentCarouselView/index.native.js | 44 ++++++++--------- .../AttachmentCarouselView/propTypes.js | 10 ---- .../AttachmentCarousel/CarouselButtons.js | 10 +++- 4 files changed, 49 insertions(+), 63 deletions(-) diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.js index 9d9b9910dcf7..7aacf9c608b7 100644 --- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.js +++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.js @@ -19,7 +19,7 @@ const propTypes = { ...windowDimensionsPropTypes, }; -function AttachmentCarouselView(props) { +function AttachmentCarouselView({carouselState, updatePage, setArrowsVisibility, ...props}) { const scrollRef = useRef(null); /** @@ -30,11 +30,11 @@ function AttachmentCarouselView(props) { */ const getItemLayout = useCallback( (_data, index) => ({ - length: props.carouselState.containerWidth, - offset: props.carouselState.containerWidth * index, + length: carouselState.containerWidth, + offset: carouselState.containerWidth * index, index, }), - [props.carouselState.containerWidth], + [carouselState.containerWidth], ); /** @@ -43,8 +43,8 @@ function AttachmentCarouselView(props) { */ const cycleThroughAttachments = useCallback( (deltaSlide) => { - const nextIndex = props.carouselState.page - deltaSlide; - const nextItem = props.carouselState.attachments[nextIndex]; + const nextIndex = carouselState.page - deltaSlide; + const nextItem = carouselState.attachments[nextIndex]; if (!nextItem || !scrollRef.current) { return; @@ -54,7 +54,7 @@ function AttachmentCarouselView(props) { // so we only enable it for mobile scrollRef.current.scrollToIndex({index: nextIndex, animated: false}); }, - [props.carouselState.attachments, props.carouselState.page], + [carouselState.attachments, carouselState.page], ); /** @@ -88,13 +88,13 @@ function AttachmentCarouselView(props) { const renderItem = useCallback( ({item}) => ( ), - [props.carouselState.activeSource], + [carouselState.activeSource], ); const handleViewableItemsChange = useCallback( @@ -102,34 +102,26 @@ function AttachmentCarouselView(props) { // Since we can have only one item in view at a time, we can use the first item in the array // to get the index of the current page const entry = _.first([...viewableItems]); - props.updatePage({item: entry.item, index: entry.index}); + updatePage({item: entry.item, index: entry.index}); }, - [props], + [updatePage], ); const viewabilityConfigCallbackPairs = useRef([{viewabilityConfig: VIEWABILITY_CONFIG, onViewableItemsChanged: handleViewableItemsChange}]); return ( props.setArrowsVisibility(true)} - onMouseLeave={() => props.setArrowsVisibility(false)} + onMouseEnter={() => setArrowsVisibility(true)} + onMouseLeave={() => setArrowsVisibility(false)} style={[styles.flex1, styles.attachmentCarouselButtonsContainer]} > { - cycleThroughAttachments(-1); - props.autoHideArrow(); - }} - onForward={() => { - cycleThroughAttachments(1); - props.autoHideArrow(); - }} - autoHideArrow={props.autoHideArrow} - cancelAutoHideArrow={props.cancelAutoHideArrow} + carouselState={carouselState} + onBack={() => cycleThroughAttachments(-1)} + onForward={() => cycleThroughAttachments(1)} /> - {props.carouselState.containerWidth > 0 && props.carouselState.containerHeight > 0 && ( + {carouselState.containerWidth > 0 && carouselState.containerHeight > 0 && ( props.carouselState.attachments.reverse(), [props.carouselState.attachments]); + const reversedAttachments = useMemo(() => carouselState.attachments.reverse(), [carouselState.attachments]); const processedItems = useMemo(() => _.map(reversedAttachments, (item) => ({key: item.source, url: addEncryptedAuthTokenToURL(item.source)})), [reversedAttachments]); - const reversePage = useCallback( - (page) => Math.max(0, Math.min(props.carouselState.attachments.length - page - 1, props.carouselState.attachments.length)), - [props.carouselState.attachments.length], - ); + const reversePage = useCallback((page) => Math.max(0, Math.min(carouselState.attachments.length - page - 1, carouselState.attachments.length)), [carouselState.attachments.length]); - const reversedPage = useMemo(() => reversePage(props.carouselState.page), [props.carouselState.page, reversePage]); + const reversedPage = useMemo(() => reversePage(carouselState.page), [carouselState.page, reversePage]); /** * Update carousel page based on next page index * @param {Number} newPageIndex */ - const updatePage = useCallback( + const updatePageInternal = useCallback( (newPageIndex) => { const nextItem = reversedAttachments[newPageIndex]; if (!nextItem) { return; } - props.updatePage({item: nextItem, index: reversePage(newPageIndex)}); + updatePage({item: nextItem, index: reversePage(newPageIndex)}); }, - [props, reversePage, reversedAttachments], + [reversePage, reversedAttachments, updatePage], ); /** @@ -67,16 +64,17 @@ function AttachmentCarouselView(props) { const autoHideArrow = useCallback(() => { cancelAutoHideArrow(); autoHideArrowTimeout.current = setTimeout(() => { - props.setArrowsVisibility(false); + setArrowsVisibility(false); }, CONST.ARROW_HIDE_DELAY); - }, [cancelAutoHideArrow, props]); + }, [cancelAutoHideArrow, setArrowsVisibility]); - useEffect(() => props.setArrowsVisibility(true), [props]); + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(() => setArrowsVisibility(true), []); return ( { cycleThroughAttachments(-1); autoHideArrow(); @@ -85,23 +83,23 @@ function AttachmentCarouselView(props) { cycleThroughAttachments(1); autoHideArrow(); }} - autoHideArrow={props.autoHideArrow} - cancelAutoHideArrow={props.cancelAutoHideArrow} + autoHideArrow={autoHideArrow} + cancelAutoHideArrow={cancelAutoHideArrow} /> - {props.carouselState.containerWidth > 0 && props.carouselState.containerHeight > 0 && ( + {carouselState.containerWidth > 0 && carouselState.containerHeight > 0 && ( { console.log('page updated'); - updatePage(newPage); + updatePageInternal(newPage); }} - onTap={() => props.setArrowsVisibility(!props.carouselState.shouldShowArrow)} - onPinchGestureChange={(isPinchGestureRunning) => props.setArrowsVisibility(!isPinchGestureRunning)} - onSwipeDown={props.onClose} - containerWidth={props.carouselState.containerWidth} - containerHeight={props.carouselState.containerHeight} + onTap={() => setArrowsVisibility()} + onPinchGestureChange={(isPinchGestureRunning) => setArrowsVisibility(!isPinchGestureRunning)} + onSwipeDown={onClose} + containerWidth={carouselState.containerWidth} + containerHeight={carouselState.containerHeight} ref={pagerRef} /> )} diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/propTypes.js b/src/components/AttachmentCarousel/AttachmentCarouselView/propTypes.js index fda059acaf1c..7d3b50f446a5 100644 --- a/src/components/AttachmentCarousel/AttachmentCarouselView/propTypes.js +++ b/src/components/AttachmentCarousel/AttachmentCarouselView/propTypes.js @@ -29,16 +29,6 @@ const propTypes = { * A callback for toggling the visibility of the arrows */ setArrowsVisibility: PropTypes.func.isRequired, - - /** - * Trigger "auto-hiding" of the arrow buttons in the carousel - */ - autoHideArrow: PropTypes.func.isRequired, - - /** - * Cancel "auto-hiding" of the arrow buttons in the carousel - */ - cancelAutoHideArrow: PropTypes.func.isRequired, }; export default propTypes; diff --git a/src/components/AttachmentCarousel/CarouselButtons.js b/src/components/AttachmentCarousel/CarouselButtons.js index 10eabb502582..6cefefd37a53 100644 --- a/src/components/AttachmentCarousel/CarouselButtons.js +++ b/src/components/AttachmentCarousel/CarouselButtons.js @@ -19,12 +19,17 @@ const propTypes = { /** Callback to go one page forward */ onForward: PropTypes.func.isRequired, - autoHideArrow: PropTypes.func.isRequired, - cancelAutoHideArrow: PropTypes.func.isRequired, + autoHideArrow: PropTypes.func, + cancelAutoHideArrow: PropTypes.func, ...withLocalizePropTypes, }; +const defaultProps = { + autoHideArrow: () => {}, + cancelAutoHideArrow: () => {}, +}; + function CarouselButtons(props) { const isForwardDisabled = props.carouselState.page === 0; const isBackDisabled = props.carouselState.page === _.size(props.carouselState.attachments) - 1; @@ -68,5 +73,6 @@ function CarouselButtons(props) { } CarouselButtons.propTypes = propTypes; +CarouselButtons.defaultProps = defaultProps; export default withLocalize(CarouselButtons); From 05bc9791cd7cda2b43c66475c23f49c73aa2651f Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 18 Jul 2023 17:43:28 +0200 Subject: [PATCH 0249/1075] add more propTypes --- src/components/AttachmentCarousel/Lightbox.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/AttachmentCarousel/Lightbox.js b/src/components/AttachmentCarousel/Lightbox.js index bb123206d33a..d6944d91d355 100644 --- a/src/components/AttachmentCarousel/Lightbox.js +++ b/src/components/AttachmentCarousel/Lightbox.js @@ -41,7 +41,10 @@ function clamp(value, lowerBound, upperBound) { return Math.min(Math.max(lowerBound, value), upperBound); } -// eslint-disable-next-line react/prop-types +const imageWrapperPropTypes = { + children: PropTypes.node.isRequired, +}; + function ImageWrapper({children}) { return ( ); } +ImageWrapper.propTypes = imageWrapperPropTypes; const imageTransformerPropTypes = { imageWidth: PropTypes.number.isRequired, @@ -606,12 +610,12 @@ function getCanvasFitScale({canvasWidth, canvasHeight, imageWidth, imageHeight}) const cachedDimensions = new Map(); const pagePropTypes = { + isActive: PropTypes.bool.isRequired, item: PropTypes.shape({ url: PropTypes.string, }).isRequired, }; -// eslint-disable-next-line react/prop-types function Page({isActive, item}) { const {canvasWidth, canvasHeight} = useContext(Context); From 0b3d13b57fad9fde09917f8519cf0a70b2339831 Mon Sep 17 00:00:00 2001 From: Andrew Gable Date: Tue, 18 Jul 2023 12:00:08 -0400 Subject: [PATCH 0250/1075] Cleaning up native platforms --- assets/images/flash.svg | 5 --- assets/images/shutter.svg | 33 +++++++++---------- src/components/Icon/Expensicons.js | 2 -- src/pages/iou/ReceiptSelector/index.native.js | 9 +++-- 4 files changed, 19 insertions(+), 30 deletions(-) delete mode 100644 assets/images/flash.svg diff --git a/assets/images/flash.svg b/assets/images/flash.svg deleted file mode 100644 index 3f0de2cfcc03..000000000000 --- a/assets/images/flash.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/images/shutter.svg b/assets/images/shutter.svg index ef84e9df41be..e4dadcea8089 100644 --- a/assets/images/shutter.svg +++ b/assets/images/shutter.svg @@ -1,21 +1,18 @@ - - - - - - + + + + + + + + + + - - - - - - - - - - - - diff --git a/src/components/Icon/Expensicons.js b/src/components/Icon/Expensicons.js index 80ad6d9283a2..3b1470197f73 100644 --- a/src/components/Icon/Expensicons.js +++ b/src/components/Icon/Expensicons.js @@ -116,7 +116,6 @@ import Linkedin from '../../../assets/images/social-linkedin.svg'; import Instagram from '../../../assets/images/social-instagram.svg'; import AddReaction from '../../../assets/images/add-reaction.svg'; import Task from '../../../assets/images/task.svg'; -import Flash from '../../../assets/images/flash.svg'; export { ActiveRoomAvatar, @@ -172,7 +171,6 @@ export { FlagLevelOne, FlagLevelTwo, FlagLevelThree, - Flash, Gallery, Gear, Globe, diff --git a/src/pages/iou/ReceiptSelector/index.native.js b/src/pages/iou/ReceiptSelector/index.native.js index e5af42a2c851..58d2034bb545 100644 --- a/src/pages/iou/ReceiptSelector/index.native.js +++ b/src/pages/iou/ReceiptSelector/index.native.js @@ -290,7 +290,6 @@ function ReceiptSelector(props) { takePhoto()} > @@ -367,8 +366,8 @@ function ReceiptSelector(props) { From a56e28dbeb1afc45799ccce7279b866df1e738b0 Mon Sep 17 00:00:00 2001 From: Andrew Gable Date: Tue, 18 Jul 2023 12:16:45 -0400 Subject: [PATCH 0251/1075] Tweaking web styling --- src/components/TabSelector.js | 2 +- src/components/TabSelectorItem.js | 3 +-- src/pages/iou/ReceiptSelector/index.js | 2 +- src/styles/styles.js | 10 +++++++++- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/components/TabSelector.js b/src/components/TabSelector.js index f96af7d2252d..0b104b02f08c 100644 --- a/src/components/TabSelector.js +++ b/src/components/TabSelector.js @@ -25,7 +25,7 @@ const defaultProps = { function TabSelector(props) { const selectedTab = lodashGet(props.tabSelected, 'selected', TAB_MANUAL); return ( - + + {!props.isDraggingOver ? defaultView() : null} {props.isDraggingOver && } diff --git a/src/styles/styles.js b/src/styles/styles.js index 22714669bdcd..66a6fe3a7d77 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -758,11 +758,19 @@ const styles = { borderColor: themeColors.danger, }, - uploadReceiptBorder: { + uploadReceiptView: { borderRadius: variables.componentBorderRadiusLarge, borderWidth: 2, borderColor: themeColors.borderFocus, borderStyle: 'dotted', + marginBottom: 20, + marginLeft: 20, + marginRight: 20, + justifyContent: 'center', + alignItems: 'center', + padding: 40, + gap: 4, + flex: 1, }, headerAnonymousFooter: { From a530909c07934509b7cfb0980120eaeaf329cddd Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 18 Jul 2023 18:55:36 +0200 Subject: [PATCH 0252/1075] further simplify Lightbox --- src/components/AttachmentCarousel/Lightbox.js | 40 +++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/src/components/AttachmentCarousel/Lightbox.js b/src/components/AttachmentCarousel/Lightbox.js index d6944d91d355..a405439a5579 100644 --- a/src/components/AttachmentCarousel/Lightbox.js +++ b/src/components/AttachmentCarousel/Lightbox.js @@ -58,19 +58,23 @@ function ImageWrapper({children}) { ImageWrapper.propTypes = imageWrapperPropTypes; const imageTransformerPropTypes = { - imageWidth: PropTypes.number.isRequired, - imageHeight: PropTypes.number.isRequired, - imageScale: PropTypes.number.isRequired, - scaledImageWidth: PropTypes.number.isRequired, - scaledImageHeight: PropTypes.number.isRequired, + imageHeight: PropTypes.number, + imageScale: PropTypes.number, + scaledImageWidth: PropTypes.number, + scaledImageHeight: PropTypes.number, isActive: PropTypes.bool.isRequired, children: PropTypes.node.isRequired, }; -function ImageTransformer({imageWidth = 0, imageHeight = 0, imageScale = 1, scaledImageWidth = 0, scaledImageHeight = 0, isActive, children}) { - const {canvasWidth, canvasHeight, onTap, onSwipe, onSwipeSuccess, pagerRef, shouldPagerScroll, isScrolling, onPinchGestureChange} = useContext(Context); +const imageTransformerDefaultProps = { + imageHeight: 0, + imageScale: 1, + scaledImageWidth: 0, + scaledImageHeight: 0, +}; - const [imageDimensions, setImageDimensions] = useState({width: imageWidth, height: imageHeight}); +function ImageTransformer({imageHeight, imageScale, scaledImageWidth, scaledImageHeight, isActive, children}) { + const {canvasWidth, canvasHeight, onTap, onSwipe, onSwipeSuccess, pagerRef, shouldPagerScroll, isScrolling, onPinchGestureChange} = useContext(Context); const canvas = { width: useSharedValue(canvasWidth), @@ -94,12 +98,6 @@ function ImageTransformer({imageWidth = 0, imageHeight = 0, imageScale = 1, scal })(); }, [canvas.height, canvas.width, canvasFitScale, canvasHeight, canvasWidth, imageScale]); - useEffect(() => { - if (imageWidth === 0 || imageHeight === 0) return; - - setImageDimensions({width: imageWidth, height: imageHeight}); - }, [imageDimensions.height, imageDimensions.width, imageHeight, imageWidth]); - // used for pan gesture const translateY = useSharedValue(0); const translateX = useSharedValue(0); @@ -125,7 +123,7 @@ function ImageTransformer({imageWidth = 0, imageHeight = 0, imageScale = 1, scal const scaleOffset = useSharedValue(1); // disable pan vertically when image is smaller than screen - const canPanVertically = useDerivedValue(() => canvas.height.value < imageDimensions.height * totalScale.value, [imageDimensions.height]); + const canPanVertically = useDerivedValue(() => canvas.height.value < imageHeight * totalScale.value, []); // calculates bounds of the scaled image // can we pan left/right/up/down @@ -159,7 +157,7 @@ function ImageTransformer({imageWidth = 0, imageHeight = 0, imageScale = 1, scal canPanLeft: target.x < maxVector.x, canPanRight: target.x > minVector.x, }; - }, [imageDimensions]); + }); const afterGesture = useWorkletCallback(() => { const {target, isInBoundaryX, isInBoundaryY, minVector, maxVector} = getBounds(); @@ -237,8 +235,8 @@ function ImageTransformer({imageWidth = 0, imageHeight = 0, imageScale = 1, scal stopAnimation(); const usableImage = { - x: imageDimensions.width * canvasFitScale.value, - y: imageDimensions.height * canvasFitScale.value, + x: scaledImageWidth, + y: scaledImageHeight, }; const targetImageSize = { @@ -282,7 +280,7 @@ function ImageTransformer({imageWidth = 0, imageHeight = 0, imageScale = 1, scal zoomScale.value = withSpring(DOUBLE_TAP_SCALE, SPRING_CONFIG); scaleOffset.value = DOUBLE_TAP_SCALE; }, - [imageDimensions], + [scaledImageWidth, scaledImageHeight], ); const reset = useWorkletCallback((animated) => { @@ -414,7 +412,7 @@ function ImageTransformer({imageWidth = 0, imageHeight = 0, imageScale = 1, scal }; offsetY.value = withSpring( - maybeInvert(imageDimensions.height * 2), + maybeInvert(imageHeight * 2), { stiffness: 50, damping: 30, @@ -599,6 +597,7 @@ function ImageTransformer({imageWidth = 0, imageHeight = 0, imageScale = 1, scal ); } ImageTransformer.propTypes = imageTransformerPropTypes; +ImageTransformer.defaultProps = imageTransformerDefaultProps; function getCanvasFitScale({canvasWidth, canvasHeight, imageWidth, imageHeight}) { const scaleFactorX = canvasWidth / imageWidth; @@ -636,7 +635,6 @@ function Page({isActive, item}) { Date: Tue, 18 Jul 2023 13:20:04 -0400 Subject: [PATCH 0253/1075] Fix drag and drop hover style --- src/CONST.js | 1 + src/pages/iou/ReceiptDropUI.js | 31 +++++++++++++++++++------- src/pages/iou/ReceiptSelector/index.js | 22 ++++++++++++------ src/styles/styles.js | 1 + 4 files changed, 40 insertions(+), 15 deletions(-) diff --git a/src/CONST.js b/src/CONST.js index e5afc1e9a861..535a97c39d62 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -480,6 +480,7 @@ const CONST = { DROP_HOST_NAME: 'ReceiptDropZone', DROP_NATIVE_ID: 'receipt-dropzone', ACTIVE_DROP_NATIVE_ID: 'receipt-dropzone', + ICON_SIZE: 164, }, REPORT: { DROP_HOST_NAME: 'ReportDropZone', diff --git a/src/pages/iou/ReceiptDropUI.js b/src/pages/iou/ReceiptDropUI.js index 446f5f33a096..b8441c889df4 100644 --- a/src/pages/iou/ReceiptDropUI.js +++ b/src/pages/iou/ReceiptDropUI.js @@ -1,16 +1,26 @@ import React from 'react'; -import {Text} from 'react-native'; +import {Text, View} from 'react-native'; import CONST from '../../CONST'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; import DropZone from '../../components/DragAndDrop/DropZone'; import styles from '../../styles/styles'; import ReceiptUpload from '../../../assets/images/receipt-upload.svg'; +import Button from '../../components/Button'; +import * as IOU from '../../libs/actions/IOU'; +import {PressableWithFeedback} from '../../components/Pressable'; +import PropTypes from 'prop-types'; const propTypes = { ...withLocalizePropTypes, + + receiptImageTopPosition: PropTypes.number, +}; + +const defaultProps = { + receiptImageTopPosition: 0, }; -function ReceiptDropUI() { +function ReceiptDropUI(props) { return ( - - Let it go - Drop your file here + + + + + Let it go + Drop your file here + ); } ReceiptDropUI.displayName = 'ReportDropUI'; ReceiptDropUI.propTypes = propTypes; +ReceiptDropUI.defaultProps = defaultProps; export default withLocalize(ReceiptDropUI); diff --git a/src/pages/iou/ReceiptSelector/index.js b/src/pages/iou/ReceiptSelector/index.js index 978f27f2b1f9..27883bfc99ca 100644 --- a/src/pages/iou/ReceiptSelector/index.js +++ b/src/pages/iou/ReceiptSelector/index.js @@ -1,5 +1,5 @@ -import {View, Text} from 'react-native'; -import React, {useRef} from 'react'; +import {View, Text, PixelRatio} from 'react-native'; +import React, {useRef, useState} from 'react'; import _ from 'underscore'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; @@ -76,6 +76,8 @@ function ReceiptSelector(props) { const iouType = useRef(lodashGet(props.route, 'params.iouType', '')); const reportID = useRef(lodashGet(props.route, 'params.reportID', '')); + const [receiptImageTopPosition, setReceiptImageTopPosition] = useState(0); + const navigateToNextPage = () => { const moneyRequestID = `${iouType.current}${reportID.current}`; const shouldReset = props.iou.id !== moneyRequestID; @@ -109,10 +111,16 @@ function ReceiptSelector(props) { const defaultView = () => ( <> - + { + setReceiptImageTopPosition(PixelRatio.roundToNearestPixel(nativeEvent.layout.top)); + }} + > + + Upload receipt Drag a receipt onto this page, forward a receipt to{' '} @@ -152,7 +160,7 @@ function ReceiptSelector(props) { return ( {!props.isDraggingOver ? defaultView() : null} - {props.isDraggingOver && } + {props.isDraggingOver && } ); } diff --git a/src/styles/styles.js b/src/styles/styles.js index 66a6fe3a7d77..dc1df1e15ead 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -1074,6 +1074,7 @@ const styles = { ...headlineFont, fontSize: variables.fontSizeXLarge, color: themeColors.textLight, + textAlign: 'center', }, subTextReceiptUpload: { From b87f796a049f1d88b253c65bda955225a4390ac3 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 18 Jul 2023 20:10:32 +0200 Subject: [PATCH 0254/1075] omit updating the image dimensions if already set --- src/components/AttachmentCarousel/Lightbox.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/components/AttachmentCarousel/Lightbox.js b/src/components/AttachmentCarousel/Lightbox.js index a405439a5579..c14249326720 100644 --- a/src/components/AttachmentCarousel/Lightbox.js +++ b/src/components/AttachmentCarousel/Lightbox.js @@ -649,7 +649,11 @@ function Page({isActive, item}) { const imageScale = getCanvasFitScale({canvasWidth, canvasHeight, imageWidth, imageHeight}); + // Don't update the dimensions if they are already set + if (dimensions?.imageWidth === imageWidth && dimensions?.imageHeight === imageHeight && dimensions?.imageScale === imageScale) return; + cachedDimensions.set(item.url, { + ...dimensions, imageWidth, imageHeight, imageScale, @@ -677,7 +681,11 @@ function Page({isActive, item}) { const scaledImageWidth = imageWidth * scale; const scaledImageHeight = imageHeight * scale; + // Don't update the dimensions if they are already set + if (dimensions?.scaledImageWidth === scaledImageWidth && dimensions?.scaledImageHeight === scaledImageHeight) return; + cachedDimensions.set(item.url, { + ...dimensions, scaledImageWidth, scaledImageHeight, }); From d23c2ccef8898878cd3e5c34e366690f8f900429 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 18 Jul 2023 20:42:40 +0200 Subject: [PATCH 0255/1075] fix: further simplify Lightbox --- src/components/AttachmentCarousel/Lightbox.js | 58 ++++++++----------- 1 file changed, 24 insertions(+), 34 deletions(-) diff --git a/src/components/AttachmentCarousel/Lightbox.js b/src/components/AttachmentCarousel/Lightbox.js index c14249326720..edeb6873502d 100644 --- a/src/components/AttachmentCarousel/Lightbox.js +++ b/src/components/AttachmentCarousel/Lightbox.js @@ -58,6 +58,7 @@ function ImageWrapper({children}) { ImageWrapper.propTypes = imageWrapperPropTypes; const imageTransformerPropTypes = { + imageWidth: PropTypes.number, imageHeight: PropTypes.number, imageScale: PropTypes.number, scaledImageWidth: PropTypes.number, @@ -67,36 +68,23 @@ const imageTransformerPropTypes = { }; const imageTransformerDefaultProps = { + imageWidth: 0, imageHeight: 0, imageScale: 1, scaledImageWidth: 0, scaledImageHeight: 0, }; -function ImageTransformer({imageHeight, imageScale, scaledImageWidth, scaledImageHeight, isActive, children}) { +function ImageTransformer({imageWidth, imageHeight, imageScale, scaledImageWidth, scaledImageHeight, isActive, children}) { const {canvasWidth, canvasHeight, onTap, onSwipe, onSwipeSuccess, pagerRef, shouldPagerScroll, isScrolling, onPinchGestureChange} = useContext(Context); - const canvas = { - width: useSharedValue(canvasWidth), - height: useSharedValue(canvasHeight), - }; - - const canvasFitScale = useSharedValue(imageScale); const zoomScale = useSharedValue(1); // Adding together the pinch zoom scale and the initial scale to fit the image into the canvas // Substracting 1, because both scales have the initial image as the base reference - const totalScale = useDerivedValue(() => zoomScale.value + canvasFitScale.value - 1); - - // Update shared values on UI thread when canvas dimensions change - useEffect(() => { - runOnUI(() => { - 'worklet'; + const totalScale = useDerivedValue(() => zoomScale.value + imageScale - 1, [imageScale]); - canvasFitScale.value = imageScale; - canvas.width.value = canvasWidth; - canvas.height.value = canvasHeight; - })(); - }, [canvas.height, canvas.width, canvasFitScale, canvasHeight, canvasWidth, imageScale]); + const zoomScaledImageWidth = useDerivedValue(() => imageWidth * totalScale.value, [imageWidth]); + const zoomScaledImageHeight = useDerivedValue(() => imageHeight * totalScale.value, [imageHeight]); // used for pan gesture const translateY = useSharedValue(0); @@ -123,18 +111,18 @@ function ImageTransformer({imageHeight, imageScale, scaledImageWidth, scaledImag const scaleOffset = useSharedValue(1); // disable pan vertically when image is smaller than screen - const canPanVertically = useDerivedValue(() => canvas.height.value < imageHeight * totalScale.value, []); + const canPanVertically = useDerivedValue(() => canvasHeight < zoomScaledImageHeight.value, [canvasHeight]); // calculates bounds of the scaled image // can we pan left/right/up/down // can be used to limit gesture or implementing tension effect const getBounds = useWorkletCallback(() => { - const rightBoundary = Math.abs(canvas.width.value - scaledImageWidth) / 2; + const rightBoundary = Math.abs(canvasWidth - zoomScaledImageWidth.value) / 2; let topBoundary = 0; - if (canvas.height.value < scaledImageHeight) { - topBoundary = Math.abs(scaledImageHeight - canvas.height.value) / 2; + if (canvasHeight < zoomScaledImageHeight.value) { + topBoundary = Math.abs(zoomScaledImageHeight.value - canvasHeight) / 2; } const maxVector = {x: rightBoundary, y: topBoundary}; @@ -157,7 +145,7 @@ function ImageTransformer({imageHeight, imageScale, scaledImageWidth, scaledImag canPanLeft: target.x < maxVector.x, canPanRight: target.x > minVector.x, }; - }); + }, [canvasWidth, canvasHeight]); const afterGesture = useWorkletCallback(() => { const {target, isInBoundaryX, isInBoundaryY, minVector, maxVector} = getBounds(); @@ -245,8 +233,8 @@ function ImageTransformer({imageHeight, imageScale, scaledImageWidth, scaledImag }; const CENTER = { - x: canvas.width.value / 2, - y: canvas.height.value / 2, + x: canvasWidth / 2, + y: canvasHeight / 2, }; const imageCenter = { @@ -271,7 +259,7 @@ function ImageTransformer({imageHeight, imageScale, scaledImageWidth, scaledImag y: currentOrigin.y * koef.y, }; - if (targetImageSize.y < canvas.height.value) { + if (targetImageSize.y < canvasHeight) { target.y = 0; } @@ -280,7 +268,7 @@ function ImageTransformer({imageHeight, imageScale, scaledImageWidth, scaledImag zoomScale.value = withSpring(DOUBLE_TAP_SCALE, SPRING_CONFIG); scaleOffset.value = DOUBLE_TAP_SCALE; }, - [scaledImageWidth, scaledImageHeight], + [scaledImageWidth, scaledImageHeight, canvasWidth, canvasHeight], ); const reset = useWorkletCallback((animated) => { @@ -437,10 +425,13 @@ function ImageTransformer({imageHeight, imageScale, scaledImageWidth, scaledImag }) .withRef(panGestureRef); - const getAdjustedFocal = useWorkletCallback((focalX, focalY) => ({ - x: focalX - (canvas.width.value / 2 + offsetX.value), - y: focalY - (canvas.height.value / 2 + offsetY.value), - })); + const getAdjustedFocal = useWorkletCallback( + (focalX, focalY) => ({ + x: focalX - (canvasWidth / 2 + offsetX.value), + y: focalY - (canvasHeight / 2 + offsetY.value), + }), + [canvasWidth, canvasHeight], + ); // used to store event scale value when we limit scale const gestureScale = useSharedValue(1); @@ -465,9 +456,7 @@ function ImageTransformer({imageHeight, imageScale, scaledImageWidth, scaledImag origin.y.value = adjustFocal.y; }) .onChange((evt) => { - const newZoomScale = clamp(scaleOffset.value * evt.scale, MIN_SCALE, MAX_SCALE); - - zoomScale.value = newZoomScale; + zoomScale.value = clamp(scaleOffset.value * evt.scale, MIN_SCALE, MAX_SCALE); if (zoomScale.value > MIN_SCALE && zoomScale.value < MAX_SCALE) { gestureScale.value = evt.scale; @@ -635,6 +624,7 @@ function Page({isActive, item}) { Date: Tue, 18 Jul 2023 20:54:29 +0200 Subject: [PATCH 0256/1075] fix: loading state never ending --- src/components/AttachmentCarousel/Lightbox.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/AttachmentCarousel/Lightbox.js b/src/components/AttachmentCarousel/Lightbox.js index edeb6873502d..965cf8b55f7f 100644 --- a/src/components/AttachmentCarousel/Lightbox.js +++ b/src/components/AttachmentCarousel/Lightbox.js @@ -640,14 +640,14 @@ function Page({isActive, item}) { const imageScale = getCanvasFitScale({canvasWidth, canvasHeight, imageWidth, imageHeight}); // Don't update the dimensions if they are already set - if (dimensions?.imageWidth === imageWidth && dimensions?.imageHeight === imageHeight && dimensions?.imageScale === imageScale) return; - - cachedDimensions.set(item.url, { - ...dimensions, - imageWidth, - imageHeight, - imageScale, - }); + if (dimensions?.imageWidth !== imageWidth || dimensions?.imageHeight !== imageHeight || dimensions?.imageScale !== imageScale) { + cachedDimensions.set(item.url, { + ...dimensions, + imageWidth, + imageHeight, + imageScale, + }); + } if (imageWidth === 0 || imageHeight === 0) return; setIsImageLoading(false); From ad5e5751b6630c00c7ca7632ef3770f6b3e0bf41 Mon Sep 17 00:00:00 2001 From: Andrew Gable Date: Tue, 18 Jul 2023 15:45:53 -0400 Subject: [PATCH 0257/1075] Get mobile gallery working --- src/pages/iou/ReceiptDropUI.js | 5 +- src/pages/iou/ReceiptSelector/index.js | 2 +- src/pages/iou/ReceiptSelector/index.native.js | 82 ++++++------------- .../iou/steps/MoneyRequestConfirmPage.js | 4 - 4 files changed, 28 insertions(+), 65 deletions(-) diff --git a/src/pages/iou/ReceiptDropUI.js b/src/pages/iou/ReceiptDropUI.js index b8441c889df4..618dd0bcd461 100644 --- a/src/pages/iou/ReceiptDropUI.js +++ b/src/pages/iou/ReceiptDropUI.js @@ -1,14 +1,11 @@ import React from 'react'; import {Text, View} from 'react-native'; +import PropTypes from 'prop-types'; import CONST from '../../CONST'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; import DropZone from '../../components/DragAndDrop/DropZone'; import styles from '../../styles/styles'; import ReceiptUpload from '../../../assets/images/receipt-upload.svg'; -import Button from '../../components/Button'; -import * as IOU from '../../libs/actions/IOU'; -import {PressableWithFeedback} from '../../components/Pressable'; -import PropTypes from 'prop-types'; const propTypes = { ...withLocalizePropTypes, diff --git a/src/pages/iou/ReceiptSelector/index.js b/src/pages/iou/ReceiptSelector/index.js index 27883bfc99ca..7a1873af0f43 100644 --- a/src/pages/iou/ReceiptSelector/index.js +++ b/src/pages/iou/ReceiptSelector/index.js @@ -12,7 +12,7 @@ import personalDetailsPropType from '../../personalDetailsPropType'; import CONST from '../../../CONST'; import {withCurrentUserPersonalDetailsDefaultProps} from '../../../components/withCurrentUserPersonalDetails'; import ReceiptUpload from '../../../../assets/images/receipt-upload.svg'; -import {PressableWithFeedback} from '../../../components/Pressable'; +import PressableWithFeedback from '../../../components/Pressable/PressableWithFeedback'; import Button from '../../../components/Button'; import styles from '../../../styles/styles'; import CopyTextToClipboard from '../../../components/CopyTextToClipboard'; diff --git a/src/pages/iou/ReceiptSelector/index.native.js b/src/pages/iou/ReceiptSelector/index.native.js index 58d2034bb545..9634c162418e 100644 --- a/src/pages/iou/ReceiptSelector/index.native.js +++ b/src/pages/iou/ReceiptSelector/index.native.js @@ -4,7 +4,8 @@ import {Camera, useCameraDevices} from 'react-native-vision-camera'; import _ from 'underscore'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import {PressableWithFeedback} from '../../../components/Pressable'; +import {launchImageLibrary} from 'react-native-image-picker'; +import PressableWithFeedback from '../../../components/Pressable/PressableWithFeedback'; import Colors from '../../../styles/colors'; import Icon from '../../../components/Icon'; import * as Expensicons from '../../../components/Icon/Expensicons'; @@ -19,9 +20,7 @@ import reportPropTypes from '../../reportPropTypes'; import personalDetailsPropType from '../../personalDetailsPropType'; import CONST from '../../../CONST'; import {withCurrentUserPersonalDetailsDefaultProps} from '../../../components/withCurrentUserPersonalDetails'; -import {launchImageLibrary} from 'react-native-image-picker'; -import AttachmentView from '../../../components/AttachmentView'; -import AttachmentPicker from '../../../components/AttachmentPicker'; +import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; const propTypes = { /** Route params */ @@ -55,6 +54,8 @@ const propTypes = { * Current user personal details */ currentUserPersonalDetails: personalDetailsPropType, + + ...withLocalizePropTypes, }; const defaultProps = { @@ -139,49 +140,20 @@ function ReceiptSelector(props) { Navigation.navigate(ROUTES.getMoneyRequestParticipantsRoute(iouType.current)); }; - /** - * Handles the image/document picker result and - * sends the selected attachment to the caller (parent component) - * - * @param {Array} attachments - * @returns {Promise} - */ - const pickAttachment = (attachments = []) => { - if (attachments.length === 0) { - return; - } - - const fileData = _.first(attachments); - - if (fileData.width === -1 || fileData.height === -1) { - this.showImageCorruptionAlert(); - return; - } - - return getDataForUpload(fileData) - .then((result) => { - this.completeAttachmentSelection(result); - }) - .catch((error) => { - this.showGeneralAlert(error.message); - throw error; - }); - }; - /** * Inform the users when they need to grant camera access and guide them to settings */ const showPermissionsAlert = () => { Alert.alert( - this.props.translate('attachmentPicker.cameraPermissionRequired'), - this.props.translate('attachmentPicker.expensifyDoesntHaveAccessToCamera'), + props.translate('attachmentPicker.cameraPermissionRequired'), + props.translate('attachmentPicker.expensifyDoesntHaveAccessToCamera'), [ { - text: this.props.translate('common.cancel'), + text: props.translate('common.cancel'), style: 'cancel', }, { - text: this.props.translate('common.settings'), + text: props.translate('common.settings'), onPress: () => Linking.openSettings(), }, ], @@ -189,6 +161,14 @@ function ReceiptSelector(props) { ); }; + /** + * A generic handling when we don't know the exact reason for an error + * + */ + const showGeneralAlert = () => { + Alert.alert(props.translate('attachmentPicker.attachmentError'), props.translate('attachmentPicker.errorWhileSelectingAttachment')); + }; + /** * Common image picker handling * @@ -220,21 +200,6 @@ function ReceiptSelector(props) { }); }; - /** - * A generic handling when we don't know the exact reason for an error - * - */ - const showGeneralAlert = () => { - Alert.alert(this.props.translate('attachmentPicker.attachmentError'), this.props.translate('attachmentPicker.errorWhileSelectingAttachment')); - }; - - /** - * An attachment error dialog when user selected malformed images - */ - const showImageCorruptionAlert = () => { - Alert.alert(this.props.translate('attachmentPicker.attachmentError'), this.props.translate('attachmentPicker.errorWhileSelectingCorruptedImage')); - }; - const takePhoto = () => { camera.current .takePhoto({ @@ -277,7 +242,10 @@ function ReceiptSelector(props) { accessibilityRole="button" style={[styles.alignItemsStart]} onPress={() => { - showImagePicker(launchImageLibrary); + showImagePicker(launchImageLibrary).then((receiptImage) => { + IOU.setMoneyRequestReceipt(receiptImage[0].uri); + navigateToNextPage(); + }); }} > { - showImagePicker(launchImageLibrary).catch((error) => { - console.log(error); + showImagePicker(launchImageLibrary).then((receiptImage) => { + console.log(receiptImage[0].uri); + IOU.setMoneyRequestReceipt(receiptImage[0].uri); + navigateToNextPage(); }); }} > @@ -379,4 +349,4 @@ ReceiptSelector.defaultProps = defaultProps; ReceiptSelector.propTypes = propTypes; ReceiptSelector.displayName = 'ReceiptSelector'; -export default ReceiptSelector; +export default withLocalize(ReceiptSelector); diff --git a/src/pages/iou/steps/MoneyRequestConfirmPage.js b/src/pages/iou/steps/MoneyRequestConfirmPage.js index c9ff54f125ff..dc98a2b0a358 100644 --- a/src/pages/iou/steps/MoneyRequestConfirmPage.js +++ b/src/pages/iou/steps/MoneyRequestConfirmPage.js @@ -73,14 +73,11 @@ function MoneyRequestConfirmPage(props) { [props.iou.participants, props.personalDetails], ); - console.log(`MoneyRequestConfirmPage ${JSON.stringify(props)}`); - useEffect(() => { // ID in Onyx could change by initiating a new request in a separate browser tab or completing a request if (prevMoneyRequestId.current !== props.iou.id) { // The ID is cleared on completing a request. In that case, we will do nothing. if (props.iou.id) { - console.log(`Going back ${JSON.stringify(props)}`); Navigation.goBack(ROUTES.getMoneyRequestRoute(iouType.current, reportID.current), true); } return; @@ -110,7 +107,6 @@ function MoneyRequestConfirmPage(props) { } else { fallback = ROUTES.getMoneyRequestParticipantsRoute(iouType.current); } - console.log(`Going back ${JSON.stringify(props)}`); Navigation.goBack(fallback); }; From 0d201fe3e2c6e938166eb4d2aed9429a1207b1f6 Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Tue, 18 Jul 2023 15:47:28 -0400 Subject: [PATCH 0258/1075] rename and move notification cache to its own file --- .../CustomNotificationProvider.java | 58 +++++++------------ .../NotificationCache.java | 30 ++++++++++ 2 files changed, 50 insertions(+), 38 deletions(-) create mode 100644 android/app/src/main/java/com/expensify/chat/customairshipextender/NotificationCache.java diff --git a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java index d7dff0ffcf0f..76cc1e32b519 100644 --- a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java +++ b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java @@ -49,6 +49,8 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import com.expensify.chat.customairshipextender.NotificationCache.NotificationData; + public class CustomNotificationProvider extends ReactNotificationProvider { // Resize icons to 100 dp x 100 dp private static final int MAX_ICON_SIZE_DPS = 100; @@ -71,7 +73,7 @@ public class CustomNotificationProvider extends ReactNotificationProvider { private static final String ONYX_DATA_KEY = "onyxData"; private final ExecutorService executorService = Executors.newCachedThreadPool(); - public final HashMap cache = new HashMap<>(); + public final HashMap cache = new HashMap<>(); public CustomNotificationProvider(@NonNull Context context, @NonNull AirshipConfigOptions configOptions) { super(context, configOptions); @@ -168,8 +170,8 @@ private void applyMessageStyle(@NonNull Context context, NotificationCompat.Buil } // Retrieve and check for cached notifications - NotificationCache notificationCache = findOrCreateNotificationCache(reportID); - boolean hasExistingNotification = notificationCache.messages.size() >= 1; + NotificationData notificationData = findOrCreateNotificationData(reportID); + boolean hasExistingNotification = notificationData.messages.size() >= 1; try { JsonMap reportMap = payload.get(ONYX_DATA_KEY).getList().get(1).getMap().get("value").getMap(); @@ -183,8 +185,8 @@ private void applyMessageStyle(@NonNull Context context, NotificationCompat.Buil String conversationName = payload.get("roomName") == null ? "" : payload.get("roomName").getString(""); // Retrieve or create the Person object who sent the latest report comment - Person person = notificationCache.people.get(accountID); - Bitmap personIcon = notificationCache.bitmapIcons.get(accountID); + Person person = notificationData.people.get(accountID); + Bitmap personIcon = notificationData.bitmapIcons.get(accountID); if (personIcon == null) { personIcon = fetchIcon(context, avatar); @@ -200,13 +202,13 @@ private void applyMessageStyle(@NonNull Context context, NotificationCompat.Buil .setName(name) .build(); - notificationCache.people.put(accountID, person); - notificationCache.bitmapIcons.put(accountID, personIcon); + notificationData.people.put(accountID, person); + notificationData.bitmapIcons.put(accountID, personIcon); } // Despite not using conversation style for the initial notification from each chat, we need to cache it to enable conversation style for future notifications long createdTimeInMillis = getMessageTimeInMillis(messageData.get("created").getString("")); - notificationCache.messages.add(new NotificationCache.Message(person, message, createdTimeInMillis)); + notificationData.messages.add(new NotificationData.Message(person, message, createdTimeInMillis)); // Conversational styling should be applied to groups chats, rooms, and any 1:1 chats with more than one notification (ensuring the large profile image is always shown) @@ -217,7 +219,7 @@ private void applyMessageStyle(@NonNull Context context, NotificationCompat.Buil .setConversationTitle(conversationName); // Add all conversation messages to the notification, including the last one we just received. - for (NotificationCache.Message cachedMessage : notificationCache.messages) { + for (NotificationData.Message cachedMessage : notificationData.messages) { messagingStyle.addMessage(cachedMessage.text, cachedMessage.time, cachedMessage.person); } builder.setStyle(messagingStyle); @@ -225,8 +227,8 @@ private void applyMessageStyle(@NonNull Context context, NotificationCompat.Buil // Clear the previous notification associated to this conversation so it looks like we are // replacing them with this new one we just built. - if (notificationCache.prevNotificationID != -1) { - NotificationManagerCompat.from(context).cancel(notificationCache.prevNotificationID); + if (notificationData.prevNotificationID != -1) { + NotificationManagerCompat.from(context).cancel(notificationData.prevNotificationID); } } catch (Exception e) { @@ -235,7 +237,7 @@ private void applyMessageStyle(@NonNull Context context, NotificationCompat.Buil // Store the new notification ID so we can replace the notification if this conversation // receives more messages - notificationCache.prevNotificationID = notificationID; + notificationData.prevNotificationID = notificationID; } /** @@ -262,15 +264,15 @@ private long getMessageTimeInMillis(String createdTime) { * @param reportID Report ID. * @return Notification Cache. */ - private NotificationCache findOrCreateNotificationCache(long reportID) { - NotificationCache notificationCache = cache.get(reportID); + private NotificationData findOrCreateNotificationData(long reportID) { + NotificationData notificationData = cache.get(reportID); - if (notificationCache == null) { - notificationCache = new NotificationCache(); - cache.put(reportID, notificationCache); + if (notificationData == null) { + notificationData = new NotificationData(); + cache.put(reportID, notificationData); } - return notificationCache; + return notificationData; } /** @@ -328,24 +330,4 @@ private Bitmap fetchIcon(@NonNull Context context, String urlString) { return null; } - - private static class NotificationCache { - public Map people = new HashMap<>(); - public ArrayList messages = new ArrayList<>(); - - public Map bitmapIcons = new HashMap<>(); - public int prevNotificationID = -1; - - public static class Message { - public Person person; - public String text; - public long time; - - Message(Person person, String text, long time) { - this.person = person; - this.text = text; - this.time = time; - } - } - } } diff --git a/android/app/src/main/java/com/expensify/chat/customairshipextender/NotificationCache.java b/android/app/src/main/java/com/expensify/chat/customairshipextender/NotificationCache.java new file mode 100644 index 000000000000..fbe6e802722d --- /dev/null +++ b/android/app/src/main/java/com/expensify/chat/customairshipextender/NotificationCache.java @@ -0,0 +1,30 @@ +package com.expensify.chat.customairshipextender; + +import android.graphics.Bitmap; +import androidx.core.app.Person; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +public class NotificationCache { + public static class NotificationData { + public Map people = new HashMap<>(); + public ArrayList messages = new ArrayList<>(); + + public Map bitmapIcons = new HashMap<>(); + public int prevNotificationID = -1; + + public static class Message { + public Person person; + public String text; + public long time; + + Message(Person person, String text, long time) { + this.person = person; + this.text = text; + this.time = time; + } + } + } +} From ef99391386be985bc061808b55357b6f02fd2b5b Mon Sep 17 00:00:00 2001 From: Andrew Gable Date: Tue, 18 Jul 2023 15:48:04 -0400 Subject: [PATCH 0259/1075] Remove some debug lines --- src/components/MoneyRequestConfirmationList.js | 2 -- src/pages/iou/ReceiptSelector/index.native.js | 1 - src/pages/iou/steps/MoneyRequestConfirmPage.js | 1 - .../MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js | 1 - 4 files changed, 5 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index d992c1c150a5..a68bcf1bab2b 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -104,8 +104,6 @@ function MoneyRequestConfirmationList(props) { // Prop functions pass props itself as a "this" value to the function which means they change every time props change. const {translate, onSendMoney, onConfirm, onSelectParticipant} = props; - console.log(`MoneyRequestConfirmationList`); - /** * Returns the participants with amount * @param {Array} participants diff --git a/src/pages/iou/ReceiptSelector/index.native.js b/src/pages/iou/ReceiptSelector/index.native.js index 9634c162418e..f1bfe4619207 100644 --- a/src/pages/iou/ReceiptSelector/index.native.js +++ b/src/pages/iou/ReceiptSelector/index.native.js @@ -305,7 +305,6 @@ function ReceiptSelector(props) { style={[styles.alignItemsStart]} onPress={() => { showImagePicker(launchImageLibrary).then((receiptImage) => { - console.log(receiptImage[0].uri); IOU.setMoneyRequestReceipt(receiptImage[0].uri); navigateToNextPage(); }); diff --git a/src/pages/iou/steps/MoneyRequestConfirmPage.js b/src/pages/iou/steps/MoneyRequestConfirmPage.js index dc98a2b0a358..a9c58381c84d 100644 --- a/src/pages/iou/steps/MoneyRequestConfirmPage.js +++ b/src/pages/iou/steps/MoneyRequestConfirmPage.js @@ -91,7 +91,6 @@ function MoneyRequestConfirmPage(props) { } if (_.isEmpty(props.iou.participants) || (props.iou.amount === 0 && !props.iou.receiptPath) || shouldReset) { - console.log(`Going back ${JSON.stringify(props)}`); Navigation.goBack(ROUTES.getMoneyRequestRoute(iouType.current, reportID.current), true); } diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js index 437f6c114d08..af181f9b3f94 100644 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js @@ -75,7 +75,6 @@ function MoneyRequestParticipantsPage(props) { } if ((props.iou.amount === 0 && !props.iou.receiptPath) || shouldReset) { - console.log(`Going back ${props}`); navigateBack(true); } From 7608579bae39054d425582eff4dc178aa0b5b2cd Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 18 Jul 2023 21:48:57 +0200 Subject: [PATCH 0260/1075] improve state in AttachmentCarousel --- .../AttachmentCarouselView/index.js | 68 +++++++++++++------ .../AttachmentCarouselView/index.native.js | 62 ++++++++--------- .../AttachmentCarouselView/propTypes.js | 45 ++++++------ .../AttachmentCarousel/CarouselButtons.js | 29 ++++---- src/components/AttachmentCarousel/index.js | 54 +++------------ 5 files changed, 130 insertions(+), 128 deletions(-) diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.js index 7aacf9c608b7..74e229db20b8 100644 --- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.js +++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.js @@ -1,12 +1,12 @@ -import React, {useRef, useCallback} from 'react'; -import {View, FlatList, PixelRatio} from 'react-native'; +import React, {useRef, useCallback, useState} from 'react'; +import {View, FlatList, PixelRatio, Keyboard} from 'react-native'; import _ from 'underscore'; import styles from '../../../styles/styles'; import CarouselActions from '../CarouselActions'; import AttachmentView from '../../AttachmentView'; import withWindowDimensions, {windowDimensionsPropTypes} from '../../withWindowDimensions'; import CarouselButtons from '../CarouselButtons'; -import attachmentCarouselViewPropTypes from './propTypes'; +import {propTypes as attachmentCarouselViewPropTypes} from './propTypes'; const VIEWABILITY_CONFIG = { // To facilitate paging through the attachments, we want to consider an item "viewable" when it is @@ -19,9 +19,37 @@ const propTypes = { ...windowDimensionsPropTypes, }; -function AttachmentCarouselView({carouselState, updatePage, setArrowsVisibility, ...props}) { +function AttachmentCarouselView({containerDimensions, attachments, initialPage, initialActiveSource, onNavigate, ...props}) { const scrollRef = useRef(null); + const [shouldShowArrows, setShouldShowArrows] = useState(false); + const [page, setPage] = useState(initialPage); + const [activeSource, setActiveSource] = useState(initialActiveSource); + + /** + * Updates the page state when the user navigates between attachments + * @param {Object} item + * @param {number} index + */ + const updatePage = useCallback( + ({item, index}) => { + Keyboard.dismiss(); + + if (!item) { + // eslint-disable-next-line react/no-unused-state + setActiveSource(null); + return; + } + + const pageIndex = index; + onNavigate(item); + // eslint-disable-next-line react/no-unused-state + setPage(pageIndex); + setActiveSource(item.source); + }, + [onNavigate], + ); + /** * Calculate items layout information to optimize scrolling performance * @param {*} data @@ -30,11 +58,11 @@ function AttachmentCarouselView({carouselState, updatePage, setArrowsVisibility, */ const getItemLayout = useCallback( (_data, index) => ({ - length: carouselState.containerWidth, - offset: carouselState.containerWidth * index, + length: containerDimensions.width, + offset: containerDimensions.height * index, index, }), - [carouselState.containerWidth], + [containerDimensions.height, containerDimensions.width], ); /** @@ -43,8 +71,8 @@ function AttachmentCarouselView({carouselState, updatePage, setArrowsVisibility, */ const cycleThroughAttachments = useCallback( (deltaSlide) => { - const nextIndex = carouselState.page - deltaSlide; - const nextItem = carouselState.attachments[nextIndex]; + const nextIndex = page - deltaSlide; + const nextItem = attachments[nextIndex]; if (!nextItem || !scrollRef.current) { return; @@ -54,7 +82,7 @@ function AttachmentCarouselView({carouselState, updatePage, setArrowsVisibility, // so we only enable it for mobile scrollRef.current.scrollToIndex({index: nextIndex, animated: false}); }, - [carouselState.attachments, carouselState.page], + [attachments, page], ); /** @@ -88,13 +116,13 @@ function AttachmentCarouselView({carouselState, updatePage, setArrowsVisibility, const renderItem = useCallback( ({item}) => ( ), - [carouselState.activeSource], + [activeSource], ); const handleViewableItemsChange = useCallback( @@ -111,17 +139,19 @@ function AttachmentCarouselView({carouselState, updatePage, setArrowsVisibility, return ( setArrowsVisibility(true)} - onMouseLeave={() => setArrowsVisibility(false)} + onMouseEnter={() => setShouldShowArrows(true)} + onMouseLeave={() => setShouldShowArrows(false)} style={[styles.flex1, styles.attachmentCarouselButtonsContainer]} > cycleThroughAttachments(-1)} onForward={() => cycleThroughAttachments(1)} /> - {carouselState.containerWidth > 0 && carouselState.containerHeight > 0 && ( + {containerDimensions.width > 0 && containerDimensions.height > 0 && ( carouselState.attachments.reverse(), [carouselState.attachments]); + const reversedAttachments = useMemo(() => attachments.reverse(), [attachments]); const processedItems = useMemo(() => _.map(reversedAttachments, (item) => ({key: item.source, url: addEncryptedAuthTokenToURL(item.source)})), [reversedAttachments]); - const reversePage = useCallback((page) => Math.max(0, Math.min(carouselState.attachments.length - page - 1, carouselState.attachments.length)), [carouselState.attachments.length]); + const reversePage = useCallback((page) => Math.max(0, Math.min(attachments.length - page - 1, attachments.length)), [attachments.length]); - const reversedPage = useMemo(() => reversePage(carouselState.page), [carouselState.page, reversePage]); + const [shouldShowArrows, setShouldShowArrows] = useState(true); + const [page, setPage] = useState(reversePage(initialPage)); /** - * Update carousel page based on next page index - * @param {Number} newPageIndex + * Updates the page state when the user navigates between attachments + * @param {Object} item + * @param {number} index */ - const updatePageInternal = useCallback( + const updatePage = useCallback( (newPageIndex) => { - const nextItem = reversedAttachments[newPageIndex]; - if (!nextItem) { - return; - } + setPage(newPageIndex); + Keyboard.dismiss(); - updatePage({item: nextItem, index: reversePage(newPageIndex)}); + const item = reversedAttachments[newPageIndex]; + + onNavigate(item); }, - [reversePage, reversedAttachments, updatePage], + [onNavigate, reversedAttachments], ); /** @@ -43,11 +45,11 @@ function AttachmentCarouselView({carouselState, updatePage, setArrowsVisibility, */ const cycleThroughAttachments = useCallback( (deltaSlide) => { - const nextPageIndex = reversedPage + deltaSlide; + const nextPageIndex = page + deltaSlide; updatePage(nextPageIndex); pagerRef.current.setPage(nextPageIndex); }, - [reversedPage, updatePage], + [page, updatePage], ); const autoHideArrowTimeout = useRef(null); @@ -64,17 +66,15 @@ function AttachmentCarouselView({carouselState, updatePage, setArrowsVisibility, const autoHideArrow = useCallback(() => { cancelAutoHideArrow(); autoHideArrowTimeout.current = setTimeout(() => { - setArrowsVisibility(false); + setShouldShowArrows(false); }, CONST.ARROW_HIDE_DELAY); - }, [cancelAutoHideArrow, setArrowsVisibility]); - - // eslint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => setArrowsVisibility(true), []); + }, [cancelAutoHideArrow]); return ( { cycleThroughAttachments(-1); autoHideArrow(); @@ -87,19 +87,19 @@ function AttachmentCarouselView({carouselState, updatePage, setArrowsVisibility, cancelAutoHideArrow={cancelAutoHideArrow} /> - {carouselState.containerWidth > 0 && carouselState.containerHeight > 0 && ( + {containerDimensions.width > 0 && containerDimensions.height > 0 && ( { console.log('page updated'); - updatePageInternal(newPage); + updatePage(newPage); }} - onTap={() => setArrowsVisibility()} - onPinchGestureChange={(isPinchGestureRunning) => setArrowsVisibility(!isPinchGestureRunning)} + onTap={() => setShouldShowArrows(!shouldShowArrows)} + onPinchGestureChange={(isPinchGestureRunning) => setShouldShowArrows(!isPinchGestureRunning)} onSwipeDown={onClose} - containerWidth={carouselState.containerWidth} - containerHeight={carouselState.containerHeight} + containerWidth={containerDimensions.width} + containerHeight={containerDimensions.height} ref={pagerRef} /> )} diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/propTypes.js b/src/components/AttachmentCarousel/AttachmentCarouselView/propTypes.js index 7d3b50f446a5..bac717cd0442 100644 --- a/src/components/AttachmentCarousel/AttachmentCarouselView/propTypes.js +++ b/src/components/AttachmentCarousel/AttachmentCarouselView/propTypes.js @@ -1,34 +1,37 @@ import PropTypes from 'prop-types'; +const attachmentsPropType = PropTypes.arrayOf( + PropTypes.shape({ + // eslint-disable-next-line react/forbid-prop-types + file: PropTypes.object.isRequired, + isAuthTokenRequired: PropTypes.bool.isRequired, + source: PropTypes.string.isRequired, + }), +); + const propTypes = { /** - * The current state of the carousel + * The initial page of the carousel */ - carouselState: PropTypes.shape({ - page: PropTypes.number.isRequired, - attachments: PropTypes.arrayOf( - PropTypes.shape({ - // eslint-disable-next-line react/forbid-prop-types - file: PropTypes.object.isRequired, - isAuthTokenRequired: PropTypes.bool.isRequired, - source: PropTypes.string.isRequired, - }), - ).isRequired, - shouldShowArrow: PropTypes.bool.isRequired, - containerWidth: PropTypes.number.isRequired, - containerHeight: PropTypes.number.isRequired, - activeSource: PropTypes.string, - }).isRequired, + initialPage: PropTypes.number.isRequired, + + /** + * The attachments of the carousel + */ + attachments: attachmentsPropType.isRequired, /** - * A callback to update the page in the carousel component + * The initial active ource of the carousel */ - updatePage: PropTypes.func.isRequired, + initialActiveSource: PropTypes.string.isRequired, /** - * A callback for toggling the visibility of the arrows + * The container dimensions */ - setArrowsVisibility: PropTypes.func.isRequired, + containerDimensions: PropTypes.shape({ + width: PropTypes.number.isRequired, + height: PropTypes.number.isRequired, + }).isRequired, }; -export default propTypes; +export {propTypes, attachmentsPropType}; diff --git a/src/components/AttachmentCarousel/CarouselButtons.js b/src/components/AttachmentCarousel/CarouselButtons.js index 6cefefd37a53..d94a4114b677 100644 --- a/src/components/AttachmentCarousel/CarouselButtons.js +++ b/src/components/AttachmentCarousel/CarouselButtons.js @@ -8,11 +8,14 @@ import Tooltip from '../Tooltip'; import Button from '../Button'; import styles from '../../styles/styles'; import themeColors from '../../styles/themes/default'; +import * as AttachmentCarouselViewPropTypes from './AttachmentCarouselView/propTypes'; const propTypes = { - /** Callback to go one page back */ - // eslint-disable-next-line react/forbid-prop-types - carouselState: PropTypes.object.isRequired, + /** The current page index */ + page: PropTypes.number.isRequired, + + /** The attachments from the carousel */ + attachments: AttachmentCarouselViewPropTypes.attachmentsPropType.isRequired, /** Callback to go one page back */ onBack: PropTypes.func.isRequired, @@ -30,11 +33,11 @@ const defaultProps = { cancelAutoHideArrow: () => {}, }; -function CarouselButtons(props) { - const isForwardDisabled = props.carouselState.page === 0; - const isBackDisabled = props.carouselState.page === _.size(props.carouselState.attachments) - 1; +function CarouselButtons({page, attachments, shouldShowArrow, onBack, onForward, cancelAutoHideArrow, autoHideArrow, ...props}) { + const isForwardDisabled = page === 0; + const isBackDisabled = page === _.size(attachments) - 1; - return props.carouselState.shouldShowArrow ? ( + return shouldShowArrow ? ( <> {!isBackDisabled && ( @@ -45,9 +48,9 @@ function CarouselButtons(props) { icon={Expensicons.BackArrow} iconFill={themeColors.text} iconStyles={[styles.mr0]} - onPress={props.onBack} - onPressIn={props.cancelAutoHideArrow} - onPressOut={props.autoHideArrow} + onPress={onBack} + onPressIn={cancelAutoHideArrow} + onPressOut={autoHideArrow} /> @@ -61,9 +64,9 @@ function CarouselButtons(props) { icon={Expensicons.ArrowRight} iconFill={themeColors.text} iconStyles={[styles.mr0]} - onPress={props.onForward} - onPressIn={props.cancelAutoHideArrow} - onPressOut={props.autoHideArrow} + onPress={onForward} + onPressIn={cancelAutoHideArrow} + onPressOut={autoHideArrow} /> diff --git a/src/components/AttachmentCarousel/index.js b/src/components/AttachmentCarousel/index.js index d142ceda4899..99fc9ebe2c54 100644 --- a/src/components/AttachmentCarousel/index.js +++ b/src/components/AttachmentCarousel/index.js @@ -1,5 +1,5 @@ import React from 'react'; -import {View, PixelRatio, Keyboard} from 'react-native'; +import {View, PixelRatio} from 'react-native'; import PropTypes from 'prop-types'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -46,25 +46,11 @@ class AttachmentCarousel extends React.Component { constructor(props) { super(props); - this.updatePage = this.updatePage.bind(this); - this.setArrowsVisibility = this.setArrowsVisibility.bind(this); this.createInitialState = this.createInitialState.bind(this); this.state = this.createInitialState(); } - /** - * Toggles the visibility of the arrows - * @param {Boolean} shouldShowArrow - * @param {Boolean} isGestureInUse - */ - setArrowsVisibility(shouldShowArrow) { - this.setState((current) => { - const newShouldShowArrow = _.isBoolean(shouldShowArrow) ? shouldShowArrow : !current.shouldShowArrow; - return {shouldShowArrow: newShouldShowArrow}; - }); - } - /** * Constructs the initial component state from report actions * @returns {{page: Number, attachments: Array, shouldShowArrow: Boolean, containerWidth: Number}} @@ -99,44 +85,23 @@ class AttachmentCarousel extends React.Component { }); htmlParser.end(); - const page = _.findIndex(attachments, (a) => a.source === this.props.source); - if (page === -1) { + const initialPage = _.findIndex(attachments, (a) => a.source === this.props.source); + if (initialPage === -1) { throw new Error('Attachment not found'); } // Update the parent modal's state with the source and name from the mapped attachments - this.props.onNavigate(attachments[page]); + this.props.onNavigate(attachments[initialPage]); return { - page, + initialPage, attachments, - shouldShowArrow: false, containerWidth: 0, containerHeight: 0, - activeSource: null, + initialActiveSource: null, }; } - /** - * Updates the page state when the user navigates between attachments - * @param {Object} item - * @param {number} index - */ - updatePage({item, index}) { - Keyboard.dismiss(); - - if (!item) { - // eslint-disable-next-line react/no-unused-state - this.setState({activeSource: null}); - return; - } - - const page = index; - this.props.onNavigate(item); - // eslint-disable-next-line react/no-unused-state - this.setState({page, activeSource: item.source}); - } - render() { return ( From ef0952d7740b8e49608eb17c5995625d0c19dce8 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 18 Jul 2023 21:57:08 +0200 Subject: [PATCH 0261/1075] fix: state --- .../AttachmentCarousel/AttachmentCarouselView/index.js | 5 +++-- .../AttachmentCarouselView/index.native.js | 9 +++++---- .../AttachmentCarouselView/propTypes.js | 2 +- src/components/AttachmentCarousel/CarouselButtons.js | 7 +++++-- src/components/AttachmentCarousel/index.js | 1 + 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.js index 74e229db20b8..d09d831e143d 100644 --- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.js +++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.js @@ -42,10 +42,11 @@ function AttachmentCarouselView({containerDimensions, attachments, initialPage, } const pageIndex = index; - onNavigate(item); - // eslint-disable-next-line react/no-unused-state + setPage(pageIndex); setActiveSource(item.source); + + onNavigate(item); }, [onNavigate], ); diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js index 3610f477ce44..50326d46bc6d 100644 --- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js +++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js @@ -19,8 +19,8 @@ function AttachmentCarouselView({attachments, initialPage, containerDimensions, const reversePage = useCallback((page) => Math.max(0, Math.min(attachments.length - page - 1, attachments.length)), [attachments.length]); - const [shouldShowArrows, setShouldShowArrows] = useState(true); const [page, setPage] = useState(reversePage(initialPage)); + const [shouldShowArrows, setShouldShowArrows] = useState(true); /** * Updates the page state when the user navigates between attachments @@ -29,11 +29,11 @@ function AttachmentCarouselView({attachments, initialPage, containerDimensions, */ const updatePage = useCallback( (newPageIndex) => { - setPage(newPageIndex); Keyboard.dismiss(); - const item = reversedAttachments[newPageIndex]; + setPage(newPageIndex); + const item = reversedAttachments[newPageIndex]; onNavigate(item); }, [onNavigate, reversedAttachments], @@ -73,7 +73,8 @@ function AttachmentCarouselView({attachments, initialPage, containerDimensions, return ( { cycleThroughAttachments(-1); diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/propTypes.js b/src/components/AttachmentCarousel/AttachmentCarouselView/propTypes.js index bac717cd0442..19874ada5e21 100644 --- a/src/components/AttachmentCarousel/AttachmentCarouselView/propTypes.js +++ b/src/components/AttachmentCarousel/AttachmentCarouselView/propTypes.js @@ -23,7 +23,7 @@ const propTypes = { /** * The initial active ource of the carousel */ - initialActiveSource: PropTypes.string.isRequired, + initialActiveSource: PropTypes.string, /** * The container dimensions diff --git a/src/components/AttachmentCarousel/CarouselButtons.js b/src/components/AttachmentCarousel/CarouselButtons.js index d94a4114b677..59b4037f41c5 100644 --- a/src/components/AttachmentCarousel/CarouselButtons.js +++ b/src/components/AttachmentCarousel/CarouselButtons.js @@ -11,6 +11,9 @@ import themeColors from '../../styles/themes/default'; import * as AttachmentCarouselViewPropTypes from './AttachmentCarouselView/propTypes'; const propTypes = { + /** Where the arrows should be visible */ + shouldShowArrows: PropTypes.bool.isRequired, + /** The current page index */ page: PropTypes.number.isRequired, @@ -33,11 +36,11 @@ const defaultProps = { cancelAutoHideArrow: () => {}, }; -function CarouselButtons({page, attachments, shouldShowArrow, onBack, onForward, cancelAutoHideArrow, autoHideArrow, ...props}) { +function CarouselButtons({page, attachments, shouldShowArrows, onBack, onForward, cancelAutoHideArrow, autoHideArrow, ...props}) { const isForwardDisabled = page === 0; const isBackDisabled = page === _.size(attachments) - 1; - return shouldShowArrow ? ( + return shouldShowArrows ? ( <> {!isBackDisabled && ( diff --git a/src/components/AttachmentCarousel/index.js b/src/components/AttachmentCarousel/index.js index 99fc9ebe2c54..08f3fd12ea9c 100644 --- a/src/components/AttachmentCarousel/index.js +++ b/src/components/AttachmentCarousel/index.js @@ -117,6 +117,7 @@ class AttachmentCarousel extends React.Component { initialActiveSource={this.state.initialActiveSource} containerDimensions={{width: this.state.containerWidth, height: this.state.containerHeight}} onClose={this.props.onClose} + onNavigate={this.props.onNavigate} /> ); From b6a87218f89d7b364d7297f8547ca703fc0adb1c Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 18 Jul 2023 21:58:46 +0200 Subject: [PATCH 0262/1075] fix: image should not be "flippable" --- src/components/AttachmentCarousel/Lightbox.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AttachmentCarousel/Lightbox.js b/src/components/AttachmentCarousel/Lightbox.js index 965cf8b55f7f..41d0de8c6f8f 100644 --- a/src/components/AttachmentCarousel/Lightbox.js +++ b/src/components/AttachmentCarousel/Lightbox.js @@ -81,7 +81,7 @@ function ImageTransformer({imageWidth, imageHeight, imageScale, scaledImageWidth const zoomScale = useSharedValue(1); // Adding together the pinch zoom scale and the initial scale to fit the image into the canvas // Substracting 1, because both scales have the initial image as the base reference - const totalScale = useDerivedValue(() => zoomScale.value + imageScale - 1, [imageScale]); + const totalScale = useDerivedValue(() => Math.max(zoomScale.value + imageScale - 1, 0), [imageScale]); const zoomScaledImageWidth = useDerivedValue(() => imageWidth * totalScale.value, [imageWidth]); const zoomScaledImageHeight = useDerivedValue(() => imageHeight * totalScale.value, [imageHeight]); From 975df225577cb661692507bde95e9c828b5bbc35 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 18 Jul 2023 21:58:58 +0200 Subject: [PATCH 0263/1075] remove log --- .../AttachmentCarouselView/index.native.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js index 50326d46bc6d..5f5bd81b7cab 100644 --- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js +++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js @@ -92,10 +92,7 @@ function AttachmentCarouselView({attachments, initialPage, containerDimensions, { - console.log('page updated'); - updatePage(newPage); - }} + onPageSelected={({nativeEvent: {position: newPage}}) => updatePage(newPage)} onTap={() => setShouldShowArrows(!shouldShowArrows)} onPinchGestureChange={(isPinchGestureRunning) => setShouldShowArrows(!isPinchGestureRunning)} onSwipeDown={onClose} From 77b4ea45df9a2876c3ec419e219c8a92a44876ec Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 18 Jul 2023 22:03:35 +0200 Subject: [PATCH 0264/1075] fix: on tap not working --- .../AttachmentCarouselView/index.native.js | 43 +++++++++---------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js index 5f5bd81b7cab..86b95b3a98b8 100644 --- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js +++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js @@ -20,6 +20,7 @@ function AttachmentCarouselView({attachments, initialPage, containerDimensions, const reversePage = useCallback((page) => Math.max(0, Math.min(attachments.length - page - 1, attachments.length)), [attachments.length]); const [page, setPage] = useState(reversePage(initialPage)); + const [isPinchGestureRunning, setIsPinchGestureRunning] = useState(true); const [shouldShowArrows, setShouldShowArrows] = useState(true); /** @@ -39,19 +40,6 @@ function AttachmentCarouselView({attachments, initialPage, containerDimensions, [onNavigate, reversedAttachments], ); - /** - * Increments or decrements the index to get another selected item - * @param {Number} deltaSlide - */ - const cycleThroughAttachments = useCallback( - (deltaSlide) => { - const nextPageIndex = page + deltaSlide; - updatePage(nextPageIndex); - pagerRef.current.setPage(nextPageIndex); - }, - [page, updatePage], - ); - const autoHideArrowTimeout = useRef(null); /** @@ -70,20 +58,29 @@ function AttachmentCarouselView({attachments, initialPage, containerDimensions, }, CONST.ARROW_HIDE_DELAY); }, [cancelAutoHideArrow]); + /** + * Increments or decrements the index to get another selected item + * @param {Number} deltaSlide + */ + const cycleThroughAttachments = useCallback( + (deltaSlide) => { + const nextPageIndex = page + deltaSlide; + updatePage(nextPageIndex); + pagerRef.current.setPage(nextPageIndex); + + autoHideArrow(); + }, + [autoHideArrow, page, updatePage], + ); + return ( { - cycleThroughAttachments(-1); - autoHideArrow(); - }} - onForward={() => { - cycleThroughAttachments(1); - autoHideArrow(); - }} + onBack={() => cycleThroughAttachments(-1)} + onForward={() => cycleThroughAttachments(1)} autoHideArrow={autoHideArrow} cancelAutoHideArrow={cancelAutoHideArrow} /> @@ -94,7 +91,7 @@ function AttachmentCarouselView({attachments, initialPage, containerDimensions, initialIndex={page} onPageSelected={({nativeEvent: {position: newPage}}) => updatePage(newPage)} onTap={() => setShouldShowArrows(!shouldShowArrows)} - onPinchGestureChange={(isPinchGestureRunning) => setShouldShowArrows(!isPinchGestureRunning)} + onPinchGestureChange={(newIsPinchtGestureRunning) => setIsPinchGestureRunning(newIsPinchtGestureRunning)} onSwipeDown={onClose} containerWidth={containerDimensions.width} containerHeight={containerDimensions.height} From be8bac1570c5630cac54a8fedceddd8404ffac50 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 18 Jul 2023 22:15:57 +0200 Subject: [PATCH 0265/1075] fix: arrows --- .../AttachmentCarouselView/index.native.js | 58 ++++++++++++------- src/components/AttachmentCarousel/Lightbox.js | 31 +++------- 2 files changed, 44 insertions(+), 45 deletions(-) diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js index 86b95b3a98b8..bfc10feaea8e 100644 --- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js +++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js @@ -1,4 +1,4 @@ -import React, {useCallback, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View, Keyboard} from 'react-native'; import _ from 'underscore'; import addEncryptedAuthTokenToURL from '../../../libs/addEncryptedAuthTokenToURL'; @@ -23,23 +23,6 @@ function AttachmentCarouselView({attachments, initialPage, containerDimensions, const [isPinchGestureRunning, setIsPinchGestureRunning] = useState(true); const [shouldShowArrows, setShouldShowArrows] = useState(true); - /** - * Updates the page state when the user navigates between attachments - * @param {Object} item - * @param {number} index - */ - const updatePage = useCallback( - (newPageIndex) => { - Keyboard.dismiss(); - - setPage(newPageIndex); - - const item = reversedAttachments[newPageIndex]; - onNavigate(item); - }, - [onNavigate, reversedAttachments], - ); - const autoHideArrowTimeout = useRef(null); /** @@ -48,8 +31,7 @@ function AttachmentCarouselView({attachments, initialPage, containerDimensions, const cancelAutoHideArrow = useCallback(() => clearTimeout(autoHideArrowTimeout.current), []); /** - * On a touch screen device, automatically hide the arrows - * if there is no interaction for 3 seconds. + * Automatically hide the arrows if there is no interaction for 3 seconds. */ const autoHideArrow = useCallback(() => { cancelAutoHideArrow(); @@ -58,6 +40,30 @@ function AttachmentCarouselView({attachments, initialPage, containerDimensions, }, CONST.ARROW_HIDE_DELAY); }, [cancelAutoHideArrow]); + const showArrows = useCallback(() => { + setShouldShowArrows(true); + autoHideArrow(); + }, [autoHideArrow]); + + /** + * Updates the page state when the user navigates between attachments + * @param {Object} item + * @param {number} index + */ + const updatePage = useCallback( + (newPageIndex) => { + Keyboard.dismiss(); + + setPage(newPageIndex); + + showArrows(); + + const item = reversedAttachments[newPageIndex]; + onNavigate(item); + }, + [onNavigate, reversedAttachments, showArrows], + ); + /** * Increments or decrements the index to get another selected item * @param {Number} deltaSlide @@ -73,6 +79,11 @@ function AttachmentCarouselView({attachments, initialPage, containerDimensions, [autoHideArrow, page, updatePage], ); + useEffect(() => { + autoHideArrow(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return ( updatePage(newPage)} - onTap={() => setShouldShowArrows(!shouldShowArrows)} - onPinchGestureChange={(newIsPinchtGestureRunning) => setIsPinchGestureRunning(newIsPinchtGestureRunning)} + onTap={() => (shouldShowArrows ? setShouldShowArrows(false) : showArrows())} + onPinchGestureChange={(newIsPinchGestureRunning) => { + setIsPinchGestureRunning(newIsPinchGestureRunning); + if (!newIsPinchGestureRunning && !shouldShowArrows) showArrows(); + }} onSwipeDown={onClose} containerWidth={containerDimensions.width} containerHeight={containerDimensions.height} diff --git a/src/components/AttachmentCarousel/Lightbox.js b/src/components/AttachmentCarousel/Lightbox.js index 41d0de8c6f8f..d9c95a0681dc 100644 --- a/src/components/AttachmentCarousel/Lightbox.js +++ b/src/components/AttachmentCarousel/Lightbox.js @@ -491,21 +491,18 @@ function ImageTransformer({imageWidth, imageHeight, imageScale, scaledImageWidth pinchGestureRunning.value = false; }); - const isPinchGestureInUse = useSharedValue(false); + const [isPinchGestureInUse, setIsPinchGestureInUse] = useState(false); useAnimatedReaction( () => [zoomScale.value, pinchGestureRunning.value], - ([s, running]) => { - const newIsPinchGestureInUse = s !== 1 || running; - if (isPinchGestureInUse.value !== newIsPinchGestureInUse) { - isPinchGestureInUse.value = newIsPinchGestureInUse; + ([zoom, running]) => { + const newIsPinchGestureInUse = zoom !== 1 || running; + if (isPinchGestureInUse !== newIsPinchGestureInUse) { + runOnJS(setIsPinchGestureInUse)(newIsPinchGestureInUse); } }, ); - - useAnimatedReaction( - () => isPinchGestureInUse.value, - (zoomed) => runOnJS(onPinchGestureChange)(zoomed), - ); + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(() => onPinchGestureChange(isPinchGestureInUse), [isPinchGestureInUse]); const animatedStyles = useAnimatedStyle(() => { const x = scaleTranslateX.value + translateX.value + offsetX.value; @@ -753,19 +750,7 @@ const pagerDefaultProps = { forwardedRef: null, }; -function Pager({ - items, - initialIndex = 0, - onPageSelected, - onTap, - onSwipe = noopWorklet, - onSwipeSuccess = () => {}, - onSwipeDown = () => {}, - onPinchGestureChange = () => {}, - forwardedRef, - containerWidth, - containerHeight, -}) { +function Pager({items, initialIndex, onPageSelected, onTap, onSwipe = noopWorklet, onSwipeSuccess, onSwipeDown, onPinchGestureChange, forwardedRef, containerWidth, containerHeight}) { const shouldPagerScroll = useSharedValue(true); const pagerRef = useRef(null); From 56d1d50e45a0e8ba4f0489a8175de6c58eb9d000 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 18 Jul 2023 22:35:03 +0200 Subject: [PATCH 0266/1075] fix: flashing --- src/components/AttachmentCarousel/Lightbox.js | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/components/AttachmentCarousel/Lightbox.js b/src/components/AttachmentCarousel/Lightbox.js index d9c95a0681dc..5e24d4c54d5d 100644 --- a/src/components/AttachmentCarousel/Lightbox.js +++ b/src/components/AttachmentCarousel/Lightbox.js @@ -1,5 +1,5 @@ /* eslint-disable es/no-optional-chaining */ -import React, {createContext, useContext, useEffect, useRef, useState, useImperativeHandle} from 'react'; +import React, {createContext, useContext, useEffect, useRef, useState, useImperativeHandle, useMemo} from 'react'; import {ActivityIndicator, PixelRatio, StyleSheet, View} from 'react-native'; import PropTypes from 'prop-types'; import {Gesture, GestureDetector, GestureHandlerRootView, createNativeWrapper} from 'react-native-gesture-handler'; @@ -601,7 +601,7 @@ const pagePropTypes = { }).isRequired, }; -function Page({isActive, item}) { +function Page({isActive: isActiveProp, item}) { const {canvasWidth, canvasHeight} = useContext(Context); const dimensions = cachedDimensions.get(item.url); @@ -610,15 +610,31 @@ function Page({isActive, item}) { const [isImageLoading, setIsImageLoading] = useState(!areImageDimensionsSet); + const [isActive, setIsActive] = useState(isActiveProp); + // We delay setting a page to active state by a (few) millisecond(s), + // to prevent the image transformer from flashing while still rendering + // Instead, we show the fallback image while the image transformer is loading the image useEffect(() => { - if (!isActive) return; + if (isActiveProp) setTimeout(() => setIsActive(true), 1); + else setIsActive(false); + }, [isActiveProp]); + + useMemo(() => { + if (!isActiveProp) return; setIsImageLoading(true); - }, [isActive]); + }, [isActiveProp]); + + const [showFallback, setShowFallback] = useState(isImageLoading); + // We delay hiding the fallback image while image transformer is still rendering + useEffect(() => { + if (isImageLoading) setShowFallback(true); + else setTimeout(() => setShowFallback(false), 100); + }, [isImageLoading]); return ( <> {isActive && ( - + Date: Tue, 18 Jul 2023 16:46:03 -0400 Subject: [PATCH 0267/1075] move notification cache hashmap over --- .../CustomNotificationProvider.java | 24 ++---------------- .../NotificationCache.java | 25 +++++++++++++++++++ 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java index 76cc1e32b519..a1080c507823 100644 --- a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java +++ b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java @@ -73,7 +73,6 @@ public class CustomNotificationProvider extends ReactNotificationProvider { private static final String ONYX_DATA_KEY = "onyxData"; private final ExecutorService executorService = Executors.newCachedThreadPool(); - public final HashMap cache = new HashMap<>(); public CustomNotificationProvider(@NonNull Context context, @NonNull AirshipConfigOptions configOptions) { super(context, configOptions); @@ -170,7 +169,7 @@ private void applyMessageStyle(@NonNull Context context, NotificationCompat.Buil } // Retrieve and check for cached notifications - NotificationData notificationData = findOrCreateNotificationData(reportID); + NotificationData notificationData = NotificationCache.getNotificationData(reportID); boolean hasExistingNotification = notificationData.messages.size() >= 1; try { @@ -256,25 +255,6 @@ private long getMessageTimeInMillis(String createdTime) { return Calendar.getInstance().getTimeInMillis(); } - /** - * Check if we are showing a notification related to a reportID. - * If not, create a new NotificationCache so we can build a conversation notification - * as the messages come. - * - * @param reportID Report ID. - * @return Notification Cache. - */ - private NotificationData findOrCreateNotificationData(long reportID) { - NotificationData notificationData = cache.get(reportID); - - if (notificationData == null) { - notificationData = new NotificationData(); - cache.put(reportID, notificationData); - } - - return notificationData; - } - /** * Remove the notification data from the cache when the user dismisses the notification. * @@ -289,7 +269,7 @@ public void onDismissNotification(PushMessage message) { return; } - cache.remove(reportID); + NotificationCache.setNotificationData(reportID, null); } catch (Exception e) { Log.e(TAG, "Failed to delete conversation cache. SendID=" + message.getSendId(), e); } diff --git a/android/app/src/main/java/com/expensify/chat/customairshipextender/NotificationCache.java b/android/app/src/main/java/com/expensify/chat/customairshipextender/NotificationCache.java index fbe6e802722d..98edaa4d3a92 100644 --- a/android/app/src/main/java/com/expensify/chat/customairshipextender/NotificationCache.java +++ b/android/app/src/main/java/com/expensify/chat/customairshipextender/NotificationCache.java @@ -8,6 +8,31 @@ import java.util.Map; public class NotificationCache { + + public static final HashMap cache = new HashMap<>(); + + /* + * Get NotificationData for an existing notification or create a new instance + * if it doesn't exist + */ + public static NotificationData getNotificationData(long reportID) { + NotificationData notificationData = cache.get(reportID); + + if (notificationData == null) { + notificationData = new NotificationData(); + setNotificationData(reportID, notificationData); + } + + return notificationData; + } + + /* + * Set and persist NotificationData in the cache + */ + public static void setNotificationData(long reportID, NotificationData data) { + cache.put(reportID, data); + } + public static class NotificationData { public Map people = new HashMap<>(); public ArrayList messages = new ArrayList<>(); From 3e1ca2cbbbeb629ca8645f9cee7b34097c176ca3 Mon Sep 17 00:00:00 2001 From: Andrew Gable Date: Tue, 18 Jul 2023 18:11:18 -0400 Subject: [PATCH 0268/1075] Add permission page for native platforms --- assets/images/hand.svg | 110 ++++++++++++++++++ src/pages/iou/ReceiptSelector/index.native.js | 100 +++++++++++++++- 2 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 assets/images/hand.svg diff --git a/assets/images/hand.svg b/assets/images/hand.svg new file mode 100644 index 000000000000..0d20ca8360df --- /dev/null +++ b/assets/images/hand.svg @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pages/iou/ReceiptSelector/index.native.js b/src/pages/iou/ReceiptSelector/index.native.js index f1bfe4619207..09c89517d37a 100644 --- a/src/pages/iou/ReceiptSelector/index.native.js +++ b/src/pages/iou/ReceiptSelector/index.native.js @@ -1,4 +1,4 @@ -import {ActivityIndicator, Alert, Linking, View} from 'react-native'; +import {ActivityIndicator, Alert, Linking, View, Text} from 'react-native'; import React, {useRef, useState} from 'react'; import {Camera, useCameraDevices} from 'react-native-vision-camera'; import _ from 'underscore'; @@ -11,6 +11,7 @@ import Icon from '../../../components/Icon'; import * as Expensicons from '../../../components/Icon/Expensicons'; import styles from '../../../styles/styles'; import Shutter from '../../../../assets/images/shutter.svg'; +import Hand from '../../../../assets/images/hand.svg'; import * as IOU from '../../../libs/actions/IOU'; import Navigation from '../../../libs/Navigation/Navigation'; import ROUTES from '../../../ROUTES'; @@ -21,6 +22,8 @@ import personalDetailsPropType from '../../personalDetailsPropType'; import CONST from '../../../CONST'; import {withCurrentUserPersonalDetailsDefaultProps} from '../../../components/withCurrentUserPersonalDetails'; import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; +import CopyTextToClipboard from '../../../components/CopyTextToClipboard'; +import Button from '../../../components/Button'; const propTypes = { /** Route params */ @@ -106,6 +109,8 @@ function ReceiptSelector(props) { const camera = useRef(null); const [flash, setFlash] = useState(false); + const [permissions, setPermissions] = useState('authorized'); + const iouType = useRef(lodashGet(props.route, 'params.iouType', '')); const reportID = useRef(lodashGet(props.route, 'params.reportID', '')); @@ -215,6 +220,99 @@ function ReceiptSelector(props) { }); }; + Camera.getCameraPermissionStatus().then((permissions) => { + console.log(`Permissions: ${JSON.stringify(permissions)}`); + setPermissions(permissions); + }); + + if (permissions !== 'authorized') { + return ( + + + + Take a photo + Camera access is required to take pictures of receipts. + +