Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement FlatList everywhere #536

Merged
merged 35 commits into from
Sep 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
d7ced98
add comment
marcaaron Sep 23, 2020
dc6b35a
remove reverse
marcaaron Sep 24, 2020
d9fc537
remove unneeded things
marcaaron Sep 24, 2020
6c4d266
fix starting point of flex items
marcaaron Sep 24, 2020
941a8eb
Fix up comments
marcaaron Sep 24, 2020
3d05786
space
marcaaron Sep 24, 2020
be11914
more comments
marcaaron Sep 24, 2020
f4b578d
use a boolean flag for needs layout. fix scroll to bottom
marcaaron Sep 24, 2020
d9f5efe
Improve comments and clean up
marcaaron Sep 24, 2020
1796453
Render all reports at once
marcaaron Sep 24, 2020
b76b425
mush better
marcaaron Sep 24, 2020
2c81b34
get FlatList to work hehe
marcaaron Sep 25, 2020
e97b820
remove react-window
marcaaron Sep 25, 2020
96bc392
update package json
marcaaron Sep 25, 2020
90f6273
fix up comment
marcaaron Sep 25, 2020
33ad0ef
fix conflicts
marcaaron Sep 25, 2020
dc11865
move renderItem to method
marcaaron Sep 25, 2020
9f7171c
start with 50 1 is too few
marcaaron Sep 25, 2020
5b3677e
use Scroll to index as end is actualy the top of the list in this case
marcaaron Sep 25, 2020
6b99ee8
fix the grouping issues
marcaaron Sep 25, 2020
617da50
make more epic
marcaaron Sep 25, 2020
11928e9
remove opacity stuff
marcaaron Sep 25, 2020
eccd61c
bind more methods for performance or something
marcaaron Sep 25, 2020
b43de0c
offset 0 is the secret
marcaaron Sep 25, 2020
e1064b6
Fix up some comments
marcaaron Sep 25, 2020
6897b95
fix typo
marcaaron Sep 25, 2020
9b34893
Lighten up report action items
marcaaron Sep 25, 2020
94cedac
move perf things into the InvertedFlatList
marcaaron Sep 25, 2020
195e46c
Actually, nothing bad will happen if we have no reports
marcaaron Sep 25, 2020
f379508
remove loading spinner
marcaaron Sep 25, 2020
54783f5
Remove performance steps. Improve measuring logic for offset
marcaaron Sep 25, 2020
63f13e3
Add hack to make scroll direction work on web
marcaaron Sep 25, 2020
7c61301
Fix style
marcaaron Sep 25, 2020
4305201
new lines yum
marcaaron Sep 25, 2020
924831a
Update src/lib/CollectionUtils.js
marcaaron Sep 25, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 151 additions & 0 deletions src/components/InvertedFlatList/BaseInvertedFlatList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import _ from 'underscore';
import React, {forwardRef, Component} from 'react';
import PropTypes from 'prop-types';
import {FlatList, View} from 'react-native';
import {lastItem} from '../../lib/CollectionUtils';

const propTypes = {
// Same as FlatList can be any array of anything
data: PropTypes.arrayOf(PropTypes.any),

// Same as FlatList although we wrap it in a measuring helper
// before passing to the actual FlatList component
renderItem: PropTypes.func.isRequired,

// This must be set to the minimum size of one of the
// renderItem rows. Web will have issues with FlatList
// if this is inaccurate.
initialRowHeight: PropTypes.number.isRequired,
};

const defaultProps = {
data: [],
};

