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)),
clearInput: () => dispatch(clearInput())
}))
export default class MessageBox extends React.Component {
export default class MessageBox extends React.PureComponent {
static propTypes = {
onSubmit: PropTypes.func.isRequired,
rid: PropTypes.string.isRequired,

View File

@ -19,7 +19,10 @@ const styles = StyleSheet.create({
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() {
const users = this.props.usersTyping.filter(_username => this.props.username !== _username);
return users.length ? `${ users.join(' ,') } ${ users.length > 1 ? 'are' : 'is' } typing` : '';

View File

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

View File

@ -1,6 +1,6 @@
import React from 'react';
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 Icon from 'react-native-vector-icons/MaterialIcons';
import moment from 'moment';
@ -15,27 +15,11 @@ import Markdown from './Markdown';
import Url from './Url';
import Reply from './Reply';
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 => ({
message: state.messages.message,
@ -53,7 +37,30 @@ export default class Message extends React.Component {
user: PropTypes.object.isRequired,
editing: PropTypes.bool,
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() {
@ -157,11 +164,14 @@ export default class Message extends React.Component {
item, message, editing, baseUrl
} = this.props;
const extraStyle = {};
if (item.status === messageStatus.TEMP || item.status === messageStatus.ERROR) {
extraStyle.opacity = 0.3;
}
const marginLeft = this._visibility.interpolate({
inputRange: [0, 1],
outputRange: [-30, 0]
});
const opacity = this._visibility.interpolate({
inputRange: [0, 1],
outputRange: [0, 1]
});
const username = item.alias || item.u.username;
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]}
accessibilityLabel={accessibilityLabel}
>
<View style={{ flexDirection: 'row', flex: 1 }}>
<Animated.View style={[flex, { opacity, marginLeft }]}>
{this.renderError()}
<View style={[extraStyle, { flexDirection: 'row', flex: 1 }]}>
<View style={[this.extraStyle, flex]}>
<Avatar
style={{ marginRight: 10 }}
style={avatar}
text={item.avatar ? '' : username}
size={40}
baseUrl={baseUrl}
@ -198,7 +208,7 @@ export default class Message extends React.Component {
{this.renderUrl()}
</View>
</View>
</View>
</Animated.View>
</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,
errorMessage: '',
failure: false,
server: {}
server: ''
};

View File

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

View File

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