Merge pull request #3867 from oswaldoacauan/chat-performance-improvement

[HTML5] Chat performance improvement
This commit is contained in:
Anton Georgiev 2017-04-28 15:40:08 -04:00 committed by GitHub
commit 8363d76f98
4 changed files with 104 additions and 35 deletions

View File

@ -107,9 +107,8 @@ export default injectIntl(createContainer(({ params, intl }) => {
handleClosePrivateChat: chatID => ChatService.closePrivateChat(chatID), handleClosePrivateChat: chatID => ChatService.closePrivateChat(chatID),
handleSendMessage: message => { handleSendMessage: message => {
ChatService.updateScrollPosition(chatID, null);
let sentMessage = ChatService.sendMessage(chatID, message); let sentMessage = ChatService.sendMessage(chatID, message);
ChatService.updateScrollPosition(chatID, null); //null so its scrolls to bottom
// ChatService.updateUnreadMessage(chatID, sentMessage.from_time);
}, },
handleScrollUpdate: position => ChatService.updateScrollPosition(chatID, position), handleScrollUpdate: position => ChatService.updateScrollPosition(chatID, position),

View File

@ -18,7 +18,7 @@ const intlMessages = defineMessages({
emptyLogLabel: { emptyLogLabel: {
id: 'app.chat.emptyLogLabel', id: 'app.chat.emptyLogLabel',
description: 'aria-label used when chat log is empty', description: 'aria-label used when chat log is empty',
} },
}); });
class MessageList extends Component { class MessageList extends Component {
@ -73,7 +73,6 @@ class MessageList extends Component {
} }
componentWillUpdate(nextProps) { componentWillUpdate(nextProps) {
if (this.props.chatId !== nextProps.chatId) { if (this.props.chatId !== nextProps.chatId) {
this.shouldScrollBottom = false; this.shouldScrollBottom = false;
return; return;
@ -86,7 +85,8 @@ class MessageList extends Component {
//Compare with <1 to account for the chance scrollArea.scrollTop is a float //Compare with <1 to account for the chance scrollArea.scrollTop is a float
//value in some browsers. //value in some browsers.
this.shouldScrollBottom = position === scrollArea.scrollHeight || this.shouldScrollBottom = position === scrollArea.scrollHeight ||
(scrollArea.scrollHeight - position < 1); (scrollArea.scrollHeight - position < 1) ||
nextProps.scrollPosition === null;
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {

View File

@ -1,6 +1,7 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { FormattedTime } from 'react-intl'; import { FormattedTime } from 'react-intl';
import cx from 'classnames'; import cx from 'classnames';
import _ from 'lodash';
import UserAvatar from '/imports/ui/components/user-avatar/component'; import UserAvatar from '/imports/ui/components/user-avatar/component';
import Message from './message/component'; import Message from './message/component';
@ -16,10 +17,74 @@ const propTypes = {
const defaultProps = { const defaultProps = {
}; };
const eventsToBeBound = [
'scroll',
'resize',
];
const isElementInViewport = (el) => {
const rect = el.getBoundingClientRect();
const clientHeight = window.innerHeight || document.documentElement.clientHeight;
const prefetchHeight = 125;
return (
rect.top >= -(prefetchHeight) &&
rect.bottom <= clientHeight + prefetchHeight
);
};
export default class MessageListItem extends Component { export default class MessageListItem extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = {
pendingChanges: false,
preventRender: true,
};
this.handleMessageInViewport = _.debounce(this.handleMessageInViewport.bind(this), 50);
}
handleMessageInViewport() {
window.requestAnimationFrame(() => {
const node = this.refs.item;
this.setState({ preventRender: !isElementInViewport(node) });
});
}
componentDidMount() {
const scrollArea = document.getElementById(this.props.chatAreaId);
eventsToBeBound.forEach(
e => scrollArea.addEventListener(e, this.handleMessageInViewport, false)
);
this.handleMessageInViewport();
}
componentWillUnmount() {
const scrollArea = document.getElementById(this.props.chatAreaId);
eventsToBeBound.forEach(
e => scrollArea.removeEventListener(e, this.handleMessageInViewport, false)
);
}
componentDidUpdate(prevProps, prevState) {
if (prevState.preventRender && !this.state.preventRender && this.state.pendingChanges) {
this.setState({ pendingChanges: false });
}
}
componentWillReceiveProps(nextProps) {
if (this.state.pendingChanges) return;
const hasNewMessage = this.props.messages.length !== nextProps.messages.length;
const hasUserChanged = !_.isEqual(this.props.user, nextProps.user);
this.setState({ pendingChanges: hasNewMessage || hasUserChanged });
}
shouldComponentUpdate(nextProps, nextState) {
return !nextState.preventRender && nextState.pendingChanges;
} }
render() { render() {
@ -36,32 +101,34 @@ export default class MessageListItem extends Component {
} }
return ( return (
<div className={styles.item}> <div className={styles.item}>
<div className={styles.avatar}> <div className={styles.wrapper} ref="item">
<UserAvatar user={user} /> <div className={styles.avatar}>
</div> <UserAvatar user={user} />
<div className={styles.content}>
<div className={styles.meta}>
<div className={user.isOnline ? styles.name : styles.logout} aria-label={user.name}>
<span>{user.name}</span>
{user.isOnline ? null : <span className={styles.offline}>(offline)</span>}
</div>
<time className={styles.time} dateTime={dateTime}>
<FormattedTime value={dateTime}/>
</time>
</div> </div>
<div className={styles.messages}> <div className={styles.content}>
{messages.map((message, i) => ( <div className={styles.meta}>
<Message <div className={!user.isOnline ? styles.name : styles.logout}>
className={styles.message} <span>{user.name}</span>
key={message.id} {user.isOnline ? null : <span className={styles.offline}>(offline)</span>}
text={message.text} </div>
time={message.time} <time className={styles.time} dateTime={dateTime}>
chatAreaId={this.props.chatAreaId} <FormattedTime value={dateTime}/>
lastReadMessageTime={this.props.lastReadMessageTime} </time>
handleReadMessage={this.props.handleReadMessage} </div>
/> <div className={styles.messages}>
))} {messages.map((message, i) => (
<Message
className={styles.message}
key={message.id}
text={message.text}
time={message.time}
chatAreaId={this.props.chatAreaId}
lastReadMessageTime={this.props.lastReadMessageTime}
handleReadMessage={this.props.handleReadMessage}
/>
))}
</div>
</div> </div>
</div> </div>
</div> </div>
@ -75,7 +142,7 @@ export default class MessageListItem extends Component {
return ( return (
<div className={cx(styles.item, styles.systemMessage)}> <div className={cx(styles.item, styles.systemMessage)}>
<div className={styles.content}> <div className={styles.content} ref="item">
<div className={styles.messages}> <div className={styles.messages}>
{messages.map((message, i) => ( {messages.map((message, i) => (
<Message <Message

View File

@ -1,17 +1,20 @@
@import "../../../../stylesheets/variables/_all"; @import "../../../../stylesheets/variables/_all";
.item { .item {
display: flex;
flex-flow: row;
flex: 1;
margin-bottom: $line-height-computed;
font-size: $font-size-base * .90; font-size: $font-size-base * .90;
margin-bottom: $line-height-computed;
&:last-child { &:last-child {
margin-bottom: 0 !important; margin-bottom: 0 !important;
} }
} }
.wrapper {
display: flex;
flex-flow: row;
flex: 1;
}
.systemMessage { .systemMessage {
.item + &, .item + &,