parent
3cd281b66f
commit
7a1d359a4f
|
@ -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,
|
||||
|
|
|
@ -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` : '';
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
});
|
|
@ -5,7 +5,7 @@ const initialState = {
|
|||
connected: false,
|
||||
errorMessage: '',
|
||||
failure: false,
|
||||
server: {}
|
||||
server: ''
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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];
|
||||
RocketChat.loadMessagesForRoom(this.rid, lastRowData.ts, ({ end }) => {
|
||||
this.setState({
|
||||
loadingMore: false,
|
||||
end
|
||||
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'
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue