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 d394b45939d5..00adb58e2214 100644 --- a/background.html +++ b/background.html @@ -480,6 +480,7 @@ + diff --git a/js/background.js b/js/background.js index 2eb48c0466ab..c8f34aebe8ae 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 000000000000..40fa55650353 --- /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 909320d86454..69e586c71232 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 ade716c46da8..aaa83e12d172 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 1b276630ad6b..72eeaf09f694 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 20dea9cd5cfe..3a206fb12e64 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 24e32c6f0fca..a13c1cd55bdf 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 50628b4470f0..61dea07ffc01 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; }