Test improve render (#159)

[NEW] ListView
This commit is contained in:
Guilherme Gazzo 2018-01-09 15:12:55 -02:00 committed by GitHub
parent 3cd281b66f
commit 7a1d359a4f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 275 additions and 68 deletions

View File

@ -30,7 +30,7 @@ const onlyUnique = function onlyUnique(value, index, self) {
typing: status => dispatch(userTyping(status)), typing: status => dispatch(userTyping(status)),
clearInput: () => dispatch(clearInput()) clearInput: () => dispatch(clearInput())
})) }))
export default class MessageBox extends React.Component { export default class MessageBox extends React.PureComponent {
static propTypes = { static propTypes = {
onSubmit: PropTypes.func.isRequired, onSubmit: PropTypes.func.isRequired,
rid: PropTypes.string.isRequired, rid: PropTypes.string.isRequired,

View File

@ -19,7 +19,10 @@ const styles = StyleSheet.create({
usersTyping: state.room.usersTyping usersTyping: state.room.usersTyping
})) }))
export default class Typing extends React.PureComponent { export default class Typing extends React.Component {
shouldComponentUpdate(nextProps) {
return this.props.usersTyping.join() !== nextProps.usersTyping.join();
}
get usersTyping() { get usersTyping() {
const users = this.props.usersTyping.filter(_username => this.props.username !== _username); const users = this.props.usersTyping.filter(_username => this.props.username !== _username);
return users.length ? `${ users.join(' ,') } ${ users.length > 1 ? 'are' : 'is' } typing` : ''; return users.length ? `${ users.join(' ,') } ${ users.length > 1 ? 'are' : 'is' } typing` : '';

View File

@ -25,7 +25,7 @@ const BlockCode = ({ node, state }) => (
{node.content} {node.content}
</Text> </Text>
); );
const mentionStyle = { color: '#13679a' };
const rules = { const rules = {
username: { username: {
order: -1, order: -1,
@ -38,7 +38,7 @@ const rules = {
children: ( children: (
<Text <Text
key={state.key} key={state.key}
style={{ color: '#13679a' }} style={mentionStyle}
onPress={() => alert('Username')} onPress={() => alert('Username')}
> >
{node.content} {node.content}
@ -58,7 +58,7 @@ const rules = {
children: ( children: (
<Text <Text
key={state.key} key={state.key}
style={{ color: '#13679a' }} style={mentionStyle}
onPress={() => alert('Room')} onPress={() => alert('Room')}
> >
{node.content} {node.content}

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { View, StyleSheet, TouchableHighlight, Text, TouchableOpacity } from 'react-native'; import { View, TouchableHighlight, Text, TouchableOpacity, Animated } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Icon from 'react-native-vector-icons/MaterialIcons'; import Icon from 'react-native-vector-icons/MaterialIcons';
import moment from 'moment'; import moment from 'moment';
@ -15,27 +15,11 @@ import Markdown from './Markdown';
import Url from './Url'; import Url from './Url';
import Reply from './Reply'; import Reply from './Reply';
import messageStatus from '../../constants/messagesStatus'; import messageStatus from '../../constants/messagesStatus';
import styles from './styles';
const avatar = { marginRight: 10 };
const flex = { flexDirection: 'row', flex: 1 };
const styles = StyleSheet.create({
content: {
flexGrow: 1,
flexShrink: 1
},
message: {
padding: 12,
paddingTop: 6,
paddingBottom: 6,
flexDirection: 'row',
transform: [{ scaleY: -1 }]
},
textInfo: {
fontStyle: 'italic',
color: '#a0a0a0'
},
editing: {
backgroundColor: '#fff5df'
}
});
@connect(state => ({ @connect(state => ({
message: state.messages.message, message: state.messages.message,
@ -53,7 +37,30 @@ export default class Message extends React.Component {
user: PropTypes.object.isRequired, user: PropTypes.object.isRequired,
editing: PropTypes.bool, editing: PropTypes.bool,
actionsShow: PropTypes.func, actionsShow: PropTypes.func,
errorActionsShow: PropTypes.func errorActionsShow: PropTypes.func,
animate: PropTypes.bool
}
componentWillMount() {
this._visibility = new Animated.Value(this.props.animate ? 0 : 1);
}
componentDidMount() {
if (this.props.animate) {
Animated.timing(this._visibility, {
toValue: 1,
duration: 300
}).start();
}
}
componentWillReceiveProps() {
this.extraStyle = this.extraStyle || {};
if (this.props.item.status === messageStatus.TEMP || this.props.item.status === messageStatus.ERROR) {
this.extraStyle.opacity = 0.3;
}
}
shouldComponentUpdate(nextProps) {
return this.props.item._updatedAt.toGMTString() !== nextProps.item._updatedAt.toGMTString() || this.props.item.status !== nextProps.item.status;
} }
onLongPress() { onLongPress() {
@ -157,11 +164,14 @@ export default class Message extends React.Component {
item, message, editing, baseUrl item, message, editing, baseUrl
} = this.props; } = this.props;
const extraStyle = {}; const marginLeft = this._visibility.interpolate({
if (item.status === messageStatus.TEMP || item.status === messageStatus.ERROR) { inputRange: [0, 1],
extraStyle.opacity = 0.3; outputRange: [-30, 0]
} });
const opacity = this._visibility.interpolate({
inputRange: [0, 1],
outputRange: [0, 1]
});
const username = item.alias || item.u.username; const username = item.alias || item.u.username;
const isEditing = message._id === item._id && editing; const isEditing = message._id === item._id && editing;
@ -176,11 +186,11 @@ export default class Message extends React.Component {
style={[styles.message, isEditing ? styles.editing : null]} style={[styles.message, isEditing ? styles.editing : null]}
accessibilityLabel={accessibilityLabel} accessibilityLabel={accessibilityLabel}
> >
<View style={{ flexDirection: 'row', flex: 1 }}> <Animated.View style={[flex, { opacity, marginLeft }]}>
{this.renderError()} {this.renderError()}
<View style={[extraStyle, { flexDirection: 'row', flex: 1 }]}> <View style={[this.extraStyle, flex]}>
<Avatar <Avatar
style={{ marginRight: 10 }} style={avatar}
text={item.avatar ? '' : username} text={item.avatar ? '' : username}
size={40} size={40}
baseUrl={baseUrl} baseUrl={baseUrl}
@ -198,7 +208,7 @@ export default class Message extends React.Component {
{this.renderUrl()} {this.renderUrl()}
</View> </View>
</View> </View>
</View> </Animated.View>
</TouchableHighlight> </TouchableHighlight>
); );
} }

View File

@ -0,0 +1,22 @@
import { StyleSheet } from 'react-native';
export default StyleSheet.create({
content: {
flexGrow: 1,
flexShrink: 1
},
message: {
padding: 12,
paddingTop: 6,
paddingBottom: 6,
flexDirection: 'row',
transform: [{ scaleY: -1 }]
},
textInfo: {
fontStyle: 'italic',
color: '#a0a0a0'
},
editing: {
backgroundColor: '#fff5df'
}
});

View File

@ -5,7 +5,7 @@ const initialState = {
connected: false, connected: false,
errorMessage: '', errorMessage: '',
failure: false, failure: false,
server: {} server: ''
}; };

View File

@ -15,7 +15,7 @@ import styles from './styles';
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '', baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
activeUsers: state.activeUsers activeUsers: state.activeUsers
})) }))
export default class extends React.Component { export default class extends React.PureComponent {
static propTypes = { static propTypes = {
navigation: PropTypes.object.isRequired, navigation: PropTypes.object.isRequired,
user: PropTypes.object.isRequired, user: PropTypes.object.isRequired,

View File

@ -0,0 +1,144 @@
import { ListView as OldList } from 'realm/react-native';
import React from 'react';
import cloneReferencedElement from 'react-clone-referenced-element';
import { ScrollView, ListView as OldList2 } from 'react-native';
const DEFAULT_SCROLL_CALLBACK_THROTTLE = 50;
export class DataSource extends OldList.DataSource {
getRowData(sectionIndex: number, rowIndex: number): any {
const sectionID = this.sectionIdentities[sectionIndex];
const rowID = this.rowIdentities[sectionIndex][rowIndex];
return this._getRowData(this._dataBlob, sectionID, rowID);
}
_calculateDirtyArrays() { // eslint-disable-line
return false;
}
}
export class ListView extends OldList2 {
constructor(props) {
super(props);
this.state = {
curRenderedRowsCount: this.props.initialListSize,
highlightedRow: ({}: Object)
};
this.renderRow = this.renderRow.bind(this);
}
renderRow(_, sectionId, rowId, ...args) {
const { props } = this;
const item = props.dataSource.getRow(sectionId, rowId);
// The item could be null because our data is a snapshot and it was deleted.
return item ? props.renderRow(item, sectionId, rowId, ...args) : null;
}
getInnerViewNode() {
return this.refs.listView.getInnerViewNode();
}
scrollTo(...args) {
this.refs.listView.scrollTo(...args);
}
setNativeProps(props) {
this.refs.listView.setNativeProps(props);
}
static DataSource = DataSource;
render() {
const bodyComponents = [];
const { dataSource } = this.props;
const allRowIDs = dataSource.rowIdentities;
let rowCount = 0;
// const stickySectionHeaderIndices = [];
// const { renderSectionHeader } = this.props;
const header = this.props.renderHeader && this.props.renderHeader();
const footer = this.props.renderFooter && this.props.renderFooter();
// let totalIndex = header ? 1 : 0;
for (let sectionIdx = 0; sectionIdx < allRowIDs.length; sectionIdx += 1) {
const sectionID = dataSource.sectionIdentities[sectionIdx];
const rowIDs = allRowIDs[sectionIdx];
if (rowIDs.length === 0) {
continue; // eslint-disable-line
}
// if (renderSectionHeader) {
// const element = renderSectionHeader(
// dataSource.getSectionHeaderData(sectionIdx),
// sectionID,
// );
// if (element) {
// bodyComponents.push(React.cloneElement(element, { key: `s_${ sectionID }` }), );
// if (this.props.stickySectionHeadersEnabled) {
// stickySectionHeaderIndices.push(totalIndex);
// }
// totalIndex++;
// }
// }
for (let rowIdx = 0; rowIdx < rowIDs.length; rowIdx += 1) {
const rowID = rowIDs[rowIdx];
const data = dataSource._dataBlob[sectionID][rowID];
bodyComponents.push(this.props.renderRow.bind(
null,
data,
sectionID,
rowID,
this._onRowHighlighted,
)());
// totalIndex += 1;
rowCount += 1;
if (rowCount === this.state.curRenderedRowsCount) {
break;
}
}
if (rowCount >= this.state.curRenderedRowsCount) {
break;
}
}
const { ...props } = this.props;
if (!props.scrollEventThrottle) {
props.scrollEventThrottle = DEFAULT_SCROLL_CALLBACK_THROTTLE;
}
if (props.removeClippedSubviews === undefined) {
props.removeClippedSubviews = true;
}
/* $FlowFixMe(>=0.54.0 site=react_native_fb,react_native_oss) This comment
* suppresses an error found when Flow v0.54 was deployed. To see the error
* delete this comment and run Flow. */
Object.assign(props, {
onScroll: this._onScroll,
/* $FlowFixMe(>=0.53.0 site=react_native_fb,react_native_oss) This
* comment suppresses an error when upgrading Flow's support for React.
* To see the error delete this comment and run Flow. */
// stickyHeaderIndices: this.props.stickyHeaderIndices.concat(stickySectionHeaderIndices,),
// Do not pass these events downstream to ScrollView since they will be
// registered in ListView's own ScrollResponder.Mixin
onKeyboardWillShow: undefined,
onKeyboardWillHide: undefined,
onKeyboardDidShow: undefined,
onKeyboardDidHide: undefined
});
return cloneReferencedElement(
<ScrollView {...props} />,
{
ref: this._setScrollComponentRef,
onContentSizeChange: this._onContentSizeChange,
onLayout: this._onLayout
},
header,
bodyComponents,
footer,
);
}
}
ListView.DataSource = DataSource;

View File

@ -0,0 +1,22 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Text, View } from 'react-native';
import { connect } from 'react-redux';
import styles from './styles';
@connect(state => ({
loading: state.messages.isFetching
}), null)
export default class Banner extends React.PureComponent {
static propTypes = {
loading: PropTypes.bool
};
render() {
return (this.props.loading ? (
<View style={styles.bannerContainer}>
<Text style={styles.bannerText}>Loading new messages...</Text>
</View>
) : null);
}
}

View File

@ -1,10 +1,11 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Text, View, Button, SafeAreaView } from 'react-native'; import { Text, View, Button, SafeAreaView } from 'react-native';
import { ListView } from 'realm/react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import equal from 'deep-equal';
import { ListView } from './ListView';
import * as actions from '../../actions'; import * as actions from '../../actions';
import { openRoom } from '../../actions/room'; import { openRoom } from '../../actions/room';
import { editCancel } from '../../actions/messages'; import { editCancel } from '../../actions/messages';
@ -18,8 +19,11 @@ import Typing from '../../containers/Typing';
import KeyboardView from '../../presentation/KeyboardView'; import KeyboardView from '../../presentation/KeyboardView';
import Header from '../../containers/Header'; import Header from '../../containers/Header';
import RoomsHeader from './Header'; import RoomsHeader from './Header';
import Banner from './banner';
import styles from './styles'; import styles from './styles';
import debounce from '../../utils/debounce';
const ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1._id !== r2._id }); const ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1._id !== r2._id });
const typing = () => <Typing />; const typing = () => <Typing />;
@ -45,8 +49,7 @@ export default class RoomView extends React.Component {
rid: PropTypes.string, rid: PropTypes.string,
name: PropTypes.string, name: PropTypes.string,
Site_Url: PropTypes.string, Site_Url: PropTypes.string,
Message_TimeFormat: PropTypes.string, Message_TimeFormat: PropTypes.string
loading: PropTypes.bool
}; };
static navigationOptions = ({ navigation }) => ({ static navigationOptions = ({ navigation }) => ({
@ -61,13 +64,15 @@ export default class RoomView extends React.Component {
this.name = this.props.name || this.name = this.props.name ||
this.props.navigation.state.params.name || this.props.navigation.state.params.name ||
this.props.navigation.state.params.room.name; this.props.navigation.state.params.room.name;
this.opened = new Date();
this.data = database.objects('messages') this.data = database
.objects('messages')
.filtered('rid = $0', this.rid) .filtered('rid = $0', this.rid)
.sorted('ts', true); .sorted('ts', true);
const rowIds = this.data.map((row, index) => index);
this.room = database.objects('subscriptions').filtered('rid = $0', this.rid); this.room = database.objects('subscriptions').filtered('rid = $0', this.rid);
this.state = { this.state = {
dataSource: ds.cloneWithRows([]), dataSource: ds.cloneWithRows(this.data, rowIds),
loaded: true, loaded: true,
joined: typeof props.rid === 'undefined' joined: typeof props.rid === 'undefined'
}; };
@ -80,8 +85,8 @@ export default class RoomView extends React.Component {
this.props.openRoom({ rid: this.rid, name: this.name }); this.props.openRoom({ rid: this.rid, name: this.name });
this.data.addListener(this.updateState); this.data.addListener(this.updateState);
} }
componentDidMount() { shouldComponentUpdate(nextProps, nextState) {
this.updateState(); return !(equal(this.props, nextProps) && equal(this.state, nextState));
} }
componentWillUnmount() { componentWillUnmount() {
clearTimeout(this.timer); clearTimeout(this.timer);
@ -90,9 +95,8 @@ export default class RoomView extends React.Component {
} }
onEndReached = () => { onEndReached = () => {
const rowCount = this.state.dataSource.getRowCount();
if ( if (
rowCount && // rowCount &&
this.state.loaded && this.state.loaded &&
this.state.loadingMore !== true && this.state.loadingMore !== true &&
this.state.end !== true this.state.end !== true
@ -100,22 +104,27 @@ export default class RoomView extends React.Component {
this.setState({ this.setState({
loadingMore: true loadingMore: true
}); });
requestAnimationFrame(() => {
const lastRowData = this.data[rowCount - 1]; const lastRowData = this.data[this.data.length - 1];
RocketChat.loadMessagesForRoom(this.rid, lastRowData.ts, ({ end }) => { if (!lastRowData) {
this.setState({ return;
loadingMore: false, }
end RocketChat.loadMessagesForRoom(this.rid, lastRowData.ts, ({ end }) => {
this.setState({
loadingMore: false,
end
});
}); });
}); });
} }
} }
updateState = () => { updateState = debounce(() => {
const rowIds = this.data.map((row, index) => index);
this.setState({ this.setState({
dataSource: ds.cloneWithRows(this.data) dataSource: this.state.dataSource.cloneWithRows(this.data, rowIds)
}); });
}; }, 50);
sendMessage = message => RocketChat.sendMessage(this.rid, message); sendMessage = message => RocketChat.sendMessage(this.rid, message);
@ -126,17 +135,11 @@ export default class RoomView extends React.Component {
}); });
}; };
renderBanner = () => renderItem = item => (
(this.props.loading ? (
<View style={styles.bannerContainer}>
<Text style={styles.bannerText}>Loading new messages...</Text>
</View>
) : null);
renderItem = ({ item }) => (
<Message <Message
key={item._id} key={item._id}
item={item} item={item}
animate={this.opened.toISOString() < item.ts.toISOString()}
baseUrl={this.props.Site_Url} baseUrl={this.props.Site_Url}
Message_TimeFormat={this.props.Message_TimeFormat} Message_TimeFormat={this.props.Message_TimeFormat}
user={this.props.user} user={this.props.user}
@ -169,17 +172,18 @@ export default class RoomView extends React.Component {
render() { render() {
return ( return (
<KeyboardView contentContainerStyle={styles.container} keyboardVerticalOffset={64}> <KeyboardView contentContainerStyle={styles.container} keyboardVerticalOffset={64}>
{this.renderBanner()}
<Banner />
<SafeAreaView style={styles.safeAreaView}> <SafeAreaView style={styles.safeAreaView}>
<ListView <ListView
enableEmptySections enableEmptySections
style={styles.list} style={styles.list}
onEndReachedThreshold={0.5} onEndReachedThreshold={500}
renderFooter={this.renderHeader} renderFooter={this.renderHeader}
renderHeader={typing} renderHeader={typing}
onEndReached={this.onEndReached} onEndReached={this.onEndReached}
dataSource={this.state.dataSource} dataSource={this.state.dataSource}
renderRow={item => this.renderItem({ item })} renderRow={item => this.renderItem(item)}
initialListSize={10} initialListSize={10}
keyboardShouldPersistTaps='always' keyboardShouldPersistTaps='always'
keyboardDismissMode='interactive' keyboardDismissMode='interactive'

View File

@ -26,10 +26,12 @@
"babel-plugin-transform-decorators-legacy": "^1.3.4", "babel-plugin-transform-decorators-legacy": "^1.3.4",
"babel-plugin-transform-remove-console": "^6.8.5", "babel-plugin-transform-remove-console": "^6.8.5",
"babel-polyfill": "^6.26.0", "babel-polyfill": "^6.26.0",
"deep-equal": "^1.0.1",
"ejson": "^2.1.2", "ejson": "^2.1.2",
"moment": "^2.20.1", "moment": "^2.20.1",
"prop-types": "^15.6.0", "prop-types": "^15.6.0",
"react": "^16.2.0", "react": "^16.2.0",
"react-clone-referenced-element": "^1.0.1",
"react-emojione": "^5.0.0", "react-emojione": "^5.0.0",
"react-native": "^0.51.0", "react-native": "^0.51.0",
"react-native-action-button": "^2.8.3", "react-native-action-button": "^2.8.3",