From 65283d27944065d67f015c18bc34d6913a87457e Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Wed, 26 Jul 2017 14:55:59 -0700 Subject: [PATCH] Unload conversations and old messages every half-hour FREEBIE --- js/conversation_controller.js | 9 +++ js/models/messages.js | 1 + js/views/attachment_view.js | 23 +++++-- js/views/conversation_view.js | 120 ++++++++++++++++++++++++++++++---- js/views/message_view.js | 74 +++++++++++++++++---- 5 files changed, 198 insertions(+), 29 deletions(-) diff --git a/js/conversation_controller.js b/js/conversation_controller.js index 236f93807..faf11091e 100644 --- a/js/conversation_controller.js +++ b/js/conversation_controller.js @@ -19,6 +19,7 @@ this.on('add remove change:unreadCount', _.debounce(this.updateUnreadCount.bind(this), 1000) ); + this.startPruning(); }, comparator: function(m1, m2) { var timestamp1 = m1.get('timestamp'); @@ -65,6 +66,14 @@ if (newUnreadCount === 0) { window.clearAttention(); } + }, + startPruning: function() { + var halfHour = 30 * 60 * 1000; + this.interval = setInterval(function() { + this.forEach(function(conversation) { + conversation.trigger('prune'); + }); + }.bind(this), halfHour); } }))(); diff --git a/js/models/messages.js b/js/models/messages.js index d84f1b4e8..40164116f 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -13,6 +13,7 @@ this.on('destroy', this.revokeImageUrl); this.on('change:expirationStartTimestamp', this.setToExpire); this.on('change:expireTimer', this.setToExpire); + this.on('unload', this.revokeImageUrl); this.setToExpire(); }, idForLogging: function() { diff --git a/js/views/attachment_view.js b/js/views/attachment_view.js index 34323de8c..844db9fb0 100644 --- a/js/views/attachment_view.js +++ b/js/views/attachment_view.js @@ -81,6 +81,21 @@ events: { 'click': 'onclick' }, + unload: function() { + this.blob = null; + + if (this.lightBoxView) { + this.lightBoxView.remove(); + } + if (this.fileView) { + this.fileView.remove(); + } + if (this.view) { + this.view.remove(); + } + + this.remove(); + }, getFileType: function() { switch(this.model.contentType) { case 'video/quicktime': return 'mov'; @@ -89,10 +104,10 @@ }, onclick: function(e) { if (this.isImage()) { - var view = new Whisper.LightboxView({ model: this }); - view.render(); - view.$el.appendTo(this.el); - view.$el.trigger('show'); + this.lightBoxView = new Whisper.LightboxView({ model: this }); + this.lightBoxView.render(); + this.lightBoxView.$el.appendTo(this.el); + this.lightBoxView.$el.trigger('show'); } else { this.saveFile(); diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index 8ba5a0ba6..42e73d2eb 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -109,6 +109,7 @@ this.listenTo(this.model, 'delivered', this.updateMessage); this.listenTo(this.model, 'opened', this.onOpened); this.listenTo(this.model, 'expired', this.onExpired); + this.listenTo(this.model, 'prune', this.onPrune); this.listenTo(this.model.messageCollection, 'expired', this.onExpiredCollection); this.lazyUpdateVerified = _.debounce( @@ -126,7 +127,7 @@ this.loadingScreen.render(); this.loadingScreen.$el.prependTo(this.el); - new TimerMenuView({ el: this.$('.timer-menu'), model: this.model }); + this.timerMenu = new TimerMenuView({ el: this.$('.timer-menu'), model: this.model }); emoji_util.parse(this.$('.conversation-name')); @@ -151,15 +152,15 @@ this.$messageField = this.$('.send-message'); - var onResize = this.forceUpdateMessageFieldSize.bind(this); - this.window.addEventListener('resize', onResize); + this.onResize = this.forceUpdateMessageFieldSize.bind(this); + this.window.addEventListener('resize', this.onResize); - var onFocus = function() { + this.onFocus = function() { if (this.$el.css('display') !== 'none') { this.markRead(); } }.bind(this); - this.window.addEventListener('focus', onFocus); + this.window.addEventListener('focus', this.onFocus); extension.windows.onClosed(function () { this.window.removeEventListener('resize', onResize); @@ -207,6 +208,83 @@ 'show-identity': 'showSafetyNumber' }, + onPrune: function() { + if (!this.model.messageCollection.length || !this.lastActivity) { + return; + } + + var oneHourAgo = Date.now() - 60 * 60 * 1000; + if (this.isHidden() && this.lastActivity < oneHourAgo) { + this.unload(); + } else if (this.view.atBottom()) { + this.trim(); + } + }, + + unload: function() { + console.log('unloading conversation', this.model.id, 'due to inactivity'); + + this.timerMenu.remove(); + this.fileInput.remove(); + this.titleView.remove(); + + if (this.captureAudioView) { + this.captureAudioView.remove(); + } + if (this.banner) { + this.banner.remove(); + } + if (this.lastSeenIndicator) { + this.lastSeenIndicator.remove(); + } + if (this.scrollDownButton) { + this.scrollDownButton.remove(); + } + if (this.panels && this.panels.length) { + for (var i = 0, max = this.panels.length; i < max; i += 1) { + var panel = this.panels[i]; + panel.remove(); + } + } + + this.window.removeEventListener('resize', this.onResize); + this.window.removeEventListener('focus', this.onFocus); + + this.view.remove(); + + this.remove(); + + this.model.messageCollection.forEach(function(model) { + model.trigger('unload'); + }); + this.model.messageCollection.reset([]); + this.model.revokeAvatarUrl(); + }, + + trim: function() { + var MAX = 100; + var toRemove = this.model.messageCollection.length - MAX; + if (toRemove <= 0) { + return; + } + + var models = []; + for (var i = 0; i < toRemove; i += 1) { + var model = this.model.messageCollection.at(i); + models.push(model); + } + + if (!models.length) { + return; + } + + console.log('trimming conversation', this.model.id, 'of', models.length, 'old messages'); + + this.model.messageCollection.remove(models); + _.forEach(models, function(model) { + model.trigger('unload'); + }); + }, markAllAsVerifiedDefault: function(unverified) { return Promise.all(unverified.map(function(contact) { @@ -292,10 +370,18 @@ }, captureAudio: function(e) { e.preventDefault(); - var view = new Whisper.RecorderView().render(); + + if (this.captureAudioView) { + this.captureAudioView.remove(); + this.captureAudioView = null; + } + + var view = this.captureAudioView = new Whisper.RecorderView(); + view.render(); view.on('send', this.handleAudioCapture.bind(this)); view.on('closed', this.endCaptureAudio.bind(this)); view.$el.appendTo(this.$('.capture-audio')); + this.$('.send-message').attr('disabled','disabled'); this.$('.microphone').hide(); }, @@ -308,6 +394,7 @@ endCaptureAudio: function() { this.$('.send-message').removeAttr('disabled'); this.$('.microphone').show(); + this.captureAudioView = null; }, unfocusBottomBar: function() { @@ -322,6 +409,7 @@ // of messages are added to the DOM, one by one, changing window size and // generating scroll events. if (!this.isHidden() && window.isFocused() && !this.inProgressFetch) { + this.lastActivity = Date.now(); this.markRead(); } }, @@ -340,6 +428,8 @@ }, onOpened: function() { + this.lastActivity = Date.now(); + this.statusFetch = this.throttledGetProfiles().then(function() { this.model.updateVerified().then(function() { this.onVerifiedChange(); @@ -664,6 +754,16 @@ view.render(); }, + // not currently in use + newGroupUpdate: function() { + var view = new Whisper.NewGroupUpdateView({ + model: this.model, + window: this.window + }); + view.render(); + this.listenBack(view); + }, + listenBack: function(view) { this.panels = this.panels || []; if (this.panels.length > 0) { @@ -715,14 +815,6 @@ this.$('.conversation-menu .menu-list').toggle(); }, - newGroupUpdate: function() { - this.newGroupUpdateView = new Whisper.NewGroupUpdateView({ - model: this.model, - window: this.window - }); - this.listenBack(this.newGroupUpdateView); - }, - destroyMessages: function(e) { this.confirm(i18n('deleteConversationConfirmation')).then(function() { this.model.destroyMessages(); diff --git a/js/views/message_view.js b/js/views/message_view.js index 2310b2099..bbd175187 100644 --- a/js/views/message_view.js +++ b/js/views/message_view.js @@ -43,6 +43,9 @@ }); var TimerView = Whisper.View.extend({ templateName: 'hourglass', + initialize: function() { + this.listenTo(this.model, 'unload', this.remove); + }, update: function() { if (this.timeout) { clearTimeout(this.timeout); @@ -74,6 +77,7 @@ initialize: function() { this.conversation = this.model.getExpirationTimerUpdateSource(); this.listenTo(this.conversation, 'change', this.render); + this.listenTo(this.model, 'unload', this.remove); }, render_attributes: function() { var seconds = this.model.get('expirationTimerUpdate').expireTimer; @@ -100,6 +104,7 @@ initialize: function() { this.conversation = this.model.getModelForKeyChange(); this.listenTo(this.conversation, 'change', this.render); + this.listenTo(this.model, 'unload', this.remove); }, events: { 'click .content': 'showIdentity' @@ -124,6 +129,7 @@ initialize: function() { this.conversation = this.model.getModelForVerifiedChange(); this.listenTo(this.conversation, 'change', this.render); + this.listenTo(this.model, 'unload', this.remove); }, events: { 'click .content': 'showIdentity' @@ -173,6 +179,7 @@ this.listenTo(this.model, 'change', this.renderSent); this.listenTo(this.model, 'change:flags change:group_update', this.renderControl); this.listenTo(this.model, 'destroy', this.onDestroy); + this.listenTo(this.model, 'unload', this.onUnload); this.listenTo(this.model, 'expired', this.onExpired); this.listenTo(this.model, 'pending', this.renderPending); this.listenTo(this.model, 'done', this.renderDone); @@ -209,11 +216,39 @@ // Failsafe: if in the background, animation events don't fire setTimeout(this.remove.bind(this), 1000); }, + onUnload: function() { + if (this.avatarView) { + this.avatarView.remove(); + } + if (this.errorIconView) { + this.errorIconView.remove(); + } + if (this.networkErrorView) { + this.networkErrorView.remove(); + } + if (this.someFailedView) { + this.someFailedView.remove(); + } + if (this.timeStampView) { + this.timeStampView.remove(); + } + if (this.loadedAttachments && this.loadedAttachments.length) { + for (var i = 0, max = this.loadedAttachments.length; i < max; i += 1) { + var view = this.loadedAttachments[i]; + view.unload(); + } + } + + // No need to handle this one, since it listens to 'unload' itself: + // this.timerView + + this.remove(); + }, onDestroy: function() { if (this.$el.hasClass('expired')) { return; } - this.remove(); + this.onUnload(); }, select: function(e) { this.$el.trigger('select', {message: this.model}); @@ -245,24 +280,41 @@ }, renderErrors: function() { var errors = this.model.get('errors'); + if (_.size(errors) > 0) { if (this.model.isIncoming()) { this.$('.content').text(this.model.getDescription()).addClass('error-message'); } - var view = new ErrorIconView({ model: errors[0] }); - view.render().$el.appendTo(this.$('.bubble')); + this.errorIconView = new ErrorIconView({ model: errors[0] }); + this.errorIconView.render().$el.appendTo(this.$('.bubble')); } else { this.$('.error-icon-container').remove(); + if (this.errorIconView) { + this.errorIconView.remove(); + this.errorIconView = null; + } } - this.$('.meta .hasRetry').remove(); + if (this.model.hasNetworkError()) { - var networkErrorView = new NetworkErrorView({model: this.model}); - this.$('.meta').prepend(networkErrorView.render().el); + this.networkErrorView = new NetworkErrorView({model: this.model}); + this.$('.meta').prepend(this.networkErrorView.render().el); + } else { + this.$('.meta .hasRetry').remove(); + if (this.networkErrorView) { + this.networkErrorView.remove(); + this.networkErrorView = null; + } } - this.$('.meta .some-failed').remove(); + if (this.model.someRecipientsFailed()) { - var someFailedView = new SomeFailedView(); - this.$('.meta').prepend(someFailedView.render().el); + this.someFailedView = new SomeFailedView(); + this.$('.meta').prepend(this.someFailedView.render().el); + } else { + this.$('.meta .some-failed').remove(); + if (this.someFailedView) { + this.someFailedView.remove(); + this.someFailedView = null; + } } }, renderControl: function() { @@ -321,11 +373,11 @@ if (color) { bubble.addClass(color); } - var avatarView = new (Whisper.View.extend({ + this.avatarView = new (Whisper.View.extend({ templateName: 'avatar', render_attributes: { avatar: model.getAvatar() } }))(); - this.$('.avatar').replaceWith(avatarView.render().$('.avatar')); + this.$('.avatar').replaceWith(this.avatarView.render().$('.avatar')); }, appendAttachmentView: function(view) { // We check for a truthy 'updated' here to ensure that a race condition in a