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.
This commit is contained in:
lilia 2016-09-20 17:19:51 -07:00
parent b888e01044
commit 96fd017890
9 changed files with 102 additions and 5 deletions

View File

@ -480,6 +480,7 @@
<script type='text/javascript' src='js/models/messages.js'></script>
<script type='text/javascript' src='js/models/conversations.js'></script>
<script type='text/javascript' src='js/models/blockedNumbers.js'></script>
<script type='text/javascript' src='js/expiring_messages.js'></script>
<script type='text/javascript' src='js/chromium.js'></script>
<script type='text/javascript' src='js/registration.js'></script>

View File

@ -169,7 +169,8 @@
received_at : now,
conversationId : data.destination,
type : 'outgoing',
sent : true
sent : true,
expirationStartTimestamp: data.expirationStartTimestamp,
});
message.handleDataMessage(data.message);

14
js/expiring_messages.js Normal file
View File

@ -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();
}
}))();
})();

View File

@ -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));
}
},

View File

@ -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(); });
}

View File

@ -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();

View File

@ -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();

View File

@ -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 {

View File

@ -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; }