From 2861fa26a7046981b141240246de8665f420b84b Mon Sep 17 00:00:00 2001 From: lilia Date: Tue, 10 Nov 2015 16:03:19 -0800 Subject: [PATCH] Implement infinite scrolling message lists Only load the most recent messages when initially rendering a conversation. Scrolling to the top of a message list loads older messages. This required some slight refactoring of how we insert message elements into the dom. If the message is added to the end of the collection, append it at the end. Otherwise, assume it is an older message and prepend it. When adding elements to the top, reset the scrollPosition to its previous distance from scrollHeight. This keeps the current set of elements fixed in the viewport. // FREEBIE --- js/models/messages.js | 33 +++++++++++++++++++++------------ js/views/conversation_view.js | 17 +++++++++++++---- js/views/inbox_view.js | 8 -------- js/views/message_list_view.js | 31 ++++++++++++++++++++++++------- stylesheets/_conversation.scss | 8 ++++++++ stylesheets/_global.scss | 2 +- stylesheets/manifest.css | 9 ++++++++- 7 files changed, 75 insertions(+), 33 deletions(-) diff --git a/js/models/messages.js b/js/models/messages.js index 871e0088..7611dd3d 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -357,18 +357,27 @@ }, fetchConversation: function(conversationId) { - var options = {remove: false}; - options.index = { - // 'conversation' index on [conversationId, received_at] - name : 'conversation', - lower : [conversationId], - upper : [conversationId, Number.MAX_VALUE] - // SELECT messages WHERE conversationId = this.id ORDER - // received_at DESC - }; - // TODO pagination/infinite scroll - // limit: 10, offset: page*10, - return this.fetch(options); + return new Promise(function(resolve) { + var upper; + if (this.length === 0) { + // fetch the most recent messages first + upper = Number.MAX_VALUE; + } else { + // not our first rodeo, fetch older messages. + upper = this.at(0).get('received_at'); + } + var options = {remove: false, limit: 100}; + options.index = { + // 'conversation' index on [conversationId, received_at] + name : 'conversation', + lower : [conversationId], + upper : [conversationId, upper], + order : 'desc' + // SELECT messages WHERE conversationId = this.id ORDER + // received_at DESC + }; + this.fetch(options).then(resolve); + }.bind(this)); }, hasKeyConflicts: function() { diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index d5860269..4c81d5da 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -64,9 +64,7 @@ this.remove(); }.bind(this)); - setTimeout(function() { - this.view.scrollToBottom(); - }.bind(this), 10); + this.fetchMessages(); }, events: { @@ -84,12 +82,23 @@ 'click' : 'onClick', 'select .message-list .entry': 'messageDetail', 'force-resize': 'forceUpdateMessageFieldSize', - 'click .choose-file': 'focusMessageField' + 'click .choose-file': 'focusMessageField', + 'loadMore .message-list': 'fetchMessages' }, focusMessageField: function() { this.$messageField.focus(); }, + fetchMessages: function() { + this.$('.message-list').addClass('loading'); + return this.model.fetchContacts().then(function() { + return this.model.fetchMessages().then(function() { + this.$('.message-list').removeClass('loading'); + }.bind(this)); + }.bind(this)); + // TODO catch? + }, + addMessage: function(message) { this.model.messageCollection.add(message, {merge: true}); }, diff --git a/js/views/inbox_view.js b/js/views/inbox_view.js index 55d710fa..59bf9dd5 100644 --- a/js/views/inbox_view.js +++ b/js/views/inbox_view.js @@ -54,18 +54,10 @@ appWindow: this.model.appWindow }); $el = view.$el; - if (conversation.messageCollection.length === 0) { - $el.find('.message-list').addClass('loading'); - } } $el.prependTo(this.el); $el.find('.message-list').trigger('reset-scroll'); $el.trigger('force-resize'); - conversation.fetchContacts().then(function() { - conversation.fetchMessages().then(function() { - $el.find('.message-list').removeClass('loading'); - }); - }); conversation.markRead(); conversation.trigger('opened'); } diff --git a/js/views/message_list_view.js b/js/views/message_list_view.js index a30eaeb6..049f8d3f 100644 --- a/js/views/message_list_view.js +++ b/js/views/message_list_view.js @@ -10,14 +10,15 @@ className: 'message-list', itemView: Whisper.MessageView, events: { - 'add': 'onAdd', - 'update *': 'scrollToBottom', - 'scroll': 'measureScrollPosition', + 'update *': 'scrollToBottomIfNeeded', + 'scroll': 'onScroll', 'reset-scroll': 'resetScrollPosition' }, - onAdd: function() { - this.$el.removeClass('loading'); - this.scrollToBottom(); + onScroll: function() { + this.measureScrollPosition(); + if (this.$el.scrollTop() === 0) { + this.$el.trigger('loadMore'); + } }, measureScrollPosition: function() { if (this.el.scrollHeight === 0) { // hidden @@ -47,6 +48,22 @@ addAll: function() { Whisper.ListView.prototype.addAll.apply(this, arguments); // super() this.scrollToBottom(); - } + }, + addOne: function(model) { + if (this.itemView) { + var view = new this.itemView({model: model}).render(); + if (this.collection.indexOf(model) === this.collection.length - 1) { + // add to the bottom. + this.$el.append(view.el); + this.scrollToBottom(); + } else { + // add to the top. + var offset = this.el.scrollHeight - this.$el.scrollTop(); + this.$el.prepend(view.el); + this.$el.scrollTop(this.el.scrollHeight - offset); + } + } + this.$el.removeClass('loading'); + }, }); })(); diff --git a/stylesheets/_conversation.scss b/stylesheets/_conversation.scss index 230780c2..746bf068 100644 --- a/stylesheets/_conversation.scss +++ b/stylesheets/_conversation.scss @@ -219,6 +219,14 @@ } .message-list { + position: relative; + &::before { + display: block; + margin: $header-height auto; + content: " "; + height: $header-height; + width: $header-height; + } margin: 0; padding: 1em 0; overflow-y: auto; diff --git a/stylesheets/_global.scss b/stylesheets/_global.scss index c5442ee2..ec6f98ae 100644 --- a/stylesheets/_global.scss +++ b/stylesheets/_global.scss @@ -396,7 +396,7 @@ $avatar-size: 44px; .loading { position: relative; - &::after { + &::before { display: block; margin: $header-height auto; content: " "; diff --git a/stylesheets/manifest.css b/stylesheets/manifest.css index c393c0dd..9d7e2224 100644 --- a/stylesheets/manifest.css +++ b/stylesheets/manifest.css @@ -315,7 +315,7 @@ img.emoji { .loading { position: relative; } - .loading::after { + .loading::before { display: block; margin: 36px auto; content: " "; @@ -666,9 +666,16 @@ input.search { opacity: 1; } .message-list { + position: relative; margin: 0; padding: 1em 0; overflow-y: auto; } + .message-list::before { + display: block; + margin: 36px auto; + content: " "; + height: 36px; + width: 36px; } .message-list .timestamp { cursor: pointer; } .message-list .timestamp:hover {