class BaseInvertedFlatList extends Component {
constructor(props) {
super(props);

this.renderItem = this.renderItem.bind(this);
this.getItemLayout = this.getItemLayout.bind(this);

// Stores each item's computed height after it renders
// once and is then referenced for the life of this component.
// This is essential to getting FlatList inverted to work on web
// and also enables more predictable scrolling on native platforms.
this.sizeMap = {};
}

shouldComponentUpdate(prevProps) {
// The FlatList itself should only re-render if items are added
return prevProps.data.length !== this.props.data.length;
}

/**
* Return default or previously cached height for
* a renderItem row
*
* @param {*} data
* @param {Number} index
*
* @return {Object}
*/
getItemLayout(data, index) {
const size = this.sizeMap[index];

if (size) {
return {
length: size.length,
offset: size.offset,
index,
};
}

// If we don't have a size yet means we haven't measured this
// item yet. However, we can still calculate the offset by looking
// at the last size we have recorded (if any)
const lastMeasuredItem = lastItem(this.sizeMap);

return {
// We haven't measured this so we must return the minimum row height
length: this.props.initialRowHeight,

// Offset will either be based on the lastMeasuredItem or the index +
// initialRowHeight since we can only assume that all previous items
// have not yet been measured
offset: _.isUndefined(lastMeasuredItem)
? this.props.initialRowHeight * index
: lastMeasuredItem.offset + this.props.initialRowHeight,
index
};
}

/**
* Measure item and cache the returned length (a.k.a. height)
*
* @param {React.NativeSyntheticEvent} nativeEvent
* @param {Number} index
*/
measureItemLayout(nativeEvent, index) {
const computedHeight = nativeEvent.layout.height;

// We've already measured this item so we don't need to
// measure it again.
if (this.sizeMap[index]) {
return;
}

const previousItem = this.sizeMap[index - 1] || {};

// If there is no previousItem this can mean we haven't yet measured
// the previous item or that we are at index 0 and there is no previousItem
const previousLength = previousItem.length || 0;
const previousOffset = previousItem.offset || 0;
this.sizeMap[index] = {
length: computedHeight,
offset: previousLength + previousOffset,
};
}

/**
* Render item method wraps the prop renderItem to render in a
* View component so we can attach an onLayout handler and
* measure it when it renders.
*
* @param {Object} params
* @param {Object} params.item
* @param {Number} params.index
*
* @return {React.Component}
*/
renderItem({item, index}) {
return (
<View onLayout={({nativeEvent}) => this.measureItemLayout(nativeEvent, index)}>
{this.props.renderItem({item, index})}
</View>
);
}

render() {
return (
<FlatList
// eslint-disable-next-line react/jsx-props-no-spreading
{...this.props}
ref={this.props.innerRef}
inverted
renderItem={this.renderItem}
getItemLayout={this.getItemLayout}
removeClippedSubviews
bounces={false}
/>
);
}
}

BaseInvertedFlatList.propTypes = propTypes;
BaseInvertedFlatList.defaultProps = defaultProps;

export default forwardRef((props, ref) => (
// eslint-disable-next-line react/jsx-props-no-spreading
<BaseInvertedFlatList {...props} innerRef={ref} />
));
58 changes: 58 additions & 0 deletions src/components/InvertedFlatList/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React, {
useEffect,
useRef,
useCallback,
forwardRef
} from 'react';
import BaseInvertedFlatList from './BaseInvertedFlatList';

// This is copied from https://codesandbox.io/s/react-native-dsyse
// It's a HACK alert since FlatList has inverted scrolling on web
const InvertedFlatList = (props) => {
const ref = useRef(null);

const invertedWheelEvent = useCallback((e) => {
ref.current.getScrollableNode().scrollTop -= e.deltaY;
e.preventDefault();
}, []);

useEffect(() => {
props.forwardedRef(ref);
}, []);

useEffect(() => {
const currentRef = ref.current;
if (currentRef != null) {
currentRef
.getScrollableNode()
.addEventListener('wheel', invertedWheelEvent);

currentRef.setNativeProps({
style: {
transform: 'translate3d(0,0,0) scaleY(-1)'
},
});
}

return () => {
if (currentRef != null) {
currentRef
.getScrollableNode()
.removeEventListener('wheel', invertedWheelEvent);
}
};
}, [ref, invertedWheelEvent]);

return (
<BaseInvertedFlatList
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
ref={ref}
/>
);
};

export default forwardRef((props, ref) => (
// eslint-disable-next-line react/jsx-props-no-spreading
<InvertedFlatList {...props} forwardedRef={ref} />
));
4 changes: 4 additions & 0 deletions src/components/InvertedFlatList/index.native.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import BaseInvertedFlatList from './BaseInvertedFlatList';

BaseInvertedFlatList.displayName = 'InvertedFlatList';
export default BaseInvertedFlatList;
18 changes: 18 additions & 0 deletions src/lib/CollectionUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import _ from 'underscore';

/**
* Return the highest item in a numbered collection
*
* e.g. {1: '1', 2: '2', 3: '3'} -> '3'
*
* @param {Object} object
* @return {*}
*/
export function lastItem(object = {}) {
const lastKey = _.last(_.keys(object)) || 0;
return object[lastKey];
}

export default {
lastItem,
};
12 changes: 3 additions & 9 deletions src/page/home/MainView.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, {Component} from 'react';
import {View} from 'react-native';
import PropTypes from 'prop-types';
import _ from 'underscore';
Expand Down Expand Up @@ -26,19 +26,13 @@ const defaultProps = {
reports: {},
};

