From 138546e4c98bde71fd1b94269947455138845f2f Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Fri, 11 Aug 2017 15:18:09 -0300 Subject: [PATCH] listview --- app/components/Message.js | 1 - app/components/MessageBox.js | 18 +++-- app/lib/realm.js | 19 +++-- app/lib/rocketchat.js | 150 ++++++++++++++++++++++++++--------- app/navigation.js | 4 +- app/utils/debounce.js | 14 ++++ app/utils/throttle.js | 21 +++++ app/views/room.js | 64 ++++++++------- app/views/roomsList.js | 108 ++++++++++++------------- package-lock.json | 30 +++++++ package.json | 5 ++ 11 files changed, 293 insertions(+), 141 deletions(-) create mode 100644 app/utils/debounce.js create mode 100644 app/utils/throttle.js diff --git a/app/components/Message.js b/app/components/Message.js index bbf3d0cbf..8a7e74b23 100644 --- a/app/components/Message.js +++ b/app/components/Message.js @@ -71,7 +71,6 @@ export default class Message extends React.PureComponent { let initials = usernameParts.length > 1 ? usernameParts[0][0] + usernameParts[usernameParts.length - 1][0] : username.replace(/[^A-Za-z0-9]/g, '').substr(0, 2); initials = initials.toUpperCase(); - return ( diff --git a/app/components/MessageBox.js b/app/components/MessageBox.js index 8bd34bca7..2485032a7 100644 --- a/app/components/MessageBox.js +++ b/app/components/MessageBox.js @@ -17,6 +17,7 @@ const styles = StyleSheet.create({ }, textBoxInput: { height: 40, + alignSelf: 'stretch', backgroundColor: '#fff', flexGrow: 1 }, @@ -44,14 +45,15 @@ export default class MessageBox extends React.PureComponent { }; } - submit = () => { - if (this.state.text.trim() === '') { + submit(message) { + // console.log(this.state); + const text = message; + this.setState({ text: '' }); + if (text.trim() === '') { return; } - - this.props.onSubmit(this.state.text); - this.setState({ text: '' }); - }; + this.props.onSubmit(text); + } addFile = () => { const options = { @@ -106,13 +108,15 @@ export default class MessageBox extends React.PureComponent { this.component = component} style={styles.textBoxInput} value={this.state.text} onChangeText={text => this.setState({ text })} returnKeyType='send' - onSubmitEditing={this.submit} + onSubmitEditing={event => this.submit(event.nativeEvent.text)} blurOnSubmit={false} placeholder='New message' + underlineColorAndroid='transparent' /> ); diff --git a/app/lib/realm.js b/app/lib/realm.js index 62c90dac4..f7ad194b1 100644 --- a/app/lib/realm.js +++ b/app/lib/realm.js @@ -35,10 +35,10 @@ const subscriptionSchema = { open: { type: 'bool', optional: true }, alert: { type: 'bool', optional: true }, // roles: [ 'owner' ], - unread: { type: 'int', optional: true } + unread: { type: 'int', optional: true }, // userMentions: 0, // groupMentions: 0, - // _updatedAt: Fri Jul 28 2017 18:31:35 GMT-0300 (-03), + _updatedAt: { type: 'date', optional: true } } }; @@ -80,11 +80,10 @@ const realm = new Realm({ export default realm; // Clear settings -realm.write(() => { - // const allSettins = realm.objects('settings'); - // realm.delete(allSettins); - - // realm.create('servers', { id: 'https://demo.rocket.chat', current: false }, true); - // realm.create('servers', { id: 'http://localhost:3000', current: false }, true); - // realm.create('servers', { id: 'http://10.0.2.2:3000', current: false }, true); -}); +// realm.write(() => { +// // const allSettins = realm.objects('settings'); +// // realm.delete(allSettins); +// +// // realm.create('servers', { id: 'https://demo.rocket.chat', current: false }, true); +// // realm.create('servers', { id: 'http://localhost:3000', current: false }, true); +// }); diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index 980ae3c16..2426ef309 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -1,9 +1,37 @@ import Meteor from 'react-native-meteor'; import Random from 'react-native-meteor/lib/Random'; import realm from './realm'; +import debounce from '../utils/debounce'; export { Accounts } from 'react-native-meteor'; +const call = (method, ...params) => new Promise((resolve, reject) => { + Meteor.call(method, ...params, (err, data) => { + if (err) { + reject(err); + } + resolve(data); + }); +}); + +const write = (() => { + const cache = []; + const run = debounce(() => { + if (!cache.length) { + return; + } + realm.write(() => { + cache.forEach(([name, obj]) => { + realm.create(name, obj, true); + }); + }); + // cache = []; + }, 1000); + return (name, obj) => { + cache.push([name, obj]); + run(); + }; +})(); const RocketChat = { createChannel({ name, users, type }) { @@ -45,6 +73,7 @@ const RocketChat = { if (typeof item.value === 'string') { setting.value = item.value; } + // write('settings', setting); realm.create('settings', setting, true); }); }); @@ -61,17 +90,36 @@ const RocketChat = { const message = ddbMessage.fields.args[0]; message.temp = false; message._server = { id: RocketChat.currentServer }; + // write('messages', message); realm.create('messages', message, true); }); } - + this.subCache = this.subCache || {}; + this.roomCache = this.roomCache || {}; + this.cache = {}; if (ddbMessage.collection === 'stream-notify-user') { - console.log(ddbMessage); - realm.write(() => { - const data = ddbMessage.fields.args[1]; - data._server = { id: RocketChat.currentServer }; - realm.create('subscriptions', data, true); - }); + const data = ddbMessage.fields.args[1]; + let key; + if (ddbMessage.fields.eventName && ddbMessage.fields.eventName.indexOf('rooms-changed') > -1) { + this.roomCache[data._id] = data; + key = data._id; + } else { + this.subCache[data.rid] = data; + key = data.rid; + delete this.subCache[key]._updatedAt; + } + this.cache[key] = this.cache[key] || + setTimeout(() => { + this.subCache[key] = this.subCache[key] || realm.objects('subscriptions').filtered('rid = $0', key).slice(0, 1)[0]; + if (this.roomCache[key]) { + this.subCache[key]._updatedAt = this.roomCache[key]._updatedAt; + } + + write('subscriptions', this.subCache[key]); + delete this.subCache[key]; + delete this.roomCache[key]; + delete this.cache[key]; + }, 550); } }); }); @@ -86,19 +134,21 @@ const RocketChat = { if (err) { console.error(err); } - - realm.write(() => { - data.forEach((subscription) => { - // const subscription = { - // _id: item._id - // }; - // if (typeof item.value === 'string') { - // subscription.value = item.value; - // } - subscription._server = { id: RocketChat.currentServer }; - realm.create('subscriptions', subscription, true); + if (data.length) { + realm.write(() => { + data.forEach((subscription) => { + // const subscription = { + // _id: item._id + // }; + // if (typeof item.value === 'string') { + // subscription.value = item.value; + // } + subscription._server = { id: RocketChat.currentServer }; + write('subscriptions', subscription); + realm.create('subscriptions', subscription, true); + }); }); - }); + } return cb && cb(); }); @@ -113,14 +163,16 @@ const RocketChat = { } return; } - - realm.write(() => { - data.messages.forEach((message) => { - message.temp = false; - message._server = { id: RocketChat.currentServer }; - realm.create('messages', message, true); + if (data.messages.length) { + realm.write(() => { + data.messages.forEach((message) => { + message.temp = false; + message._server = { id: RocketChat.currentServer }; + // write('messages', message); + realm.create('messages', message, true); + }); }); - }); + } if (cb) { if (data.messages.length < 20) { @@ -139,6 +191,19 @@ const RocketChat = { const user = Meteor.user(); realm.write(() => { + // write('messages', { + // _id, + // rid, + // msg, + // ts: new Date(), + // _updatedAt: new Date(), + // temp: true, + // _server: { id: RocketChat.currentServer }, + // u: { + // _id: user._id, + // username: user.username + // } + // }); realm.create('messages', { _id, rid, @@ -185,7 +250,9 @@ const RocketChat = { }); }); }, - + readMessages(rid) { + return call('readMessages', rid); + }, joinRoom(rid) { return new Promise((resolve, reject) => { Meteor.call('joinRoom', rid, (error, result) => { @@ -252,25 +319,30 @@ const RocketChat = { }; export default RocketChat; - Meteor.Accounts.onLogin(() => { - Meteor.call('subscriptions/get', (err, data) => { - if (err) { - console.error(err); - } - + Promise.all([call('subscriptions/get'), call('rooms/get')]).then(([subscriptions, rooms]) => { + subscriptions = subscriptions.sort((s1, s2) => (s1.rid > s2.rid ? 1 : -1)); + rooms = rooms.sort((s1, s2) => (s1._id > s2._id ? 1 : -1)); + const data = subscriptions.map((subscription, index) => { + subscription._updatedAt = rooms[index]._updatedAt; + return subscription; + }); realm.write(() => { data.forEach((subscription) => { - // const subscription = { - // _id: item._id - // }; - // if (typeof item.value === 'string') { - // subscription.value = item.value; - // } + // const subscription = { + // _id: item._id + // }; + // if (typeof item.value === 'string') { + // subscription.value = item.value; + // } subscription._server = { id: RocketChat.currentServer }; + // write('subscriptions', subscription); realm.create('subscriptions', subscription, true); }); }); + }).then(() => { Meteor.subscribe('stream-notify-user', `${ Meteor.userId() }/subscriptions-changed`, false); + Meteor.subscribe('stream-notify-user', `${ Meteor.userId() }/rooms-changed`, false); + console.log('subscriptions done.'); }); }); diff --git a/app/navigation.js b/app/navigation.js index d465d6092..8044889c6 100644 --- a/app/navigation.js +++ b/app/navigation.js @@ -61,6 +61,6 @@ export default new StackNavigator({ initialRouteName: 'Main', cardStyle: { backgroundColor: '#fff' - }, - mode: 'modal' + } + // mode: 'modal' }); diff --git a/app/utils/debounce.js b/app/utils/debounce.js new file mode 100644 index 000000000..2291167c7 --- /dev/null +++ b/app/utils/debounce.js @@ -0,0 +1,14 @@ +export default function debounce(func, wait, immediate) { + let timeout; + return function _debounce(...args) { + const context = this; + const later = function __debounce() { + timeout = null; + if (!immediate) { func.apply(context, args); } + }; + const callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) { func.apply(context, args); } + }; +} diff --git a/app/utils/throttle.js b/app/utils/throttle.js new file mode 100644 index 000000000..58afcbffd --- /dev/null +++ b/app/utils/throttle.js @@ -0,0 +1,21 @@ +export default function throttle(fn, threshhold = 250, scope) { + let last, + deferTimer; + return function() { + const context = scope || this; + + let now = +new Date(), + args = arguments; + if (last && now < last + threshhold) { + // hold on to it + clearTimeout(deferTimer); + deferTimer = setTimeout(() => { + last = now; + fn.apply(context, args); + }, threshhold); + } else { + last = now; + fn.apply(context, args); + } + }; +} diff --git a/app/views/room.js b/app/views/room.js index 18d6bf223..cc798fd1a 100644 --- a/app/views/room.js +++ b/app/views/room.js @@ -1,13 +1,14 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Text, View, FlatList, StyleSheet, Button } from 'react-native'; +import { Text, View, StyleSheet, Button } from 'react-native'; +import { ListView } from 'realm/react-native'; import realm from '../lib/realm'; import RocketChat from '../lib/rocketchat'; - +import debounce from '../utils/throttle'; import Message from '../components/Message'; import MessageBox from '../components/MessageBox'; -import KeyboardView from '../components/KeyboardView'; - +// import KeyboardView from '../components/KeyboardView'; +const ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 }); const styles = StyleSheet.create({ container: { flex: 1 @@ -45,17 +46,18 @@ export default class RoomView extends React.Component { title: navigation.state.params.name || realm.objectForPrimaryKey('subscriptions', navigation.state.params.sid).name }); + constructor(props) { super(props); this.rid = props.navigation.state.params.rid || realm.objectForPrimaryKey('subscriptions', props.navigation.state.params.sid).rid; // this.rid = 'GENERAL'; - + this.data = realm.objects('messages').filtered('_server.id = $0 AND rid = $1', RocketChat.currentServer, this.rid).sorted('ts', true); this.state = { - dataSource: [], + dataSource: ds.cloneWithRows(this.data.slice(0, 10)), loaded: true, joined: typeof props.navigation.state.params.rid === 'undefined' }; - + // console.log(this.messages); this.url = realm.objectForPrimaryKey('settings', 'Site_Url').value; } @@ -68,16 +70,13 @@ export default class RoomView extends React.Component { this.setState({ loaded: true }); + this.data.addListener(this.updateState); }); - - this.data = realm.objects('messages').filtered('_server.id = $0 AND rid = $1', RocketChat.currentServer, this.rid).sorted('ts', true); - - this.setState({ - dataSource: this.data - }); - this.data.addListener(this.updateState); + this.updateState(); + } + componentDidMount() { + return RocketChat.readMessages(this.rid); } - componentWillUnmount() { this.data.removeListener(this.updateState); } @@ -85,12 +84,12 @@ export default class RoomView extends React.Component { onEndReached = () => { if (this.state.dataSource.length && this.state.loaded && this.state.loadingMore !== true && this.state.end !== true) { this.setState({ - ...this.state, + // ...this.state, loadingMore: true }); RocketChat.loadMessagesForRoom(this.rid, this.state.dataSource[this.state.dataSource.length - 1].ts, ({ end }) => { this.setState({ - ...this.state, + // ...this.state, loadingMore: false, end }); @@ -98,11 +97,15 @@ export default class RoomView extends React.Component { } } - updateState = (data) => { + updateState = debounce(() => { this.setState({ - dataSource: data + dataSource: ds.cloneWithRows(this.data) }); - }; + // RocketChat.readMessages(this.rid); + // this.setState({ + // messages: this.messages + // }); + }, 100); sendMessage = message => RocketChat.sendMessage(this.rid, message); @@ -148,6 +151,7 @@ export default class RoomView extends React.Component { } return ( this.box = box} onSubmit={this.sendMessage} rid={this.rid} /> @@ -165,22 +169,24 @@ export default class RoomView extends React.Component { } render() { + // data={this.state.dataSource} + // extraData={this.state} + // renderItem={this.renderItem} + // keyExtractor={item => item._id} + // return ( - + {this.renderBanner()} - this.listView = ref} + item._id} - onEndReached={this.onEndReached} onEndReachedThreshold={0.1} ListFooterComponent={this.renderHeader()} + onEndReached={this.onEndReached} + dataSource={this.state.dataSource} + renderRow={item => this.renderItem({ item })} /> {this.renderFooter()} - + ); } } diff --git a/app/views/roomsList.js b/app/views/roomsList.js index 6690c8d6d..30eac8c0d 100644 --- a/app/views/roomsList.js +++ b/app/views/roomsList.js @@ -1,12 +1,14 @@ import ActionButton from 'react-native-action-button'; +import { ListView } from 'realm/react-native'; import Icon from 'react-native-vector-icons/Ionicons'; import React from 'react'; import PropTypes from 'prop-types'; -import { Button, Text, View, FlatList, StyleSheet, TouchableOpacity, Platform, TextInput } from 'react-native'; +import { Button, Text, View, StyleSheet, TouchableOpacity, Platform, TextInput } from 'react-native'; import Meteor from 'react-native-meteor'; import realm from '../lib/realm'; import RocketChat from '../lib/rocketchat'; import RoomItem from '../components/RoomItem'; +import debounce from '../utils/debounce'; const styles = StyleSheet.create({ container: { @@ -72,6 +74,28 @@ Meteor.Accounts.onLogin(() => { console.log('onLogin'); }); +const ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 }); +class RoomsListItem extends React.PureComponent { + static propTypes = { + item: PropTypes.object.isRequired, + onPress: PropTypes.func.isRequired + } + _onPress = (...args) => { + this.props.onPress(...args); + }; + + render() { + const { item } = this.props; + return ( + this.props.onPress(item._id, item)}> + + + ); + } +} export default class RoomsListView extends React.Component { static propTypes = { navigation: PropTypes.object.isRequired @@ -95,9 +119,9 @@ export default class RoomsListView extends React.Component { constructor(props) { super(props); - + this.data = realm.objects('subscriptions').filtered('_server.id = $0', RocketChat.currentServer).sorted('_updatedAt', true); this.state = { - dataSource: [], + dataSource: ds.cloneWithRows(this.data.sorted('_updatedAt', true).slice(0, 10)), searching: false, searchDataSource: [], searchText: '' @@ -168,40 +192,23 @@ export default class RoomsListView extends React.Component { } } } - setInitialData = () => { if (this.data) { this.data.removeListener(this.updateState); } - this.data = realm.objects('subscriptions').filtered('_server.id = $0', RocketChat.currentServer).sorted('name'); - this.data.addListener(this.updateState); + this.updateState(); + } + + getSubscriptions = () => this.data.sorted('_updatedAt', true) + + updateState = debounce(() => { this.setState({ - dataSource: this.sort(this.data) + dataSource: ds.cloneWithRows(this.data) }); - } - - sort = (data) => { - return data.slice().sort((a, b) => { - if (a.unread < b.unread) { - return 1; - } - - if (a.unread > b.unread) { - return -1; - } - - return 0; - }); - } - - updateState = (data) => { - this.setState({ - dataSource: this.sort(data) - }); - } + }, 500); _onPressItem = (id, item = {}) => { const { navigate } = this.props.navigation; @@ -222,8 +229,8 @@ export default class RoomsListView extends React.Component { .then(subs => navigate('Room', { sid: subs[0]._id })) .then(() => clearSearch()); } else { - navigate('Room', { rid: item._id, name: item.name }); clearSearch(); + navigate('Room', { rid: item._id, name: item.name }); } return; } @@ -256,12 +263,7 @@ export default class RoomsListView extends React.Component { } renderItem = ({ item }) => ( - this._onPressItem(item._id, item)}> - - + this._onPressItem(item._id, item)} /> ); renderSeparator = () => ( @@ -282,25 +284,25 @@ export default class RoomsListView extends React.Component { ); - renderList = () => { - if (!this.state.searching && !this.state.dataSource.length) { - return ( - - No rooms - - ); - } + // if (!this.state.searching && !this.state.dataSource.length) { + // return ( + // + // No rooms + // + // ); + // } + renderList = () => ( + // data={this.state.searching ? this.state.searchDataSource : this.state.dataSource} + // keyExtractor={item => item._id} + // ItemSeparatorComponent={this.renderSeparator} + // renderItem={this.renderItem} + this.renderItem({ item })} + /> + ) - return ( - item._id} - ItemSeparatorComponent={this.renderSeparator} - /> - ); - } renderCreateButtons() { return ( diff --git a/package-lock.json b/package-lock.json index 906c8db80..77753daf3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3683,6 +3683,26 @@ "resolved": "https://registry.npmjs.org/react-native-action-button/-/react-native-action-button-2.7.2.tgz", "integrity": "sha1-BvEYjo/h0Y0D/JBg1LYEybgbtso=" }, + "react-native-auto-grow-textinput": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/react-native-auto-grow-textinput/-/react-native-auto-grow-textinput-1.2.0.tgz", + "integrity": "sha512-O+mT2GOrDRzJdg2GbdfuGlO/nn/J8c9pdBCPahLYA8yiAjayAG67XOujGrfuv/wNCF7W94NsYdyfaf2hlOIhYQ==" + }, + "react-native-autogrow-input": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/react-native-autogrow-input/-/react-native-autogrow-input-0.2.1.tgz", + "integrity": "sha512-vWcfqGqzDw4XqRJr4HnHC+dcGAfJDYZiF2B0tBZjtjA6MNSv2TNz5knYZjvLggRgmEflj02r88scvfFputsRig==" + }, + "react-native-autogrow-textinput": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/react-native-autogrow-textinput/-/react-native-autogrow-textinput-4.1.0.tgz", + "integrity": "sha1-p+WxfrPBarCOMbv7iNkkiO2H8nY=" + }, + "react-native-console-time-polyfill": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/react-native-console-time-polyfill/-/react-native-console-time-polyfill-0.0.6.tgz", + "integrity": "sha1-eCPYb+g0OcdEgNGxJKkrGnhXGIk=" + }, "react-native-dismiss-keyboard": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/react-native-dismiss-keyboard/-/react-native-dismiss-keyboard-1.0.0.tgz", @@ -3718,6 +3738,11 @@ "resolved": "https://registry.npmjs.org/react-native-form-generator/-/react-native-form-generator-0.9.9.tgz", "integrity": "sha1-aKribR6Nw+MAc8zXuymPvf3OG8o=" }, + "react-native-image-picker": { + "version": "0.26.3", + "resolved": "https://registry.npmjs.org/react-native-image-picker/-/react-native-image-picker-0.26.3.tgz", + "integrity": "sha1-CtLu3klQGnBG2ARqc4E2llOcPc0=" + }, "react-native-img-cache": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/react-native-img-cache/-/react-native-img-cache-1.4.0.tgz", @@ -3735,6 +3760,11 @@ "resolved": "https://registry.npmjs.org/react-native-meteor/-/react-native-meteor-1.1.0.tgz", "integrity": "sha1-Vake/i1GbTqMzrW1QZeZru3NZ1Y=" }, + "react-native-optimized-flatlist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/react-native-optimized-flatlist/-/react-native-optimized-flatlist-1.0.1.tgz", + "integrity": "sha1-2+6C8gi0i+8jxssm8dXzrFjmdbI=" + }, "react-native-svg": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-5.4.1.tgz", diff --git a/package.json b/package.json index 5cc619e61..bd8f8ba14 100644 --- a/package.json +++ b/package.json @@ -16,12 +16,17 @@ "react-emojione": "^3.1.10", "react-native": "0.46.1", "react-native-action-button": "^2.7.2", + "react-native-auto-grow-textinput": "^1.2.0", + "react-native-autogrow-input": "^0.2.1", + "react-native-autogrow-textinput": "^4.1.0", + "react-native-console-time-polyfill": "0.0.6", "react-native-easy-markdown": "git+https://github.com/lappalj4/react-native-easy-markdown.git", "react-native-fetch-blob": "^0.10.8", "react-native-form-generator": "^0.9.9", "react-native-image-picker": "^0.26.3", "react-native-img-cache": "^1.4.0", "react-native-meteor": "^1.1.0", + "react-native-optimized-flatlist": "^1.0.1", "react-native-svg": "^5.4.1", "react-native-svg-image": "^1.1.4", "react-native-vector-icons": "^4.3.0",