Animated loading screens on startup and first conversation load

FREEBIE
This commit is contained in:
Scott Nonnenberg 2017-07-24 18:43:35 -07:00
parent 3e8b34f3d0
commit 53f2bfbb57
15 changed files with 444 additions and 79 deletions

View file

@ -2,6 +2,26 @@
<html> <html>
<head> <head>
<meta charset='utf-8'> <meta charset='utf-8'>
<script type='text/x-tmpl-mustache' id='app-loading-screen'>
<div class='content'>
<img src='/images/icon_128.png'>
<div class='container'>
<span class='dot'></span>
<span class='dot'></span>
<span class='dot'></span>
</div>
</div>
</script>
<script type='text/x-tmpl-mustache' id='conversation-loading-screen'>
<div class='content'>
<img src='/images/icon_128.png'>
<div class='container'>
<span class='dot'></span>
<span class='dot'></span>
<span class='dot'></span>
</div>
</div>
</script>
<script type='text/x-tmpl-mustache' id='two-column'> <script type='text/x-tmpl-mustache' id='two-column'>
<div class='gutter'> <div class='gutter'>
<div class='network-status-container'></div> <div class='network-status-container'></div>

View file

@ -13,6 +13,7 @@
// Close and reopen existing windows // Close and reopen existing windows
var open = false; var open = false;
var initialLoadComplete = false;
extension.windows.getAll().forEach(function(appWindow) { extension.windows.getAll().forEach(function(appWindow) {
open = true; open = true;
appWindow.close(); appWindow.close();
@ -121,7 +122,7 @@
messageReceiver.addEventListener('read', onReadReceipt); messageReceiver.addEventListener('read', onReadReceipt);
messageReceiver.addEventListener('verified', onVerified); messageReceiver.addEventListener('verified', onVerified);
messageReceiver.addEventListener('error', onError); messageReceiver.addEventListener('error', onError);
messageReceiver.addEventListener('empty', onEmpty);
window.textsecure.messaging = new textsecure.MessageSender( window.textsecure.messaging = new textsecure.MessageSender(
SERVER_URL, SERVER_PORTS, USERNAME, PASSWORD SERVER_URL, SERVER_PORTS, USERNAME, PASSWORD
@ -145,6 +146,19 @@
} }
} }
function onEmpty() {
initialLoadComplete = true;
var interval = setInterval(function() {
var view = window.owsDesktopApp.inboxView;
if (view) {
clearInterval(interval);
interval = null;
view.onEmpty();
}
}, 500);
}
function onContactReceived(ev) { function onContactReceived(ev) {
var details = ev.contactDetails; var details = ev.contactDetails;
@ -196,14 +210,16 @@
var data = ev.data; var data = ev.data;
var message = initIncomingMessage(data); var message = initIncomingMessage(data);
isMessageDuplicate(message).then(function(isDuplicate) { return isMessageDuplicate(message).then(function(isDuplicate) {
if (isDuplicate) { if (isDuplicate) {
console.log('Received duplicate message', message.idForLogging()); console.log('Received duplicate message', message.idForLogging());
ev.confirm(); ev.confirm();
return; return;
} }
message.handleDataMessage(data.message, ev.confirm); return message.handleDataMessage(data.message, ev.confirm, {
initialLoadComplete: initialLoadComplete
});
}); });
} }
@ -222,14 +238,16 @@
expirationStartTimestamp: data.expirationStartTimestamp, expirationStartTimestamp: data.expirationStartTimestamp,
}); });
isMessageDuplicate(message).then(function(isDuplicate) { return isMessageDuplicate(message).then(function(isDuplicate) {
if (isDuplicate) { if (isDuplicate) {
console.log('Received duplicate message', message.idForLogging()); console.log('Received duplicate message', message.idForLogging());
ev.confirm(); ev.confirm();
return; return;
} }
message.handleDataMessage(data.message, ev.confirm); return message.handleDataMessage(data.message, ev.confirm, {
initialLoadComplete: initialLoadComplete
});
}); });
} }
@ -312,9 +330,9 @@
var envelope = ev.proto; var envelope = ev.proto;
var message = initIncomingMessage(envelope.source, envelope.timestamp.toNumber()); var message = initIncomingMessage(envelope.source, envelope.timestamp.toNumber());
message.saveErrors(e).then(function() { return message.saveErrors(e).then(function() {
var id = message.get('conversationId'); var id = message.get('conversationId');
ConversationController.findOrCreateById(id, 'private').then(function(conversation) { return ConversationController.findOrCreateById(id, 'private').then(function(conversation) {
conversation.set({ conversation.set({
active_at: Date.now(), active_at: Date.now(),
unreadCount: conversation.get('unreadCount') + 1 unreadCount: conversation.get('unreadCount') + 1
@ -327,10 +345,11 @@
} }
conversation.save(); conversation.save();
conversation.trigger('newmessage', message); conversation.trigger('newmessage', message);
conversation.notify(message); if (initialLoadComplete) {
conversation.notify(message);
}
}); });
}); });
return;
} }
throw e; throw e;
@ -341,11 +360,13 @@
var timestamp = ev.read.timestamp; var timestamp = ev.read.timestamp;
var sender = ev.read.sender; var sender = ev.read.sender;
console.log('read receipt', sender, timestamp); console.log('read receipt', sender, timestamp);
var receipt = Whisper.ReadReceipts.add({ var receipt = Whisper.ReadReceipts.add({
sender : sender, sender : sender,
timestamp : timestamp, timestamp : timestamp,
read_at : read_at read_at : read_at
}); });
receipt.on('remove', ev.confirm); receipt.on('remove', ev.confirm);
} }
@ -411,12 +432,12 @@
timestamp: timestamp, timestamp: timestamp,
source: pushMessage.source source: pushMessage.source
}); });
receipt.on('remove', ev.confirm); receipt.on('remove', ev.confirm);
} }
window.owsDesktopApp = { window.owsDesktopApp = {
getAppView: function(destWindow) { getAppView: function(destWindow) {
var self = this; var self = this;
return ConversationController.updateInbox().then(function() { return ConversationController.updateInbox().then(function() {

View file

@ -37484,13 +37484,16 @@ window.textsecure.utils = function() {
this.listeners = {}; this.listeners = {};
} }
var listeners = this.listeners[ev.type]; var listeners = this.listeners[ev.type];
var results = [];
if (typeof listeners === 'object') { if (typeof listeners === 'object') {
for (var i=0; i < listeners.length; ++i) { for (var i = 0, max = listeners.length; i < max; i += 1) {
if (typeof listeners[i] === 'function') { var listener = listeners[i];
listeners[i].call(null, ev); if (typeof listener === 'function') {
results.push(listener.call(null, ev));
} }
} }
} }
return results;
}, },
addEventListener: function(eventName, callback) { addEventListener: function(eventName, callback) {
if (typeof eventName !== 'string') { if (typeof eventName !== 'string') {
@ -38284,22 +38287,25 @@ MessageReceiver.prototype.extend({
onerror: function(error) { onerror: function(error) {
console.log('websocket error'); console.log('websocket error');
}, },
dispatchAndWait: function(event) {
return Promise.all(this.dispatchEvent(event));
},
onclose: function(ev) { onclose: function(ev) {
console.log('websocket closed', ev.code, ev.reason || ''); console.log('websocket closed', ev.code, ev.reason || '');
if (ev.code === 3000) { if (ev.code === 3000) {
return; return;
} }
var eventTarget = this;
// possible 403 or network issue. Make an request to confirm // possible 403 or network issue. Make an request to confirm
this.server.getDevices(this.number). this.server.getDevices(this.number)
then(this.connect.bind(this)). // No HTTP error? Reconnect .then(this.connect.bind(this)) // No HTTP error? Reconnect
catch(function(e) { .catch(function(e) {
var ev = new Event('error'); var ev = new Event('error');
ev.error = e; ev.error = e;
eventTarget.dispatchEvent(ev); this.dispatchAndWait(ev);
}); }.bind(this));
}, },
handleRequest: function(request) { handleRequest: function(request) {
this.incoming = this.incoming || [];
// We do the message decryption here, instead of in the ordered pending queue, // We do the message decryption here, instead of in the ordered pending queue,
// to avoid exposing the time it took us to process messages through the time-to-ack. // to avoid exposing the time it took us to process messages through the time-to-ack.
@ -38307,10 +38313,14 @@ MessageReceiver.prototype.extend({
if (request.path !== '/api/v1/message') { if (request.path !== '/api/v1/message') {
console.log('got request', request.verb, request.path); console.log('got request', request.verb, request.path);
request.respond(200, 'OK'); request.respond(200, 'OK');
if (request.verb === 'PUT' && request.path === '/api/v1/queue/empty') {
this.onEmpty();
}
return; return;
} }
textsecure.crypto.decryptWebsocketMessage(request.body, this.signalingKey).then(function(plaintext) { this.incoming.push(textsecure.crypto.decryptWebsocketMessage(request.body, this.signalingKey).then(function(plaintext) {
var envelope = textsecure.protobuf.Envelope.decode(plaintext); var envelope = textsecure.protobuf.Envelope.decode(plaintext);
// After this point, decoding errors are not the server's // After this point, decoding errors are not the server's
// fault, and we should handle them gracefully and tell the // fault, and we should handle them gracefully and tell the
@ -38320,7 +38330,7 @@ MessageReceiver.prototype.extend({
return request.respond(200, 'OK'); return request.respond(200, 'OK');
} }
this.addToCache(envelope, plaintext).then(function() { return this.addToCache(envelope, plaintext).then(function() {
request.respond(200, 'OK'); request.respond(200, 'OK');
this.queueEnvelope(envelope); this.queueEnvelope(envelope);
}.bind(this), function(error) { }.bind(this), function(error) {
@ -38334,8 +38344,23 @@ MessageReceiver.prototype.extend({
console.log("Error handling incoming message:", e && e.stack ? e.stack : e); console.log("Error handling incoming message:", e && e.stack ? e.stack : e);
var ev = new Event('error'); var ev = new Event('error');
ev.error = e; ev.error = e;
this.dispatchEvent(ev); return this.dispatchAndWait(ev);
}.bind(this)); }.bind(this)));
},
onEmpty: function() {
var incoming = this.incoming;
this.incoming = [];
var dispatchEmpty = function() {
var ev = new Event('empty');
return this.dispatchAndWait(ev);
}.bind(this);
var scheduleDispatch = function() {
this.pending = this.pending.then(dispatchEmpty, dispatchEmpty);
}.bind(this);
Promise.all(incoming).then(scheduleDispatch, scheduleDispatch);
}, },
queueAllCached: function() { queueAllCached: function() {
this.getAllFromCache().then(function(items) { this.getAllFromCache().then(function(items) {
@ -38484,12 +38509,11 @@ MessageReceiver.prototype.extend({
} }
}, },
onDeliveryReceipt: function (envelope) { onDeliveryReceipt: function (envelope) {
return new Promise(function(resolve) { return new Promise(function(resolve, reject) {
var ev = new Event('receipt'); var ev = new Event('receipt');
ev.confirm = this.removeFromCache.bind(this, envelope); ev.confirm = this.removeFromCache.bind(this, envelope);
ev.proto = envelope; ev.proto = envelope;
this.dispatchEvent(ev); this.dispatchAndWait(ev).then(resolve, reject);
return resolve();
}.bind(this)); }.bind(this));
}, },
unpad: function(paddedPlaintext) { unpad: function(paddedPlaintext) {
@ -38548,8 +38572,11 @@ MessageReceiver.prototype.extend({
var ev = new Event('error'); var ev = new Event('error');
ev.error = error; ev.error = error;
ev.proto = envelope; ev.proto = envelope;
this.dispatchEvent(ev);
return Promise.reject(error); var returnError = function() {
return Promise.reject(error);
};
this.dispatchAndWait(ev).then(returnError, returnError);
}.bind(this)); }.bind(this));
}, },
decryptPreKeyWhisperMessage: function(ciphertext, sessionCipher, address) { decryptPreKeyWhisperMessage: function(ciphertext, sessionCipher, address) {
@ -38586,7 +38613,7 @@ MessageReceiver.prototype.extend({
if (expirationStartTimestamp) { if (expirationStartTimestamp) {
ev.data.expirationStartTimestamp = expirationStartTimestamp.toNumber(); ev.data.expirationStartTimestamp = expirationStartTimestamp.toNumber();
} }
this.dispatchEvent(ev); return this.dispatchAndWait(ev);
}.bind(this)); }.bind(this));
}.bind(this)); }.bind(this));
}, },
@ -38608,7 +38635,7 @@ MessageReceiver.prototype.extend({
timestamp : envelope.timestamp.toNumber(), timestamp : envelope.timestamp.toNumber(),
message : message message : message
}; };
this.dispatchEvent(ev); return this.dispatchAndWait(ev);
}.bind(this)); }.bind(this));
}.bind(this)); }.bind(this));
}, },
@ -38696,9 +38723,10 @@ MessageReceiver.prototype.extend({
identityKey: verified.identityKey.toArrayBuffer() identityKey: verified.identityKey.toArrayBuffer()
}; };
ev.viaContactSync = options.viaContactSync; ev.viaContactSync = options.viaContactSync;
this.dispatchEvent(ev); return this.dispatchAndWait(ev);
}, },
handleRead: function(envelope, read) { handleRead: function(envelope, read) {
var results = [];
for (var i = 0; i < read.length; ++i) { for (var i = 0; i < read.length; ++i) {
var ev = new Event('read'); var ev = new Event('read');
ev.confirm = this.removeFromCache.bind(this, envelope); ev.confirm = this.removeFromCache.bind(this, envelope);
@ -38707,24 +38735,30 @@ MessageReceiver.prototype.extend({
timestamp : read[i].timestamp.toNumber(), timestamp : read[i].timestamp.toNumber(),
sender : read[i].sender sender : read[i].sender
} }
this.dispatchEvent(ev); results.push(this.dispatchAndWait(ev));
} }
return Promise.all(results);
}, },
handleContacts: function(envelope, contacts) { handleContacts: function(envelope, contacts) {
console.log('contact sync'); console.log('contact sync');
var eventTarget = this; var eventTarget = this;
var attachmentPointer = contacts.blob; var attachmentPointer = contacts.blob;
return this.handleAttachment(attachmentPointer).then(function() { return this.handleAttachment(attachmentPointer).then(function() {
var results = [];
var contactBuffer = new ContactBuffer(attachmentPointer.data); var contactBuffer = new ContactBuffer(attachmentPointer.data);
var contactDetails = contactBuffer.next(); var contactDetails = contactBuffer.next();
while (contactDetails !== undefined) { while (contactDetails !== undefined) {
var ev = new Event('contact'); var ev = new Event('contact');
ev.confirm = this.removeFromCache.bind(this, envelope); ev.confirm = this.removeFromCache.bind(this, envelope);
ev.contactDetails = contactDetails; ev.contactDetails = contactDetails;
eventTarget.dispatchEvent(ev); results.push(eventTarget.dispatchAndWait(ev));
if (contactDetails.verified) { if (contactDetails.verified) {
this.handleVerified(envelope, contactDetails.verified, {viaContactSync: true}); results.push(this.handleVerified(
envelope,
contactDetails.verified,
{viaContactSync: true}
));
} }
contactDetails = contactBuffer.next(); contactDetails = contactBuffer.next();
@ -38732,12 +38766,13 @@ MessageReceiver.prototype.extend({
var ev = new Event('contactsync'); var ev = new Event('contactsync');
ev.confirm = this.removeFromCache.bind(this, envelope); ev.confirm = this.removeFromCache.bind(this, envelope);
eventTarget.dispatchEvent(ev); results.push(eventTarget.dispatchAndWait(ev));
return Promise.all(results);
}.bind(this)); }.bind(this));
}, },
handleGroups: function(envelope, groups) { handleGroups: function(envelope, groups) {
console.log('group sync'); console.log('group sync');
var eventTarget = this;
var attachmentPointer = groups.blob; var attachmentPointer = groups.blob;
return this.handleAttachment(attachmentPointer).then(function() { return this.handleAttachment(attachmentPointer).then(function() {
var groupBuffer = new GroupBuffer(attachmentPointer.data); var groupBuffer = new GroupBuffer(attachmentPointer.data);
@ -38766,7 +38801,7 @@ MessageReceiver.prototype.extend({
var ev = new Event('group'); var ev = new Event('group');
ev.confirm = this.removeFromCache.bind(this, envelope); ev.confirm = this.removeFromCache.bind(this, envelope);
ev.groupDetails = groupDetails; ev.groupDetails = groupDetails;
eventTarget.dispatchEvent(ev); return this.dispatchAndWait(ev);
}.bind(this)).catch(function(e) { }.bind(this)).catch(function(e) {
console.log('error processing group', e); console.log('error processing group', e);
}); });
@ -38777,7 +38812,7 @@ MessageReceiver.prototype.extend({
Promise.all(promises).then(function() { Promise.all(promises).then(function() {
var ev = new Event('groupsync'); var ev = new Event('groupsync');
ev.confirm = this.removeFromCache.bind(this, envelope); ev.confirm = this.removeFromCache.bind(this, envelope);
eventTarget.dispatchEvent(ev); return this.dispatchAndWait(ev);
}.bind(this)); }.bind(this));
}.bind(this)); }.bind(this));
}, },
@ -38805,9 +38840,9 @@ MessageReceiver.prototype.extend({
attachment.data = data; attachment.data = data;
} }
return this.server.getAttachment(attachment.id). return this.server.getAttachment(attachment.id)
then(decryptAttachment). .then(decryptAttachment)
then(updateAttachment); .then(updateAttachment);
}, },
tryMessageAgain: function(from, ciphertext) { tryMessageAgain: function(from, ciphertext) {
var address = libsignal.SignalProtocolAddress.fromString(from); var address = libsignal.SignalProtocolAddress.fromString(from);

View file

@ -344,7 +344,7 @@
// Lastly, we don't send read receipts for any message marked read due to a read // Lastly, we don't send read receipts for any message marked read due to a read
// receipt. That's a notification explosion we don't need. // receipt. That's a notification explosion we don't need.
this.queueJob(function() { return this.queueJob(function() {
return this.markRead(message.get('received_at'), {sendReadReceipts: false}); return this.markRead(message.get('received_at'), {sendReadReceipts: false});
}.bind(this)); }.bind(this));
}, },

View file

@ -342,7 +342,10 @@
this.send(promise); this.send(promise);
} }
}, },
handleDataMessage: function(dataMessage, confirm) { handleDataMessage: function(dataMessage, confirm, options) {
options = options || {};
_.defaults(options, {initialLoadComplete: true});
// This function can be called from the background script on an // This function can be called from the background script on an
// incoming message or from the frontend after the user accepts an // incoming message or from the frontend after the user accepts an
// identity key change. // identity key change.
@ -357,7 +360,7 @@
console.log('queuing handleDataMessage', message.idForLogging()); console.log('queuing handleDataMessage', message.idForLogging());
var conversation = ConversationController.create({id: conversationId}); var conversation = ConversationController.create({id: conversationId});
conversation.queueJob(function() { return conversation.queueJob(function() {
return new Promise(function(resolve) { return new Promise(function(resolve) {
conversation.fetch().always(function() { conversation.fetch().always(function() {
console.log('starting handleDataMessage', message.idForLogging()); console.log('starting handleDataMessage', message.idForLogging());
@ -500,7 +503,7 @@
// because we need to start expiration timers, etc. // because we need to start expiration timers, etc.
message.markRead(); message.markRead();
} }
if (message.get('unread')) { if (message.get('unread') && options.initialLoadComplete) {
conversation.notify(message); conversation.notify(message);
} }

View file

@ -57,6 +57,14 @@
} }
}); });
Whisper.ConversationLoadingScreen = Whisper.View.extend({
templateName: 'conversation-loading-screen',
className: 'conversation-loading-screen',
render_attributes: {
loading: i18n('loading')
}
});
Whisper.ConversationTitleView = Whisper.View.extend({ Whisper.ConversationTitleView = Whisper.View.extend({
templateName: 'conversation-title', templateName: 'conversation-title',
initialize: function() { initialize: function() {
@ -116,6 +124,11 @@
); );
this.render(); this.render();
this.loadingScreen = new Whisper.ConversationLoadingScreen();
this.loadingScreen.render();
this.loadingScreen.$el.prependTo(this.el);
new TimerMenuView({ el: this.$('.timer-menu'), model: this.model }); new TimerMenuView({ el: this.$('.timer-menu'), model: this.model });
emoji_util.parse(this.$('.conversation-name')); emoji_util.parse(this.$('.conversation-name'));
@ -321,9 +334,15 @@
setTimeout(this.markRead.bind(this), 1); setTimeout(this.markRead.bind(this), 1);
}, },
onLoaded: function () {
var view = this.loadingScreen;
if (view) {
this.loadingScreen = null;
view.remove();
}
},
onOpened: function() { onOpened: function() {
// TODO: we may want to show a loading dialog until this status fetch
// and potentially the below message fetch are complete.
this.statusFetch = this.throttledGetProfiles().then(function() { this.statusFetch = this.throttledGetProfiles().then(function() {
this.model.updateVerified().then(function() { this.model.updateVerified().then(function() {
this.onVerifiedChange(); this.onVerifiedChange();
@ -332,6 +351,9 @@
}.bind(this)); }.bind(this));
}.bind(this)); }.bind(this));
Promise.all([this.statusFetch, this.inProgressFetch])
.then(this.onLoaded.bind(this), this.onLoaded.bind(this));
this.view.resetScrollPosition(); this.view.resetScrollPosition();
this.$el.trigger('force-resize'); this.$el.trigger('force-resize');
this.focusMessageField(); this.focusMessageField();

View file

@ -60,6 +60,15 @@
} }
}); });
Whisper.AppLoadingScreen = Whisper.View.extend({
templateName: 'app-loading-screen',
className: 'app-loading-screen',
render_attributes: {
loading: i18n('loading')
}
});
Whisper.InboxView = Whisper.View.extend({ Whisper.InboxView = Whisper.View.extend({
templateName: 'two-column', templateName: 'two-column',
className: 'inbox', className: 'inbox',
@ -71,6 +80,7 @@
.addClass(theme); .addClass(theme);
}, },
initialize: function (options) { initialize: function (options) {
this.ready = false;
this.render(); this.render();
this.applyTheme(); this.applyTheme();
this.$el.attr('tabindex', '1'); this.$el.attr('tabindex', '1');
@ -80,6 +90,10 @@
model: { window: options.window } model: { window: options.window }
}); });
this.appLoadingScreen = new Whisper.AppLoadingScreen();
this.appLoadingScreen.render();
this.appLoadingScreen.$el.prependTo(this.el);
var inboxCollection = getInboxCollection(); var inboxCollection = getInboxCollection();
inboxCollection.on('messageError', function() { inboxCollection.on('messageError', function() {
@ -146,6 +160,13 @@
'click .restart-signal': 'reloadBackgroundPage', 'click .restart-signal': 'reloadBackgroundPage',
'show .lightbox': 'showLightbox' 'show .lightbox': 'showLightbox'
}, },
onEmpty: function() {
var view = this.appLoadingScreen;
if (view) {
this.appLoadingScreen = null;
view.remove();
}
},
focusConversation: function(e) { focusConversation: function(e) {
if (e && this.$(e.target).closest('.placeholder').length) { if (e && this.$(e.target).closest('.placeholder').length) {
return; return;

View file

@ -23,13 +23,16 @@
this.listeners = {}; this.listeners = {};
} }
var listeners = this.listeners[ev.type]; var listeners = this.listeners[ev.type];
var results = [];
if (typeof listeners === 'object') { if (typeof listeners === 'object') {
for (var i=0; i < listeners.length; ++i) { for (var i = 0, max = listeners.length; i < max; i += 1) {
if (typeof listeners[i] === 'function') { var listener = listeners[i];
listeners[i].call(null, ev); if (typeof listener === 'function') {
results.push(listener.call(null, ev));
} }
} }
} }
return results;
}, },
addEventListener: function(eventName, callback) { addEventListener: function(eventName, callback) {
if (typeof eventName !== 'string') { if (typeof eventName !== 'string') {

View file

@ -45,22 +45,25 @@ MessageReceiver.prototype.extend({
onerror: function(error) { onerror: function(error) {
console.log('websocket error'); console.log('websocket error');
}, },
dispatchAndWait: function(event) {
return Promise.all(this.dispatchEvent(event));
},
onclose: function(ev) { onclose: function(ev) {
console.log('websocket closed', ev.code, ev.reason || ''); console.log('websocket closed', ev.code, ev.reason || '');
if (ev.code === 3000) { if (ev.code === 3000) {
return; return;
} }
var eventTarget = this;
// possible 403 or network issue. Make an request to confirm // possible 403 or network issue. Make an request to confirm
this.server.getDevices(this.number). this.server.getDevices(this.number)
then(this.connect.bind(this)). // No HTTP error? Reconnect .then(this.connect.bind(this)) // No HTTP error? Reconnect
catch(function(e) { .catch(function(e) {
var ev = new Event('error'); var ev = new Event('error');
ev.error = e; ev.error = e;
eventTarget.dispatchEvent(ev); this.dispatchAndWait(ev);
}); }.bind(this));
}, },
handleRequest: function(request) { handleRequest: function(request) {
this.incoming = this.incoming || [];
// We do the message decryption here, instead of in the ordered pending queue, // We do the message decryption here, instead of in the ordered pending queue,
// to avoid exposing the time it took us to process messages through the time-to-ack. // to avoid exposing the time it took us to process messages through the time-to-ack.
@ -68,10 +71,14 @@ MessageReceiver.prototype.extend({
if (request.path !== '/api/v1/message') { if (request.path !== '/api/v1/message') {
console.log('got request', request.verb, request.path); console.log('got request', request.verb, request.path);
request.respond(200, 'OK'); request.respond(200, 'OK');
if (request.verb === 'PUT' && request.path === '/api/v1/queue/empty') {
this.onEmpty();
}
return; return;
} }
textsecure.crypto.decryptWebsocketMessage(request.body, this.signalingKey).then(function(plaintext) { this.incoming.push(textsecure.crypto.decryptWebsocketMessage(request.body, this.signalingKey).then(function(plaintext) {
var envelope = textsecure.protobuf.Envelope.decode(plaintext); var envelope = textsecure.protobuf.Envelope.decode(plaintext);
// After this point, decoding errors are not the server's // After this point, decoding errors are not the server's
// fault, and we should handle them gracefully and tell the // fault, and we should handle them gracefully and tell the
@ -81,7 +88,7 @@ MessageReceiver.prototype.extend({
return request.respond(200, 'OK'); return request.respond(200, 'OK');
} }
this.addToCache(envelope, plaintext).then(function() { return this.addToCache(envelope, plaintext).then(function() {
request.respond(200, 'OK'); request.respond(200, 'OK');
this.queueEnvelope(envelope); this.queueEnvelope(envelope);
}.bind(this), function(error) { }.bind(this), function(error) {
@ -95,8 +102,23 @@ MessageReceiver.prototype.extend({
console.log("Error handling incoming message:", e && e.stack ? e.stack : e); console.log("Error handling incoming message:", e && e.stack ? e.stack : e);
var ev = new Event('error'); var ev = new Event('error');
ev.error = e; ev.error = e;
this.dispatchEvent(ev); return this.dispatchAndWait(ev);
}.bind(this)); }.bind(this)));
},
onEmpty: function() {
var incoming = this.incoming;
this.incoming = [];
var dispatchEmpty = function() {
var ev = new Event('empty');
return this.dispatchAndWait(ev);
}.bind(this);
var scheduleDispatch = function() {
this.pending = this.pending.then(dispatchEmpty, dispatchEmpty);
}.bind(this);
Promise.all(incoming).then(scheduleDispatch, scheduleDispatch);
}, },
queueAllCached: function() { queueAllCached: function() {
this.getAllFromCache().then(function(items) { this.getAllFromCache().then(function(items) {
@ -245,12 +267,11 @@ MessageReceiver.prototype.extend({
} }
}, },
onDeliveryReceipt: function (envelope) { onDeliveryReceipt: function (envelope) {
return new Promise(function(resolve) { return new Promise(function(resolve, reject) {
var ev = new Event('receipt'); var ev = new Event('receipt');
ev.confirm = this.removeFromCache.bind(this, envelope); ev.confirm = this.removeFromCache.bind(this, envelope);
ev.proto = envelope; ev.proto = envelope;
this.dispatchEvent(ev); this.dispatchAndWait(ev).then(resolve, reject);
return resolve();
}.bind(this)); }.bind(this));
}, },
unpad: function(paddedPlaintext) { unpad: function(paddedPlaintext) {
@ -309,8 +330,11 @@ MessageReceiver.prototype.extend({
var ev = new Event('error'); var ev = new Event('error');
ev.error = error; ev.error = error;
ev.proto = envelope; ev.proto = envelope;
this.dispatchEvent(ev);
return Promise.reject(error); var returnError = function() {
return Promise.reject(error);
};
this.dispatchAndWait(ev).then(returnError, returnError);
}.bind(this)); }.bind(this));
}, },
decryptPreKeyWhisperMessage: function(ciphertext, sessionCipher, address) { decryptPreKeyWhisperMessage: function(ciphertext, sessionCipher, address) {
@ -347,7 +371,7 @@ MessageReceiver.prototype.extend({
if (expirationStartTimestamp) { if (expirationStartTimestamp) {
ev.data.expirationStartTimestamp = expirationStartTimestamp.toNumber(); ev.data.expirationStartTimestamp = expirationStartTimestamp.toNumber();
} }
this.dispatchEvent(ev); return this.dispatchAndWait(ev);
}.bind(this)); }.bind(this));
}.bind(this)); }.bind(this));
}, },
@ -369,7 +393,7 @@ MessageReceiver.prototype.extend({
timestamp : envelope.timestamp.toNumber(), timestamp : envelope.timestamp.toNumber(),
message : message message : message
}; };
this.dispatchEvent(ev); return this.dispatchAndWait(ev);
}.bind(this)); }.bind(this));
}.bind(this)); }.bind(this));
}, },
@ -457,9 +481,10 @@ MessageReceiver.prototype.extend({
identityKey: verified.identityKey.toArrayBuffer() identityKey: verified.identityKey.toArrayBuffer()
}; };
ev.viaContactSync = options.viaContactSync; ev.viaContactSync = options.viaContactSync;
this.dispatchEvent(ev); return this.dispatchAndWait(ev);
}, },
handleRead: function(envelope, read) { handleRead: function(envelope, read) {
var results = [];
for (var i = 0; i < read.length; ++i) { for (var i = 0; i < read.length; ++i) {
var ev = new Event('read'); var ev = new Event('read');
ev.confirm = this.removeFromCache.bind(this, envelope); ev.confirm = this.removeFromCache.bind(this, envelope);
@ -468,24 +493,30 @@ MessageReceiver.prototype.extend({
timestamp : read[i].timestamp.toNumber(), timestamp : read[i].timestamp.toNumber(),
sender : read[i].sender sender : read[i].sender
} }
this.dispatchEvent(ev); results.push(this.dispatchAndWait(ev));
} }
return Promise.all(results);
}, },
handleContacts: function(envelope, contacts) { handleContacts: function(envelope, contacts) {
console.log('contact sync'); console.log('contact sync');
var eventTarget = this; var eventTarget = this;
var attachmentPointer = contacts.blob; var attachmentPointer = contacts.blob;
return this.handleAttachment(attachmentPointer).then(function() { return this.handleAttachment(attachmentPointer).then(function() {
var results = [];
var contactBuffer = new ContactBuffer(attachmentPointer.data); var contactBuffer = new ContactBuffer(attachmentPointer.data);
var contactDetails = contactBuffer.next(); var contactDetails = contactBuffer.next();
while (contactDetails !== undefined) { while (contactDetails !== undefined) {
var ev = new Event('contact'); var ev = new Event('contact');
ev.confirm = this.removeFromCache.bind(this, envelope); ev.confirm = this.removeFromCache.bind(this, envelope);
ev.contactDetails = contactDetails; ev.contactDetails = contactDetails;
eventTarget.dispatchEvent(ev); results.push(eventTarget.dispatchAndWait(ev));
if (contactDetails.verified) { if (contactDetails.verified) {
this.handleVerified(envelope, contactDetails.verified, {viaContactSync: true}); results.push(this.handleVerified(
envelope,
contactDetails.verified,
{viaContactSync: true}
));
} }
contactDetails = contactBuffer.next(); contactDetails = contactBuffer.next();
@ -493,12 +524,13 @@ MessageReceiver.prototype.extend({
var ev = new Event('contactsync'); var ev = new Event('contactsync');
ev.confirm = this.removeFromCache.bind(this, envelope); ev.confirm = this.removeFromCache.bind(this, envelope);
eventTarget.dispatchEvent(ev); results.push(eventTarget.dispatchAndWait(ev));
return Promise.all(results);
}.bind(this)); }.bind(this));
}, },
handleGroups: function(envelope, groups) { handleGroups: function(envelope, groups) {
console.log('group sync'); console.log('group sync');
var eventTarget = this;
var attachmentPointer = groups.blob; var attachmentPointer = groups.blob;
return this.handleAttachment(attachmentPointer).then(function() { return this.handleAttachment(attachmentPointer).then(function() {
var groupBuffer = new GroupBuffer(attachmentPointer.data); var groupBuffer = new GroupBuffer(attachmentPointer.data);
@ -527,7 +559,7 @@ MessageReceiver.prototype.extend({
var ev = new Event('group'); var ev = new Event('group');
ev.confirm = this.removeFromCache.bind(this, envelope); ev.confirm = this.removeFromCache.bind(this, envelope);
ev.groupDetails = groupDetails; ev.groupDetails = groupDetails;
eventTarget.dispatchEvent(ev); return this.dispatchAndWait(ev);
}.bind(this)).catch(function(e) { }.bind(this)).catch(function(e) {
console.log('error processing group', e); console.log('error processing group', e);
}); });
@ -538,7 +570,7 @@ MessageReceiver.prototype.extend({
Promise.all(promises).then(function() { Promise.all(promises).then(function() {
var ev = new Event('groupsync'); var ev = new Event('groupsync');
ev.confirm = this.removeFromCache.bind(this, envelope); ev.confirm = this.removeFromCache.bind(this, envelope);
eventTarget.dispatchEvent(ev); return this.dispatchAndWait(ev);
}.bind(this)); }.bind(this));
}.bind(this)); }.bind(this));
}, },
@ -566,9 +598,9 @@ MessageReceiver.prototype.extend({
attachment.data = data; attachment.data = data;
} }
return this.server.getAttachment(attachment.id). return this.server.getAttachment(attachment.id)
then(decryptAttachment). .then(decryptAttachment)
then(updateAttachment); .then(updateAttachment);
}, },
tryMessageAgain: function(from, ciphertext) { tryMessageAgain: function(from, ciphertext) {
var address = libsignal.SignalProtocolAddress.fromString(from); var address = libsignal.SignalProtocolAddress.fromString(from);

View file

@ -32,6 +32,51 @@
.conversation { .conversation {
background-color: white; background-color: white;
height: 100%; height: 100%;
position: relative;
.conversation-loading-screen {
z-index: 99;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: #eee;
display: flex;
align-items: center;
.content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.container {
position: absolute;
left: 50%;
width: 78px;
transform: translate(-50%, 0);
}
.dot {
width: 14px;
height: 14px;
border: 3px solid $blue;
border-radius: 50%;
float: left;
margin: 0 6px;
transform: scale(0);
animation: loading 1500ms ease infinite 0ms;
&:nth-child(2) {
animation: loading 1500ms ease infinite 333ms;
}
&:nth-child(3) {
animation: loading 1500ms ease infinite 666ms;
}
}
}
.panel { .panel {
height: calc(100% - #{$header-height}); height: calc(100% - #{$header-height});

View file

@ -539,6 +539,64 @@ input[type=text], input[type=search], textarea {
} }
} }
.inbox {
position: relative;
}
@keyframes loading {
50% {
transform: scale(1);
opacity: 1;
}
100% {
opacity: 0;
}
}
.app-loading-screen {
z-index: 1000;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: white;
display: flex;
align-items: center;
.content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.container {
position: absolute;
left: 50%;
transform: translate(-50%, 0);
width: 78px;
}
.dot {
width: 14px;
height: 14px;
border: 3px solid $blue;
border-radius: 50%;
float: left;
margin: 0 6px;
transform: scale(0);
animation: loading 1500ms ease infinite 0ms;
&:nth-child(2) {
animation: loading 1500ms ease infinite 333ms;
}
&:nth-child(3) {
animation: loading 1500ms ease infinite 666ms;
}
}
}
//yellow border fix //yellow border fix
.inbox:focus { .inbox:focus {
outline: none; outline: none;

View file

@ -88,6 +88,9 @@ $text-dark_l2: darken($text-dark, 30%);
.conversation.placeholder .conversation-header { .conversation.placeholder .conversation-header {
display: none; display: none;
} }
.conversation .conversation-loading-screen {
background-color: $grey-dark_l3;
}
.avatar, .conversation-header, .bubble { .avatar, .conversation-header, .bubble {
@include dark-avatar-colors; @include dark-avatar-colors;
} }

View file

@ -484,6 +484,49 @@ input[type=text]:active, input[type=text]:focus, input[type=search]:active, inpu
background: #2090ea; background: #2090ea;
margin-left: 20px; } margin-left: 20px; }
.inbox {
position: relative; }
@keyframes loading {
50% {
transform: scale(1);
opacity: 1; }
100% {
opacity: 0; } }
.app-loading-screen {
z-index: 1000;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: white;
display: flex;
align-items: center; }
.app-loading-screen .content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%); }
.app-loading-screen .container {
position: absolute;
left: 50%;
transform: translate(-50%, 0);
width: 78px; }
.app-loading-screen .dot {
width: 14px;
height: 14px;
border: 3px solid #2090ea;
border-radius: 50%;
float: left;
margin: 0 6px;
transform: scale(0);
animation: loading 1500ms ease infinite 0ms; }
.app-loading-screen .dot:nth-child(2) {
animation: loading 1500ms ease infinite 333ms; }
.app-loading-screen .dot:nth-child(3) {
animation: loading 1500ms ease infinite 666ms; }
.inbox:focus { .inbox:focus {
outline: none; } outline: none; }
@ -1051,7 +1094,41 @@ input.search {
.conversation { .conversation {
background-color: white; background-color: white;
height: 100%; } height: 100%;
position: relative; }
.conversation .conversation-loading-screen {
z-index: 99;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: #eee;
display: flex;
align-items: center; }
.conversation .conversation-loading-screen .content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%); }
.conversation .conversation-loading-screen .container {
position: absolute;
left: 50%;
width: 78px;
transform: translate(-50%, 0); }
.conversation .conversation-loading-screen .dot {
width: 14px;
height: 14px;
border: 3px solid #2090ea;
border-radius: 50%;
float: left;
margin: 0 6px;
transform: scale(0);
animation: loading 1500ms ease infinite 0ms; }
.conversation .conversation-loading-screen .dot:nth-child(2) {
animation: loading 1500ms ease infinite 333ms; }
.conversation .conversation-loading-screen .dot:nth-child(3) {
animation: loading 1500ms ease infinite 666ms; }
.conversation .panel { .conversation .panel {
height: calc(100% - 64px); height: calc(100% - 64px);
overflow-y: scroll; } overflow-y: scroll; }
@ -2099,6 +2176,8 @@ li.entry .error-icon-container {
border-color: #333333; } border-color: #333333; }
.android-dark .conversation.placeholder .conversation-header { .android-dark .conversation.placeholder .conversation-header {
display: none; } display: none; }
.android-dark .conversation .conversation-loading-screen {
background-color: #171717; }
.android-dark .avatar.red, .android-dark .conversation-header.red, .android-dark .bubble.red { .android-dark .avatar.red, .android-dark .conversation-header.red, .android-dark .bubble.red {
background-color: #D32F2F; } background-color: #D32F2F; }
.android-dark .avatar.pink, .android-dark .conversation-header.pink, .android-dark .bubble.pink { .android-dark .avatar.pink, .android-dark .conversation-header.pink, .android-dark .bubble.pink {

View file

@ -13,14 +13,17 @@ describe("Fixtures", function() {
it('renders', function(done) { it('renders', function(done) {
ConversationController.updateInbox().then(function() { ConversationController.updateInbox().then(function() {
var view = new Whisper.InboxView({window: window}); var view = new Whisper.InboxView({window: window});
view.onEmpty();
view.$el.prependTo($('#render-android')); view.$el.prependTo($('#render-android'));
var view = new Whisper.InboxView({window: window}); var view = new Whisper.InboxView({window: window});
view.$el.removeClass('android').addClass('ios'); view.$el.removeClass('android').addClass('ios');
view.onEmpty();
view.$el.prependTo($('#render-ios')); view.$el.prependTo($('#render-ios'));
var view = new Whisper.InboxView({window: window}); var view = new Whisper.InboxView({window: window});
view.$el.removeClass('android').addClass('android-dark'); view.$el.removeClass('android').addClass('android-dark');
view.onEmpty();
view.$el.prependTo($('#render-android-dark')); view.$el.prependTo($('#render-android-dark'));
}).then(done, done); }).then(done, done);
}); });

View file

@ -16,6 +16,26 @@
</div> </div>
<div id="render-ios" class='index' style="width: 800; height: 500; margin:10px; border: solid 1px black;"> <div id="render-ios" class='index' style="width: 800; height: 500; margin:10px; border: solid 1px black;">
</div> </div>
<script type='text/x-tmpl-mustache' id='app-loading-screen'>
<div class='content'>
<img src='/images/icon_128.png'>
<div class='container'>
<span class='dot'></span>
<span class='dot'></span>
<span class='dot'></span>
</div>
</div>
</script>
<script type='text/x-tmpl-mustache' id='conversation-loading-screen'>
<div class='content'>
<img src='/images/icon_128.png'>
<div class='container'>
<span class='dot'></span>
<span class='dot'></span>
<span class='dot'></span>
</div>
</div>
</script>
<script type='text/x-tmpl-mustache' id='two-column'> <script type='text/x-tmpl-mustache' id='two-column'>
<div class='gutter'> <div class='gutter'>
<div class='network-status-container'></div> <div class='network-status-container'></div>