diff --git a/background.html b/background.html
index 8d021db40877..1d6d25d17b7a 100644
--- a/background.html
+++ b/background.html
@@ -339,7 +339,7 @@
+
diff --git a/js/background.js b/js/background.js
index f395ec7631b3..993e04011945 100644
--- a/js/background.js
+++ b/js/background.js
@@ -182,15 +182,17 @@
SERVER_URL, USERNAME, PASSWORD, mySignalingKey, options
);
messageReceiver.addEventListener('message', onMessageReceived);
- messageReceiver.addEventListener('receipt', onDeliveryReceipt);
+ messageReceiver.addEventListener('delivery', onDeliveryReceipt);
messageReceiver.addEventListener('contact', onContactReceived);
messageReceiver.addEventListener('group', onGroupReceived);
messageReceiver.addEventListener('sent', onSentMessage);
+ messageReceiver.addEventListener('readSync', onReadSync);
messageReceiver.addEventListener('read', onReadReceipt);
messageReceiver.addEventListener('verified', onVerified);
messageReceiver.addEventListener('error', onError);
messageReceiver.addEventListener('empty', onEmpty);
messageReceiver.addEventListener('progress', onProgress);
+ messageReceiver.addEventListener('settings', onSettings);
window.textsecure.messaging = new textsecure.MessageSender(
SERVER_URL, USERNAME, PASSWORD, CDN_URL
@@ -208,6 +210,21 @@
}
}
+ // If we've just upgraded to read receipt support on desktop, kick off a
+ // one-time configuration sync request to get the read-receipt setting
+ // from the master device.
+ var readReceiptConfigurationSync = 'read-receipt-configuration-sync';
+ if (!storage.get(readReceiptConfigurationSync)) {
+
+ if (!firstRun && textsecure.storage.user.getDeviceId() != '1') {
+ textsecure.messaging.sendRequestConfigurationSyncMessage().then(function() {
+ storage.put(readReceiptConfigurationSync, true);
+ }).catch(function(e) {
+ console.log(e);
+ });
+ }
+ }
+
if (firstRun === true && textsecure.storage.user.getDeviceId() != '1') {
if (!storage.get('theme-setting') && textsecure.storage.get('userAgent') === 'OWI') {
storage.put('theme-setting', 'ios');
@@ -223,6 +240,12 @@
console.log('sync timed out');
Whisper.events.trigger('contactsync');
});
+
+ if (Whisper.Import.isComplete()) {
+ textsecure.messaging.sendRequestConfigurationSyncMessage().catch(function(e) {
+ console.log(e);
+ });
+ }
}
}
@@ -248,6 +271,13 @@
view.onProgress(count);
}
}
+ function onSettings(ev) {
+ if (ev.settings.readReceipts) {
+ storage.put('read-receipt-setting', true);
+ } else {
+ storage.put('read-receipt-setting', false);
+ }
+ }
function onContactReceived(ev) {
var details = ev.contactDetails;
@@ -366,9 +396,17 @@
var now = new Date().getTime();
var data = ev.data;
+ var type, id;
+ if (data.message.group) {
+ type = 'group';
+ id = data.message.group.id;
+ } else {
+ type = 'private';
+ id = data.destination;
+ }
+
if (data.message.flags & textsecure.protobuf.DataMessage.Flags.PROFILE_KEY_UPDATE) {
- var id = data.message.group ? data.message.group.id : data.destination;
- return ConversationController.getOrCreateAndWait(id, 'private').then(function(convo) {
+ return ConversationController.getOrCreateAndWait(id, type).then(function(convo) {
return convo.save({profileSharing: true}).then(ev.confirm);
});
}
@@ -391,15 +429,6 @@
return;
}
- var type, id;
- if (data.message.group) {
- type = 'group';
- id = data.message.group.id;
- } else {
- type = 'private';
- id = data.destination;
- }
-
return ConversationController.getOrCreateAndWait(id, type).then(function() {
return message.handleDataMessage(data.message, ev.confirm, {
initialLoadComplete: initialLoadComplete
@@ -528,10 +557,32 @@
function onReadReceipt(ev) {
var read_at = ev.timestamp;
var timestamp = ev.read.timestamp;
- var sender = ev.read.sender;
- console.log('read receipt', sender, timestamp);
+ var reader = ev.read.reader;
+ console.log('read receipt', reader, timestamp);
+
+ if (!storage.get('read-receipt-setting')) {
+ return ev.confirm();
+ }
var receipt = Whisper.ReadReceipts.add({
+ reader : reader,
+ timestamp : timestamp,
+ read_at : read_at,
+ });
+
+ receipt.on('remove', ev.confirm);
+
+ // Calling this directly so we can wait for completion
+ return Whisper.ReadReceipts.onReceipt(receipt);
+ }
+
+ function onReadSync(ev) {
+ var read_at = ev.timestamp;
+ var timestamp = ev.read.timestamp;
+ var sender = ev.read.sender;
+ console.log('read sync', sender, timestamp);
+
+ var receipt = Whisper.ReadSyncs.add({
sender : sender,
timestamp : timestamp,
read_at : read_at
@@ -540,7 +591,7 @@
receipt.on('remove', ev.confirm);
// Calling this directly so we can wait for completion
- return Whisper.ReadReceipts.onReceipt(receipt);
+ return Whisper.ReadSyncs.onReceipt(receipt);
}
function onVerified(ev) {
@@ -593,17 +644,16 @@
}
function onDeliveryReceipt(ev) {
- var pushMessage = ev.proto;
- var timestamp = pushMessage.timestamp.toNumber();
+ var deliveryReceipt = ev.deliveryReceipt;
console.log(
'delivery receipt from',
- pushMessage.source + '.' + pushMessage.sourceDevice,
- timestamp
+ deliveryReceipt.source + '.' + deliveryReceipt.sourceDevice,
+ deliveryReceipt.timestamp
);
var receipt = Whisper.DeliveryReceipts.add({
- timestamp: timestamp,
- source: pushMessage.source
+ timestamp: deliveryReceipt.timestamp,
+ source: deliveryReceipt.source
});
receipt.on('remove', ev.confirm);
diff --git a/js/delivery_receipts.js b/js/delivery_receipts.js
index 90346cfd8e59..e4289a57fbfd 100644
--- a/js/delivery_receipts.js
+++ b/js/delivery_receipts.js
@@ -41,8 +41,10 @@
}).then(function(message) {
if (message) {
var deliveries = message.get('delivered') || 0;
+ var delivered_to = message.get('delivered_to') || [];
return new Promise(function(resolve, reject) {
message.save({
+ delivered_to: _.union(delivered_to, [receipt.get('source')]),
delivered: deliveries + 1
}).then(function() {
// notify frontend listeners
diff --git a/js/libtextsecure.js b/js/libtextsecure.js
index b7bc609a346b..2f16f47783c9 100644
--- a/js/libtextsecure.js
+++ b/js/libtextsecure.js
@@ -37999,9 +37999,13 @@ var TextSecureServer = (function() {
return res;
});
},
- sendMessages: function(destination, messageArray, timestamp) {
+ sendMessages: function(destination, messageArray, timestamp, silent) {
var jsonData = { messages: messageArray, timestamp: timestamp};
+ if (silent) {
+ jsonData.silent = true;
+ }
+
return this.ajax({
call : 'messages',
httpType : 'PUT',
@@ -38143,7 +38147,8 @@ var TextSecureServer = (function() {
provisionMessage.identityKeyPair,
provisionMessage.profileKey,
deviceName,
- provisionMessage.userAgent
+ provisionMessage.userAgent,
+ provisionMessage.readReceipts
).then(generateKeys).
then(registerKeys).
then(registrationDone);
@@ -38242,7 +38247,8 @@ var TextSecureServer = (function() {
});
});
},
- createAccount: function(number, verificationCode, identityKeyPair, profileKey, deviceName, userAgent) {
+ createAccount: function(number, verificationCode, identityKeyPair,
+ profileKey, deviceName, userAgent, readReceipts) {
var signalingKey = libsignal.crypto.getRandomBytes(32 + 20);
var password = btoa(getString(libsignal.crypto.getRandomBytes(16)));
password = password.substring(0, password.length - 2);
@@ -38261,6 +38267,7 @@ var TextSecureServer = (function() {
textsecure.storage.remove('regionCode');
textsecure.storage.remove('userAgent');
textsecure.storage.remove('profileKey');
+ textsecure.storage.remove('read-receipts-setting');
// update our own identity key, which may have changed
// if we're relinking after a reinstall on the master device
@@ -38283,6 +38290,12 @@ var TextSecureServer = (function() {
if (userAgent) {
textsecure.storage.put('userAgent', userAgent);
}
+ if (readReceipts) {
+ textsecure.storage.put('read-receipt-setting', true);
+ } else {
+ textsecure.storage.put('read-receipt-setting', false);
+ }
+
textsecure.storage.user.setNumberAndDeviceId(number, response.deviceId || 1, deviceName);
textsecure.storage.put('regionCode', libphonenumber.util.getRegionCodeForNumber(number));
@@ -38692,9 +38705,13 @@ MessageReceiver.prototype.extend({
},
onDeliveryReceipt: function (envelope) {
return new Promise(function(resolve, reject) {
- var ev = new Event('receipt');
+ var ev = new Event('delivery');
ev.confirm = this.removeFromCache.bind(this, envelope);
- ev.proto = envelope;
+ ev.deliveryReceipt = {
+ timestamp : envelope.timestamp.toNumber(),
+ source : envelope.source,
+ sourceDevice : envelope.sourceDevice
+ };
this.dispatchAndWait(ev).then(resolve, reject);
}.bind(this));
},
@@ -38856,6 +38873,8 @@ MessageReceiver.prototype.extend({
return this.handleNullMessage(envelope, content.nullMessage);
} else if (content.callMessage) {
return this.handleCallMessage(envelope, content.callMessage);
+ } else if (content.receiptMessage) {
+ return this.handleReceiptMessage(envelope, content.receiptMessage);
} else {
this.removeFromCache(envelope);
throw new Error('Unsupported content message');
@@ -38865,6 +38884,33 @@ MessageReceiver.prototype.extend({
console.log('call message from', this.getEnvelopeId(envelope));
this.removeFromCache(envelope);
},
+ handleReceiptMessage: function(envelope, receiptMessage) {
+ var results = [];
+ if (receiptMessage.type === textsecure.protobuf.ReceiptMessage.Type.DELIVERY) {
+ for (var i = 0; i < receiptMessage.timestamps.length; ++i) {
+ var ev = new Event('delivery');
+ ev.confirm = this.removeFromCache.bind(this, envelope);
+ ev.deliveryReceipt = {
+ timestamp : receiptMessage.timestamps[i].toNumber(),
+ source : envelope.source,
+ sourceDevice : envelope.sourceDevice
+ };
+ results.push(this.dispatchAndWait(ev));
+ }
+ } else if (receiptMessage.type === textsecure.protobuf.ReceiptMessage.Type.READ) {
+ for (var i = 0; i < receiptMessage.timestamps.length; ++i) {
+ var ev = new Event('read');
+ ev.confirm = this.removeFromCache.bind(this, envelope);
+ ev.timestamp = envelope.timestamp.toNumber();
+ ev.read = {
+ timestamp : receiptMessage.timestamps[i].toNumber(),
+ reader : envelope.source
+ }
+ results.push(this.dispatchAndWait(ev));
+ }
+ }
+ return Promise.all(results);
+ },
handleNullMessage: function(envelope, nullMessage) {
console.log('null message from', this.getEnvelopeId(envelope));
this.removeFromCache(envelope);
@@ -38906,10 +38952,20 @@ MessageReceiver.prototype.extend({
return this.handleRead(envelope, syncMessage.read);
} else if (syncMessage.verified) {
return this.handleVerified(envelope, syncMessage.verified);
+ } else if (syncMessage.settings) {
+ return this.handleSettings(envelope, syncMessage.settings);
} else {
throw new Error('Got empty SyncMessage');
}
},
+ handleSettings: function(envelope, settings) {
+ var ev = new Event('settings');
+ ev.confirm = this.removeFromCache.bind(this, envelope);
+ ev.settings = {
+ readReceipts: settings.readReceipts
+ };
+ return this.dispatchAndWait(ev);
+ },
handleVerified: function(envelope, verified) {
var ev = new Event('verified');
ev.confirm = this.removeFromCache.bind(this, envelope);
@@ -38923,7 +38979,7 @@ MessageReceiver.prototype.extend({
handleRead: function(envelope, read) {
var results = [];
for (var i = 0; i < read.length; ++i) {
- var ev = new Event('read');
+ var ev = new Event('readSync');
ev.confirm = this.removeFromCache.bind(this, envelope);
ev.timestamp = envelope.timestamp.toNumber();
ev.read = {
@@ -39236,7 +39292,7 @@ textsecure.MessageReceiver.prototype = {
/*
* vim: ts=4:sw=4:expandtab
*/
-function OutgoingMessage(server, timestamp, numbers, message, callback) {
+function OutgoingMessage(server, timestamp, numbers, message, silent, callback) {
if (message instanceof textsecure.protobuf.DataMessage) {
var content = new textsecure.protobuf.Content();
content.dataMessage = message;
@@ -39247,6 +39303,7 @@ function OutgoingMessage(server, timestamp, numbers, message, callback) {
this.numbers = numbers;
this.message = message; // ContentMessage proto
this.callback = callback;
+ this.silent = silent;
this.numbersCompleted = 0;
this.errors = [];
@@ -39329,7 +39386,7 @@ OutgoingMessage.prototype = {
},
transmitMessage: function(number, jsonData, timestamp) {
- return this.server.sendMessages(number, jsonData, timestamp).catch(function(e) {
+ return this.server.sendMessages(number, jsonData, timestamp, this.silent).catch(function(e) {
if (e.name === 'HTTPError' && (e.code !== 409 && e.code !== 410)) {
// 409 and 410 should bubble and be handled by doSendMessage
// 404 should throw UnregisteredUserError
@@ -39725,13 +39782,13 @@ MessageSender.prototype = {
}.bind(this));
}.bind(this));
},
- sendMessageProto: function(timestamp, numbers, message, callback) {
+ sendMessageProto: function(timestamp, numbers, message, callback, silent) {
var rejections = textsecure.storage.get('signedKeyRotationRejected', 0);
if (rejections > 5) {
throw new textsecure.SignedPreKeyRotationError(numbers, message.toArrayBuffer(), timestamp);
}
- var outgoing = new OutgoingMessage(this.server, timestamp, numbers, message, callback);
+ var outgoing = new OutgoingMessage(this.server, timestamp, numbers, message, silent, callback);
numbers.forEach(function(number) {
this.queueJobForNumber(number, function() {
@@ -39752,14 +39809,14 @@ MessageSender.prototype = {
}.bind(this));
},
- sendIndividualProto: function(number, proto, timestamp) {
+ sendIndividualProto: function(number, proto, timestamp, silent) {
return new Promise(function(resolve, reject) {
this.sendMessageProto(timestamp, [number], proto, function(res) {
if (res.errors.length > 0)
reject(res);
else
resolve(res);
- });
+ }, silent);
}.bind(this));
},
@@ -39807,6 +39864,22 @@ MessageSender.prototype = {
return this.server.getAvatar(path);
},
+ sendRequestConfigurationSyncMessage: function() {
+ var myNumber = textsecure.storage.user.getNumber();
+ var myDevice = textsecure.storage.user.getDeviceId();
+ if (myDevice != 1) {
+ var request = new textsecure.protobuf.SyncMessage.Request();
+ request.type = textsecure.protobuf.SyncMessage.Request.Type.CONFIGURATION;
+ var syncMessage = this.createSyncMessage();
+ syncMessage.request = request;
+ var contentMessage = new textsecure.protobuf.Content();
+ contentMessage.syncMessage = syncMessage;
+
+ return this.sendIndividualProto(myNumber, contentMessage, Date.now());
+ }
+
+ return Promise.resolve();
+ },
sendRequestGroupSyncMessage: function() {
var myNumber = textsecure.storage.user.getNumber();
var myDevice = textsecure.storage.user.getDeviceId();
@@ -39840,6 +39913,16 @@ MessageSender.prototype = {
return Promise.resolve();
},
+ sendReadReceipts: function(sender, timestamps) {
+ var receiptMessage = new textsecure.protobuf.ReceiptMessage();
+ receiptMessage.type = textsecure.protobuf.ReceiptMessage.Type.READ;
+ receiptMessage.timestamps = timestamps;
+
+ var contentMessage = new textsecure.protobuf.Content();
+ contentMessage.receiptMessage = receiptMessage;
+
+ return this.sendIndividualProto(sender, contentMessage, Date.now(), true /*silent*/);
+ },
syncReadMessages: function(reads) {
var myNumber = textsecure.storage.user.getNumber();
var myDevice = textsecure.storage.user.getDeviceId();
@@ -40129,24 +40212,26 @@ textsecure.MessageSender = function(url, username, password, cdn_url) {
textsecure.replay.registerFunction(sender.sendMessage.bind(sender), textsecure.replay.Type.REBUILD_MESSAGE);
textsecure.replay.registerFunction(sender.retrySendMessageProto.bind(sender), textsecure.replay.Type.RETRY_SEND_MESSAGE_PROTO);
- this.sendExpirationTimerUpdateToNumber = sender.sendExpirationTimerUpdateToNumber.bind(sender);
- this.sendExpirationTimerUpdateToGroup = sender.sendExpirationTimerUpdateToGroup .bind(sender);
- this.sendRequestGroupSyncMessage = sender.sendRequestGroupSyncMessage .bind(sender);
- this.sendRequestContactSyncMessage = sender.sendRequestContactSyncMessage .bind(sender);
- this.sendMessageToNumber = sender.sendMessageToNumber .bind(sender);
- this.closeSession = sender.closeSession .bind(sender);
- this.sendMessageToGroup = sender.sendMessageToGroup .bind(sender);
- this.createGroup = sender.createGroup .bind(sender);
- this.updateGroup = sender.updateGroup .bind(sender);
- this.addNumberToGroup = sender.addNumberToGroup .bind(sender);
- this.setGroupName = sender.setGroupName .bind(sender);
- this.setGroupAvatar = sender.setGroupAvatar .bind(sender);
- this.leaveGroup = sender.leaveGroup .bind(sender);
- this.sendSyncMessage = sender.sendSyncMessage .bind(sender);
- this.getProfile = sender.getProfile .bind(sender);
- this.getAvatar = sender.getAvatar .bind(sender);
- this.syncReadMessages = sender.syncReadMessages .bind(sender);
- this.syncVerification = sender.syncVerification .bind(sender);
+ this.sendExpirationTimerUpdateToNumber = sender.sendExpirationTimerUpdateToNumber.bind(sender);
+ this.sendExpirationTimerUpdateToGroup = sender.sendExpirationTimerUpdateToGroup .bind(sender);
+ this.sendRequestGroupSyncMessage = sender.sendRequestGroupSyncMessage .bind(sender);
+ this.sendRequestContactSyncMessage = sender.sendRequestContactSyncMessage .bind(sender);
+ this.sendRequestConfigurationSyncMessage = sender.sendRequestConfigurationSyncMessage.bind(sender);
+ this.sendMessageToNumber = sender.sendMessageToNumber .bind(sender);
+ this.closeSession = sender.closeSession .bind(sender);
+ this.sendMessageToGroup = sender.sendMessageToGroup .bind(sender);
+ this.createGroup = sender.createGroup .bind(sender);
+ this.updateGroup = sender.updateGroup .bind(sender);
+ this.addNumberToGroup = sender.addNumberToGroup .bind(sender);
+ this.setGroupName = sender.setGroupName .bind(sender);
+ this.setGroupAvatar = sender.setGroupAvatar .bind(sender);
+ this.leaveGroup = sender.leaveGroup .bind(sender);
+ this.sendSyncMessage = sender.sendSyncMessage .bind(sender);
+ this.getProfile = sender.getProfile .bind(sender);
+ this.getAvatar = sender.getAvatar .bind(sender);
+ this.syncReadMessages = sender.syncReadMessages .bind(sender);
+ this.syncVerification = sender.syncVerification .bind(sender);
+ this.sendReadReceipts = sender.sendReadReceipts .bind(sender);
};
textsecure.MessageSender.prototype = {
@@ -40328,6 +40413,7 @@ ProvisioningCipher.prototype = {
number : provisionMessage.number,
provisioningCode : provisionMessage.provisioningCode,
userAgent : provisionMessage.userAgent,
+ readReceipts : provisionMessage.readReceipts
};
if (provisionMessage.profileKey) {
ret.profileKey = provisionMessage.profileKey.toArrayBuffer();
diff --git a/js/models/conversations.js b/js/models/conversations.js
index 74c538eb743c..6ed910e028f0 100644
--- a/js/models/conversations.js
+++ b/js/models/conversations.js
@@ -487,15 +487,15 @@
// We mark as read everything older than this message - to clean up old stuff
// still marked unread in the database. If the user generally doesn't read in
- // the desktop app, so the desktop app only gets read receipts, we can very
+ // the desktop app, so the desktop app only gets read syncs, we can very
// easily end up with messages never marked as read (our previous early read
- // receipt handling, read receipts never sent because app was offline)
+ // sync handling, read syncs never sent because app was offline)
- // We queue it because we often get a whole lot of read receipts at once, and
+ // We queue it because we often get a whole lot of read syncs at once, and
// their markRead calls could very easily overlap given the async pull from DB.
- // 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.
+ // Lastly, we don't send read syncs for any message marked read due to a read
+ // sync. That's a notification explosion we don't need.
return this.queueJob(function() {
return this.markRead(message.get('received_at'), {sendReadReceipts: false});
}.bind(this));
@@ -583,6 +583,15 @@
return current;
},
+ getRecipients: function() {
+ if (this.isPrivate()) {
+ return [ this.id ];
+ } else {
+ var me = textsecure.storage.user.getNumber();
+ return _.without(this.get('members'), me);
+ }
+ },
+
sendMessage: function(body, attachments) {
this.queueJob(function() {
var now = Date.now();
@@ -601,7 +610,8 @@
attachments : attachments,
sent_at : now,
received_at : now,
- expireTimer : this.get('expireTimer')
+ expireTimer : this.get('expireTimer'),
+ recipients : this.getRecipients()
});
if (this.isPrivate()) {
message.set({destination: this.id});
@@ -671,6 +681,9 @@
if (this.isPrivate()) {
message.set({destination: this.id});
}
+ if (message.isOutgoing()) {
+ message.set({recipients: this.getRecipients() });
+ }
message.save();
if (message.isOutgoing()) { // outgoing update, send it to the number/group
var sendFunc;
@@ -702,6 +715,7 @@
sent_at : now,
received_at : now,
destination : this.id,
+ recipients : this.getRecipients(),
flags : textsecure.protobuf.DataMessage.Flags.END_SESSION
});
message.send(textsecure.messaging.closeSession(this.id, now));
@@ -793,6 +807,13 @@
if (read.length && options.sendReadReceipts) {
console.log('Sending', read.length, 'read receipts');
promises.push(textsecure.messaging.syncReadMessages(read));
+
+ if (storage.get('read-receipt-setting')) {
+ _.each(_.groupBy(read, 'sender'), function(receipts, sender) {
+ var timestamps = _.map(receipts, 'timestamp');
+ promises.push(textsecure.messaging.sendReadReceipts(sender, timestamps));
+ });
+ }
}
return Promise.all(promises);
diff --git a/js/models/messages.js b/js/models/messages.js
index b2101b429bb7..6de9667513a1 100644
--- a/js/models/messages.js
+++ b/js/models/messages.js
@@ -188,6 +188,21 @@
return _.size(this.get('errors')) > 0;
},
+ getStatus: function(number) {
+ var read_by = this.get('read_by') || [];
+ if (read_by.indexOf(number) >= 0) {
+ return 'read';
+ }
+ var delivered_to = this.get('delivered_to') || [];
+ if (delivered_to.indexOf(number) >= 0) {
+ return 'delivered';
+ }
+ var sent_to = this.get('sent_to') || [];
+ if (sent_to.indexOf(number) >= 0) {
+ return 'sent';
+ }
+ },
+
send: function(promise) {
this.trigger('pending');
return promise.then(function(result) {
@@ -196,7 +211,12 @@
if (result.dataMessage) {
this.set({dataMessage: result.dataMessage});
}
- this.save({sent: true, expirationStartTimestamp: now});
+ var sent_to = this.get('sent_to') || [];
+ this.save({
+ sent_to: _.union(sent_to, result.successfulNumbers),
+ sent: true,
+ expirationStartTimestamp: now
+ });
this.sendSyncMessage();
}.bind(this)).catch(function(result) {
var now = Date.now();
@@ -219,7 +239,12 @@
} else {
this.saveErrors(result.errors);
if (result.successfulNumbers.length > 0) {
- this.set({sent: true, expirationStartTimestamp: now});
+ var sent_to = this.get('sent_to') || [];
+ this.set({
+ sent_to: _.union(sent_to, result.successfulNumbers),
+ sent: true,
+ expirationStartTimestamp: now
+ });
promises.push(this.sendSyncMessage());
}
promises = promises.concat(_.map(result.errors, function(error) {
@@ -428,23 +453,37 @@
}
}
if (type === 'incoming') {
- var readReceipt = Whisper.ReadReceipts.forMessage(message);
- if (readReceipt) {
+ var readSync = Whisper.ReadSyncs.forMessage(message);
+ if (readSync) {
if (message.get('expireTimer') && !message.get('expirationStartTimestamp')) {
- message.set('expirationStartTimestamp', readReceipt.get('read_at'));
+ message.set('expirationStartTimestamp', readSync.get('read_at'));
}
}
- if (readReceipt || message.isExpirationTimerUpdate()) {
+ if (readSync || message.isExpirationTimerUpdate()) {
message.unset('unread');
// This is primarily to allow the conversation to mark all older messages as
- // read, as is done when we receive a read receipt for a message we already
+ // read, as is done when we receive a read sync for a message we already
// know about.
- Whisper.ReadReceipts.notifyConversation(message);
+ Whisper.ReadSyncs.notifyConversation(message);
} else {
conversation.set('unreadCount', conversation.get('unreadCount') + 1);
}
}
+ if (type === 'outgoing') {
+ var reads = Whisper.ReadReceipts.forMessage(conversation, message);
+ if (reads.length) {
+ var read_by = reads.map(function(receipt) {
+ return receipt.get('reader');
+ });
+ message.set({
+ read_by: _.union(message.get('read_by'), read_by)
+ });
+ }
+
+ message.set({recipients: conversation.getRecipients()});
+ }
+
var conversation_timestamp = conversation.get('timestamp');
if (!conversation_timestamp || message.get('sent_at') > conversation_timestamp) {
conversation.set({
diff --git a/js/read_receipts.js b/js/read_receipts.js
index f0025ef16f28..db5363f97136 100644
--- a/js/read_receipts.js
+++ b/js/read_receipts.js
@@ -5,45 +5,75 @@
'use strict';
window.Whisper = window.Whisper || {};
Whisper.ReadReceipts = new (Backbone.Collection.extend({
- forMessage: function(message) {
- var receipt = this.findWhere({
- sender: message.get('source'),
- timestamp: message.get('sent_at')
- });
- if (receipt) {
- console.log('Found early read receipt for message');
- this.remove(receipt);
- return receipt;
+ forMessage: function(conversation, message) {
+ if (!message.isOutgoing()) {
+ return [];
}
+ var ids = [];
+ if (conversation.isPrivate()) {
+ ids = [conversation.id];
+ } else {
+ ids = conversation.get('members');
+ }
+ var receipts = this.filter(function(receipt) {
+ return receipt.get('timestamp') === message.get('sent_at')
+ && _.contains(ids, receipt.get('reader'));
+ });
+ if (receipts.length) {
+ console.log('Found early read receipts for message');
+ this.remove(receipts);
+ }
+ return receipts;
},
onReceipt: function(receipt) {
var messages = new Whisper.MessageCollection();
return messages.fetchSentAt(receipt.get('timestamp')).then(function() {
+ if (messages.length === 0) { return; }
var message = messages.find(function(message) {
- return (message.isIncoming() && message.isUnread() &&
- message.get('source') === receipt.get('sender'));
+ return (message.isOutgoing() && receipt.get('reader') === message.get('conversationId'));
});
+ if (message) { return message; }
+
+ var groups = new Whisper.GroupCollection();
+ return groups.fetchGroups(receipt.get('reader')).then(function() {
+ var ids = groups.pluck('id');
+ ids.push(receipt.get('reader'));
+ return messages.find(function(message) {
+ return (message.isOutgoing() &&
+ _.contains(ids, message.get('conversationId')));
+ });
+ });
+ }).then(function(message) {
if (message) {
- return message.markRead(receipt.get('read_at')).then(function() {
- this.notifyConversation(message);
- this.remove(receipt);
+ var read_by = message.get('read_by') || [];
+ read_by.push(receipt.get('reader'));
+ return new Promise(function(resolve, reject) {
+ message.save({ read_by: read_by }).then(function() {
+ // notify frontend listeners
+ var conversation = ConversationController.get(
+ message.get('conversationId')
+ );
+ if (conversation) {
+ conversation.trigger('read', message);
+ }
+
+ this.remove(receipt);
+ resolve();
+ }.bind(this), reject);
}.bind(this));
} else {
console.log(
'No message for read receipt',
- receipt.get('sender'), receipt.get('timestamp')
+ receipt.get('reader'),
+ receipt.get('timestamp')
);
}
- }.bind(this));
- },
- notifyConversation: function(message) {
- var conversation = ConversationController.get({
- id: message.get('conversationId')
+ }.bind(this)).catch(function(error) {
+ console.log(
+ 'ReadReceipts.onReceipt error:',
+ error && error.stack ? error.stack : error
+ );
});
-
- if (conversation) {
- conversation.onReadMessage(message);
- }
},
}))();
})();
diff --git a/js/read_syncs.js b/js/read_syncs.js
new file mode 100644
index 000000000000..fbb78d785060
--- /dev/null
+++ b/js/read_syncs.js
@@ -0,0 +1,49 @@
+/*
+ * vim: ts=4:sw=4:expandtab
+ */
+;(function() {
+ 'use strict';
+ window.Whisper = window.Whisper || {};
+ Whisper.ReadSyncs = new (Backbone.Collection.extend({
+ forMessage: function(message) {
+ var receipt = this.findWhere({
+ sender: message.get('source'),
+ timestamp: message.get('sent_at')
+ });
+ if (receipt) {
+ console.log('Found early read sync for message');
+ this.remove(receipt);
+ return receipt;
+ }
+ },
+ onReceipt: function(receipt) {
+ var messages = new Whisper.MessageCollection();
+ return messages.fetchSentAt(receipt.get('timestamp')).then(function() {
+ var message = messages.find(function(message) {
+ return (message.isIncoming() && message.isUnread() &&
+ message.get('source') === receipt.get('sender'));
+ });
+ if (message) {
+ return message.markRead(receipt.get('read_at')).then(function() {
+ this.notifyConversation(message);
+ this.remove(receipt);
+ }.bind(this));
+ } else {
+ console.log(
+ 'No message for read sync',
+ receipt.get('sender'), receipt.get('timestamp')
+ );
+ }
+ }.bind(this));
+ },
+ notifyConversation: function(message) {
+ var conversation = ConversationController.get({
+ id: message.get('conversationId')
+ });
+
+ if (conversation) {
+ conversation.onReadMessage(message);
+ }
+ },
+ }))();
+})();
diff --git a/js/views/contact_list_view.js b/js/views/contact_list_view.js
index eea919f4f48c..4aacbdbd72c6 100644
--- a/js/views/contact_list_view.js
+++ b/js/views/contact_list_view.js
@@ -23,7 +23,6 @@
render_attributes: function() {
if (this.model.id === this.ourNumber) {
return {
- class: 'not-clickable',
title: i18n('me'),
number: this.model.getNumber(),
avatar: this.model.getAvatar()
@@ -31,6 +30,7 @@
}
return {
+ class: 'clickable',
title: this.model.getTitle(),
number: this.model.getNumber(),
avatar: this.model.getAvatar(),
diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js
index 55b4ee82a074..55af530c55a9 100644
--- a/js/views/conversation_view.js
+++ b/js/views/conversation_view.js
@@ -109,6 +109,7 @@
this.listenTo(this.model, 'change:avatar change:profileAvatar', this.updateAvatar);
this.listenTo(this.model, 'newmessage', this.addMessage);
this.listenTo(this.model, 'delivered', this.updateMessage);
+ this.listenTo(this.model, 'read', this.updateMessage);
this.listenTo(this.model, 'opened', this.onOpened);
this.listenTo(this.model, 'expired', this.onExpired);
this.listenTo(this.model, 'prune', this.onPrune);
diff --git a/js/views/message_detail_view.js b/js/views/message_detail_view.js
index a8e3306efbfd..0de79ec43f69 100644
--- a/js/views/message_detail_view.js
+++ b/js/views/message_detail_view.js
@@ -67,6 +67,7 @@
var showButton = Boolean(this.outgoingKeyError);
return {
+ status : this.message.getStatus(this.model.id),
name : this.model.getTitle(),
avatar : this.model.getAvatar(),
errors : this.errors,
@@ -105,20 +106,22 @@
this.$el.prepend(dialog.el);
dialog.focusCancel();
},
- getContact: function(number) {
- var c = ConversationController.get(number);
- return {
- number: number,
- title: c ? c.getTitle() : number
- };
- },
- contacts: function() {
+ getContacts: function() {
+ // Return the set of models to be rendered in this view
+ var ids;
if (this.model.isIncoming()) {
- var number = this.model.get('source');
- return [this.conversation.contactCollection.get(number)];
- } else {
- return this.conversation.contactCollection.models;
+ ids = [ this.model.get('source') ];
+ } else if (this.model.isOutgoing()) {
+ ids = this.model.get('recipients');
+ if (!ids) {
+ // older messages have no recipients field
+ // use the current set of recipients
+ ids = this.conversation.getRecipients();
+ }
}
+ return Promise.all(ids.map(function(number) {
+ return ConversationController.getOrCreateAndWait(number, 'private');
+ }));
},
renderContact: function(contact) {
var view = new ContactView({
@@ -150,21 +153,15 @@
this.view.$el.prependTo(this.$('.message-container'));
this.grouped = _.groupBy(this.model.get('errors'), 'number');
- if (this.model.isOutgoing()) {
- var contacts = this.conversation.contactCollection.reject(function(c) {
- return c.isMe();
- });
+ this.getContacts().then(function(contacts) {
_.sortBy(contacts, function(c) {
var prefix = this.grouped[c.id] ? '0' : '1';
// this prefix ensures that contacts with errors are listed first;
// otherwise it's alphabetical
return prefix + c.getTitle();
}.bind(this)).forEach(this.renderContact.bind(this));
- } else {
- var c = this.conversation.contactCollection.get(this.model.get('source'));
- this.renderContact(c);
- }
+ }.bind(this));
}
});
diff --git a/js/views/message_view.js b/js/views/message_view.js
index 963cc3f34e2b..90cfca7bf50a 100644
--- a/js/views/message_view.js
+++ b/js/views/message_view.js
@@ -175,6 +175,7 @@
this.listenTo(this.model, 'change:errors', this.onErrorsChanged);
this.listenTo(this.model, 'change:body', this.render);
this.listenTo(this.model, 'change:delivered', this.renderDelivered);
+ this.listenTo(this.model, 'change:read_by', this.renderRead);
this.listenTo(this.model, 'change:expirationStartTimestamp', this.renderExpiring);
this.listenTo(this.model, 'change', this.renderSent);
this.listenTo(this.model, 'change:flags change:group_update', this.renderControl);
@@ -271,6 +272,11 @@
renderDelivered: function() {
if (this.model.get('delivered')) { this.$el.addClass('delivered'); }
},
+ renderRead: function() {
+ if (!_.isEmpty(this.model.get('read_by'))) {
+ this.$el.addClass('read');
+ }
+ },
onErrorsChanged: function() {
if (this.model.isIncoming()) {
this.render();
@@ -359,6 +365,7 @@
this.renderSent();
this.renderDelivered();
+ this.renderRead();
this.renderErrors();
this.renderExpiring();
diff --git a/libtextsecure/ProvisioningCipher.js b/libtextsecure/ProvisioningCipher.js
index b50d7e00b151..080faf9db162 100644
--- a/libtextsecure/ProvisioningCipher.js
+++ b/libtextsecure/ProvisioningCipher.js
@@ -36,6 +36,7 @@ ProvisioningCipher.prototype = {
number : provisionMessage.number,
provisioningCode : provisionMessage.provisioningCode,
userAgent : provisionMessage.userAgent,
+ readReceipts : provisionMessage.readReceipts
};
if (provisionMessage.profileKey) {
ret.profileKey = provisionMessage.profileKey.toArrayBuffer();
diff --git a/libtextsecure/account_manager.js b/libtextsecure/account_manager.js
index 01d18c0803c5..61c06a44c6f9 100644
--- a/libtextsecure/account_manager.js
+++ b/libtextsecure/account_manager.js
@@ -86,7 +86,8 @@
provisionMessage.identityKeyPair,
provisionMessage.profileKey,
deviceName,
- provisionMessage.userAgent
+ provisionMessage.userAgent,
+ provisionMessage.readReceipts
).then(generateKeys).
then(registerKeys).
then(registrationDone);
@@ -185,7 +186,8 @@
});
});
},
- createAccount: function(number, verificationCode, identityKeyPair, profileKey, deviceName, userAgent) {
+ createAccount: function(number, verificationCode, identityKeyPair,
+ profileKey, deviceName, userAgent, readReceipts) {
var signalingKey = libsignal.crypto.getRandomBytes(32 + 20);
var password = btoa(getString(libsignal.crypto.getRandomBytes(16)));
password = password.substring(0, password.length - 2);
@@ -204,6 +206,7 @@
textsecure.storage.remove('regionCode');
textsecure.storage.remove('userAgent');
textsecure.storage.remove('profileKey');
+ textsecure.storage.remove('read-receipts-setting');
// update our own identity key, which may have changed
// if we're relinking after a reinstall on the master device
@@ -226,6 +229,12 @@
if (userAgent) {
textsecure.storage.put('userAgent', userAgent);
}
+ if (readReceipts) {
+ textsecure.storage.put('read-receipt-setting', true);
+ } else {
+ textsecure.storage.put('read-receipt-setting', false);
+ }
+
textsecure.storage.user.setNumberAndDeviceId(number, response.deviceId || 1, deviceName);
textsecure.storage.put('regionCode', libphonenumber.util.getRegionCodeForNumber(number));
diff --git a/libtextsecure/api.js b/libtextsecure/api.js
index 62bb8782bfcc..9910ef7863d1 100644
--- a/libtextsecure/api.js
+++ b/libtextsecure/api.js
@@ -357,9 +357,13 @@ var TextSecureServer = (function() {
return res;
});
},
- sendMessages: function(destination, messageArray, timestamp) {
+ sendMessages: function(destination, messageArray, timestamp, silent) {
var jsonData = { messages: messageArray, timestamp: timestamp};
+ if (silent) {
+ jsonData.silent = true;
+ }
+
return this.ajax({
call : 'messages',
httpType : 'PUT',
diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js
index 2f96cc5e2c42..8aa4c48be2d2 100644
--- a/libtextsecure/message_receiver.js
+++ b/libtextsecure/message_receiver.js
@@ -338,9 +338,13 @@ MessageReceiver.prototype.extend({
},
onDeliveryReceipt: function (envelope) {
return new Promise(function(resolve, reject) {
- var ev = new Event('receipt');
+ var ev = new Event('delivery');
ev.confirm = this.removeFromCache.bind(this, envelope);
- ev.proto = envelope;
+ ev.deliveryReceipt = {
+ timestamp : envelope.timestamp.toNumber(),
+ source : envelope.source,
+ sourceDevice : envelope.sourceDevice
+ };
this.dispatchAndWait(ev).then(resolve, reject);
}.bind(this));
},
@@ -502,6 +506,8 @@ MessageReceiver.prototype.extend({
return this.handleNullMessage(envelope, content.nullMessage);
} else if (content.callMessage) {
return this.handleCallMessage(envelope, content.callMessage);
+ } else if (content.receiptMessage) {
+ return this.handleReceiptMessage(envelope, content.receiptMessage);
} else {
this.removeFromCache(envelope);
throw new Error('Unsupported content message');
@@ -511,6 +517,33 @@ MessageReceiver.prototype.extend({
console.log('call message from', this.getEnvelopeId(envelope));
this.removeFromCache(envelope);
},
+ handleReceiptMessage: function(envelope, receiptMessage) {
+ var results = [];
+ if (receiptMessage.type === textsecure.protobuf.ReceiptMessage.Type.DELIVERY) {
+ for (var i = 0; i < receiptMessage.timestamps.length; ++i) {
+ var ev = new Event('delivery');
+ ev.confirm = this.removeFromCache.bind(this, envelope);
+ ev.deliveryReceipt = {
+ timestamp : receiptMessage.timestamps[i].toNumber(),
+ source : envelope.source,
+ sourceDevice : envelope.sourceDevice
+ };
+ results.push(this.dispatchAndWait(ev));
+ }
+ } else if (receiptMessage.type === textsecure.protobuf.ReceiptMessage.Type.READ) {
+ for (var i = 0; i < receiptMessage.timestamps.length; ++i) {
+ var ev = new Event('read');
+ ev.confirm = this.removeFromCache.bind(this, envelope);
+ ev.timestamp = envelope.timestamp.toNumber();
+ ev.read = {
+ timestamp : receiptMessage.timestamps[i].toNumber(),
+ reader : envelope.source
+ }
+ results.push(this.dispatchAndWait(ev));
+ }
+ }
+ return Promise.all(results);
+ },
handleNullMessage: function(envelope, nullMessage) {
console.log('null message from', this.getEnvelopeId(envelope));
this.removeFromCache(envelope);
@@ -552,10 +585,20 @@ MessageReceiver.prototype.extend({
return this.handleRead(envelope, syncMessage.read);
} else if (syncMessage.verified) {
return this.handleVerified(envelope, syncMessage.verified);
+ } else if (syncMessage.settings) {
+ return this.handleSettings(envelope, syncMessage.settings);
} else {
throw new Error('Got empty SyncMessage');
}
},
+ handleSettings: function(envelope, settings) {
+ var ev = new Event('settings');
+ ev.confirm = this.removeFromCache.bind(this, envelope);
+ ev.settings = {
+ readReceipts: settings.readReceipts
+ };
+ return this.dispatchAndWait(ev);
+ },
handleVerified: function(envelope, verified) {
var ev = new Event('verified');
ev.confirm = this.removeFromCache.bind(this, envelope);
@@ -569,7 +612,7 @@ MessageReceiver.prototype.extend({
handleRead: function(envelope, read) {
var results = [];
for (var i = 0; i < read.length; ++i) {
- var ev = new Event('read');
+ var ev = new Event('readSync');
ev.confirm = this.removeFromCache.bind(this, envelope);
ev.timestamp = envelope.timestamp.toNumber();
ev.read = {
diff --git a/libtextsecure/outgoing_message.js b/libtextsecure/outgoing_message.js
index b97d5ee90932..2b68762b557b 100644
--- a/libtextsecure/outgoing_message.js
+++ b/libtextsecure/outgoing_message.js
@@ -1,7 +1,7 @@
/*
* vim: ts=4:sw=4:expandtab
*/
-function OutgoingMessage(server, timestamp, numbers, message, callback) {
+function OutgoingMessage(server, timestamp, numbers, message, silent, callback) {
if (message instanceof textsecure.protobuf.DataMessage) {
var content = new textsecure.protobuf.Content();
content.dataMessage = message;
@@ -12,6 +12,7 @@ function OutgoingMessage(server, timestamp, numbers, message, callback) {
this.numbers = numbers;
this.message = message; // ContentMessage proto
this.callback = callback;
+ this.silent = silent;
this.numbersCompleted = 0;
this.errors = [];
@@ -94,7 +95,7 @@ OutgoingMessage.prototype = {
},
transmitMessage: function(number, jsonData, timestamp) {
- return this.server.sendMessages(number, jsonData, timestamp).catch(function(e) {
+ return this.server.sendMessages(number, jsonData, timestamp, this.silent).catch(function(e) {
if (e.name === 'HTTPError' && (e.code !== 409 && e.code !== 410)) {
// 409 and 410 should bubble and be handled by doSendMessage
// 404 should throw UnregisteredUserError
diff --git a/libtextsecure/sendmessage.js b/libtextsecure/sendmessage.js
index 37ff61184195..b0fb6b81c569 100644
--- a/libtextsecure/sendmessage.js
+++ b/libtextsecure/sendmessage.js
@@ -246,13 +246,13 @@ MessageSender.prototype = {
}.bind(this));
}.bind(this));
},
- sendMessageProto: function(timestamp, numbers, message, callback) {
+ sendMessageProto: function(timestamp, numbers, message, callback, silent) {
var rejections = textsecure.storage.get('signedKeyRotationRejected', 0);
if (rejections > 5) {
throw new textsecure.SignedPreKeyRotationError(numbers, message.toArrayBuffer(), timestamp);
}
- var outgoing = new OutgoingMessage(this.server, timestamp, numbers, message, callback);
+ var outgoing = new OutgoingMessage(this.server, timestamp, numbers, message, silent, callback);
numbers.forEach(function(number) {
this.queueJobForNumber(number, function() {
@@ -273,14 +273,14 @@ MessageSender.prototype = {
}.bind(this));
},
- sendIndividualProto: function(number, proto, timestamp) {
+ sendIndividualProto: function(number, proto, timestamp, silent) {
return new Promise(function(resolve, reject) {
this.sendMessageProto(timestamp, [number], proto, function(res) {
if (res.errors.length > 0)
reject(res);
else
resolve(res);
- });
+ }, silent);
}.bind(this));
},
@@ -328,6 +328,22 @@ MessageSender.prototype = {
return this.server.getAvatar(path);
},
+ sendRequestConfigurationSyncMessage: function() {
+ var myNumber = textsecure.storage.user.getNumber();
+ var myDevice = textsecure.storage.user.getDeviceId();
+ if (myDevice != 1) {
+ var request = new textsecure.protobuf.SyncMessage.Request();
+ request.type = textsecure.protobuf.SyncMessage.Request.Type.CONFIGURATION;
+ var syncMessage = this.createSyncMessage();
+ syncMessage.request = request;
+ var contentMessage = new textsecure.protobuf.Content();
+ contentMessage.syncMessage = syncMessage;
+
+ return this.sendIndividualProto(myNumber, contentMessage, Date.now());
+ }
+
+ return Promise.resolve();
+ },
sendRequestGroupSyncMessage: function() {
var myNumber = textsecure.storage.user.getNumber();
var myDevice = textsecure.storage.user.getDeviceId();
@@ -361,6 +377,16 @@ MessageSender.prototype = {
return Promise.resolve();
},
+ sendReadReceipts: function(sender, timestamps) {
+ var receiptMessage = new textsecure.protobuf.ReceiptMessage();
+ receiptMessage.type = textsecure.protobuf.ReceiptMessage.Type.READ;
+ receiptMessage.timestamps = timestamps;
+
+ var contentMessage = new textsecure.protobuf.Content();
+ contentMessage.receiptMessage = receiptMessage;
+
+ return this.sendIndividualProto(sender, contentMessage, Date.now(), true /*silent*/);
+ },
syncReadMessages: function(reads) {
var myNumber = textsecure.storage.user.getNumber();
var myDevice = textsecure.storage.user.getDeviceId();
@@ -650,24 +676,26 @@ textsecure.MessageSender = function(url, username, password, cdn_url) {
textsecure.replay.registerFunction(sender.sendMessage.bind(sender), textsecure.replay.Type.REBUILD_MESSAGE);
textsecure.replay.registerFunction(sender.retrySendMessageProto.bind(sender), textsecure.replay.Type.RETRY_SEND_MESSAGE_PROTO);
- this.sendExpirationTimerUpdateToNumber = sender.sendExpirationTimerUpdateToNumber.bind(sender);
- this.sendExpirationTimerUpdateToGroup = sender.sendExpirationTimerUpdateToGroup .bind(sender);
- this.sendRequestGroupSyncMessage = sender.sendRequestGroupSyncMessage .bind(sender);
- this.sendRequestContactSyncMessage = sender.sendRequestContactSyncMessage .bind(sender);
- this.sendMessageToNumber = sender.sendMessageToNumber .bind(sender);
- this.closeSession = sender.closeSession .bind(sender);
- this.sendMessageToGroup = sender.sendMessageToGroup .bind(sender);
- this.createGroup = sender.createGroup .bind(sender);
- this.updateGroup = sender.updateGroup .bind(sender);
- this.addNumberToGroup = sender.addNumberToGroup .bind(sender);
- this.setGroupName = sender.setGroupName .bind(sender);
- this.setGroupAvatar = sender.setGroupAvatar .bind(sender);
- this.leaveGroup = sender.leaveGroup .bind(sender);
- this.sendSyncMessage = sender.sendSyncMessage .bind(sender);
- this.getProfile = sender.getProfile .bind(sender);
- this.getAvatar = sender.getAvatar .bind(sender);
- this.syncReadMessages = sender.syncReadMessages .bind(sender);
- this.syncVerification = sender.syncVerification .bind(sender);
+ this.sendExpirationTimerUpdateToNumber = sender.sendExpirationTimerUpdateToNumber.bind(sender);
+ this.sendExpirationTimerUpdateToGroup = sender.sendExpirationTimerUpdateToGroup .bind(sender);
+ this.sendRequestGroupSyncMessage = sender.sendRequestGroupSyncMessage .bind(sender);
+ this.sendRequestContactSyncMessage = sender.sendRequestContactSyncMessage .bind(sender);
+ this.sendRequestConfigurationSyncMessage = sender.sendRequestConfigurationSyncMessage.bind(sender);
+ this.sendMessageToNumber = sender.sendMessageToNumber .bind(sender);
+ this.closeSession = sender.closeSession .bind(sender);
+ this.sendMessageToGroup = sender.sendMessageToGroup .bind(sender);
+ this.createGroup = sender.createGroup .bind(sender);
+ this.updateGroup = sender.updateGroup .bind(sender);
+ this.addNumberToGroup = sender.addNumberToGroup .bind(sender);
+ this.setGroupName = sender.setGroupName .bind(sender);
+ this.setGroupAvatar = sender.setGroupAvatar .bind(sender);
+ this.leaveGroup = sender.leaveGroup .bind(sender);
+ this.sendSyncMessage = sender.sendSyncMessage .bind(sender);
+ this.getProfile = sender.getProfile .bind(sender);
+ this.getAvatar = sender.getAvatar .bind(sender);
+ this.syncReadMessages = sender.syncReadMessages .bind(sender);
+ this.syncVerification = sender.syncVerification .bind(sender);
+ this.sendReadReceipts = sender.sendReadReceipts .bind(sender);
};
textsecure.MessageSender.prototype = {
diff --git a/protos/DeviceMessages.proto b/protos/DeviceMessages.proto
index 1322c159d3e8..d63739ca5d17 100644
--- a/protos/DeviceMessages.proto
+++ b/protos/DeviceMessages.proto
@@ -15,5 +15,6 @@ message ProvisionMessage {
optional string number = 3;
optional string provisioningCode = 4;
optional string userAgent = 5;
- optional bytes profileKey = 6;
+ optional bytes profileKey = 6;
+ optional bool readReceipts = 7;
}
diff --git a/protos/IncomingPushMessageSignal.proto b/protos/IncomingPushMessageSignal.proto
index 76070e062f7e..02160e5f3876 100644
--- a/protos/IncomingPushMessageSignal.proto
+++ b/protos/IncomingPushMessageSignal.proto
@@ -22,10 +22,21 @@ message Envelope {
}
message Content {
- optional DataMessage dataMessage = 1;
- optional SyncMessage syncMessage = 2;
- optional CallMessage callMessage = 3;
- optional NullMessage nullMessage = 4;
+ optional DataMessage dataMessage = 1;
+ optional SyncMessage syncMessage = 2;
+ optional CallMessage callMessage = 3;
+ optional NullMessage nullMessage = 4;
+ optional ReceiptMessage receiptMessage = 5;
+}
+
+message ReceiptMessage {
+ enum Type {
+ DELIVERY = 0;
+ READ = 1;
+ }
+
+ optional Type type = 1;
+ repeated uint64 timestamps = 2;
}
message NullMessage {
@@ -117,10 +128,11 @@ message SyncMessage {
message Request {
enum Type {
- UNKNOWN = 0;
- CONTACTS = 1;
- GROUPS = 2;
- BLOCKED = 3;
+ UNKNOWN = 0;
+ CONTACTS = 1;
+ GROUPS = 2;
+ BLOCKED = 3;
+ CONFIGURATION = 4;
}
optional Type type = 1;
@@ -131,6 +143,10 @@ message SyncMessage {
optional uint64 timestamp = 2;
}
+ message Settings {
+ optional bool readReceipts = 1;
+ }
+
optional Sent sent = 1;
optional Contacts contacts = 2;
optional Groups groups = 3;
@@ -139,6 +155,7 @@ message SyncMessage {
optional Blocked blocked = 6;
optional Verified verified = 7;
optional bytes padding = 8;
+ optional Settings settings = 9;
}
message AttachmentPointer {
diff --git a/stylesheets/_conversation.scss b/stylesheets/_conversation.scss
index 2be88d5cfd74..750fd19b2273 100644
--- a/stylesheets/_conversation.scss
+++ b/stylesheets/_conversation.scss
@@ -253,6 +253,7 @@
padding: 0 36px;
margin-bottom: 5px;
+ .status-icon-container,
.error-icon-container {
float: right;
}
@@ -390,6 +391,28 @@ li.entry .error-icon-container {
white-space: nowrap;
}
+.status {
+ width: 18px;
+ height: 18px;
+}
+.sent .status {
+ display: inline-block;
+ @include color-svg('../images/check.svg', black);
+}
+.delivered .status {
+ display: inline-block;
+ @include color-svg('../images/double-check.svg', black);
+}
+.read .status {
+ display: inline-block;
+ @include color-svg('../images/double-check.svg', $blue);
+}
+.pending .status {
+ display: inline-block;
+ background: none;
+ &:before { content: '...'; }
+}
+
.message-container,
.message-list {
list-style: none;
@@ -481,24 +504,6 @@ li.entry .error-icon-container {
}
}
- .status {
- width: 18px;
- height: 18px;
- }
- .sent .status {
- display: inline-block;
- @include color-svg('../images/check.svg', black);
- }
- .delivered .status {
- display: inline-block;
- @include color-svg('../images/double-check.svg', black);
- }
- .pending .status {
- display: inline-block;
- background: none;
- &:before { content: '...'; }
- }
-
.incoming {
.avatar, .bubble {
float: left;
diff --git a/stylesheets/_global.scss b/stylesheets/_global.scss
index b21b097733e6..e62547d7ba67 100644
--- a/stylesheets/_global.scss
+++ b/stylesheets/_global.scss
@@ -392,7 +392,6 @@ $avatar-size: 44px;
margin: 0 0 0 $left-margin;
width: calc(100% - #{$avatar-size} - #{$left-margin} - #{(4/14) + em});
text-align: left;
- cursor: pointer;
p {
overflow-x: hidden;
@@ -413,8 +412,8 @@ $avatar-size: 44px;
font-size: $font-size-small;
}
- &.not-clickable {
- cursor: default;
+ &.clickable {
+ cursor: pointer;
}
.verified-icon {
diff --git a/stylesheets/android-dark.scss b/stylesheets/android-dark.scss
index 6b2cc0b01a9c..a72ab9fdb625 100644
--- a/stylesheets/android-dark.scss
+++ b/stylesheets/android-dark.scss
@@ -133,6 +133,10 @@ $text-dark_l2: darken($text-dark, 30%);
display: inline-block;
@include color-svg('../images/double-check.svg', white);
}
+ .read .status {
+ display: inline-block;
+ @include color-svg('../images/double-check.svg', $blue);
+ }
.file-input .paperclip:before {
content: '';
display: inline-block;
diff --git a/stylesheets/manifest.css b/stylesheets/manifest.css
index d850a44d89b4..e1d5f51b6745 100644
--- a/stylesheets/manifest.css
+++ b/stylesheets/manifest.css
@@ -376,8 +376,7 @@ button.hamburger {
display: inline-block;
margin: 0 0 0 8px;
width: calc(100% - 44px - 8px - 0.2857142857em);
- text-align: left;
- cursor: pointer; }
+ text-align: left; }
.contact-details p {
overflow-x: hidden;
text-overflow: ellipsis; }
@@ -391,8 +390,8 @@ button.hamburger {
.contact-details .number {
color: #616161;
font-size: 0.9285714286em; }
- .contact-details.not-clickable {
- cursor: default; }
+ .contact-details.clickable {
+ cursor: pointer; }
.contact-details .verified-icon {
-webkit-mask: url("../images/verified-check.svg") no-repeat center;
-webkit-mask-size: 100%;
@@ -1289,6 +1288,7 @@ input.search {
.message-detail .contacts .contact-detail {
padding: 0 36px;
margin-bottom: 5px; }
+ .message-detail .contacts .contact-detail .status-icon-container,
.message-detail .contacts .contact-detail .error-icon-container {
float: right; }
.message-detail .contacts .contact-detail button.error {
@@ -1393,6 +1393,34 @@ li.entry .error-icon-container {
margin-right: 3px;
white-space: nowrap; }
+.status {
+ width: 18px;
+ height: 18px; }
+
+.sent .status {
+ display: inline-block;
+ -webkit-mask: url("../images/check.svg") no-repeat center;
+ -webkit-mask-size: 100%;
+ background-color: black; }
+
+.delivered .status {
+ display: inline-block;
+ -webkit-mask: url("../images/double-check.svg") no-repeat center;
+ -webkit-mask-size: 100%;
+ background-color: black; }
+
+.read .status {
+ display: inline-block;
+ -webkit-mask: url("../images/double-check.svg") no-repeat center;
+ -webkit-mask-size: 100%;
+ background-color: #2090ea; }
+
+.pending .status {
+ display: inline-block;
+ background: none; }
+ .pending .status:before {
+ content: '...'; }
+
.message-container,
.message-list {
list-style: none; }
@@ -1472,29 +1500,6 @@ li.entry .error-icon-container {
.message-list .meta .timestamp:hover,
.message-list .meta .status:hover {
opacity: 1.0; }
- .message-container .status,
- .message-list .status {
- width: 18px;
- height: 18px; }
- .message-container .sent .status,
- .message-list .sent .status {
- display: inline-block;
- -webkit-mask: url("../images/check.svg") no-repeat center;
- -webkit-mask-size: 100%;
- background-color: black; }
- .message-container .delivered .status,
- .message-list .delivered .status {
- display: inline-block;
- -webkit-mask: url("../images/double-check.svg") no-repeat center;
- -webkit-mask-size: 100%;
- background-color: black; }
- .message-container .pending .status,
- .message-list .pending .status {
- display: inline-block;
- background: none; }
- .message-container .pending .status:before,
- .message-list .pending .status:before {
- content: '...'; }
.message-container .incoming .avatar, .message-container .incoming .bubble,
.message-list .incoming .avatar,
.message-list .incoming .bubble {
@@ -2352,6 +2357,11 @@ li.entry .error-icon-container {
-webkit-mask: url("../images/double-check.svg") no-repeat center;
-webkit-mask-size: 100%;
background-color: white; }
+ .android-dark .read .status {
+ display: inline-block;
+ -webkit-mask: url("../images/double-check.svg") no-repeat center;
+ -webkit-mask-size: 100%;
+ background-color: #2090ea; }
.android-dark .file-input .paperclip:before {
content: '';
display: inline-block;