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 @@
-
-