diff --git a/src/components/FullscreenLoadingIndicator.js b/src/components/FullscreenLoadingIndicator.js new file mode 100644 index 0000000000000..56bea1f0acefb --- /dev/null +++ b/src/components/FullscreenLoadingIndicator.js @@ -0,0 +1,25 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import {ActivityIndicator, StyleSheet, View} from 'react-native'; +import styles from '../styles/styles'; +import themeColors from '../styles/themes/default'; + +const propTypes = { + /* Controls whether the loader is mounted and displayed */ + visible: PropTypes.bool.isRequired, +}; + +/** + * Loading indication component intended to cover the whole page, while the page prepares for initial render + * + * @returns {JSX.Element} + */ +const FullScreenLoadingIndicator = ({visible}) => visible && ( + + + +); + +FullScreenLoadingIndicator.propTypes = propTypes; + +export default FullScreenLoadingIndicator; diff --git a/src/components/TextInputFocusable/index.js b/src/components/TextInputFocusable/index.js index bef213b31d27e..764ca1984ba19 100644 --- a/src/components/TextInputFocusable/index.js +++ b/src/components/TextInputFocusable/index.js @@ -37,6 +37,10 @@ const propTypes = { // Whether or not this TextInput is disabled. isDisabled: PropTypes.bool, + + /* Set focus to this component the first time it renders. Override this in case you need to set focus on one + * field out of many, or when you want to disable autoFocus */ + autoFocus: PropTypes.bool, }; const defaultProps = { @@ -49,6 +53,7 @@ const defaultProps = { onDragLeave: () => {}, onDrop: () => {}, isDisabled: false, + autoFocus: false, }; const IMAGE_EXTENSIONS = { diff --git a/src/components/TextInputFocusable/index.native.js b/src/components/TextInputFocusable/index.native.js index a7d571949d029..f54704f2669b8 100644 --- a/src/components/TextInputFocusable/index.native.js +++ b/src/components/TextInputFocusable/index.native.js @@ -17,11 +17,20 @@ const propTypes = { // When the input has cleared whoever owns this input should know about it onClear: PropTypes.func, + + /* Set focus to this component the first time it renders. Override this in case you need to set focus on one + * field out of many, or when you want to disable autoFocus */ + autoFocus: PropTypes.bool, + + /* Prevent edits and interactions like focus for this input. */ + isDisabled: PropTypes.bool, }; const defaultProps = { shouldClear: false, onClear: () => {}, + autoFocus: false, + isDisabled: false, }; class TextInputFocusable extends React.Component { @@ -48,6 +57,7 @@ class TextInputFocusable extends React.Component { ref={el => this.textInput = el} maxHeight={116} rejectResponderTermination={false} + editable={!this.props.isDisabled} /* eslint-disable-next-line react/jsx-props-no-spreading */ {...this.props} /> diff --git a/src/components/withDrawerState.js b/src/components/withDrawerState.js new file mode 100644 index 0000000000000..3bcc81756e621 --- /dev/null +++ b/src/components/withDrawerState.js @@ -0,0 +1,25 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import {useIsDrawerOpen} from '@react-navigation/drawer'; +import getComponentDisplayName from '../libs/getComponentDisplayName'; + +export const withDrawerPropTypes = { + isDrawerOpen: PropTypes.bool.isRequired, +}; + +export default function withDrawerState(WrappedComponent) { + const HOC_Wrapper = (props) => { + const isDrawerOpen = useIsDrawerOpen(); + + return ( + + ); + }; + + HOC_Wrapper.displayName = `withDrawerState(${getComponentDisplayName(WrappedComponent)})`; + return HOC_Wrapper; +} diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 4689b41c95e86..0438c2f6b9101 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -1,54 +1,93 @@ import React from 'react'; import PropTypes from 'prop-types'; -import {withOnyx} from 'react-native-onyx'; -import {View} from 'react-native'; import styles from '../../styles/styles'; import ReportView from './report/ReportView'; import ScreenWrapper from '../../components/ScreenWrapper'; import HeaderView from './HeaderView'; import Navigation from '../../libs/Navigation/Navigation'; import ROUTES from '../../ROUTES'; -import ONYXKEYS from '../../ONYXKEYS'; +import FullScreenLoadingIndicator from '../../components/FullscreenLoadingIndicator'; const propTypes = { - // id of the most recently viewed report - currentlyViewedReportID: PropTypes.string, + /* Navigation route context info provided by react navigation */ + route: PropTypes.shape({ + /* Route specific parameters used on this screen */ + params: PropTypes.shape({ + /* The ID of the report this screen should display */ + reportID: PropTypes.string, + }).isRequired, + }).isRequired, }; -const defaultProps = { - currentlyViewedReportID: '0', -}; +class ReportScreen extends React.Component { + constructor(props) { + super(props); -const ReportScreen = (props) => { - const activeReportID = parseInt(props.currentlyViewedReportID, 10); - if (!activeReportID) { - return null; - } - - return ( - - Navigation.navigate(ROUTES.HOME)} - /> - - - - - ); -}; + this.state = { + isLoading: true, + }; + } + + componentDidMount() { + this.prepareTransition(); + } + + componentDidUpdate(prevProps) { + const reportChanged = this.props.route.params.reportID !== prevProps.route.params.reportID; + + if (reportChanged) { + this.prepareTransition(); + } + } + + componentWillUnmount() { + clearTimeout(this.loadingTimerId); + } + + /** + * Get the currently viewed report ID as number + * + * @returns {Number} + */ + getReportID() { + const params = this.props.route.params; + return Number.parseInt(params.reportID, 10); + } + + /** + * When reports change there's a brief time content is not ready to be displayed + * + * @returns {Boolean} + */ + shouldShowLoader() { + return this.state.isLoading || !this.getReportID(); + } + + /** + * Configures a small loading transition of fixed time and proceeds with rendering available data + */ + prepareTransition() { + this.setState({isLoading: true}); + + clearTimeout(this.loadingTimerId); + this.loadingTimerId = setTimeout(() => this.setState({isLoading: false}), 300); + } + + render() { + return ( + + Navigation.navigate(ROUTES.HOME)} + /> + + + + + + ); + } +} -ReportScreen.displayName = 'ReportScreen'; ReportScreen.propTypes = propTypes; -ReportScreen.defaultProps = defaultProps; -export default withOnyx({ - currentlyViewedReportID: { - key: ONYXKEYS.CURRENTLY_VIEWED_REPORTID, - }, -})(ReportScreen); +export default ReportScreen; diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index 42879452601ef..e2ba8f13734db 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -14,10 +14,10 @@ import AttachmentPicker from '../../../components/AttachmentPicker'; import {addAction, saveReportComment, broadcastUserIsTyping} from '../../../libs/actions/Report'; import ReportTypingIndicator from './ReportTypingIndicator'; import AttachmentModal from '../../../components/AttachmentModal'; -import withWindowDimensions, {windowDimensionsPropTypes} from '../../../components/withWindowDimensions'; import compose from '../../../libs/compose'; import CreateMenu from '../../../components/CreateMenu'; -import Navigation from '../../../libs/Navigation/Navigation'; +import withWindowDimensions from '../../../components/withWindowDimensions'; +import withDrawerState from '../../../components/withDrawerState'; const propTypes = { // A method to call when the form is submitted @@ -42,7 +42,11 @@ const propTypes = { participants: PropTypes.arrayOf(PropTypes.string), }).isRequired, - ...windowDimensionsPropTypes, + /* Is the report view covered by the drawer */ + isDrawerOpen: PropTypes.bool.isRequired, + + /* Is the window width narrow, like on a mobile device */ + isSmallScreenWidth: PropTypes.bool.isRequired, }; const defaultProps = { @@ -182,13 +186,12 @@ class ReportActionCompose extends React.Component { } render() { - // We want to make sure to disable on small screens because in iOS safari the keyboard up/down buttons will - // focus this from the chat switcher. - // https://github.com/Expensify/Expensify.cash/issues/1228 - const inputDisable = this.props.isSmallScreenWidth && Navigation.isDrawerOpen(); // eslint-disable-next-line no-unused-vars const hasMultipleParticipants = lodashGet(this.props.report, 'participants.length') > 1; + // Prevents focusing and showing the keyboard while the drawer is covering the chat. + const isComposeDisabled = this.props.isDrawerOpen && this.props.isSmallScreenWidth; + return ( displayFileInModal({file})} shouldClear={this.state.textInputShouldClear} onClear={() => this.setTextInputShouldClear(false)} - isDisabled={inputDisable} + isDisabled={isComposeDisabled} /> @@ -315,6 +318,8 @@ ReportActionCompose.propTypes = propTypes; ReportActionCompose.defaultProps = defaultProps; export default compose( + withWindowDimensions, + withDrawerState, withOnyx({ comment: { key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, @@ -326,5 +331,4 @@ export default compose( key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, }, }), - withWindowDimensions, )(ReportActionCompose); diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 708130ac689fc..d41e7a3c0d867 100644 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -76,13 +76,11 @@ class ReportActionsView extends React.Component { // Helper variable that keeps track of the unread action count before it updates to zero this.unreadActionCount = 0; - // Helper variable that prevents the unread indicator to show up for new messages - // received while the report is still active - this.shouldShowUnreadActionIndicator = true; - this.state = { isLoadingMoreChats: false, }; + + this.updateSortedReportActions(props.reportActions); } componentDidMount() { @@ -91,14 +89,13 @@ class ReportActionsView extends React.Component { this.keyboardEvent = Keyboard.addListener('keyboardDidShow', this.scrollToListBottom); this.recordMaxAction(); fetchActions(this.props.reportID); + this.setUpUnreadActionIndicator(); + Timing.end(CONST.TIMING.SWITCH_REPORT, CONST.TIMING.COLD); } shouldComponentUpdate(nextProps, nextState) { - if (nextProps.reportID !== this.props.reportID) { - return true; - } - if (!_.isEqual(nextProps.reportActions, this.props.reportActions)) { + this.updateSortedReportActions(nextProps.reportActions); return true; } @@ -110,12 +107,6 @@ class ReportActionsView extends React.Component { } componentDidUpdate(prevProps) { - // We have switched to a new report - if (prevProps.reportID !== this.props.reportID) { - this.reset(prevProps.reportID); - return; - } - // The last sequenceNumber of the same report has changed. const previousLastSequenceNumber = lodashGet(lastItem(prevProps.reportActions), 'sequenceNumber'); const currentLastSequenceNumber = lodashGet(lastItem(this.props.reportActions), 'sequenceNumber'); @@ -161,10 +152,6 @@ class ReportActionsView extends React.Component { * a flag to not show it again if the report is still open */ setUpUnreadActionIndicator() { - if (!this.shouldShowUnreadActionIndicator) { - return; - } - this.unreadActionCount = this.props.report.unreadActionCount; if (this.unreadActionCount > 0) { @@ -176,22 +163,6 @@ class ReportActionsView extends React.Component { }).start(); }, 3000)); } - - this.shouldShowUnreadActionIndicator = false; - } - - /** - * Actions to run when the report has been updated - * @param {Number} oldReportID - */ - reset(oldReportID) { - // Unsubscribe from previous report and resubscribe - unsubscribeFromReportChannel(oldReportID); - subscribeToReportTypingEvents(this.props.reportID); - Timing.end(CONST.TIMING.SWITCH_REPORT, CONST.TIMING.COLD); - - // Fetch the new set of actions - fetchActions(this.props.reportID); } /** @@ -219,9 +190,11 @@ class ReportActionsView extends React.Component { /** * Updates and sorts the report actions by sequence number + * + * @param {Array<{sequenceNumber, actionName}>} reportActions */ - updateSortedReportActions() { - this.sortedReportActions = _.chain(this.props.reportActions) + updateSortedReportActions(reportActions) { + this.sortedReportActions = _.chain(reportActions) .sortBy('sequenceNumber') .filter(action => action.actionName === 'ADDCOMMENT' || action.actionName === 'IOU') .map((item, index) => ({action: item, index})) @@ -319,7 +292,6 @@ class ReportActionsView extends React.Component { * @param {Object} args.item * @param {Number} args.index * @param {Function} args.onLayout - * @param {Boolean} args.needsLayoutCalculation * * @returns {React.Component} */ @@ -327,7 +299,6 @@ class ReportActionsView extends React.Component { item, index, onLayout, - needsLayoutCalculation, }) { return ( @@ -343,7 +314,6 @@ class ReportActionsView extends React.Component { action={item.action} displayAsGroup={this.isConsecutiveActionMadeByPreviousActor(index)} onLayout={onLayout} - needsLayoutCalculation={needsLayoutCalculation} /> ); @@ -365,7 +335,6 @@ class ReportActionsView extends React.Component { } this.setUpUnreadActionIndicator(); - this.updateSortedReportActions(); return ( this.actionListElement = el} diff --git a/src/pages/home/report/ReportView.js b/src/pages/home/report/ReportView.js index f9125a472c1a1..b00a8fb50628e 100644 --- a/src/pages/home/report/ReportView.js +++ b/src/pages/home/report/ReportView.js @@ -9,35 +9,23 @@ import styles from '../../../styles/styles'; import SwipeableView from '../../../components/SwipeableView'; const propTypes = { - // The ID of the report actions will be created for + /* The ID of the report the selected report */ reportID: PropTypes.number.isRequired, }; -// This is a PureComponent so that it only re-renders when the reportID changes or when the report changes from -// active to inactive (or vice versa). This should greatly reduce how often comments are re-rendered. -class ReportView extends React.Component { - shouldComponentUpdate(prevProps) { - return this.props.reportID !== prevProps.reportID; - } +const ReportView = ({reportID}) => ( + + - render() { - return ( - - - Keyboard.dismiss()}> - addAction(this.props.reportID, text)} - reportID={this.props.reportID} - key={this.props.reportID} - /> - - - - ); - } -} + Keyboard.dismiss()}> + addAction(reportID, text)} + reportID={reportID} + /> + + + +); ReportView.propTypes = propTypes; export default ReportView; diff --git a/src/styles/styles.js b/src/styles/styles.js index 6e7fd6395c03c..fae27fd6d72c6 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -1258,6 +1258,14 @@ const styles = { noScrollbars: { scrollbarWidth: 'none', }, + + fullScreenLoading: { + backgroundColor: themeColors.modalBackdrop, + opacity: 0.8, + justifyContent: 'center', + alignItems: 'center', + zIndex: 10, + }, }; const baseCodeTagStyles = {