Skip to content

Commit

Permalink
Merge branch 'master' into tgolen-html-comments
Browse files Browse the repository at this point in the history
  • Loading branch information
tgolen committed Aug 11, 2020
2 parents 3f1ecea + a127443 commit d8fc479
Show file tree
Hide file tree
Showing 20 changed files with 281 additions and 261 deletions.
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"html-entities": "^1.3.1",
"jquery": "^3.5.1",
"lodash.get": "^4.4.2",
"lodash.has": "^4.5.2",
"moment": "^2.27.0",
"moment-timezone": "^0.5.31",
"prop-types": "^15.7.2",
Expand All @@ -26,6 +27,7 @@
"react-beforeunload": "^2.2.2",
"react-dom": "^16.13.1",
"react-native": "0.63.2",
"react-native-keyboard-spacer": "^0.4.1",
"react-native-web": "^0.13.5",
"react-router-dom": "^5.2.0",
"react-router-native": "^5.2.0",
Expand Down
8 changes: 8 additions & 0 deletions src/components/KeyboardSpacer/index.ios.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import ReactNativeKeyboardSpacer from 'react-native-keyboard-spacer';
import React from 'react';

const KeyboardSpacer = () => (
<ReactNativeKeyboardSpacer topSpacing={-36} />
);

export default KeyboardSpacer;
3 changes: 3 additions & 0 deletions src/components/KeyboardSpacer/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const KeyboardSpacer = () => null;

export default KeyboardSpacer;
111 changes: 76 additions & 35 deletions src/components/WithStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,75 +5,118 @@
*/
import React from 'react';
import _ from 'underscore';
import get from 'lodash.get';
import has from 'lodash.has';
import * as Store from '../store/Store';