class MainView extends React.Component {
class MainView extends Component {
render() {
if (!_.size(this.props.reports)) {
return null;
}

const reportIDInURL = parseInt(this.props.match.params.reportID, 10);

// The styles for each of our reports. Basically, they are all hidden except for the one matching the
// reportID in the URL
let activeReportID;
const reportStyles = _.reduce(this.props.reports, (memo, report) => {
const isActiveReport = reportIDInURL === report.reportID;
const isActiveReport = parseInt(this.props.match.params.reportID, 10) === report.reportID;
const finalData = {...memo};
let reportStyle;

Expand Down
25 changes: 9 additions & 16 deletions src/page/home/report/ReportActionItem.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import React from 'react';
import {View} from 'react-native';
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import _ from 'underscore';
import ReportActionItemSingle from './ReportActionItemSingle';
import ReportActionPropTypes from './ReportActionPropTypes';
import ReportActionItemGrouped from './ReportActionItemGrouped';
Expand All @@ -14,24 +12,19 @@ const propTypes = {
displayAsGroup: PropTypes.bool.isRequired,
};

class ReportActionItem extends React.Component {
class ReportActionItem extends Component {
shouldComponentUpdate(nextProps) {
// This component should only render if the action's sequenceNumber or displayAsGroup props change
return nextProps.displayAsGroup !== this.props.displayAsGroup
|| !_.isEqual(nextProps.action, this.props.action);
// If the grouping changes then we want to update the UI
return nextProps.displayAsGroup !== this.props.displayAsGroup;
}

render() {
const {action, displayAsGroup} = this.props;
if (action.actionName !== 'ADDCOMMENT') {
return null;
}

return (
<View>
{!displayAsGroup && <ReportActionItemSingle action={action} />}
{displayAsGroup && <ReportActionItemGrouped action={action} />}
</View>
<>
{!this.props.displayAsGroup
? <ReportActionItemSingle action={this.props.action} />
: <ReportActionItemGrouped action={this.props.action} />}
</>
);
}
}
Expand Down
4 changes: 1 addition & 3 deletions src/page/home/report/ReportActionItemGrouped.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@ class ReportActionItemGrouped extends React.PureComponent {
return (
<View style={[styles.chatItem]}>
<View style={[styles.chatItemRightGrouped]}>
<View style={[styles.chatItemMessage]}>
<ReportActionItemMessage action={action} />
</View>
<ReportActionItemMessage action={action} />
</View>
</View>
);
Expand Down
10 changes: 6 additions & 4 deletions src/page/home/report/ReportActionItemMessage.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import React from 'react';
import {View} from 'react-native';
import PropTypes from 'prop-types';
import _ from 'underscore';
import styles from '../../../style/StyleSheet';
import ReportActionItemFragment from './ReportActionItemFragment';
import ReportActionPropTypes from './ReportActionPropTypes';

Expand All @@ -10,15 +12,15 @@ const propTypes = {
};

const ReportActionItemMessage = ({action}) => (
<>
{_.map(_.compact(action.message), fragment => (
<View style={[styles.chatItemMessage]}>
{_.map(_.compact(action.message), (fragment, index) => (
<ReportActionItemFragment
key={_.uniqueId('actionFragment', action.sequenceNumber)}
key={`actionFragment-${action.sequenceNumber}-${index}`}
fragment={fragment}
isAttachment={action.isAttachment}
/>
))}
</>
</View>
);

ReportActionItemMessage.propTypes = propTypes;
Expand Down
29 changes: 11 additions & 18 deletions src/page/home/report/ReportActionItemSingle.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,28 +22,21 @@ class ReportActionItemSingle extends React.PureComponent {
: action.avatar;
return (
<View style={[styles.chatItem]}>
<View style={[styles.chatItemLeft]}>
<View style={[styles.actionAvatarWrapper]}>
<Image
source={{uri: avatarUrl}}
style={[styles.actionAvatar]}
/>
</View>
</View>
<Image
source={{uri: avatarUrl}}
style={[styles.actionAvatar]}
/>
<View style={[styles.chatItemRight]}>
<View style={[styles.chatItemMessageHeader]}>
{action.person.map(fragment => (
<View key={_.uniqueId('person-', action.sequenceNumber)}>
<ReportActionItemFragment fragment={fragment} />
</View>
{_.map(action.person, (fragment, index) => (
<ReportActionItemFragment
key={`person-${action.sequenceNumber}-${index}`}
fragment={fragment}
/>
))}
<View>
<ReportActionItemDate timestamp={action.timestamp} />
</View>
</View>
<View style={[styles.chatItemMessage]}>
<ReportActionItemMessage action={action} />
<ReportActionItemDate timestamp={action.timestamp} />
</View>
<ReportActionItemMessage action={action} />
</View>
</View>
);
Expand Down
Loading