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:
parent
b888e01044
commit
96fd017890
9 changed files with 102 additions and 5 deletions
|
@ -480,6 +480,7 @@
|
||||||
<script type='text/javascript' src='js/models/messages.js'></script>
|
<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/conversations.js'></script>
|
||||||
<script type='text/javascript' src='js/models/blockedNumbers.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/chromium.js'></script>
|
||||||
<script type='text/javascript' src='js/registration.js'></script>
|
<script type='text/javascript' src='js/registration.js'></script>
|
||||||
|
|
|
@ -169,7 +169,8 @@
|
||||||
received_at : now,
|
received_at : now,
|
||||||
conversationId : data.destination,
|
conversationId : data.destination,
|
||||||
type : 'outgoing',
|
type : 'outgoing',
|
||||||
sent : true
|
sent : true,
|
||||||
|
expirationStartTimestamp: data.expirationStartTimestamp,
|
||||||
});
|
});
|
||||||
|
|
||||||
message.handleDataMessage(data.message);
|
message.handleDataMessage(data.message);
|
||||||
|
|
14
js/expiring_messages.js
Normal file
14
js/expiring_messages.js
Normal 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();
|
||||||
|
}
|
||||||
|
}))();
|
||||||
|
})();
|
|
@ -230,17 +230,20 @@
|
||||||
|
|
||||||
this.getUnread().then(function(unreadMessages) {
|
this.getUnread().then(function(unreadMessages) {
|
||||||
var read = unreadMessages.map(function(m) {
|
var read = unreadMessages.map(function(m) {
|
||||||
|
if (this.messageCollection.get(m.id)) {
|
||||||
|
m = this.messageCollection.get(m.id);
|
||||||
|
}
|
||||||
m.markRead();
|
m.markRead();
|
||||||
return {
|
return {
|
||||||
sender : m.get('source'),
|
sender : m.get('source'),
|
||||||
timestamp : m.get('sent_at')
|
timestamp : m.get('sent_at')
|
||||||
};
|
};
|
||||||
});
|
}.bind(this));
|
||||||
if (read.length > 0) {
|
if (read.length > 0) {
|
||||||
console.log('Sending', read.length, 'read receipts');
|
console.log('Sending', read.length, 'read receipts');
|
||||||
textsecure.messaging.syncReadMessages(read);
|
textsecure.messaging.syncReadMessages(read);
|
||||||
}
|
}
|
||||||
});
|
}.bind(this));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,9 @@
|
||||||
initialize: function() {
|
initialize: function() {
|
||||||
this.on('change:attachments', this.updateImageUrl);
|
this.on('change:attachments', this.updateImageUrl);
|
||||||
this.on('destroy', this.revokeImageUrl);
|
this.on('destroy', this.revokeImageUrl);
|
||||||
|
this.on('change:expirationStartTimestamp', this.setToExpire);
|
||||||
|
this.on('change:expireTimer', this.setToExpire);
|
||||||
|
this.setToExpire();
|
||||||
},
|
},
|
||||||
defaults : function() {
|
defaults : function() {
|
||||||
return {
|
return {
|
||||||
|
@ -344,6 +347,10 @@
|
||||||
errors : []
|
errors : []
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (dataMessage.expireTimer) {
|
||||||
|
message.set({expireTimer: dataMessage.expireTimer});
|
||||||
|
}
|
||||||
|
|
||||||
var conversation_timestamp = conversation.get('timestamp');
|
var conversation_timestamp = conversation.get('timestamp');
|
||||||
if (!conversation_timestamp || message.get('sent_at') > conversation_timestamp) {
|
if (!conversation_timestamp || message.get('sent_at') > conversation_timestamp) {
|
||||||
conversation.set({
|
conversation.set({
|
||||||
|
@ -367,12 +374,35 @@
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
markRead: function(sync) {
|
markRead: function() {
|
||||||
this.unset('unread');
|
this.unset('unread');
|
||||||
|
if (this.get('expireTimer') && !this.get('expirationStartTimestamp')) {
|
||||||
|
this.set('expirationStartTimestamp', Date.now());
|
||||||
|
}
|
||||||
Whisper.Notifications.remove(Whisper.Notifications.where({
|
Whisper.Notifications.remove(Whisper.Notifications.where({
|
||||||
messageId: this.id
|
messageId: this.id
|
||||||
}));
|
}));
|
||||||
return this.save();
|
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));
|
}.bind(this));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
fetchExpiring: function() {
|
||||||
|
this.fetch({conditions: {expireTimer: {$gte: 0}}});
|
||||||
|
},
|
||||||
|
|
||||||
hasKeyConflicts: function() {
|
hasKeyConflicts: function() {
|
||||||
return this.any(function(m) { return m.hasKeyConflicts(); });
|
return this.any(function(m) { return m.hasKeyConflicts(); });
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,7 @@
|
||||||
this.listenTo(this.model, 'change:name', this.updateTitle);
|
this.listenTo(this.model, 'change:name', this.updateTitle);
|
||||||
this.listenTo(this.model, 'newmessage', this.addMessage);
|
this.listenTo(this.model, 'newmessage', this.addMessage);
|
||||||
this.listenTo(this.model, 'opened', this.onOpened);
|
this.listenTo(this.model, 'opened', this.onOpened);
|
||||||
|
this.listenTo(this.model.messageCollection, 'expired', this.onExpired);
|
||||||
|
|
||||||
this.render();
|
this.render();
|
||||||
|
|
||||||
|
@ -166,8 +167,13 @@
|
||||||
// TODO catch?
|
// TODO catch?
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onExpired: function(message) {
|
||||||
|
this.model.messageCollection.remove(message.id);
|
||||||
|
},
|
||||||
|
|
||||||
addMessage: function(message) {
|
addMessage: function(message) {
|
||||||
this.model.messageCollection.add(message, {merge: true});
|
this.model.messageCollection.add(message, {merge: true});
|
||||||
|
message.setToExpire();
|
||||||
|
|
||||||
if (!this.isHidden() && window.isFocused()) {
|
if (!this.isHidden() && window.isFocused()) {
|
||||||
this.markRead();
|
this.markRead();
|
||||||
|
|
|
@ -35,7 +35,8 @@
|
||||||
this.listenTo(this.model, 'change:delivered', this.renderDelivered);
|
this.listenTo(this.model, 'change:delivered', this.renderDelivered);
|
||||||
this.listenTo(this.model, 'change', this.renderSent);
|
this.listenTo(this.model, 'change', this.renderSent);
|
||||||
this.listenTo(this.model, 'change:flags change:group_update', this.renderControl);
|
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, 'pending', this.renderPending);
|
||||||
this.listenTo(this.model, 'done', this.renderDone);
|
this.listenTo(this.model, 'done', this.renderDone);
|
||||||
this.timeStampView = new Whisper.ExtendedTimestampView();
|
this.timeStampView = new Whisper.ExtendedTimestampView();
|
||||||
|
@ -62,6 +63,17 @@
|
||||||
this.model.resend(number);
|
this.model.resend(number);
|
||||||
}.bind(this));
|
}.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) {
|
select: function(e) {
|
||||||
this.$el.trigger('select', {message: this.model});
|
this.$el.trigger('select', {message: this.model});
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
|
@ -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 {
|
.control {
|
||||||
.bubble {
|
.bubble {
|
||||||
.content {
|
.content {
|
||||||
|
|
|
@ -1207,6 +1207,20 @@ li.entry .error-icon-container {
|
||||||
.message-container .outgoing .bubble,
|
.message-container .outgoing .bubble,
|
||||||
.message-list .outgoing .bubble {
|
.message-list .outgoing .bubble {
|
||||||
clear: left; }
|
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-container .control .bubble .content,
|
||||||
.message-list .control .bubble .content {
|
.message-list .control .bubble .content {
|
||||||
font-style: italic; }
|
font-style: italic; }
|
||||||
|
|
Loading…
Reference in a new issue