From b60b20bde4114dd3118f589791702bc84dc061b4 Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Tue, 30 May 2017 17:29:23 -0700 Subject: [PATCH] Mark messages read only when visible, on receipt, focus, scroll - Only mark messages read when scrolling if in focus and visible - Remove last seen indicator when scrolling to the bottom with scroll down button - Update last seen indicator when we don't already have one and we're scrolled up. FREEBIE --- js/models/conversations.js | 26 ++++++++++--- js/views/conversation_view.js | 71 +++++++++++++++++++++++++++++++---- js/views/message_list_view.js | 9 +++++ 3 files changed, 94 insertions(+), 12 deletions(-) diff --git a/js/models/conversations.js b/js/models/conversations.js index 669969506..0a9bdfd3d 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -71,7 +71,7 @@ }.bind(this)); }, - getUnread: function() { + getUnread: function(newestUnreadDate) { var conversationId = this.id; var unreadMessages = new Whisper.MessageCollection(); return new Promise(function(resolve) { @@ -83,7 +83,15 @@ upper : [conversationId, Number.MAX_VALUE], } }).always(function() { - resolve(unreadMessages); + if (!newestUnreadDate) { + return resolve(unreadMessages); + } + + // TODO: look into an index which would allow us to efficiently get the + // set of unread messages before a certain date. + resolve(unreadMessages.filter(function(message) { + return message.get('received_at') <= newestUnreadDate; + })); }); }); @@ -291,15 +299,14 @@ } }, - markRead: function() { + markRead: function(newestUnreadDate) { if (this.get('unreadCount') > 0) { - this.save({ unreadCount: 0 }); var conversationId = this.id; Whisper.Notifications.remove(Whisper.Notifications.where({ conversationId: conversationId })); - this.getUnread().then(function(unreadMessages) { + this.getUnread(newestUnreadDate).then(function(unreadMessages) { var read = unreadMessages.map(function(m) { if (this.messageCollection.get(m.id)) { m = this.messageCollection.get(m.id); @@ -313,7 +320,16 @@ timestamp : m.get('sent_at') }; }.bind(this)); + if (read.length > 0) { + var unreadCount = this.get('unreadCount'); + unreadCount = unreadCount - read.length; + if (unreadCount < 0) { + console.log('conversation unreadCount went below zero!'); + unreadCount = 0; + } + this.save({ unreadCount: unreadCount }); + console.log('Sending', read.length, 'read receipts'); textsecure.messaging.syncReadMessages(read); } diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index 3139fa3bd..95c8b1971 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -160,6 +160,7 @@ 'newOffscreenMessage .message-list': 'addScrollDownButtonWithCount', 'atBottom .message-list': 'hideScrollDownButton', 'farFromBottom .message-list': 'addScrollDownButton', + 'lazyScroll .message-list': 'onLazyScroll', 'close .menu': 'closeMenu', 'select .message-list .entry': 'messageDetail', 'force-resize': 'forceUpdateMessageFieldSize', @@ -206,9 +207,14 @@ this.$('.bottom-bar form').addClass('active'); }, + onLazyScroll: function() { + if (!this.isHidden() && window.isFocused()) { + this.markRead(); + } + }, updateUnread: function() { this.updateLastSeenIndicator(); - this.model.markRead(); + this.markRead(); }, onOpened: function() { @@ -266,6 +272,8 @@ if (location > 0) { this.lastSeenIndicator.el.scrollIntoView(); return; + } else { + this.removeLastSeenIndicator(); } } this.view.scrollToBottom(); @@ -362,8 +370,6 @@ this.model.messageCollection.add(message, {merge: true}); message.setToExpire(); - // If the last seen indicator is old enough, it will go away. - // if it isn't, we want to make sure it's up to date if (this.lastSeenIndicator) { this.lastSeenIndicator.increment(1); } @@ -374,10 +380,11 @@ } else if (!this.isHidden() && window.isFocused()) { // The conversation is visible and in focus + this.markRead(); - if (this.view.atBottom()) { - this.markRead(); - } else { + // When we're scrolled up and we don't already have a last seen indicator + // we add a new one. + if (!this.view.atBottom() && !this.lastSeenIndicator) { this.updateLastSeenIndicator({scroll: false}); } } @@ -402,8 +409,58 @@ this.markRead(e); }, + findNewestVisibleUnread: function() { + var collection = this.model.messageCollection; + var length = collection.length; + var viewportBottom = this.view.outerHeight; + var unreadCount = this.model.get('unreadCount'); + + if (unreadCount < 1) { + return; + } + + // Start with the most recent message, search backwards in time + var foundUnread = 0; + for (var i = length - 1; i >= 0; i -= 1) { + // We don't want to search through all messages, so we stop after we've + // hit all unread messages. The unread should be relatively recent. + if (foundUnread >= unreadCount) { + return; + } + + var message = collection.at(i); + if (!message.get('unread')) { + continue; + } + + foundUnread += 1; + + var el = this.$('#' + message.id); + var position = el.position(); + var top = position.top; + + // We're fully below the viewport, continue searching up. + if (top > viewportBottom) { + continue; + } + + // If the bottom fits on screen, we'll call it visible. Even if the + // message is really tall. + var height = el.height(); + var bottom = top + height; + if (bottom <= viewportBottom) { + return message; + } + + // Continue searching up. + } + }, + markRead: function(e) { - this.model.markRead(); + var unread = this.findNewestVisibleUnread(); + if (unread) { + this.model.markRead(unread.get('received_at')); + } }, verifyIdentity: function(ev, model) { diff --git a/js/views/message_list_view.js b/js/views/message_list_view.js index 3d389204b..d86b34995 100644 --- a/js/views/message_list_view.js +++ b/js/views/message_list_view.js @@ -12,6 +12,13 @@ events: { 'scroll': 'onScroll', }, + initialize: function() { + Whisper.ListView.prototype.initialize.call(this); + + this.triggerLazyScroll = _.debounce(function() { + this.$el.trigger('lazyScroll'); + }.bind(this), 500); + }, onScroll: function() { this.measureScrollPosition(); if (this.$el.scrollTop() === 0) { @@ -22,6 +29,8 @@ } else if (this.bottomOffset > this.outerHeight) { this.$el.trigger('farFromBottom'); } + + this.triggerLazyScroll(); }, atBottom: function() { return this.bottomOffset < 30;