export default function (mapStoreToStates) {
return WrappedComponent => class WithStore extends React.Component {
constructor(props) {
super(props);

this.subscriptionIDs = [];
this.bind = this.bind.bind(this);
this.unbind = this.unbind.bind(this);
this.subscriptionIDs = {};
this.subscriptionIDsWithPropsData = {};

// Initialize the state with each of our property names
// Initialize the state with each of the property names from the mapping
this.state = _.reduce(_.keys(mapStoreToStates), (finalResult, propertyName) => ({
...finalResult,
[propertyName]: null,
}), {});
}

componentDidMount() {
this.bindStoreToStates(mapStoreToStates, this.wrappedComponent);
// Subscribe each of the state properties to the proper store key
_.each(mapStoreToStates, (mapping, propertyName) => {
this.bindSingleMappingToStore(mapping, propertyName, this.wrappedComponent);
});
}

componentDidUpdate(prevProps) {
// If any of the mappings use data from the props, then when the props change, all the
// subscriptions need to be rebound with the new props
_.each(mapStoreToStates, (mapping, propertyName) => {
if (has(mapping, 'pathForProps')) {
const prevPropsData = get(prevProps, mapping.pathForProps);
const currentPropsData = get(this.props, mapping.pathForProps);
if (prevPropsData !== currentPropsData) {
Store.unbind(this.subscriptionIDsWithPropsData[mapping.pathForProps]);
this.bindSingleMappingToStore(mapping, propertyName, this.wrappedComponent);
}
}
});
}

componentWillUnmount() {
this.unbind();
}

/**
* A method that is convenient to bind the state to the store. Typically used when you can't pass
* mapStoreToStates to this HOC. For example: if the key that you want to subscribe to has a piece of
* information that can only come from the component's props, then you want to use bind() directly from inside
* componentDidMount(). All subscriptions will automatically be unbound when unmounted.
*
* The options passed to bind are the exact same that you would pass to the HOC.
* Takes a single mapping and binds the state of the component to the store
*
* @param {object} mapping
* @param {string} propertyName
* @param {object} component
*/
bind(mapping, component) {
this.bindStoreToStates(mapping, component);
}
bindSingleMappingToStore(mapping, propertyName, component) {
const {
key,
path,
prefillWithKey,
loader,
loaderParams,
defaultValue,
pathForProps,
} = mapping;

bindStoreToStates(statesToStoreMap, component) {
// Subscribe each of the state properties to the proper store key
_.each(statesToStoreMap, (stateToStoreMap, propertyName) => {
const {
key,
path,
prefillWithKey,
loader,
loaderParams,
defaultValue,
} = stateToStoreMap;
// Bind to the store and keep track of the subscription ID
if (pathForProps) {
// If there is a path for props data, then the data needs to be pulled out of props and parsed
// into the key
const dataFromProps = get(this.props, pathForProps);
const keyWithPropsData = key.replace('%DATAFROMPROPS%', dataFromProps);
const subscriptionID = Store.bind(keyWithPropsData, path, defaultValue, propertyName, component);

this.subscriptionIDs.push(Store.bind(key, path, defaultValue, propertyName, component));
if (prefillWithKey) {
Store.get(prefillWithKey, path, defaultValue)
.then(data => component.setState({[propertyName]: data}));
}
if (loader) {
loader(...loaderParams || []);
// Store the subscription ID it with a key that is unique to the data coming from the props
this.subscriptionIDsWithPropsData[pathForProps] = subscriptionID;
} else {
const subscriptionID = Store.bind(key, path, defaultValue, propertyName, component);
this.subscriptionIDs[subscriptionID] = subscriptionID;
}

// Prefill the state with any data already in the store
if (prefillWithKey) {
let prefillKey = prefillWithKey;

// If there is a path for props data, then the data needs to be pulled out of props and parsed
// into the key
if (pathForProps) {
const dataFromProps = get(this.props, pathForProps);
prefillKey = prefillWithKey.replace('%DATAFROMPROPS%', dataFromProps);
}
});

Store.get(prefillKey, path, defaultValue)
.then(data => component.setState({[propertyName]: data}));
}

// Load the data from an API request if necessary
if (loader) {
const paramsForLoaderFunction = _.map(loaderParams, (loaderParam) => {
// Some params might com from the props data
if (loaderParam === '%DATAFROMPROPS%') {
return get(this.props, pathForProps);
}
return loaderParam;
});
loader(...paramsForLoaderFunction || []);
}
}

/**
* Unsubscribe from any subscriptions
*/
unbind() {
_.each(this.subscriptionIDs, Store.unbind);
_.each(this.subscriptionIDsWithPropsData, Store.unbind);
}

render() {
Expand All @@ -85,8 +128,6 @@ export default function (mapStoreToStates) {
// eslint-disable-next-line react/jsx-props-no-spreading
{...this.state}
ref={el => this.wrappedComponent = el}
bind={this.bind}
unbind={this.unbind}
/>
);
}
Expand Down
Binary file added src/images/expensify-logo_reversed.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 9 additions & 1 deletion src/lib/Network.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import _ from 'underscore';
import * as Store from '../store/Store';
import CONFIG from '../CONFIG';
import STOREKEYS from '../store/STOREKEYS';
import ROUTES from '../ROUTES';

let isAppOffline = false;

Expand All @@ -27,11 +28,18 @@ function request(command, data, type = 'post') {
}))
.then(response => response.json())
.then((responseData) => {
// Successful request
if (responseData.jsonCode === 200) {
return responseData;
}

// AuthToken expired, go to the sign in page
if (responseData.jsonCode === 407) {
return Store.set(STOREKEYS.APP_REDIRECT_TO, ROUTES.SIGNIN);
}

// eslint-disable-next-line no-console
console.info('[API] Error', responseData);
console.info('[API] UnhandledError', responseData);
})
// eslint-disable-next-line no-unused-vars
.catch(() => {
Expand Down
24 changes: 16 additions & 8 deletions src/page/HomePage/HeaderView.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@ import {fetch as getPersonalDetails} from '../../store/actions/PersonalDetailsAc
import styles from '../../style/StyleSheet';
import STOREKEYS from '../../store/STOREKEYS';
import WithStore from '../../components/WithStore';
import {withRouter} from '../../lib/Router';

class HeaderView extends React.Component {
render() {
return (
<View style={[styles.nav, styles.flexRow, styles.flexWrap]}>
<Text style={styles.brand}>Expensify Chat</Text>
{this.state && this.state.currentReportName && (
{this.state && this.state.reportName && (
<Text style={[styles.navText, styles.ml1]}>
{this.state.currentReportName}
{this.state.reportName}
</Text>
)}
<Text style={styles.flex1} />
Expand All @@ -28,18 +29,25 @@ class HeaderView extends React.Component {
}
}

export default WithStore({
// Map this.state.name to the personal details key in the store and bind it to the displayName property
export default withRouter(WithStore({
// Map this.state.userDisplayName to the personal details key in the store and bind it to the displayName property
// and load it with data from getPersonalDetails()
userDisplayName: {
key: STOREKEYS.MY_PERSONAL_DETAILS,
path: 'displayName',
loader: getPersonalDetails,
prefillWithKey: STOREKEYS.MY_PERSONAL_DETAILS,
},
currentReportName: {
key: STOREKEYS.CURRENT_REPORT,

// Map this.state.reportName to the data for a specific report in the store, and bind it to the reportName property
// It uses the data returned from the props path (ie. the reportID) to replace %DATAFROMPROPS% in the key it
// binds to
reportName: {
// Note the trailing $ so that this component only binds to the specific report and no other report keys
// like report_1234_history
key: `${STOREKEYS.REPORT}_%DATAFROMPROPS%$`,
path: 'reportName',
prefillWithKey: STOREKEYS.CURRENT_REPORT,
prefillWithKey: `${STOREKEYS.REPORT}_%DATAFROMPROPS%`,
pathForProps: 'match.params.reportID',
},
})(HeaderView);
})(HeaderView));
8 changes: 5 additions & 3 deletions src/page/HomePage/HomePage.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,13 @@ export default class App extends React.Component {
<>
<StatusBar barStyle="dark-content" />
<SafeAreaView style={[styles.flex1, styles.h100p]}>
<View style={[styles.flexColumn, styles.h100p]}>
<View style={[styles.flexRow, styles.h100p]}>
<Route path="/:reportID?">
<Header />
<View style={[styles.flex1, styles.flexRow]}>
<View style={{width: 300}}>
<Sidebar />
</View>
<View style={[styles.flex1, styles.flexColumn]}>
<Header />
<Main />
</View>
</Route>
Expand Down
2 changes: 1 addition & 1 deletion src/page/HomePage/Report/ReportHistoryCompose.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ class ReportHistoryCompose extends React.Component {
<TextInput
ref={el => this.textInput = el}
multiline
textAlignVertical
textAlignVertical="top"
numberOfLines={3}
minHeight={60}
maxHeight={60}
Expand Down
21 changes: 8 additions & 13 deletions src/page/HomePage/Report/ReportHistoryItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,14 @@ const propTypes = {
displayAsGroup: PropTypes.bool.isRequired,
};

// TODO: Fix eslint error here
// eslint-disable-next-line react/prefer-stateless-function
class ReportHistoryItem extends React.Component {
render() {
return (
<View>
{!this.props.displayAsGroup && <ReportHistoryItemSingle historyItem={this.props.historyItem} />}
{this.props.displayAsGroup && <ReportHistoryItemGrouped historyItem={this.props.historyItem} />}
{this.props.historyItem.tempGuid && <ActivityIndicator type="small" color="#7d8b8f" />}
</View>
);
}
}
const ReportHistoryItem = ({displayAsGroup, historyItem}) => (
<View>
{!displayAsGroup && <ReportHistoryItemSingle historyItem={historyItem} />}
{displayAsGroup && <ReportHistoryItemGrouped historyItem={historyItem} />}
{historyItem.tempGuid && <ActivityIndicator type="small" color="#7d8b8f" />}
</View>
);
ReportHistoryItem.propTypes = propTypes;
ReportHistoryItem.displayName = 'ReportHistoryItem';

export default ReportHistoryItem;
13 changes: 4 additions & 9 deletions src/page/HomePage/Report/ReportHistoryItemGrouped.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import {View} from 'react-native';
import {Text} from 'react-native';
import PropTypes from 'prop-types';
import ReportHistoryPropsTypes from './ReportHistoryPropsTypes';
import ReportHistoryItemMessage from './ReportHistoryItemMessage';
Expand All @@ -11,14 +11,9 @@ const propTypes = {
};

const ReportHistoryItemGrouped = ({historyItem}) => (
<View style={[styles.flexRow]}>
<View style={[styles.historyItemAvatarWrapperGrouped]} />
<View style={[styles.historyItemMessageWrapper]}>
<View style={[styles.p1]}>
<ReportHistoryItemMessage historyItem={historyItem} />
</View>
</View>
</View>
<Text style={[styles.historyItemMessageWrapper]}>
<ReportHistoryItemMessage historyItem={historyItem} />
</Text>
);

ReportHistoryItemGrouped.propTypes = propTypes;
Expand Down
Loading

0 comments on commit d8fc479

Please sign in to comment.