Skip to content

Commit

Permalink
Merge pull request #2221 from kidroca/kidroca/report-view-loading-state
Browse files Browse the repository at this point in the history
Added transition handling between the side drawer and chats
  • Loading branch information
marcaaron authored Apr 8, 2021
2 parents 09e4a86 + db7b937 commit e0a559c
Show file tree
Hide file tree
Showing 9 changed files with 186 additions and 113 deletions.
25 changes: 25 additions & 0 deletions src/components/FullscreenLoadingIndicator.js
Original file line number Diff line number Diff line change
@@ -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 && (
<View style={[StyleSheet.absoluteFillObject, styles.fullScreenLoading]}>
<ActivityIndicator color={themeColors.spinner} size="large" />
</View>
);

FullScreenLoadingIndicator.propTypes = propTypes;

export default FullScreenLoadingIndicator;
5 changes: 5 additions & 0 deletions src/components/TextInputFocusable/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -49,6 +53,7 @@ const defaultProps = {
onDragLeave: () => {},
onDrop: () => {},
isDisabled: false,
autoFocus: false,
};

const IMAGE_EXTENSIONS = {
Expand Down
10 changes: 10 additions & 0 deletions src/components/TextInputFocusable/index.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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}
/>
Expand Down
25 changes: 25 additions & 0 deletions src/components/withDrawerState.js
Original file line number Diff line number Diff line change
@@ -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 (
<WrappedComponent
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
isDrawerOpen={isDrawerOpen}
/>
);
};

HOC_Wrapper.displayName = `withDrawerState(${getComponentDisplayName(WrappedComponent)})`;
return HOC_Wrapper;
}
117 changes: 78 additions & 39 deletions src/pages/home/ReportScreen.js
Original file line number Diff line number Diff line change
@@ -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 (
<ScreenWrapper
style={[
styles.appContent,
styles.flex1,
styles.flexColumn,
]}
>
<HeaderView
reportID={activeReportID}
onNavigationMenuButtonClicked={() => Navigation.navigate(ROUTES.HOME)}
/>
<View style={[styles.dFlex, styles.flex1]}>
<ReportView reportID={activeReportID} />
</View>
</ScreenWrapper>
);
};
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 (
<ScreenWrapper style={[styles.appContent, styles.flex1]}>
<HeaderView
reportID={this.getReportID()}
onNavigationMenuButtonClicked={() => Navigation.navigate(ROUTES.HOME)}
/>

<FullScreenLoadingIndicator visible={this.shouldShowLoader()} />

<ReportView reportID={this.getReportID()} />
</ScreenWrapper>
);
}
}

ReportScreen.displayName = 'ReportScreen';
ReportScreen.propTypes = propTypes;
ReportScreen.defaultProps = defaultProps;
export default withOnyx({
currentlyViewedReportID: {
key: ONYXKEYS.CURRENTLY_VIEWED_REPORTID,
},
})(ReportScreen);
export default ReportScreen;
22 changes: 13 additions & 9 deletions src/pages/home/report/ReportActionCompose.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 = {
Expand Down Expand Up @@ -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 (
<View style={[styles.chatItemCompose]}>
<View style={[
Expand Down Expand Up @@ -288,7 +291,7 @@ class ReportActionCompose extends React.Component {
onPasteFile={file => displayFileInModal({file})}
shouldClear={this.state.textInputShouldClear}
onClear={() => this.setTextInputShouldClear(false)}
isDisabled={inputDisable}
isDisabled={isComposeDisabled}
/>

</>
Expand All @@ -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}`,
Expand All @@ -326,5 +331,4 @@ export default compose(
key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
},
}),
withWindowDimensions,
)(ReportActionCompose);
Loading

0 comments on commit e0a559c

Please sign in to comment.