message pagination

This commit is contained in:
or-else
2017-09-12 18:21:33 -07:00
parent d50da89403
commit 9adff7572e
2 changed files with 144 additions and 37 deletions
+36
View File
@@ -1447,3 +1447,39 @@ ul.tabbar li.active > a:after {
width: 0.25rem; width: 0.25rem;
height: 1.175rem; height: 1.175rem;
} }
.load-spinner-box {
position: absolute;
box-shadow: 0.1rem 0.1rem 0.15rem 1px rgba(0,0,0,0.25);
border-radius: 50%;
padding: 0.25rem;
width: 2.5rem;
height: 2.5rem;
left: 0;
right: 0;
top: 5rem;
margin-left: auto;
margin-right: auto;
background-color: white;
z-index: 2;
}
.loader-spinner {
border: 0.35rem solid #f3f3f3;
border-radius: 50%;
border-top: 0.35rem solid #1e88e5;
width: 2rem;
height: 2rem;
-webkit-animation: spin 1.5s linear infinite;
animation: spin 1.5s linear infinite;
}
@-webkit-keyframes spin {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
+106 -35
View File
@@ -47,9 +47,12 @@
// Mediaquery breakpoint between desktop and mobile, in px. Should match the value // Mediaquery breakpoint between desktop and mobile, in px. Should match the value
// in @meadia (max-size: 640px) in base.css // in @meadia (max-size: 640px) in base.css
var MEDIA_BREAKPOINT = 640; var MEDIA_BREAKPOINT = 640;
// Size of rem unit in pixels. Default 1rem = 10pt = 13px. // Size of css 'rem' unit in pixels. Default 1rem = 10pt = 13px.
var REM_SIZE = 13; var REM_SIZE = 13;
// Number of chat messages to fetch in one call.
var MESSAGES_PAGE = 24;
// Short representation of time in the past. // Short representation of time in the past.
function shortDateFormat(then) { function shortDateFormat(then) {
var locale = window.navigator.userLanguage || window.navigator.language; var locale = window.navigator.userLanguage || window.navigator.language;
@@ -285,6 +288,13 @@
} }
}); });
class LoadSpinner extends React.PureComponent {
render() {
return (this.props.show ?
<div className="load-spinner-box"><div className="loader-spinner"></div></div> : null);
}
}
/* BEGIN Lettertile: Avatar box: either a bitmap or a letter tile or a stock icon. */ /* BEGIN Lettertile: Avatar box: either a bitmap or a letter tile or a stock icon. */
class LetterTile extends React.PureComponent { class LetterTile extends React.PureComponent {
render() { render() {
@@ -2318,25 +2328,35 @@
messages: [], messages: [],
topic: '', topic: '',
title: '', title: '',
avatar: null avatar: null,
scrollPosition: 0
}; };
}, },
// Scroll last message into view on component update e.g. on message received. // Scroll last message into view on component update e.g. on message received.
componentDidUpdate: function() { componentDidUpdate: function() {
var node = this.refs.messagesPanel; if (this.messagesScroller) {
if (node) { this.messagesScroller.scrollTop = this.messagesScroller.scrollHeight - this.state.scrollPosition;
node.scrollTop = node.scrollHeight;
} }
}, },
componentWillMount: function() { componentWillMount: function() {
this.propsChange(this.props); this.propsChange(this.props);
}, },
componentWillUnmount: function() {
this.leave();
if (this.messagesScroller) {
this.messagesScroller.removeEventListener('scroll', this.fetchMoreMessages);
}
},
componentWillReceiveProps: function(nextProps) { componentWillReceiveProps: function(nextProps) {
this.propsChange(nextProps); this.propsChange(nextProps);
}, },
propsChange: function(props) { propsChange: function(props) {
if (!props.topic) { if (!props || !props.topic) {
this.setState({messages: [], topic: null}); this.setState({messages: [], topic: null});
this.leave();
} else if (props.topic != this.state.topic) { } else if (props.topic != this.state.topic) {
var msgs = []; var msgs = [];
var topic = Tinode.getTopic(props.topic); var topic = Tinode.getTopic(props.topic);
@@ -2346,8 +2366,29 @@
topic.onMetaDesc = this.handleDescChange; topic.onMetaDesc = this.handleDescChange;
topic.onSubsUpdated = this.handleSubsUpdated; topic.onSubsUpdated = this.handleSubsUpdated;
// Unbind the previous topic from this component. // Unbind the previous topic from this component.
this.leave();
if (topic.isSubscribed()) {
this.handleDescChange(topic);
topic.messages(function(msg) {
msgs = msgs.concat(msg);
});
} else {
topic.subscribe({get: {desc: {}, sub: {}, data: {limit: MESSAGES_PAGE}}});
}
// The user switched to the new topic before the timer for
// the previous topic has triggered, kill it.
clearTimeout(this.readTimer);
this.setState({messages: msgs, topic: props.topic, scrollPosition: 0});
}
},
leave: function() {
if (this.state.topic) { if (this.state.topic) {
var oldTopic = Tinode.getTopic(this.state.topic); var oldTopic = Tinode.getTopic(this.state.topic);
if (oldTopic) {
if (oldTopic.isSubscribed()) { if (oldTopic.isSubscribed()) {
oldTopic.leave(false); oldTopic.leave(false);
} }
@@ -2356,23 +2397,38 @@
oldTopic.onMetaDesc = undefined; oldTopic.onMetaDesc = undefined;
oldTopic.onSubsUpdated = undefined; oldTopic.onSubsUpdated = undefined;
} }
if (topic.isSubscribed()) {
this.handleDescChange(topic);
topic.messages(function(msg) {
msgs = msgs.concat(msg);
});
} else {
topic.subscribe({get: {desc: {}, sub: {}, data: {}}});
}
// The user switched to the new topic before the timer for
// the previous topic has triggered, kill it.
clearTimeout(this.readTimer);
this.setState({messages: msgs, topic: props.topic});
} }
}, },
handleScrollReference: function(node) {
if (node) {
node.addEventListener('scroll', this.fetchMoreMessages);
this.messagesScroller = node;
}
},
fetchMoreMessages: function(event) {
var instance = this;
if (event.target.scrollTop <= 0) {
var started = false;
var newState = {scrollPosition: event.target.scrollHeight - event.target.scrollTop};
this.setState(function(prevState, props) {
if (!prevState.fetchingMessages) {
var topic = Tinode.getTopic(instance.state.topic);
if (topic && topic.msgHasMore()) {
newState.fetchingMessages = true;
console.log("START fetching messages from " + topic._minSeq);
topic.getMessagesPage(MESSAGES_PAGE).then(function() {
instance.setState({fetchingMessages: false});
console.log("DONE fetching messages");
});
}
}
return newState;
});
}
},
handleDescChange: function(desc) { handleDescChange: function(desc) {
if (desc.public) { if (desc.public) {
this.setState({ this.setState({
@@ -2386,22 +2442,28 @@
}, },
handleNewMessage: function(msg) { handleNewMessage: function(msg) {
clearTimeout(this.readTimer); clearTimeout(this.readTimer);
// Regenerate messages list
var topic = Tinode.getTopic(this.state.topic);
var newState = {messages: []};
topic.messages(function(m) {
newState.messages = newState.messages.concat(m);
});
// msg could be null if one or more messages were deleted.
if (msg) { if (msg) {
this.setState({messages: this.state.messages.concat(msg)}); // If the message is added to the end of the message list,
// scroll to the bottom.
if (topic.msgPosition(msg.seq) <= 1) {
newState.scrollPosition = 0;
}
// Aknowledge all messages, including own messges. // Aknowledge all messages, including own messges.
this.readTimer = setTimeout(function() { this.readTimer = setTimeout(function() {
Tinode.getTopic(msg.topic).noteRead(msg.seq); topic.noteRead(msg.seq);
}, READ_DELAY); }, READ_DELAY);
this.props.onData(msg); this.props.onData(msg);
} else {
// msg could be null if one or more messages were deleted.
// Just regenerate the message list;
var _messages = [];
Tinode.getTopic(this.state.topic).messages(function(m) {
_messages.concat(m);
});
this.setState({messages: _messages});
} }
this.setState(newState);
}, },
handleInfoReceipt: function(info) { handleInfoReceipt: function(info) {
switch (info.what) { switch (info.what) {
@@ -2424,6 +2486,10 @@
this.props.showContextMenu(this.state.topic, e.pageY, e.pageX); this.props.showContextMenu(this.state.topic, e.pageY, e.pageX);
}, },
handleBackNavigation: function() {
this.props.onHideMessagesView();
},
render: function() { render: function() {
var component = null; var component = null;
if (this.state.topic) { if (this.state.topic) {
@@ -2493,7 +2559,7 @@
<div id="topic-view" className={this.props.hideSelf ? 'nodisplay' : null}> <div id="topic-view" className={this.props.hideSelf ? 'nodisplay' : null}>
<div id="topic-caption-panel" className="caption-panel"> <div id="topic-caption-panel" className="caption-panel">
{this.props.displayMobile ? {this.props.displayMobile ?
<a href="#" id="hide-message-view" onClick={this.props.onHideMessagesView}> <a href="#" id="hide-message-view" onClick={this.handleBackNavigation}>
<i className="material-icons big">arrow_back</i> <i className="material-icons big">arrow_back</i>
</a> </a>
: :
@@ -2514,8 +2580,9 @@
</a> </a>
</div> </div>
</div> </div>
<div id="messages-panel" ref="messagesPanel"> <LoadSpinner show={this.state.fetchingMessages} />
<ul className={chatBoxClass}> <div id="messages-panel" ref={this.handleScrollReference}>
<ul id="scroller" className={chatBoxClass}>
{messageNodes} {messageNodes}
</ul> </ul>
</div> </div>
@@ -2847,7 +2914,11 @@
}, },
// In mobile view user requested to show sidepanel // In mobile view user requested to show sidepanel
handleHideMessagesView: function() { handleHideMessagesView: function() {
this.setState({mobilePanel: 'sidepanel'}); this.setState({
mobilePanel: 'sidepanel',
topicSelected: null,
showInfoPanel: false
});
}, },
// User clicked on a message send button. // User clicked on a message send button.
handleSendMessage: function(msg) { handleSendMessage: function(msg) {