From 96fd0178902218c49e56c1a2851803235e4fcd5b Mon Sep 17 00:00:00 2001 From: lilia Date: Tue, 20 Sep 2016 17:19:51 -0700 Subject: [PATCH] Support for incoming expiring messages When initialized, or when expiration-related attributes change, expiring messages will set timers to self-destruct. On self-destruct they trigger 'expired' events so that frontend listeners can clean up any collections and views referencing them. At startup, load all messages pending expiration so they can start their timers even if they haven't been loaded in the frontend yet. Todo: Remove expired conversation snippets from the left pane. --- background.html | 1 + js/background.js | 3 ++- js/expiring_messages.js | 14 +++++++++++++ js/models/conversations.js | 7 +++++-- js/models/messages.js | 36 +++++++++++++++++++++++++++++++++- js/views/conversation_view.js | 6 ++++++ js/views/message_view.js | 14 ++++++++++++- stylesheets/_conversation.scss | 12 ++++++++++++ stylesheets/manifest.css | 14 +++++++++++++ 9 files changed, 102 insertions(+), 5 deletions(-) create mode 100644 js/expiring_messages.js diff --git a/background.html b/background.html index d394b4593..00adb58e2 100644 --- a/background.html +++ b/background.html @@ -480,6 +480,7 @@ + diff --git a/js/background.js b/js/background.js index 2eb48c046..c8f34aebe 100644 --- a/js/background.js +++ b/js/background.js @@ -169,7 +169,8 @@ received_at : now, conversationId : data.destination, type : 'outgoing', - sent : true + sent : true, + expirationStartTimestamp: data.expirationStartTimestamp, }); message.handleDataMessage(data.message); diff --git a/js/expiring_messages.js b/js/expiring_messages.js new file mode 100644 index 000000000..40fa55650 --- /dev/null +++ b/js/expiring_messages.js @@ -0,0 +1,14 @@ + +/* + * vim: ts=4:sw=4:expandtab + */ +;(function() { + 'use strict'; + window.Whisper = window.Whisper || {}; + Whisper.ExpiringMessages = new (Whisper.MessageCollection.extend({ + initialize: function() { + this.on('expired', this.remove); + this.fetchExpiring(); + } + }))(); +})(); diff --git a/js/models/conversations.js b/js/models/conversations.js index 909320d86..69e586c71 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -230,17 +230,20 @@ this.getUnread().then(function(unreadMessages) { var read = unreadMessages.map(function(m) { + if (this.messageCollection.get(m.id)) { + m = this.messageCollection.get(m.id); + } m.markRead(); return { sender : m.get('source'), timestamp : m.get('sent_at') }; - }); + }.bind(this)); if (read.length > 0) { console.log('Sending', read.length, 'read receipts'); textsecure.messaging.syncReadMessages(read); } - }); + }.bind(this)); } }, diff --git a/js/models/messages.js b/js/models/messages.js index ade716c46..aaa83e12d 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -11,6 +11,9 @@ initialize: function() { this.on('change:attachments', this.updateImageUrl); this.on('destroy', this.revokeImageUrl); + this.on('change:expirationStartTimestamp', this.setToExpire); + this.on('change:expireTimer', this.setToExpire); + this.setToExpire(); }, defaults : function() { return { @@ -344,6 +347,10 @@ errors : [] }); + if (dataMessage.expireTimer) { + message.set({expireTimer: dataMessage.expireTimer}); + } + var conversation_timestamp = conversation.get('timestamp'); if (!conversation_timestamp || message.get('sent_at') > conversation_timestamp) { conversation.set({ @@ -367,12 +374,35 @@ }); }); }, - markRead: function(sync) { + markRead: function() { this.unset('unread'); + if (this.get('expireTimer') && !this.get('expirationStartTimestamp')) { + this.set('expirationStartTimestamp', Date.now()); + } Whisper.Notifications.remove(Whisper.Notifications.where({ messageId: this.id })); return this.save(); + }, + markExpired: function() { + console.log('message', this.get('sent_at'), 'expired'); + clearInterval(this.expirationTimeout); + this.expirationTimeout = null; + this.trigger('expired', this); + this.destroy(); + }, + setToExpire: function() { + if (this.get('expireTimer') && this.get('expirationStartTimestamp') && !this.expireTimer) { + var now = Date.now(); + var start = this.get('expirationStartTimestamp'); + var delta = this.get('expireTimer') * 1000; + var ms_from_now = start + delta - now; + if (ms_from_now < 0) { + ms_from_now = 0; + } + console.log('message', this.get('sent_at'), 'expires in', ms_from_now, 'ms'); + this.expirationTimeout = setTimeout(this.markExpired.bind(this), ms_from_now); + } } }); @@ -434,6 +464,10 @@ }.bind(this)); }, + fetchExpiring: function() { + this.fetch({conditions: {expireTimer: {$gte: 0}}}); + }, + hasKeyConflicts: function() { return this.any(function(m) { return m.hasKeyConflicts(); }); } diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index 1b276630a..72eeaf09f 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -43,6 +43,7 @@ this.listenTo(this.model, 'change:name', this.updateTitle); this.listenTo(this.model, 'newmessage', this.addMessage); this.listenTo(this.model, 'opened', this.onOpened); + this.listenTo(this.model.messageCollection, 'expired', this.onExpired); this.render(); @@ -166,8 +167,13 @@ // TODO catch? }, + onExpired: function(message) { + this.model.messageCollection.remove(message.id); + }, + addMessage: function(message) { this.model.messageCollection.add(message, {merge: true}); + message.setToExpire(); if (!this.isHidden() && window.isFocused()) { this.markRead(); diff --git a/js/views/message_view.js b/js/views/message_view.js index 20dea9cd5..3a206fb12 100644 --- a/js/views/message_view.js +++ b/js/views/message_view.js @@ -35,7 +35,8 @@ this.listenTo(this.model, 'change:delivered', this.renderDelivered); this.listenTo(this.model, 'change', this.renderSent); this.listenTo(this.model, 'change:flags change:group_update', this.renderControl); - this.listenTo(this.model, 'destroy', this.remove); + this.listenTo(this.model, 'destroy', this.onDestroy); + this.listenTo(this.model, 'expired', this.onExpired); this.listenTo(this.model, 'pending', this.renderPending); this.listenTo(this.model, 'done', this.renderDone); this.timeStampView = new Whisper.ExtendedTimestampView(); @@ -62,6 +63,17 @@ this.model.resend(number); }.bind(this)); }, + onExpired: function() { + this.$el.addClass('expired'); + this.$el.find('.bubble').one('webkitAnimationEnd animationend', + this.remove.bind(this)); + }, + onDestroy: function() { + if (this.$el.hasClass('expired')) { + return; + } + this.remove(); + }, select: function(e) { this.$el.trigger('select', {message: this.model}); e.stopPropagation(); diff --git a/stylesheets/_conversation.scss b/stylesheets/_conversation.scss index 24e32c6f0..a13c1cd55 100644 --- a/stylesheets/_conversation.scss +++ b/stylesheets/_conversation.scss @@ -383,6 +383,18 @@ li.entry .error-icon-container { } } + @keyframes shake { + 0% { transform: translateX(0px); } + 25% { transform: translateX(-5px); } + 50% { transform: translateX(0px); } + 75% { transform: translateX(5px); } + 100% { transform: translateX(0px); } + } + + .expired .bubble { + animation: shake 0.2s linear 3; + } + .control { .bubble { .content { diff --git a/stylesheets/manifest.css b/stylesheets/manifest.css index 50628b447..61dea07ff 100644 --- a/stylesheets/manifest.css +++ b/stylesheets/manifest.css @@ -1207,6 +1207,20 @@ li.entry .error-icon-container { .message-container .outgoing .bubble, .message-list .outgoing .bubble { clear: left; } +@keyframes shake { + 0% { + transform: translateX(0px); } + 25% { + transform: translateX(-5px); } + 50% { + transform: translateX(0px); } + 75% { + transform: translateX(5px); } + 100% { + transform: translateX(0px); } } + .message-container .expired .bubble, + .message-list .expired .bubble { + animation: shake 0.2s linear 3; } .message-container .control .bubble .content, .message-list .control .bubble .content { font-style: italic; }