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 d394b459..00adb58e 100644 --- a/background.html +++ b/background.html @@ -480,6 +480,7 @@ + diff --git a/js/background.js b/js/background.js index 2eb48c04..c8f34aeb 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 00000000..40fa5565 --- /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 909320d8..69e586c7 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 ade716c4..aaa83e12 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 1b276630..72eeaf09 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 20dea9cd..3a206fb1 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 24e32c6f..a13c1cd5 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 50628b44..61dea07f 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; }