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 871e0088b..7611dd3d1 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 d5860269b..4c81d5da7 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 55d710fa0..59bf9dd5e 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 a30eaeb6d..049f8d3f8 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 230780c23..746bf068e 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 c5442ee2b..ec6f98ae6 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 c393c0dd9..9d7e22241 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 {