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 = {