diff --git a/background.html b/background.html
index 16b537e77..305232f69 100644
--- a/background.html
+++ b/background.html
@@ -38,6 +38,8 @@
+
+
diff --git a/js/api.js b/js/api.js
index 018dc883f..5c6860cb0 100644
--- a/js/api.js
+++ b/js/api.js
@@ -226,6 +226,40 @@ window.textsecure.api = function() {
});
};
+ self.putAttachment = function(encryptedBin) {
+ return doAjax({
+ call : 'attachment',
+ httpType : 'GET',
+ do_auth : true,
+ }).then(function(response) {
+ return new Promise(function(resolve, reject) {
+ $.ajax(response.location, {
+ type : "PUT",
+ headers: {
+ "Content-Type": "application/octet-stream"
+ },
+ data: encryptedBin,
+
+ success : function() {
+ resolve(response.id);
+ },
+
+ error : function(jqXHR, textStatus, errorThrown) {
+ var code = jqXHR.status;
+ if (code > 999 || code < 100)
+ code = -1;
+
+ var e = new Error(code);
+ e.name = "HTTPError";
+ if (jqXHR.responseJSON)
+ e.response = jqXHR.responseJSON;
+ reject(e);
+ }
+ });
+ });
+ });
+ };
+
self.getWebsocket = function() {
var user = textsecure.storage.getUnencrypted("number_id");
var password = textsecure.storage.getEncrypted("password");
diff --git a/js/crypto.js b/js/crypto.js
index 75a07b20d..b88d3ab17 100644
--- a/js/crypto.js
+++ b/js/crypto.js
@@ -619,18 +619,38 @@ window.textsecure.crypto = function() {
});
};
+ self.encryptAttachment = function(plaintext, keys, iv) {
+ var aes_key = keys.slice(0, 32);
+ var mac_key = keys.slice(32, 64);
+
+ return window.crypto.subtle.encrypt({name: "AES-CBC", iv: iv}, aes_key, plaintext).then(function(ciphertext) {
+ var ivAndCiphertext = new Uint8Array(16 + ciphertext.byteLength);
+ ivAndCiphertext.set(iv);
+ ivAndCiphertext.set(ciphertext, 16);
+
+ return calculateMAC(ivAndCiphertext.buffer, mac_key).then(function(mac) {
+ var encryptedBin = new Uint8Array(16 + ciphertext.byteLength + 32);
+ encryptedBin.set(ivAndCiphertext.buffer);
+ encryptedBin.set(mac, 16 + ciphertext.byteLength);
+ return encryptedBin.buffer;
+ });
+ });
+ };
+
self.handleIncomingPushMessageProto = function(proto) {
switch(proto.type) {
- case 0: //TYPE_MESSAGE_PLAINTEXT
+ case textsecure.protos.IncomingPushMessageProtobuf.Type.PLAINTEXT:
return Promise.resolve(textsecure.protos.decodePushMessageContentProtobuf(getString(proto.message)));
- case 1: //TYPE_MESSAGE_CIPHERTEXT
+ case textsecure.protos.IncomingPushMessageProtobuf.Type.CIPHERTEXT:
var from = proto.source + "." + (proto.sourceDevice == null ? 0 : proto.sourceDevice);
return decryptWhisperMessage(from, getString(proto.message));
- case 3: //TYPE_MESSAGE_PREKEY_BUNDLE
+ case textsecure.protos.IncomingPushMessageProtobuf.Type.PREKEY_BUNDLE:
if (proto.message.readUint8() != (2 << 4 | 2))
throw new Error("Bad version byte");
var from = proto.source + "." + (proto.sourceDevice == null ? 0 : proto.sourceDevice);
return handlePreKeyWhisperMessage(from, getString(proto.message));
+ default:
+ return new Promise(function(resolve, reject) { reject(new Error("Unknown message type")); });
}
}
diff --git a/js/fake_api.js b/js/fake_api.js
index 3c783a0a4..ecf09ecd6 100644
--- a/js/fake_api.js
+++ b/js/fake_api.js
@@ -33,7 +33,8 @@ textsecure.api.sendMessages = function(destination, messageArray) {
msg.destinationRegistrationId === undefined ||
msg.body === undefined ||
msg.timestamp == undefined ||
- msg.relay !== undefined)
+ msg.relay !== undefined ||
+ msg.destination !== undefined)
throw new Error("Invalid message");
messagesSentMap[destination + "." + messageArray[i].destinationDeviceId] = msg;
diff --git a/js/helpers.js b/js/helpers.js
index 421c3650b..517fca2ef 100644
--- a/js/helpers.js
+++ b/js/helpers.js
@@ -406,6 +406,21 @@ window.textsecure.storage = function() {
return self;
}();
+ /*********************
+ *** Group Storage ***
+ *********************/
+ self.groups = function() {
+ var self = {};
+
+ //TODO
+
+ self.getNumbers = function(groupId) {
+ return [];
+ }
+
+ return self;
+ }();
+
return self;
}();
@@ -567,157 +582,6 @@ window.textsecure.subscribeToPush = function() {
}
}();
-// sendMessage(numbers = [], message = PushMessageContentProto, callback(success/failure map))
-window.textsecure.sendMessage = function() {
- function getKeysForNumber(number, updateDevices) {
- return textsecure.api.getKeysForNumber(number).then(function(response) {
- var identityKey = getString(response[0].identityKey);
- for (i in response)
- if (getString(response[i].identityKey) != identityKey)
- throw new Error("Identity key not consistent");
-
- for (i in response) {
- var updateDevice = (updateDevices === undefined);
- if (!updateDevice)
- for (j in updateDevices)
- if (updateDevices[j] == response[i].deviceId)
- updateDevice = true;
-
- if (updateDevice)
- textsecure.storage.devices.saveDeviceObject({
- encodedNumber: number + "." + response[i].deviceId,
- identityKey: response[i].identityKey,
- publicKey: response[i].publicKey,
- preKeyId: response[i].keyId,
- registrationId: response[i].registrationId
- });
- }
- });
- }
-
- // success_callback(server success/failure map), error_callback(error_msg)
- // message == PushMessageContentProto (NOT STRING)
- function sendMessageToDevices(number, deviceObjectList, message, success_callback, error_callback) {
- var jsonData = [];
- var relay = undefined;
- var promises = [];
-
- var addEncryptionFor = function(i) {
- if (deviceObjectList[i].relay !== undefined) {
- if (relay === undefined)
- relay = deviceObjectList[i].relay;
- else if (relay != deviceObjectList[i].relay)
- return new Promise(function() { throw new Error("Mismatched relays for number " + number); });
- } else {
- if (relay === undefined)
- relay = "";
- else if (relay != "")
- return new Promise(function() { throw new Error("Mismatched relays for number " + number); });
- }
-
- return textsecure.crypto.encryptMessageFor(deviceObjectList[i], message).then(function(encryptedMsg) {
- jsonData[i] = {
- type: encryptedMsg.type,
- destination: number,
- destinationDeviceId: textsecure.utils.unencodeNumber(deviceObjectList[i].encodedNumber)[1],
- destinationRegistrationId: deviceObjectList[i].registrationId,
- body: encryptedMsg.body,
- timestamp: new Date().getTime()
- };
-
- if (deviceObjectList[i].relay !== undefined)
- jsonData[i].relay = deviceObjectList[i].relay;
- });
- }
-
- for (var i = 0; i < deviceObjectList.length; i++)
- promises[i] = addEncryptionFor(i);
- return Promise.all(promises).then(function() {
- return textsecure.api.sendMessages(number, jsonData);
- });
- }
-
- var tryMessageAgain = function(number, encodedMessage, callback) {
- //TODO: Wipe identity key!
- var message = textsecure.protos.decodePushMessageContentProtobuf(encodedMessage);
- textsecure.sendMessage([number], message, callback);
- }
- textsecure.replay.registerReplayFunction(tryMessageAgain, textsecure.replay.SEND_MESSAGE);
-
- return function(numbers, message, callback) {
- var numbersCompleted = 0;
- var errors = [];
- var successfulNumbers = [];
-
- var numberCompleted = function() {
- numbersCompleted++;
- if (numbersCompleted >= numbers.length)
- callback({success: successfulNumbers, failure: errors});
- }
-
- var registerError = function(number, message, error) {
- if (error.humanError)
- message = error.humanError;
- errors[errors.length] = { number: number, reason: message, error: error };
- numberCompleted();
- }
-
- var doSendMessage;
- var reloadDevicesAndSend = function(number, recurse) {
- return function() {
- var devicesForNumber = textsecure.storage.devices.getDeviceObjectsForNumber(number);
- if (devicesForNumber.length == 0)
- registerError(number, "Go empty device list when loading device keys", null);
- else
- doSendMessage(number, devicesForNumber, recurse);
- }
- }
-
- doSendMessage = function(number, devicesForNumber, recurse) {
- return sendMessageToDevices(number, devicesForNumber, message).then(function(result) {
- successfulNumbers[successfulNumbers.length] = number;
- numberCompleted();
- }).catch(function(error) {
- if (error instanceof Error && error.name == "HTTPError" && (error.message == 410 || error.message == 409)) {
- if (!recurse)
- return registerError(number, "Hit retry limit attempting to reload device list", error);
-
- if (error.message == 409)
- textsecure.storage.devices.removeDeviceIdsForNumber(number, error.response.extraDevices);
-
- var resetDevices = ((error.message == 410) ? error.response.staleDevices : error.response.missingDevices);
- getKeysForNumber(number, resetDevices)
- .then(reloadDevicesAndSend(number, false))
- .catch(function(error) {
- if (error.message !== "Identity key changed")
- registerError(number, "Failed to reload device keys", error);
- else {
- error = textsecure.replay.createReplayableError("The destination's identity key has changed", "The identity of the destination has changed. This may be malicious, or the destination may have simply reinstalled TextSecure.",
- textsecure.replay.SEND_MESSAGE, [number, getString(message.encode())]);
- registerError(number, "Identity key changed", error);
- }
- });
- } else
- registerError(number, "Failed to create or send message", error);
- });
- }
-
- for (var i = 0; i < numbers.length; i++) {
- var number = numbers[i];
- var devicesForNumber = textsecure.storage.devices.getDeviceObjectsForNumber(number);
-
- if (devicesForNumber.length == 0) {
- getKeysForNumber(number)
- .then(reloadDevicesAndSend(number, true))
- .catch(function(error) {
- registerError(number, "Failed to retreive new device keys for number " + number, error);
- });
- } else
- doSendMessage(number, devicesForNumber, true);
- }
- }
-}();
-
window.textsecure.register = function() {
return function(number, verificationCode, singleDevice, stepDone) {
var signalingKey = textsecure.crypto.getRandomBytes(32 + 20);
diff --git a/js/models/messages.js b/js/models/messages.js
index 76cae14b0..d46220a94 100644
--- a/js/models/messages.js
+++ b/js/models/messages.js
@@ -10,10 +10,6 @@ var Whisper = Whisper || {};
if (missing.length) { return "Message must have " + missing; }
},
- toProto: function() {
- return new textsecure.protos.PushMessageContentProtobuf({body: this.get('body')});
- },
-
thread: function() {
return Whisper.Threads.get(this.get('threadId'));
}
diff --git a/js/models/threads.js b/js/models/threads.js
index d947e9e53..75c8aa1b8 100644
--- a/js/models/threads.js
+++ b/js/models/threads.js
@@ -13,27 +13,26 @@ var Whisper = Whisper || {};
},
validate: function(attributes, options) {
- var required = ['id', 'type', 'recipients', 'timestamp', 'image', 'name'];
+ var required = ['id', 'type', 'timestamp', 'image', 'name'];
var missing = _.filter(required, function(attr) { return !attributes[attr]; });
if (missing.length) { return "Thread must have " + missing; }
- if (attributes.recipients.length === 0) {
- return "No recipients for thread " + this.id;
- }
- for (var person in attributes.recipients) {
- if (!person) return "Invalid recipient";
- }
},
sendMessage: function(message) {
- return new Promise(function(resolve) {
- var m = Whisper.Messages.addOutgoingMessage(message, this);
- textsecure.sendMessage(this.get('recipients'), m.toProto(),
- function(result) {
- console.log(result);
- resolve();
- }
- );
- }.bind(this));
+ var m = Whisper.Messages.addOutgoingMessage(message, this);
+ if (this.get('type') == 'private')
+ var promise = textsecure.messaging.sendMessageToNumber(this.get('id'), message, []);
+ else
+ var promise = textsecure.messaging.sendMessageToGroup(this.get('id'), message, []);
+ promise.then(
+ function(result) {
+ console.log(result);
+ }
+ ).catch(
+ function(error) {
+ console.log(error);
+ }
+ );
},
messages: function() {
@@ -51,23 +50,13 @@ var Whisper = Whisper || {};
return thread;
},
- findOrCreateForRecipients: function(recipients) {
+ findOrCreateForRecipient: function(recipient) {
var attributes = {};
- if (recipients.length > 1) {
- attributes = {
- //TODO group id formatting?
- name : recipients,
- recipients : recipients,
- type : 'group',
- };
- } else {
- attributes = {
- id : recipients[0],
- name : recipients[0],
- recipients : recipients,
- type : 'private',
- };
- }
+ attributes = {
+ id : recipient,
+ name : recipient,
+ type : 'private',
+ };
return this.findOrCreate(attributes);
},
@@ -77,14 +66,12 @@ var Whisper = Whisper || {};
attributes = {
id : decrypted.message.group.id,
name : decrypted.message.group.name,
- recipients : decrypted.message.group.members,
type : 'group',
};
} else {
attributes = {
id : decrypted.pushMessage.source,
name : decrypted.pushMessage.source,
- recipients : [decrypted.pushMessage.source],
type : 'private'
};
}
diff --git a/js/sendmessage.js b/js/sendmessage.js
new file mode 100644
index 000000000..f9267dc68
--- /dev/null
+++ b/js/sendmessage.js
@@ -0,0 +1,216 @@
+// sendMessage(numbers = [], message = PushMessageContentProto, callback(success/failure map))
+window.textsecure.messaging = function() {
+ var self = {};
+
+ function getKeysForNumber(number, updateDevices) {
+ return textsecure.api.getKeysForNumber(number).then(function(response) {
+ var identityKey = getString(response[0].identityKey);
+ for (i in response)
+ if (getString(response[i].identityKey) != identityKey)
+ throw new Error("Identity key not consistent");
+
+ for (i in response) {
+ var updateDevice = (updateDevices === undefined);
+ if (!updateDevice)
+ for (j in updateDevices)
+ if (updateDevices[j] == response[i].deviceId)
+ updateDevice = true;
+
+ if (updateDevice)
+ textsecure.storage.devices.saveDeviceObject({
+ encodedNumber: number + "." + response[i].deviceId,
+ identityKey: response[i].identityKey,
+ publicKey: response[i].publicKey,
+ preKeyId: response[i].keyId,
+ registrationId: response[i].registrationId
+ });
+ }
+ });
+ }
+
+ // success_callback(server success/failure map), error_callback(error_msg)
+ // message == PushMessageContentProto (NOT STRING)
+ function sendMessageToDevices(number, deviceObjectList, message, success_callback, error_callback) {
+ var jsonData = [];
+ var relay = undefined;
+ var promises = [];
+
+ var addEncryptionFor = function(i) {
+ if (deviceObjectList[i].relay !== undefined) {
+ if (relay === undefined)
+ relay = deviceObjectList[i].relay;
+ else if (relay != deviceObjectList[i].relay)
+ return new Promise(function() { throw new Error("Mismatched relays for number " + number); });
+ } else {
+ if (relay === undefined)
+ relay = "";
+ else if (relay != "")
+ return new Promise(function() { throw new Error("Mismatched relays for number " + number); });
+ }
+
+ return textsecure.crypto.encryptMessageFor(deviceObjectList[i], message).then(function(encryptedMsg) {
+ jsonData[i] = {
+ type: encryptedMsg.type,
+ destinationDeviceId: textsecure.utils.unencodeNumber(deviceObjectList[i].encodedNumber)[1],
+ destinationRegistrationId: deviceObjectList[i].registrationId,
+ body: encryptedMsg.body,
+ timestamp: new Date().getTime()
+ };
+
+ if (deviceObjectList[i].relay !== undefined)
+ jsonData[i].relay = deviceObjectList[i].relay;
+ });
+ }
+
+ for (var i = 0; i < deviceObjectList.length; i++)
+ promises[i] = addEncryptionFor(i);
+ return Promise.all(promises).then(function() {
+ return textsecure.api.sendMessages(number, jsonData);
+ });
+ }
+
+ var tryMessageAgain = function(number, encodedMessage, callback) {
+ //TODO: Wipe identity key!
+ var message = textsecure.protos.decodePushMessageContentProtobuf(encodedMessage);
+ textsecure.sendMessage([number], message, callback);
+ }
+ textsecure.replay.registerReplayFunction(tryMessageAgain, textsecure.replay.SEND_MESSAGE);
+
+ var sendMessageProto = function(numbers, message, callback) {
+ var numbersCompleted = 0;
+ var errors = [];
+ var successfulNumbers = [];
+
+ var numberCompleted = function() {
+ numbersCompleted++;
+ if (numbersCompleted >= numbers.length)
+ callback({success: successfulNumbers, failure: errors});
+ }
+
+ var registerError = function(number, message, error) {
+ if (error.humanError)
+ message = error.humanError;
+ errors[errors.length] = { number: number, reason: message, error: error };
+ numberCompleted();
+ }
+
+ var doSendMessage;
+ var reloadDevicesAndSend = function(number, recurse) {
+ return function() {
+ var devicesForNumber = textsecure.storage.devices.getDeviceObjectsForNumber(number);
+ if (devicesForNumber.length == 0)
+ registerError(number, "Go empty device list when loading device keys", null);
+ else
+ doSendMessage(number, devicesForNumber, recurse);
+ }
+ }
+
+ doSendMessage = function(number, devicesForNumber, recurse) {
+ return sendMessageToDevices(number, devicesForNumber, message).then(function(result) {
+ successfulNumbers[successfulNumbers.length] = number;
+ numberCompleted();
+ }).catch(function(error) {
+ if (error instanceof Error && error.name == "HTTPError" && (error.message == 410 || error.message == 409)) {
+ if (!recurse)
+ return registerError(number, "Hit retry limit attempting to reload device list", error);
+
+ if (error.message == 409)
+ textsecure.storage.devices.removeDeviceIdsForNumber(number, error.response.extraDevices);
+
+ var resetDevices = ((error.message == 410) ? error.response.staleDevices : error.response.missingDevices);
+ getKeysForNumber(number, resetDevices)
+ .then(reloadDevicesAndSend(number, false))
+ .catch(function(error) {
+ if (error.message !== "Identity key changed")
+ registerError(number, "Failed to reload device keys", error);
+ else {
+ error = textsecure.replay.createReplayableError("The destination's identity key has changed", "The identity of the destination has changed. This may be malicious, or the destination may have simply reinstalled TextSecure.",
+ textsecure.replay.SEND_MESSAGE, [number, getString(message.encode())]);
+ registerError(number, "Identity key changed", error);
+ }
+ });
+ } else
+ registerError(number, "Failed to create or send message", error);
+ });
+ }
+
+ for (var i = 0; i < numbers.length; i++) {
+ var number = numbers[i];
+ var devicesForNumber = textsecure.storage.devices.getDeviceObjectsForNumber(number);
+
+ if (devicesForNumber.length == 0) {
+ getKeysForNumber(number)
+ .then(reloadDevicesAndSend(number, true))
+ .catch(function(error) {
+ registerError(number, "Failed to retreive new device keys for number " + number, error);
+ });
+ } else
+ doSendMessage(number, devicesForNumber, true);
+ }
+ }
+
+ var makeAttachmentPointer = function(attachment) {
+ var proto = new textsecure.protos.PushMessageContentProtobuf.AttachmentPointer();
+ proto.key = textsecure.crypto.getRandomBytes(64);
+
+ var iv = textsecure.crypto.getRandomBytes(16);
+ return textsecure.crypto.encryptAttachment(attachment.data, proto.key, iv).then(function(encryptedBin) {
+ return textsecure.api.putAttachment(encryptedBin).then(function(id) {
+ proto.id = id;
+ proto.contentType = attachment.contentType;
+ return proto;
+ });
+ });
+ }
+
+ self.sendMessageToNumber = function(number, messageText, attachments) {
+ return new Promise(function(resolve, reject) {
+ var proto = new textsecure.protos.PushMessageContentProtobuf();
+ proto.body = messageText;
+
+ var promises = [];
+ for (i in attachments)
+ promises.push(makeAttachmentPointer(attachments[i]));
+ Promise.all(promises).then(function(attachmentsArray) {
+ proto.attachments = attachmentsArray;
+ sendMessageProto([number], proto, function(res) {
+ if (res.failure.length > 0)
+ reject(res.failure[0].error);
+ else
+ resolve();
+ });
+ });
+ });
+ }
+
+ self.sendMessageToGroup = function(groupId, messageText, attachments) {
+ return new Promise(function(resolve, reject) {
+ var proto = new textsecure.protos.PushMessageContentProtobuf();
+ proto.body = messageText;
+ proto.group = new textsecure.protos.PushMessageContentProtobuf.GroupContext();
+ proto.group.id = groupId;
+ proto.group.type = textsecure.protos.PushMessageContentProtobuf.GroupContext.DELIVER;
+
+ var numbers = textsecure.storage.groups.getNumbers(groupId);
+
+ var promises = [];
+ for (i in attachments)
+ promises.push(makeAttachmentPointer(attachments[i]));
+ Promise.all(promises).then(function(attachmentsArray) {
+ proto.attachments = attachmentsArray;
+ sendMessageProto(numbers, proto, function(res) {
+ if (res.failure.length > 0) {
+ reject(res.failure);
+ } else
+ resolve();
+ });
+ });
+ });
+ }
+
+ self.closeSession = function(number) {
+ //TODO
+ }
+
+ return self;
+}();
diff --git a/js/test.js b/js/test.js
index 7caf66a1d..a7847315d 100644
--- a/js/test.js
+++ b/js/test.js
@@ -114,7 +114,7 @@ textsecure.registerOnLoadFunction(function() {
var text_message = new PushMessageProto();
text_message.body = "Hi Mom";
- var server_message = {type: 0, // unencrypted
+ var server_message = {type: 4, // unencrypted
source: "+19999999999", timestamp: 42, message: text_message.encode() };
return textsecure.crypto.handleIncomingPushMessageProto(server_message).then(function(message) {
@@ -367,28 +367,20 @@ textsecure.registerOnLoadFunction(function() {
if (data.getKeys !== undefined)
getKeysForNumberMap[remoteNumber] = data.getKeys;
- var message = new textsecure.protos.PushMessageContentProtobuf();
- message.body = data.smsText;
-
- return new Promise(function(resolve) {
- textsecure.sendMessage([remoteNumber], message, function(res) {
- if (res.failure.length != 0 || res.success.length != 1 || res.success[0] != remoteNumber)
- return resolve(false);
-
- var msg = messagesSentMap[remoteNumber + "." + 0];
- delete messagesSentMap[remoteNumber + "." + 0];
- //XXX: This should be all we do: stepDone(getString(data.expectedCiphertext) == getString(res.body));
- if (msg.type == 1) {
- var expectedString = getString(data.expectedCiphertext);
- var decoded = textsecure.protos.decodeWhisperMessageProtobuf(expectedString.substring(1, expectedString.length - 8));
- var result = getString(msg.body);
- resolve(getString(decoded.encode()) == result.substring(1, result.length - 8));
- } else {
- var decoded = textsecure.protos.decodePreKeyWhisperMessageProtobuf(getString(data.expectedCiphertext).substr(1));
- var result = getString(msg.body).substring(1);
- resolve(getString(decoded.encode()) == result);
- }
- });
+ return textsecure.messaging.sendMessageToNumber(remoteNumber, data.smsText, []).then(function() {
+ var msg = messagesSentMap[remoteNumber + "." + 0];
+ delete messagesSentMap[remoteNumber + "." + 0];
+ //XXX: This should be all we do: stepDone(getString(data.expectedCiphertext) == getString(res.body));
+ if (msg.type == 1) {
+ var expectedString = getString(data.expectedCiphertext);
+ var decoded = textsecure.protos.decodeWhisperMessageProtobuf(expectedString.substring(1, expectedString.length - 8));
+ var result = getString(msg.body);
+ return getString(decoded.encode()) == result.substring(1, result.length - 8);
+ } else {
+ var decoded = textsecure.protos.decodePreKeyWhisperMessageProtobuf(getString(data.expectedCiphertext).substr(1));
+ var result = getString(msg.body).substring(1);
+ return getString(decoded.encode()) == result;
+ }
});
}
diff --git a/js/views/messages.js b/js/views/messages.js
index d5fcdd6a2..baba32d69 100644
--- a/js/views/messages.js
+++ b/js/views/messages.js
@@ -51,7 +51,7 @@ var Whisper = Whisper || {};
numbers = _.filter(numbers, _.identity); // rm undefined, null, "", etc...
if (numbers.length) {
$('#send').hide();
- Whisper.Threads.findOrCreateForRecipients(numbers).trigger('select');
+ Whisper.Threads.findOrCreateForRecipient(numbers).trigger('select');
} else {
Whisper.notify('recipient missing or invalid');
$('#send input[type=text]').focus();
diff --git a/options.html b/options.html
index 328273507..71d1b140a 100644
--- a/options.html
+++ b/options.html
@@ -66,6 +66,8 @@
+
+