diff --git a/css/base.css b/css/base.css
index 2ec07f45..8c1705f2 100644
--- a/css/base.css
+++ b/css/base.css
@@ -1447,3 +1447,39 @@ ul.tabbar li.active > a:after {
width: 0.25rem;
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); }
+}
diff --git a/index.html b/index.html
index 5a1e5cf6..f94bae4c 100644
--- a/index.html
+++ b/index.html
@@ -47,9 +47,12 @@
// Mediaquery breakpoint between desktop and mobile, in px. Should match the value
// in @meadia (max-size: 640px) in base.css
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;
+ // Number of chat messages to fetch in one call.
+ var MESSAGES_PAGE = 24;
+
// Short representation of time in the past.
function shortDateFormat(then) {
var locale = window.navigator.userLanguage || window.navigator.language;
@@ -285,6 +288,13 @@
}
});
+ class LoadSpinner extends React.PureComponent {
+ render() {
+ return (this.props.show ?
+
: null);
+ }
+ }
+
/* BEGIN Lettertile: Avatar box: either a bitmap or a letter tile or a stock icon. */
class LetterTile extends React.PureComponent {
render() {
@@ -2318,25 +2328,35 @@
messages: [],
topic: '',
title: '',
- avatar: null
+ avatar: null,
+ scrollPosition: 0
};
},
+
// Scroll last message into view on component update e.g. on message received.
componentDidUpdate: function() {
- var node = this.refs.messagesPanel;
- if (node) {
- node.scrollTop = node.scrollHeight;
+ if (this.messagesScroller) {
+ this.messagesScroller.scrollTop = this.messagesScroller.scrollHeight - this.state.scrollPosition;
}
},
+
componentWillMount: function() {
this.propsChange(this.props);
},
+
+ componentWillUnmount: function() {
+ this.leave();
+ if (this.messagesScroller) {
+ this.messagesScroller.removeEventListener('scroll', this.fetchMoreMessages);
+ }
+ },
componentWillReceiveProps: function(nextProps) {
this.propsChange(nextProps);
},
propsChange: function(props) {
- if (!props.topic) {
+ if (!props || !props.topic) {
this.setState({messages: [], topic: null});
+ this.leave();
} else if (props.topic != this.state.topic) {
var msgs = [];
var topic = Tinode.getTopic(props.topic);
@@ -2346,8 +2366,29 @@
topic.onMetaDesc = this.handleDescChange;
topic.onSubsUpdated = this.handleSubsUpdated;
// Unbind the previous topic from this component.
- if (this.state.topic) {
- var oldTopic = Tinode.getTopic(this.state.topic);
+ 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) {
+ var oldTopic = Tinode.getTopic(this.state.topic);
+ if (oldTopic) {
if (oldTopic.isSubscribed()) {
oldTopic.leave(false);
}
@@ -2356,23 +2397,38 @@
oldTopic.onMetaDesc = 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) {
if (desc.public) {
this.setState({
@@ -2386,22 +2442,28 @@
},
handleNewMessage: function(msg) {
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) {
- 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.
this.readTimer = setTimeout(function() {
- Tinode.getTopic(msg.topic).noteRead(msg.seq);
+ topic.noteRead(msg.seq);
}, READ_DELAY);
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) {
switch (info.what) {
@@ -2424,6 +2486,10 @@
this.props.showContextMenu(this.state.topic, e.pageY, e.pageX);
},
+ handleBackNavigation: function() {
+ this.props.onHideMessagesView();
+ },
+
render: function() {
var component = null;
if (this.state.topic) {
@@ -2493,7 +2559,7 @@
{this.props.displayMobile ?
-
+
arrow_back
:
@@ -2514,8 +2580,9 @@
-
-
+
+
@@ -2847,7 +2914,11 @@
},
// In mobile view user requested to show sidepanel
handleHideMessagesView: function() {
- this.setState({mobilePanel: 'sidepanel'});
+ this.setState({
+ mobilePanel: 'sidepanel',
+ topicSelected: null,
+ showInfoPanel: false
+ });
},
// User clicked on a message send button.
handleSendMessage: function(msg) {