2015-09-07 21:53:43 +00:00
|
|
|
/*
|
|
|
|
* vim: ts=4:sw=4:expandtab
|
2014-11-13 22:35:37 +00:00
|
|
|
*/
|
2014-05-17 04:48:46 +00:00
|
|
|
(function () {
|
|
|
|
'use strict';
|
2014-11-13 05:46:57 +00:00
|
|
|
window.Whisper = window.Whisper || {};
|
|
|
|
|
2015-02-08 02:18:53 +00:00
|
|
|
// TODO: Factor out private and group subclasses of Conversation
|
|
|
|
|
2017-08-12 00:16:22 +00:00
|
|
|
var COLORS = [
|
2016-08-29 06:57:33 +00:00
|
|
|
'red',
|
|
|
|
'pink',
|
|
|
|
'purple',
|
|
|
|
'deep_purple',
|
|
|
|
'indigo',
|
|
|
|
'blue',
|
|
|
|
'light_blue',
|
|
|
|
'cyan',
|
|
|
|
'teal',
|
|
|
|
'green',
|
|
|
|
'light_green',
|
|
|
|
'orange',
|
|
|
|
'deep_orange',
|
|
|
|
'amber',
|
|
|
|
'blue_grey',
|
2015-09-10 07:46:50 +00:00
|
|
|
];
|
|
|
|
|
2017-08-12 00:16:22 +00:00
|
|
|
function constantTimeEqualArrayBuffers(ab1, ab2) {
|
|
|
|
if (!(ab1 instanceof ArrayBuffer && ab2 instanceof ArrayBuffer)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (ab1.byteLength !== ab2.byteLength) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
var result = true;
|
|
|
|
var ta1 = new Uint8Array(ab1);
|
|
|
|
var ta2 = new Uint8Array(ab2);
|
|
|
|
for (var i = 0; i < ab1.byteLength; ++i) {
|
|
|
|
if (ta1[i] !== ta2[i]) { result = false; }
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2015-02-08 02:18:53 +00:00
|
|
|
Whisper.Conversation = Backbone.Model.extend({
|
2014-11-13 22:35:37 +00:00
|
|
|
database: Whisper.Database,
|
|
|
|
storeName: 'conversations',
|
2014-05-17 04:48:46 +00:00
|
|
|
defaults: function() {
|
2017-07-20 22:06:22 +00:00
|
|
|
return {
|
|
|
|
unreadCount: 0,
|
|
|
|
verified: textsecure.storage.protocol.VerifiedStatus.DEFAULT
|
|
|
|
};
|
2014-05-17 04:48:46 +00:00
|
|
|
},
|
|
|
|
|
2017-09-25 23:41:57 +00:00
|
|
|
idForLogging: function() {
|
|
|
|
if (this.isPrivate()) {
|
|
|
|
return this.id;
|
|
|
|
}
|
|
|
|
|
|
|
|
return 'group(' + this.id + ')';
|
|
|
|
},
|
|
|
|
|
2017-01-04 03:37:56 +00:00
|
|
|
handleMessageError: function(message, errors) {
|
|
|
|
this.trigger('messageError', message, errors);
|
|
|
|
},
|
|
|
|
|
2014-11-13 22:35:37 +00:00
|
|
|
initialize: function() {
|
2017-06-10 19:18:24 +00:00
|
|
|
this.ourNumber = textsecure.storage.user.getNumber();
|
2017-06-15 23:37:20 +00:00
|
|
|
this.verifiedEnum = textsecure.storage.protocol.VerifiedStatus;
|
2017-06-10 19:18:24 +00:00
|
|
|
|
2017-09-01 16:10:41 +00:00
|
|
|
// This may be overridden by ConversationController.getOrCreate, and signify
|
|
|
|
// our first save to the database. Or first fetch from the database.
|
|
|
|
this.initialPromise = Promise.resolve();
|
|
|
|
|
2015-09-08 00:22:59 +00:00
|
|
|
this.contactCollection = new Backbone.Collection();
|
2015-03-12 00:49:01 +00:00
|
|
|
this.messageCollection = new Whisper.MessageCollection([], {
|
|
|
|
conversation: this
|
|
|
|
});
|
2015-03-17 22:06:21 +00:00
|
|
|
|
2017-01-04 03:37:56 +00:00
|
|
|
this.messageCollection.on('change:errors', this.handleMessageError, this);
|
2017-06-22 21:03:05 +00:00
|
|
|
this.messageCollection.on('send-error', this.onMessageError, this);
|
2017-01-04 03:37:56 +00:00
|
|
|
|
2015-03-17 22:06:21 +00:00
|
|
|
this.on('change:avatar', this.updateAvatarUrl);
|
2017-09-11 16:50:35 +00:00
|
|
|
this.on('change:profileAvatar', this.updateAvatarUrl);
|
|
|
|
this.on('change:profileKey', this.onChangeProfileKey);
|
2015-03-18 00:10:18 +00:00
|
|
|
this.on('destroy', this.revokeAvatarUrl);
|
2016-09-18 06:55:05 +00:00
|
|
|
},
|
|
|
|
|
2017-06-14 17:58:05 +00:00
|
|
|
isMe: function() {
|
|
|
|
return this.id === this.ourNumber;
|
|
|
|
},
|
|
|
|
|
2017-06-22 21:03:05 +00:00
|
|
|
onMessageError: function() {
|
|
|
|
this.updateVerified();
|
|
|
|
},
|
2017-08-10 16:30:08 +00:00
|
|
|
safeGetVerified: function() {
|
|
|
|
return textsecure.storage.protocol.getVerified(this.id).catch(function() {
|
|
|
|
return textsecure.storage.protocol.VerifiedStatus.DEFAULT;
|
|
|
|
});
|
|
|
|
},
|
2017-06-10 19:18:24 +00:00
|
|
|
updateVerified: function() {
|
|
|
|
if (this.isPrivate()) {
|
|
|
|
return Promise.all([
|
2017-08-10 16:30:08 +00:00
|
|
|
this.safeGetVerified(),
|
2017-09-01 16:10:41 +00:00
|
|
|
this.initialPromise,
|
2017-06-10 19:18:24 +00:00
|
|
|
]).then(function(results) {
|
|
|
|
var trust = results[0];
|
2017-06-22 21:01:58 +00:00
|
|
|
// we don't return here because we don't need to wait for this to finish
|
|
|
|
this.save({verified: trust});
|
2017-06-14 00:36:32 +00:00
|
|
|
}.bind(this));
|
2017-06-10 19:18:24 +00:00
|
|
|
} else {
|
|
|
|
return this.fetchContacts().then(function() {
|
|
|
|
return Promise.all(this.contactCollection.map(function(contact) {
|
2017-06-14 17:58:05 +00:00
|
|
|
if (!contact.isMe()) {
|
2017-06-10 19:18:24 +00:00
|
|
|
return contact.updateVerified();
|
|
|
|
}
|
|
|
|
}.bind(this)));
|
2017-06-14 00:36:32 +00:00
|
|
|
}.bind(this)).then(this.onMemberVerifiedChange.bind(this));
|
2017-06-10 19:18:24 +00:00
|
|
|
}
|
|
|
|
},
|
2017-06-15 23:12:58 +00:00
|
|
|
setVerifiedDefault: function(options) {
|
2017-06-16 01:11:18 +00:00
|
|
|
var DEFAULT = this.verifiedEnum.DEFAULT;
|
2017-07-04 00:31:57 +00:00
|
|
|
return this.queueJob(function() {
|
2017-07-21 17:59:41 +00:00
|
|
|
return this._setVerified(DEFAULT, options);
|
2017-07-04 00:31:57 +00:00
|
|
|
}.bind(this));
|
2017-06-14 00:36:32 +00:00
|
|
|
},
|
2017-06-15 23:12:58 +00:00
|
|
|
setVerified: function(options) {
|
2017-06-19 23:33:50 +00:00
|
|
|
var VERIFIED = this.verifiedEnum.VERIFIED;
|
2017-07-04 00:31:57 +00:00
|
|
|
return this.queueJob(function() {
|
2017-07-21 17:59:41 +00:00
|
|
|
return this._setVerified(VERIFIED, options);
|
2017-07-04 00:31:57 +00:00
|
|
|
}.bind(this));
|
2017-06-19 23:33:50 +00:00
|
|
|
},
|
2017-06-29 02:39:55 +00:00
|
|
|
setUnverified: function(options) {
|
|
|
|
var UNVERIFIED = this.verifiedEnum.UNVERIFIED;
|
|
|
|
return this.queueJob(function() {
|
2017-07-21 17:59:41 +00:00
|
|
|
return this._setVerified(UNVERIFIED, options);
|
2017-06-29 02:39:55 +00:00
|
|
|
}.bind(this));
|
|
|
|
},
|
2017-06-19 23:33:50 +00:00
|
|
|
_setVerified: function(verified, options) {
|
2017-06-15 23:12:58 +00:00
|
|
|
options = options || {};
|
2017-07-04 00:31:57 +00:00
|
|
|
_.defaults(options, {viaSyncMessage: false, viaContactSync: false, key: null});
|
2017-06-15 23:12:58 +00:00
|
|
|
|
2017-07-04 00:31:57 +00:00
|
|
|
var DEFAULT = this.verifiedEnum.DEFAULT;
|
2017-06-29 02:39:55 +00:00
|
|
|
var VERIFIED = this.verifiedEnum.VERIFIED;
|
|
|
|
var UNVERIFIED = this.verifiedEnum.UNVERIFIED;
|
2017-06-14 00:36:32 +00:00
|
|
|
|
|
|
|
if (!this.isPrivate()) {
|
|
|
|
throw new Error('You cannot verify a group conversation. ' +
|
|
|
|
'You must verify individual contacts.');
|
|
|
|
}
|
|
|
|
|
2017-07-04 00:31:57 +00:00
|
|
|
var beginningVerified = this.get('verified');
|
2017-06-17 01:38:36 +00:00
|
|
|
var promise;
|
|
|
|
if (options.viaSyncMessage) {
|
|
|
|
// handle the incoming key from the sync messages - need different
|
|
|
|
// behavior if that key doesn't match the current key
|
|
|
|
promise = textsecure.storage.protocol.processVerifiedMessage(
|
2017-06-19 23:33:50 +00:00
|
|
|
this.id, verified, options.key
|
2017-06-17 01:38:36 +00:00
|
|
|
);
|
|
|
|
} else {
|
|
|
|
promise = textsecure.storage.protocol.setVerified(
|
2017-06-19 23:33:50 +00:00
|
|
|
this.id, verified
|
2017-06-17 01:38:36 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2017-07-04 00:31:57 +00:00
|
|
|
var keychange;
|
|
|
|
return promise.then(function(updatedKey) {
|
|
|
|
keychange = updatedKey;
|
2017-07-13 17:07:18 +00:00
|
|
|
return new Promise(function(resolve) {
|
|
|
|
return this.save({verified: verified}).always(resolve);
|
|
|
|
}.bind(this));
|
2017-06-15 19:27:41 +00:00
|
|
|
}.bind(this)).then(function() {
|
2017-07-04 00:31:57 +00:00
|
|
|
// Three situations result in a verification notice in the conversation:
|
|
|
|
// 1) The message came from an explicit verification in another client (not
|
|
|
|
// a contact sync)
|
|
|
|
// 2) The verification value received by the contact sync is different
|
2017-06-29 02:39:55 +00:00
|
|
|
// from what we have on record (and it's not a transition to UNVERIFIED)
|
|
|
|
// 3) Our local verification status is VERIFIED and it hasn't changed,
|
|
|
|
// but the key did change (Key1/VERIFIED to Key2/VERIFIED - but we don't
|
|
|
|
// want to show DEFAULT->DEFAULT or UNVERIFIED->UNVERIFIED)
|
2017-07-04 00:31:57 +00:00
|
|
|
if (!options.viaContactSync
|
2017-06-29 02:39:55 +00:00
|
|
|
|| (beginningVerified !== verified && verified !== UNVERIFIED)
|
|
|
|
|| (keychange && verified === VERIFIED)) {
|
2017-07-04 00:31:57 +00:00
|
|
|
|
2017-06-30 00:32:40 +00:00
|
|
|
this.addVerifiedChange(this.id, verified === VERIFIED, {local: !options.viaSyncMessage});
|
2017-07-04 00:31:57 +00:00
|
|
|
}
|
2017-06-15 23:12:58 +00:00
|
|
|
if (!options.viaSyncMessage) {
|
2017-06-20 17:56:04 +00:00
|
|
|
return this.sendVerifySyncMessage(this.id, verified);
|
2017-06-15 23:12:58 +00:00
|
|
|
}
|
2017-06-14 00:36:32 +00:00
|
|
|
}.bind(this));
|
|
|
|
},
|
2017-06-15 23:12:58 +00:00
|
|
|
sendVerifySyncMessage: function(number, state) {
|
2017-06-27 19:20:53 +00:00
|
|
|
return textsecure.storage.protocol.loadIdentityKey(number).then(function(key) {
|
2017-06-20 17:56:04 +00:00
|
|
|
return textsecure.messaging.syncVerification(number, state, key);
|
2017-06-15 23:37:20 +00:00
|
|
|
});
|
2017-06-15 23:12:58 +00:00
|
|
|
},
|
2017-08-12 00:16:22 +00:00
|
|
|
getIdentityKeys: function() {
|
|
|
|
var lookup = {};
|
|
|
|
|
|
|
|
if (this.isPrivate()) {
|
|
|
|
return textsecure.storage.protocol.loadIdentityKey(this.id).then(function(key) {
|
|
|
|
lookup[this.id] = key;
|
|
|
|
return lookup;
|
|
|
|
}.bind(this)).catch(function(error) {
|
|
|
|
console.log(
|
|
|
|
'getIdentityKeys error for conversation',
|
2017-09-25 23:41:57 +00:00
|
|
|
this.idForLogging(),
|
2017-08-12 00:16:22 +00:00
|
|
|
error && error.stack ? error.stack : error
|
|
|
|
);
|
|
|
|
return lookup;
|
|
|
|
}.bind(this));
|
|
|
|
} else {
|
|
|
|
return Promise.all(this.contactCollection.map(function(contact) {
|
|
|
|
return textsecure.storage.protocol.loadIdentityKey(contact.id).then(function(key) {
|
|
|
|
lookup[contact.id] = key;
|
|
|
|
}).catch(function(error) {
|
|
|
|
console.log(
|
|
|
|
'getIdentityKeys error for group member',
|
2017-09-25 23:41:57 +00:00
|
|
|
contact.idForLogging(),
|
2017-08-12 00:16:22 +00:00
|
|
|
error && error.stack ? error.stack : error
|
|
|
|
);
|
|
|
|
});
|
|
|
|
})).then(function() {
|
|
|
|
return lookup;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
},
|
|
|
|
replay: function(error, message) {
|
|
|
|
var replayable = new textsecure.ReplayableError(error);
|
|
|
|
return replayable.replay(message.attributes).catch(function(error) {
|
|
|
|
console.log(
|
|
|
|
'replay error:',
|
|
|
|
error && error.stack ? error.stack : error
|
|
|
|
);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
decryptOldIncomingKeyErrors: function() {
|
|
|
|
// We want to run just once per conversation
|
|
|
|
if (this.get('decryptedOldIncomingKeyErrors')) {
|
|
|
|
return Promise.resolve();
|
|
|
|
}
|
2017-09-25 23:41:57 +00:00
|
|
|
console.log('decryptOldIncomingKeyErrors start for', this.idForLogging());
|
2017-08-12 00:16:22 +00:00
|
|
|
|
|
|
|
var messages = this.messageCollection.filter(function(message) {
|
|
|
|
var errors = message.get('errors');
|
|
|
|
if (!errors || !errors[0]) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
var error = _.find(errors, function(error) {
|
|
|
|
return error.name === 'IncomingIdentityKeyError';
|
|
|
|
});
|
|
|
|
|
|
|
|
return Boolean(error);
|
|
|
|
});
|
|
|
|
|
|
|
|
var markComplete = function() {
|
2017-09-25 23:41:57 +00:00
|
|
|
console.log('decryptOldIncomingKeyErrors complete for', this.idForLogging());
|
2017-08-12 00:16:22 +00:00
|
|
|
return new Promise(function(resolve) {
|
|
|
|
this.save({decryptedOldIncomingKeyErrors: true}).always(resolve);
|
|
|
|
}.bind(this));
|
|
|
|
}.bind(this);
|
|
|
|
|
|
|
|
if (!messages.length) {
|
|
|
|
return markComplete();
|
|
|
|
}
|
|
|
|
|
|
|
|
console.log('decryptOldIncomingKeyErrors found', messages.length, 'messages to process');
|
|
|
|
var safeDelete = function(message) {
|
|
|
|
return new Promise(function(resolve) {
|
|
|
|
message.destroy().always(resolve);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
return this.getIdentityKeys().then(function(lookup) {
|
|
|
|
return Promise.all(_.map(messages, function(message) {
|
|
|
|
var source = message.get('source');
|
|
|
|
var error = _.find(message.get('errors'), function(error) {
|
|
|
|
return error.name === 'IncomingIdentityKeyError';
|
|
|
|
});
|
|
|
|
|
|
|
|
var key = lookup[source];
|
|
|
|
if (!key) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (constantTimeEqualArrayBuffers(key, error.identityKey)) {
|
|
|
|
return this.replay(error, message).then(function() {
|
|
|
|
return safeDelete(message);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}.bind(this)));
|
|
|
|
}.bind(this)).catch(function(error) {
|
|
|
|
console.log(
|
|
|
|
'decryptOldIncomingKeyErrors error:',
|
|
|
|
error && error.stack ? error.stack : error
|
|
|
|
);
|
|
|
|
}).then(markComplete);
|
|
|
|
},
|
2017-06-10 19:18:24 +00:00
|
|
|
isVerified: function() {
|
|
|
|
if (this.isPrivate()) {
|
2017-06-14 00:36:32 +00:00
|
|
|
return this.get('verified') === this.verifiedEnum.VERIFIED;
|
2017-06-10 19:18:24 +00:00
|
|
|
} else {
|
2017-06-14 00:36:32 +00:00
|
|
|
if (!this.contactCollection.length) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2017-06-10 19:18:24 +00:00
|
|
|
return this.contactCollection.every(function(contact) {
|
2017-06-14 17:58:05 +00:00
|
|
|
if (contact.isMe()) {
|
2017-06-10 19:18:24 +00:00
|
|
|
return true;
|
|
|
|
} else {
|
|
|
|
return contact.isVerified();
|
|
|
|
}
|
|
|
|
}.bind(this));
|
|
|
|
}
|
|
|
|
},
|
2017-06-14 00:36:32 +00:00
|
|
|
isUnverified: function() {
|
2017-06-10 19:18:24 +00:00
|
|
|
if (this.isPrivate()) {
|
|
|
|
var verified = this.get('verified');
|
2017-06-14 00:36:32 +00:00
|
|
|
return verified !== this.verifiedEnum.VERIFIED && verified !== this.verifiedEnum.DEFAULT;
|
2017-06-10 19:18:24 +00:00
|
|
|
} else {
|
2017-06-14 00:36:32 +00:00
|
|
|
if (!this.contactCollection.length) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.contactCollection.any(function(contact) {
|
2017-06-14 17:58:05 +00:00
|
|
|
if (contact.isMe()) {
|
2017-06-14 00:36:32 +00:00
|
|
|
return false;
|
|
|
|
} else {
|
|
|
|
return contact.isUnverified();
|
|
|
|
}
|
|
|
|
}.bind(this));
|
2017-06-10 19:18:24 +00:00
|
|
|
}
|
|
|
|
},
|
2017-06-14 00:36:32 +00:00
|
|
|
getUnverified: function() {
|
2017-06-10 19:18:24 +00:00
|
|
|
if (this.isPrivate()) {
|
2017-06-14 00:36:32 +00:00
|
|
|
return this.isUnverified() ? new Backbone.Collection([this]) : new Backbone.Collection();
|
2017-06-10 19:18:24 +00:00
|
|
|
} else {
|
2017-06-14 00:36:32 +00:00
|
|
|
return new Backbone.Collection(this.contactCollection.filter(function(contact) {
|
2017-06-14 17:58:05 +00:00
|
|
|
if (contact.isMe()) {
|
2017-06-10 19:18:24 +00:00
|
|
|
return false;
|
|
|
|
} else {
|
2017-06-14 00:36:32 +00:00
|
|
|
return contact.isUnverified();
|
2017-06-10 19:18:24 +00:00
|
|
|
}
|
2017-06-14 00:36:32 +00:00
|
|
|
}.bind(this)));
|
|
|
|
}
|
|
|
|
},
|
2017-07-06 17:32:59 +00:00
|
|
|
setApproved: function() {
|
2017-07-03 18:52:12 +00:00
|
|
|
if (!this.isPrivate()) {
|
|
|
|
throw new Error('You cannot set a group conversation as trusted. ' +
|
|
|
|
'You must set individual contacts as trusted.');
|
|
|
|
}
|
|
|
|
|
|
|
|
return textsecure.storage.protocol.setApproval(this.id, true);
|
|
|
|
},
|
2017-08-10 16:30:08 +00:00
|
|
|
safeIsUntrusted: function() {
|
|
|
|
return textsecure.storage.protocol.isUntrusted(this.id).catch(function() {
|
|
|
|
return false;
|
|
|
|
});
|
|
|
|
},
|
2017-06-14 00:36:32 +00:00
|
|
|
isUntrusted: function() {
|
|
|
|
if (this.isPrivate()) {
|
2017-08-10 16:30:08 +00:00
|
|
|
return this.safeIsUntrusted();
|
2017-06-14 00:36:32 +00:00
|
|
|
} else {
|
|
|
|
if (!this.contactCollection.length) {
|
|
|
|
return Promise.resolve(false);
|
|
|
|
}
|
|
|
|
|
|
|
|
return Promise.all(this.contactCollection.map(function(contact) {
|
2017-06-14 17:58:05 +00:00
|
|
|
if (contact.isMe()) {
|
2017-06-14 00:36:32 +00:00
|
|
|
return false;
|
|
|
|
} else {
|
2017-08-10 16:30:08 +00:00
|
|
|
return contact.safeIsUntrusted();
|
2017-06-14 00:36:32 +00:00
|
|
|
}
|
|
|
|
}.bind(this))).then(function(results) {
|
|
|
|
return _.any(results, function(result) {
|
|
|
|
return result;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
},
|
|
|
|
getUntrusted: function() {
|
|
|
|
// This is a bit ugly because isUntrusted() is async. Could do the work to cache
|
|
|
|
// it locally, but we really only need it for this call.
|
|
|
|
if (this.isPrivate()) {
|
|
|
|
return this.isUntrusted().then(function(untrusted) {
|
|
|
|
if (untrusted) {
|
|
|
|
return new Backbone.Collection([this]);
|
|
|
|
}
|
|
|
|
|
|
|
|
return new Backbone.Collection();
|
|
|
|
}.bind(this));
|
|
|
|
} else {
|
|
|
|
return Promise.all(this.contactCollection.map(function(contact) {
|
2017-06-14 17:58:05 +00:00
|
|
|
if (contact.isMe()) {
|
2017-06-14 00:36:32 +00:00
|
|
|
return [false, contact];
|
|
|
|
} else {
|
2017-08-10 16:30:08 +00:00
|
|
|
return Promise.all([contact.isUntrusted(), contact]);
|
2017-06-14 00:36:32 +00:00
|
|
|
}
|
|
|
|
}.bind(this))).then(function(results) {
|
|
|
|
results = _.filter(results, function(result) {
|
|
|
|
var untrusted = result[0];
|
|
|
|
return untrusted;
|
|
|
|
});
|
|
|
|
return new Backbone.Collection(_.map(results, function(result) {
|
|
|
|
var contact = result[1];
|
|
|
|
return contact;
|
|
|
|
}));
|
2017-06-10 19:18:24 +00:00
|
|
|
}.bind(this));
|
|
|
|
}
|
|
|
|
},
|
|
|
|
onMemberVerifiedChange: function() {
|
|
|
|
// If the verified state of a member changes, our aggregate state changes.
|
|
|
|
// We trigger both events to replicate the behavior of Backbone.Model.set()
|
|
|
|
this.trigger('change:verified');
|
|
|
|
this.trigger('change');
|
|
|
|
},
|
|
|
|
toggleVerified: function() {
|
|
|
|
if (this.isVerified()) {
|
2017-06-14 00:36:32 +00:00
|
|
|
return this.setVerifiedDefault();
|
2017-06-10 19:18:24 +00:00
|
|
|
} else {
|
2017-06-14 00:36:32 +00:00
|
|
|
return this.setVerified();
|
2017-06-10 19:18:24 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2016-09-18 06:55:05 +00:00
|
|
|
addKeyChange: function(id) {
|
2017-09-25 23:41:57 +00:00
|
|
|
console.log(
|
|
|
|
'adding key change advisory for',
|
|
|
|
this.idForLogging(),
|
|
|
|
id,
|
|
|
|
this.get('timestamp')
|
|
|
|
);
|
|
|
|
|
2017-01-23 04:26:10 +00:00
|
|
|
var timestamp = Date.now();
|
|
|
|
var message = new Whisper.Message({
|
2016-09-18 06:55:05 +00:00
|
|
|
conversationId : this.id,
|
|
|
|
type : 'keychange',
|
2016-10-12 02:10:21 +00:00
|
|
|
sent_at : this.get('timestamp'),
|
2017-01-23 04:26:10 +00:00
|
|
|
received_at : timestamp,
|
2017-07-05 19:17:55 +00:00
|
|
|
key_changed : id,
|
|
|
|
unread : 1
|
2016-09-18 06:55:05 +00:00
|
|
|
});
|
2017-01-23 04:26:10 +00:00
|
|
|
message.save().then(this.trigger.bind(this,'newmessage', message));
|
2016-02-22 05:37:47 +00:00
|
|
|
},
|
2017-06-19 18:45:42 +00:00
|
|
|
addVerifiedChange: function(id, verified, options) {
|
|
|
|
options = options || {};
|
|
|
|
_.defaults(options, {local: true});
|
|
|
|
|
2017-09-08 18:14:01 +00:00
|
|
|
if (this.isMe()) {
|
|
|
|
console.log('refusing to add verified change advisory for our own number');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-07-20 22:06:22 +00:00
|
|
|
var lastMessage = this.get('timestamp') || Date.now();
|
|
|
|
|
2017-09-25 23:41:57 +00:00
|
|
|
console.log(
|
|
|
|
'adding verified change advisory for',
|
|
|
|
this.idForLogging(),
|
|
|
|
id,
|
|
|
|
lastMessage
|
|
|
|
);
|
2017-07-20 22:06:22 +00:00
|
|
|
|
2017-06-15 19:27:41 +00:00
|
|
|
var timestamp = Date.now();
|
|
|
|
var message = new Whisper.Message({
|
|
|
|
conversationId : this.id,
|
|
|
|
type : 'verified-change',
|
2017-07-20 22:06:22 +00:00
|
|
|
sent_at : lastMessage,
|
2017-06-15 19:27:41 +00:00
|
|
|
received_at : timestamp,
|
|
|
|
verifiedChanged : id,
|
2017-06-19 18:45:42 +00:00
|
|
|
verified : verified,
|
2017-07-05 19:17:55 +00:00
|
|
|
local : options.local,
|
|
|
|
unread : 1
|
2017-06-15 19:27:41 +00:00
|
|
|
});
|
|
|
|
message.save().then(this.trigger.bind(this,'newmessage', message));
|
|
|
|
|
|
|
|
if (this.isPrivate()) {
|
2017-07-21 18:00:25 +00:00
|
|
|
ConversationController.getAllGroupsInvolvingId(id).then(function(groups) {
|
|
|
|
_.forEach(groups, function(group) {
|
|
|
|
group.addVerifiedChange(id, verified, options);
|
|
|
|
});
|
2017-06-15 19:27:41 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
},
|
2016-02-22 05:37:47 +00:00
|
|
|
|
2016-09-21 23:26:42 +00:00
|
|
|
onReadMessage: function(message) {
|
|
|
|
if (this.messageCollection.get(message.id)) {
|
|
|
|
this.messageCollection.get(message.id).fetch();
|
|
|
|
}
|
|
|
|
|
2017-06-07 18:24:21 +00:00
|
|
|
// 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
|
Feature: Blue check marks for read messages if opted in (#1489)
* Refactor delivery receipt event handler
* Rename the delivery receipt event
For less ambiguity with read receipts.
* Rename synced read event
For less ambiguity with read receipts from other Signal users.
* Add support for incoming receipt messages
Handle ReceiptMessages, which may include encrypted delivery receipts or read
receipts from recipients of our sent messages.
// FREEBIE
* Rename ReadReceipts to ReadSyncs
* Render read messages with blue double checks
* Send read receipts to senders of incoming messages
// FREEBIE
* Move ReadSyncs to their own file
// FREEBIE
* Fixup old comments on read receipts (now read syncs)
And some variable renaming for extra clarity.
// FREEBIE
* Add global setting for read receipts
Don't send read receipt messages unless the setting is enabled.
Don't process read receipts if the setting is disabled.
// FREEBIE
* Sync read receipt setting from mobile
Toggling this setting on your mobile device should sync it to Desktop. When
linking, use the setting in the provisioning message.
// FREEBIE
* Send receipt messages silently
Avoid generating phantom messages on ios
// FREEBIE
* Save recipients on the outgoing message models
For accurate tracking and display of sent/delivered/read state, even if group
membership changes later.
// FREEBIE
* Fix conversation type in profile key update handling
// FREEBIE
* Set recipients on synced sent messages
* Render saved recipients in message detail if available
For older messages, where we did not save the intended set of recipients at the
time of sending, fall back to the current group membership.
// FREEBIE
* Record who has been successfully sent to
// FREEBIE
* Record who a message has been delivered to
* Invert the not-clickable class
* Fix readReceipt setting sync when linking
* Render per recipient sent/delivered/read status
In the message detail view for outgoing messages, render each recipient's
individual sent/delivered/read status with respect to this message, as long as
there are no errors associated with the recipient (ie, safety number changes,
user not registered, etc...) since the error icon is displayed in that case.
*Messages sent before this change may not have per-recipient status lists
and will simply show no status icon.
// FREEBIE
* Add configuration sync request
Send these requests in a one-off fashion when:
1. We have just setup from a chrome app import
2. We have just upgraded to read-receipt support
// FREEBIE
* Expose sendRequestConfigurationSyncMessage
// FREEBIE
* Fix handling of incoming delivery receipts - union with array
FREEBIE
2017-10-04 22:28:43 +00:00
|
|
|
// the desktop app, so the desktop app only gets read syncs, we can very
|
2017-06-07 18:24:21 +00:00
|
|
|
// easily end up with messages never marked as read (our previous early read
|
Feature: Blue check marks for read messages if opted in (#1489)
* Refactor delivery receipt event handler
* Rename the delivery receipt event
For less ambiguity with read receipts.
* Rename synced read event
For less ambiguity with read receipts from other Signal users.
* Add support for incoming receipt messages
Handle ReceiptMessages, which may include encrypted delivery receipts or read
receipts from recipients of our sent messages.
// FREEBIE
* Rename ReadReceipts to ReadSyncs
* Render read messages with blue double checks
* Send read receipts to senders of incoming messages
// FREEBIE
* Move ReadSyncs to their own file
// FREEBIE
* Fixup old comments on read receipts (now read syncs)
And some variable renaming for extra clarity.
// FREEBIE
* Add global setting for read receipts
Don't send read receipt messages unless the setting is enabled.
Don't process read receipts if the setting is disabled.
// FREEBIE
* Sync read receipt setting from mobile
Toggling this setting on your mobile device should sync it to Desktop. When
linking, use the setting in the provisioning message.
// FREEBIE
* Send receipt messages silently
Avoid generating phantom messages on ios
// FREEBIE
* Save recipients on the outgoing message models
For accurate tracking and display of sent/delivered/read state, even if group
membership changes later.
// FREEBIE
* Fix conversation type in profile key update handling
// FREEBIE
* Set recipients on synced sent messages
* Render saved recipients in message detail if available
For older messages, where we did not save the intended set of recipients at the
time of sending, fall back to the current group membership.
// FREEBIE
* Record who has been successfully sent to
// FREEBIE
* Record who a message has been delivered to
* Invert the not-clickable class
* Fix readReceipt setting sync when linking
* Render per recipient sent/delivered/read status
In the message detail view for outgoing messages, render each recipient's
individual sent/delivered/read status with respect to this message, as long as
there are no errors associated with the recipient (ie, safety number changes,
user not registered, etc...) since the error icon is displayed in that case.
*Messages sent before this change may not have per-recipient status lists
and will simply show no status icon.
// FREEBIE
* Add configuration sync request
Send these requests in a one-off fashion when:
1. We have just setup from a chrome app import
2. We have just upgraded to read-receipt support
// FREEBIE
* Expose sendRequestConfigurationSyncMessage
// FREEBIE
* Fix handling of incoming delivery receipts - union with array
FREEBIE
2017-10-04 22:28:43 +00:00
|
|
|
// sync handling, read syncs never sent because app was offline)
|
2017-06-07 19:06:23 +00:00
|
|
|
|
Feature: Blue check marks for read messages if opted in (#1489)
* Refactor delivery receipt event handler
* Rename the delivery receipt event
For less ambiguity with read receipts.
* Rename synced read event
For less ambiguity with read receipts from other Signal users.
* Add support for incoming receipt messages
Handle ReceiptMessages, which may include encrypted delivery receipts or read
receipts from recipients of our sent messages.
// FREEBIE
* Rename ReadReceipts to ReadSyncs
* Render read messages with blue double checks
* Send read receipts to senders of incoming messages
// FREEBIE
* Move ReadSyncs to their own file
// FREEBIE
* Fixup old comments on read receipts (now read syncs)
And some variable renaming for extra clarity.
// FREEBIE
* Add global setting for read receipts
Don't send read receipt messages unless the setting is enabled.
Don't process read receipts if the setting is disabled.
// FREEBIE
* Sync read receipt setting from mobile
Toggling this setting on your mobile device should sync it to Desktop. When
linking, use the setting in the provisioning message.
// FREEBIE
* Send receipt messages silently
Avoid generating phantom messages on ios
// FREEBIE
* Save recipients on the outgoing message models
For accurate tracking and display of sent/delivered/read state, even if group
membership changes later.
// FREEBIE
* Fix conversation type in profile key update handling
// FREEBIE
* Set recipients on synced sent messages
* Render saved recipients in message detail if available
For older messages, where we did not save the intended set of recipients at the
time of sending, fall back to the current group membership.
// FREEBIE
* Record who has been successfully sent to
// FREEBIE
* Record who a message has been delivered to
* Invert the not-clickable class
* Fix readReceipt setting sync when linking
* Render per recipient sent/delivered/read status
In the message detail view for outgoing messages, render each recipient's
individual sent/delivered/read status with respect to this message, as long as
there are no errors associated with the recipient (ie, safety number changes,
user not registered, etc...) since the error icon is displayed in that case.
*Messages sent before this change may not have per-recipient status lists
and will simply show no status icon.
// FREEBIE
* Add configuration sync request
Send these requests in a one-off fashion when:
1. We have just setup from a chrome app import
2. We have just upgraded to read-receipt support
// FREEBIE
* Expose sendRequestConfigurationSyncMessage
// FREEBIE
* Fix handling of incoming delivery receipts - union with array
FREEBIE
2017-10-04 22:28:43 +00:00
|
|
|
// We queue it because we often get a whole lot of read syncs at once, and
|
2017-06-07 19:06:23 +00:00
|
|
|
// their markRead calls could very easily overlap given the async pull from DB.
|
|
|
|
|
Feature: Blue check marks for read messages if opted in (#1489)
* Refactor delivery receipt event handler
* Rename the delivery receipt event
For less ambiguity with read receipts.
* Rename synced read event
For less ambiguity with read receipts from other Signal users.
* Add support for incoming receipt messages
Handle ReceiptMessages, which may include encrypted delivery receipts or read
receipts from recipients of our sent messages.
// FREEBIE
* Rename ReadReceipts to ReadSyncs
* Render read messages with blue double checks
* Send read receipts to senders of incoming messages
// FREEBIE
* Move ReadSyncs to their own file
// FREEBIE
* Fixup old comments on read receipts (now read syncs)
And some variable renaming for extra clarity.
// FREEBIE
* Add global setting for read receipts
Don't send read receipt messages unless the setting is enabled.
Don't process read receipts if the setting is disabled.
// FREEBIE
* Sync read receipt setting from mobile
Toggling this setting on your mobile device should sync it to Desktop. When
linking, use the setting in the provisioning message.
// FREEBIE
* Send receipt messages silently
Avoid generating phantom messages on ios
// FREEBIE
* Save recipients on the outgoing message models
For accurate tracking and display of sent/delivered/read state, even if group
membership changes later.
// FREEBIE
* Fix conversation type in profile key update handling
// FREEBIE
* Set recipients on synced sent messages
* Render saved recipients in message detail if available
For older messages, where we did not save the intended set of recipients at the
time of sending, fall back to the current group membership.
// FREEBIE
* Record who has been successfully sent to
// FREEBIE
* Record who a message has been delivered to
* Invert the not-clickable class
* Fix readReceipt setting sync when linking
* Render per recipient sent/delivered/read status
In the message detail view for outgoing messages, render each recipient's
individual sent/delivered/read status with respect to this message, as long as
there are no errors associated with the recipient (ie, safety number changes,
user not registered, etc...) since the error icon is displayed in that case.
*Messages sent before this change may not have per-recipient status lists
and will simply show no status icon.
// FREEBIE
* Add configuration sync request
Send these requests in a one-off fashion when:
1. We have just setup from a chrome app import
2. We have just upgraded to read-receipt support
// FREEBIE
* Expose sendRequestConfigurationSyncMessage
// FREEBIE
* Fix handling of incoming delivery receipts - union with array
FREEBIE
2017-10-04 22:28:43 +00:00
|
|
|
// 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.
|
2017-07-25 01:43:35 +00:00
|
|
|
return this.queueJob(function() {
|
2017-06-07 19:06:23 +00:00
|
|
|
return this.markRead(message.get('received_at'), {sendReadReceipts: false});
|
|
|
|
}.bind(this));
|
2016-02-22 05:37:47 +00:00
|
|
|
},
|
|
|
|
|
2017-06-01 22:47:55 +00:00
|
|
|
getUnread: function() {
|
2016-02-22 05:37:47 +00:00
|
|
|
var conversationId = this.id;
|
|
|
|
var unreadMessages = new Whisper.MessageCollection();
|
|
|
|
return new Promise(function(resolve) {
|
|
|
|
return unreadMessages.fetch({
|
|
|
|
index: {
|
|
|
|
// 'unread' index
|
2016-02-26 20:25:32 +00:00
|
|
|
name : 'unread',
|
|
|
|
lower : [conversationId],
|
|
|
|
upper : [conversationId, Number.MAX_VALUE],
|
2016-02-22 05:37:47 +00:00
|
|
|
}
|
|
|
|
}).always(function() {
|
2017-06-01 22:47:55 +00:00
|
|
|
resolve(unreadMessages);
|
2016-02-22 05:37:47 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2014-11-13 22:35:37 +00:00
|
|
|
},
|
|
|
|
|
2014-05-17 04:48:46 +00:00
|
|
|
validate: function(attributes, options) {
|
2015-06-03 17:29:20 +00:00
|
|
|
var required = ['id', 'type'];
|
2015-01-28 12:19:58 +00:00
|
|
|
var missing = _.filter(required, function(attr) { return !attributes[attr]; });
|
|
|
|
if (missing.length) { return "Conversation must have " + missing; }
|
|
|
|
|
2015-02-08 00:24:56 +00:00
|
|
|
if (attributes.type !== 'private' && attributes.type !== 'group') {
|
|
|
|
return "Invalid conversation type: " + attributes.type;
|
|
|
|
}
|
2015-10-16 20:00:38 +00:00
|
|
|
|
2015-12-05 01:38:41 +00:00
|
|
|
var error = this.validateNumber();
|
|
|
|
if (error) { return error; }
|
|
|
|
|
|
|
|
this.updateTokens();
|
2015-12-01 23:29:39 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
validateNumber: function() {
|
2015-12-05 01:38:41 +00:00
|
|
|
if (this.isPrivate()) {
|
|
|
|
var regionCode = storage.get('regionCode');
|
|
|
|
var number = libphonenumber.util.parseNumber(this.id, regionCode);
|
|
|
|
if (number.isValidNumber) {
|
|
|
|
this.set({ id: number.e164 });
|
2015-12-01 23:29:39 +00:00
|
|
|
} else {
|
2015-12-05 01:38:41 +00:00
|
|
|
return number.error || "Invalid phone number";
|
2015-12-01 23:29:39 +00:00
|
|
|
}
|
2015-10-16 20:00:38 +00:00
|
|
|
}
|
2015-10-15 19:10:03 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
updateTokens: function() {
|
|
|
|
var tokens = [];
|
|
|
|
var name = this.get('name');
|
|
|
|
if (typeof name === 'string') {
|
2015-12-05 01:38:41 +00:00
|
|
|
tokens.push(name.toLowerCase());
|
|
|
|
tokens = tokens.concat(name.trim().toLowerCase().split(/[\s\-_\(\)\+]+/));
|
2015-10-15 19:10:03 +00:00
|
|
|
}
|
2015-09-10 00:47:45 +00:00
|
|
|
if (this.isPrivate()) {
|
2015-12-05 01:38:41 +00:00
|
|
|
var regionCode = storage.get('regionCode');
|
|
|
|
var number = libphonenumber.util.parseNumber(this.id, regionCode);
|
|
|
|
tokens.push(
|
|
|
|
number.nationalNumber,
|
|
|
|
number.countryCode + number.nationalNumber
|
|
|
|
);
|
2015-01-28 12:19:58 +00:00
|
|
|
}
|
2015-10-15 19:10:03 +00:00
|
|
|
this.set({tokens: tokens});
|
2014-05-17 04:48:46 +00:00
|
|
|
},
|
|
|
|
|
2016-03-15 20:09:06 +00:00
|
|
|
queueJob: function(callback) {
|
|
|
|
var previous = this.pending || Promise.resolve();
|
2017-07-19 19:05:24 +00:00
|
|
|
|
2017-09-25 23:41:57 +00:00
|
|
|
var taskWithTimeout = textsecure.createTaskWithTimeout(
|
|
|
|
callback,
|
|
|
|
'conversation ' + this.idForLogging()
|
|
|
|
);
|
2017-07-19 19:05:24 +00:00
|
|
|
|
|
|
|
var current = this.pending = previous.then(taskWithTimeout, taskWithTimeout);
|
2016-03-15 20:09:06 +00:00
|
|
|
|
|
|
|
current.then(function() {
|
|
|
|
if (this.pending === current) {
|
|
|
|
delete this.pending;
|
|
|
|
}
|
|
|
|
}.bind(this));
|
|
|
|
|
|
|
|
return current;
|
|
|
|
},
|
|
|
|
|
Feature: Blue check marks for read messages if opted in (#1489)
* Refactor delivery receipt event handler
* Rename the delivery receipt event
For less ambiguity with read receipts.
* Rename synced read event
For less ambiguity with read receipts from other Signal users.
* Add support for incoming receipt messages
Handle ReceiptMessages, which may include encrypted delivery receipts or read
receipts from recipients of our sent messages.
// FREEBIE
* Rename ReadReceipts to ReadSyncs
* Render read messages with blue double checks
* Send read receipts to senders of incoming messages
// FREEBIE
* Move ReadSyncs to their own file
// FREEBIE
* Fixup old comments on read receipts (now read syncs)
And some variable renaming for extra clarity.
// FREEBIE
* Add global setting for read receipts
Don't send read receipt messages unless the setting is enabled.
Don't process read receipts if the setting is disabled.
// FREEBIE
* Sync read receipt setting from mobile
Toggling this setting on your mobile device should sync it to Desktop. When
linking, use the setting in the provisioning message.
// FREEBIE
* Send receipt messages silently
Avoid generating phantom messages on ios
// FREEBIE
* Save recipients on the outgoing message models
For accurate tracking and display of sent/delivered/read state, even if group
membership changes later.
// FREEBIE
* Fix conversation type in profile key update handling
// FREEBIE
* Set recipients on synced sent messages
* Render saved recipients in message detail if available
For older messages, where we did not save the intended set of recipients at the
time of sending, fall back to the current group membership.
// FREEBIE
* Record who has been successfully sent to
// FREEBIE
* Record who a message has been delivered to
* Invert the not-clickable class
* Fix readReceipt setting sync when linking
* Render per recipient sent/delivered/read status
In the message detail view for outgoing messages, render each recipient's
individual sent/delivered/read status with respect to this message, as long as
there are no errors associated with the recipient (ie, safety number changes,
user not registered, etc...) since the error icon is displayed in that case.
*Messages sent before this change may not have per-recipient status lists
and will simply show no status icon.
// FREEBIE
* Add configuration sync request
Send these requests in a one-off fashion when:
1. We have just setup from a chrome app import
2. We have just upgraded to read-receipt support
// FREEBIE
* Expose sendRequestConfigurationSyncMessage
// FREEBIE
* Fix handling of incoming delivery receipts - union with array
FREEBIE
2017-10-04 22:28:43 +00:00
|
|
|
getRecipients: function() {
|
|
|
|
if (this.isPrivate()) {
|
|
|
|
return [ this.id ];
|
|
|
|
} else {
|
|
|
|
var me = textsecure.storage.user.getNumber();
|
|
|
|
return _.without(this.get('members'), me);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2014-12-20 08:36:44 +00:00
|
|
|
sendMessage: function(body, attachments) {
|
2016-03-15 20:09:06 +00:00
|
|
|
this.queueJob(function() {
|
|
|
|
var now = Date.now();
|
2017-09-25 23:41:57 +00:00
|
|
|
|
|
|
|
console.log(
|
|
|
|
'Sending message to conversation',
|
|
|
|
this.idForLogging(),
|
|
|
|
'with timestamp',
|
|
|
|
now
|
|
|
|
);
|
|
|
|
|
2016-03-15 20:09:06 +00:00
|
|
|
var message = this.messageCollection.add({
|
|
|
|
body : body,
|
|
|
|
conversationId : this.id,
|
|
|
|
type : 'outgoing',
|
|
|
|
attachments : attachments,
|
|
|
|
sent_at : now,
|
2016-09-28 23:54:05 +00:00
|
|
|
received_at : now,
|
Feature: Blue check marks for read messages if opted in (#1489)
* Refactor delivery receipt event handler
* Rename the delivery receipt event
For less ambiguity with read receipts.
* Rename synced read event
For less ambiguity with read receipts from other Signal users.
* Add support for incoming receipt messages
Handle ReceiptMessages, which may include encrypted delivery receipts or read
receipts from recipients of our sent messages.
// FREEBIE
* Rename ReadReceipts to ReadSyncs
* Render read messages with blue double checks
* Send read receipts to senders of incoming messages
// FREEBIE
* Move ReadSyncs to their own file
// FREEBIE
* Fixup old comments on read receipts (now read syncs)
And some variable renaming for extra clarity.
// FREEBIE
* Add global setting for read receipts
Don't send read receipt messages unless the setting is enabled.
Don't process read receipts if the setting is disabled.
// FREEBIE
* Sync read receipt setting from mobile
Toggling this setting on your mobile device should sync it to Desktop. When
linking, use the setting in the provisioning message.
// FREEBIE
* Send receipt messages silently
Avoid generating phantom messages on ios
// FREEBIE
* Save recipients on the outgoing message models
For accurate tracking and display of sent/delivered/read state, even if group
membership changes later.
// FREEBIE
* Fix conversation type in profile key update handling
// FREEBIE
* Set recipients on synced sent messages
* Render saved recipients in message detail if available
For older messages, where we did not save the intended set of recipients at the
time of sending, fall back to the current group membership.
// FREEBIE
* Record who has been successfully sent to
// FREEBIE
* Record who a message has been delivered to
* Invert the not-clickable class
* Fix readReceipt setting sync when linking
* Render per recipient sent/delivered/read status
In the message detail view for outgoing messages, render each recipient's
individual sent/delivered/read status with respect to this message, as long as
there are no errors associated with the recipient (ie, safety number changes,
user not registered, etc...) since the error icon is displayed in that case.
*Messages sent before this change may not have per-recipient status lists
and will simply show no status icon.
// FREEBIE
* Add configuration sync request
Send these requests in a one-off fashion when:
1. We have just setup from a chrome app import
2. We have just upgraded to read-receipt support
// FREEBIE
* Expose sendRequestConfigurationSyncMessage
// FREEBIE
* Fix handling of incoming delivery receipts - union with array
FREEBIE
2017-10-04 22:28:43 +00:00
|
|
|
expireTimer : this.get('expireTimer'),
|
|
|
|
recipients : this.getRecipients()
|
2016-03-15 20:09:06 +00:00
|
|
|
});
|
|
|
|
if (this.isPrivate()) {
|
|
|
|
message.set({destination: this.id});
|
|
|
|
}
|
|
|
|
message.save();
|
2014-10-26 07:29:01 +00:00
|
|
|
|
2016-03-15 20:09:06 +00:00
|
|
|
this.save({
|
|
|
|
active_at : now,
|
|
|
|
timestamp : now,
|
|
|
|
lastMessage : message.getNotificationText()
|
|
|
|
});
|
2014-08-11 06:34:29 +00:00
|
|
|
|
2016-03-15 20:09:06 +00:00
|
|
|
var sendFunc;
|
|
|
|
if (this.get('type') == 'private') {
|
|
|
|
sendFunc = textsecure.messaging.sendMessageToNumber;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
sendFunc = textsecure.messaging.sendMessageToGroup;
|
|
|
|
}
|
2017-09-11 16:50:35 +00:00
|
|
|
|
|
|
|
var profileKey;
|
|
|
|
if (this.get('profileSharing')) {
|
|
|
|
profileKey = storage.get('profileKey');
|
|
|
|
}
|
|
|
|
|
|
|
|
message.send(sendFunc(this.get('id'), body, attachments, now, this.get('expireTimer'), profileKey));
|
2016-03-15 20:09:06 +00:00
|
|
|
}.bind(this));
|
2014-05-17 04:48:46 +00:00
|
|
|
},
|
|
|
|
|
2016-09-29 22:26:11 +00:00
|
|
|
updateLastMessage: function() {
|
2017-01-25 08:42:06 +00:00
|
|
|
var collection = new Whisper.MessageCollection();
|
|
|
|
return collection.fetchConversation(this.id, 1).then(function() {
|
|
|
|
var lastMessage = collection.at(0);
|
|
|
|
if (lastMessage) {
|
2017-06-15 23:11:31 +00:00
|
|
|
if (lastMessage.get('type') === 'verified-change') {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.set({
|
|
|
|
lastMessage : lastMessage.getNotificationText(),
|
|
|
|
timestamp : lastMessage.get('sent_at')
|
|
|
|
});
|
2017-01-25 08:42:06 +00:00
|
|
|
} else {
|
2017-06-15 23:11:31 +00:00
|
|
|
this.set({ lastMessage: '', timestamp: null });
|
2017-02-22 03:51:33 +00:00
|
|
|
}
|
|
|
|
if (this.hasChanged('lastMessage') || this.hasChanged('timestamp')) {
|
|
|
|
this.save();
|
2017-01-25 08:42:06 +00:00
|
|
|
}
|
|
|
|
}.bind(this));
|
2016-09-29 22:26:11 +00:00
|
|
|
},
|
|
|
|
|
2017-01-03 12:52:29 +00:00
|
|
|
updateExpirationTimer: function(expireTimer, source, received_at) {
|
2017-02-21 03:12:53 +00:00
|
|
|
if (!expireTimer) { expireTimer = null; }
|
2017-01-03 12:52:29 +00:00
|
|
|
source = source || textsecure.storage.user.getNumber();
|
|
|
|
var timestamp = received_at || Date.now();
|
2016-10-04 08:43:11 +00:00
|
|
|
this.save({ expireTimer: expireTimer });
|
2016-09-27 06:15:20 +00:00
|
|
|
var message = this.messageCollection.add({
|
2016-09-28 23:47:57 +00:00
|
|
|
conversationId : this.id,
|
2017-01-03 13:14:55 +00:00
|
|
|
type : received_at ? 'incoming' : 'outgoing',
|
2017-01-03 12:52:29 +00:00
|
|
|
sent_at : timestamp,
|
|
|
|
received_at : timestamp,
|
2016-09-28 23:47:57 +00:00
|
|
|
flags : textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
|
|
|
|
expirationTimerUpdate : {
|
2016-10-04 08:43:11 +00:00
|
|
|
expireTimer : expireTimer,
|
2016-09-27 06:15:20 +00:00
|
|
|
source : source
|
|
|
|
}
|
|
|
|
});
|
2016-10-02 23:42:46 +00:00
|
|
|
if (this.isPrivate()) {
|
|
|
|
message.set({destination: this.id});
|
|
|
|
}
|
Feature: Blue check marks for read messages if opted in (#1489)
* Refactor delivery receipt event handler
* Rename the delivery receipt event
For less ambiguity with read receipts.
* Rename synced read event
For less ambiguity with read receipts from other Signal users.
* Add support for incoming receipt messages
Handle ReceiptMessages, which may include encrypted delivery receipts or read
receipts from recipients of our sent messages.
// FREEBIE
* Rename ReadReceipts to ReadSyncs
* Render read messages with blue double checks
* Send read receipts to senders of incoming messages
// FREEBIE
* Move ReadSyncs to their own file
// FREEBIE
* Fixup old comments on read receipts (now read syncs)
And some variable renaming for extra clarity.
// FREEBIE
* Add global setting for read receipts
Don't send read receipt messages unless the setting is enabled.
Don't process read receipts if the setting is disabled.
// FREEBIE
* Sync read receipt setting from mobile
Toggling this setting on your mobile device should sync it to Desktop. When
linking, use the setting in the provisioning message.
// FREEBIE
* Send receipt messages silently
Avoid generating phantom messages on ios
// FREEBIE
* Save recipients on the outgoing message models
For accurate tracking and display of sent/delivered/read state, even if group
membership changes later.
// FREEBIE
* Fix conversation type in profile key update handling
// FREEBIE
* Set recipients on synced sent messages
* Render saved recipients in message detail if available
For older messages, where we did not save the intended set of recipients at the
time of sending, fall back to the current group membership.
// FREEBIE
* Record who has been successfully sent to
// FREEBIE
* Record who a message has been delivered to
* Invert the not-clickable class
* Fix readReceipt setting sync when linking
* Render per recipient sent/delivered/read status
In the message detail view for outgoing messages, render each recipient's
individual sent/delivered/read status with respect to this message, as long as
there are no errors associated with the recipient (ie, safety number changes,
user not registered, etc...) since the error icon is displayed in that case.
*Messages sent before this change may not have per-recipient status lists
and will simply show no status icon.
// FREEBIE
* Add configuration sync request
Send these requests in a one-off fashion when:
1. We have just setup from a chrome app import
2. We have just upgraded to read-receipt support
// FREEBIE
* Expose sendRequestConfigurationSyncMessage
// FREEBIE
* Fix handling of incoming delivery receipts - union with array
FREEBIE
2017-10-04 22:28:43 +00:00
|
|
|
if (message.isOutgoing()) {
|
|
|
|
message.set({recipients: this.getRecipients() });
|
|
|
|
}
|
2016-09-27 06:15:20 +00:00
|
|
|
message.save();
|
2017-01-03 13:14:55 +00:00
|
|
|
if (message.isOutgoing()) { // outgoing update, send it to the number/group
|
2017-01-03 12:52:29 +00:00
|
|
|
var sendFunc;
|
|
|
|
if (this.get('type') == 'private') {
|
|
|
|
sendFunc = textsecure.messaging.sendExpirationTimerUpdateToNumber;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
sendFunc = textsecure.messaging.sendExpirationTimerUpdateToGroup;
|
|
|
|
}
|
2017-09-11 16:50:35 +00:00
|
|
|
var profileKey;
|
|
|
|
if (this.get('profileSharing')) {
|
|
|
|
profileKey = storage.get('profileKey');
|
|
|
|
}
|
|
|
|
message.send(sendFunc(this.get('id'), this.get('expireTimer'), message.get('sent_at'), profileKey));
|
2016-09-29 00:10:39 +00:00
|
|
|
}
|
2017-01-03 12:52:29 +00:00
|
|
|
return message;
|
2016-09-28 23:47:57 +00:00
|
|
|
},
|
2016-09-27 06:15:20 +00:00
|
|
|
|
2016-02-19 01:27:57 +00:00
|
|
|
isSearchable: function() {
|
|
|
|
return !this.get('left') || !!this.get('lastMessage');
|
|
|
|
},
|
|
|
|
|
2015-02-13 04:36:44 +00:00
|
|
|
endSession: function() {
|
2015-09-10 00:47:45 +00:00
|
|
|
if (this.isPrivate()) {
|
2015-03-24 02:08:05 +00:00
|
|
|
var now = Date.now();
|
2015-09-28 20:33:26 +00:00
|
|
|
var message = this.messageCollection.create({
|
2015-03-24 02:08:05 +00:00
|
|
|
conversationId : this.id,
|
|
|
|
type : 'outgoing',
|
|
|
|
sent_at : now,
|
|
|
|
received_at : now,
|
2016-02-23 20:27:49 +00:00
|
|
|
destination : this.id,
|
Feature: Blue check marks for read messages if opted in (#1489)
* Refactor delivery receipt event handler
* Rename the delivery receipt event
For less ambiguity with read receipts.
* Rename synced read event
For less ambiguity with read receipts from other Signal users.
* Add support for incoming receipt messages
Handle ReceiptMessages, which may include encrypted delivery receipts or read
receipts from recipients of our sent messages.
// FREEBIE
* Rename ReadReceipts to ReadSyncs
* Render read messages with blue double checks
* Send read receipts to senders of incoming messages
// FREEBIE
* Move ReadSyncs to their own file
// FREEBIE
* Fixup old comments on read receipts (now read syncs)
And some variable renaming for extra clarity.
// FREEBIE
* Add global setting for read receipts
Don't send read receipt messages unless the setting is enabled.
Don't process read receipts if the setting is disabled.
// FREEBIE
* Sync read receipt setting from mobile
Toggling this setting on your mobile device should sync it to Desktop. When
linking, use the setting in the provisioning message.
// FREEBIE
* Send receipt messages silently
Avoid generating phantom messages on ios
// FREEBIE
* Save recipients on the outgoing message models
For accurate tracking and display of sent/delivered/read state, even if group
membership changes later.
// FREEBIE
* Fix conversation type in profile key update handling
// FREEBIE
* Set recipients on synced sent messages
* Render saved recipients in message detail if available
For older messages, where we did not save the intended set of recipients at the
time of sending, fall back to the current group membership.
// FREEBIE
* Record who has been successfully sent to
// FREEBIE
* Record who a message has been delivered to
* Invert the not-clickable class
* Fix readReceipt setting sync when linking
* Render per recipient sent/delivered/read status
In the message detail view for outgoing messages, render each recipient's
individual sent/delivered/read status with respect to this message, as long as
there are no errors associated with the recipient (ie, safety number changes,
user not registered, etc...) since the error icon is displayed in that case.
*Messages sent before this change may not have per-recipient status lists
and will simply show no status icon.
// FREEBIE
* Add configuration sync request
Send these requests in a one-off fashion when:
1. We have just setup from a chrome app import
2. We have just upgraded to read-receipt support
// FREEBIE
* Expose sendRequestConfigurationSyncMessage
// FREEBIE
* Fix handling of incoming delivery receipts - union with array
FREEBIE
2017-10-04 22:28:43 +00:00
|
|
|
recipients : this.getRecipients(),
|
2015-06-01 21:08:21 +00:00
|
|
|
flags : textsecure.protobuf.DataMessage.Flags.END_SESSION
|
2015-07-16 18:05:47 +00:00
|
|
|
});
|
2017-11-21 23:20:07 +00:00
|
|
|
message.send(textsecure.messaging.resetSession(this.id, now));
|
2015-02-13 04:36:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
},
|
|
|
|
|
2015-09-22 02:12:06 +00:00
|
|
|
updateGroup: function(group_update) {
|
|
|
|
if (this.isPrivate()) {
|
|
|
|
throw new Error("Called update group on private conversation");
|
|
|
|
}
|
|
|
|
if (group_update === undefined) {
|
|
|
|
group_update = this.pick(['name', 'avatar', 'members']);
|
|
|
|
}
|
|
|
|
var now = Date.now();
|
2015-09-28 20:33:26 +00:00
|
|
|
var message = this.messageCollection.create({
|
2015-09-22 02:12:06 +00:00
|
|
|
conversationId : this.id,
|
|
|
|
type : 'outgoing',
|
|
|
|
sent_at : now,
|
|
|
|
received_at : now,
|
|
|
|
group_update : group_update
|
|
|
|
});
|
2015-09-28 20:33:26 +00:00
|
|
|
message.send(textsecure.messaging.updateGroup(
|
2015-09-22 02:12:06 +00:00
|
|
|
this.id,
|
|
|
|
this.get('name'),
|
|
|
|
this.get('avatar'),
|
|
|
|
this.get('members')
|
2015-09-28 20:33:26 +00:00
|
|
|
));
|
2015-09-22 02:12:06 +00:00
|
|
|
},
|
|
|
|
|
2015-02-13 04:36:44 +00:00
|
|
|
leaveGroup: function() {
|
2015-03-24 02:08:05 +00:00
|
|
|
var now = Date.now();
|
2015-02-13 04:36:44 +00:00
|
|
|
if (this.get('type') === 'group') {
|
2016-02-11 22:12:09 +00:00
|
|
|
this.save({left: true});
|
2015-09-28 20:33:26 +00:00
|
|
|
var message = this.messageCollection.create({
|
2015-03-24 02:08:05 +00:00
|
|
|
group_update: { left: 'You' },
|
|
|
|
conversationId : this.id,
|
|
|
|
type : 'outgoing',
|
|
|
|
sent_at : now,
|
|
|
|
received_at : now
|
2015-09-18 20:39:22 +00:00
|
|
|
});
|
2015-09-28 20:33:26 +00:00
|
|
|
message.send(textsecure.messaging.leaveGroup(this.id));
|
2015-02-13 04:36:44 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2017-06-07 19:06:23 +00:00
|
|
|
markRead: function(newestUnreadDate, options) {
|
|
|
|
options = options || {};
|
|
|
|
_.defaults(options, {sendReadReceipts: true});
|
|
|
|
|
2017-06-07 19:20:25 +00:00
|
|
|
var conversationId = this.id;
|
|
|
|
Whisper.Notifications.remove(Whisper.Notifications.where({
|
|
|
|
conversationId: conversationId
|
|
|
|
}));
|
|
|
|
|
2017-06-07 22:16:01 +00:00
|
|
|
return this.getUnread().then(function(unreadMessages) {
|
2017-07-28 22:12:51 +00:00
|
|
|
var promises = [];
|
2017-06-07 19:20:25 +00:00
|
|
|
var oldUnread = unreadMessages.filter(function(message) {
|
|
|
|
return message.get('received_at') <= newestUnreadDate;
|
|
|
|
});
|
|
|
|
|
|
|
|
var read = _.map(oldUnread, function(m) {
|
|
|
|
if (this.messageCollection.get(m.id)) {
|
|
|
|
m = this.messageCollection.get(m.id);
|
|
|
|
} else {
|
|
|
|
console.log('Marked a message as read in the database, but ' +
|
|
|
|
'it was not in messageCollection.');
|
2016-02-26 20:35:02 +00:00
|
|
|
}
|
2017-07-28 22:12:51 +00:00
|
|
|
promises.push(m.markRead());
|
2017-10-24 17:29:45 +00:00
|
|
|
var errors = m.get('errors');
|
2017-06-07 19:20:25 +00:00
|
|
|
return {
|
|
|
|
sender : m.get('source'),
|
2017-10-23 23:16:01 +00:00
|
|
|
timestamp : m.get('sent_at'),
|
2017-10-24 17:29:45 +00:00
|
|
|
hasErrors : Boolean(errors && errors.length)
|
2017-06-07 19:20:25 +00:00
|
|
|
};
|
2016-09-21 00:19:51 +00:00
|
|
|
}.bind(this));
|
2017-06-07 19:20:25 +00:00
|
|
|
|
2017-07-05 19:17:55 +00:00
|
|
|
// Some messages we're marking read are local notifications with no sender
|
|
|
|
read = _.filter(read, function(m) {
|
|
|
|
return Boolean(m.sender);
|
|
|
|
});
|
|
|
|
unreadMessages = unreadMessages.filter(function(m) {
|
2017-07-05 20:52:27 +00:00
|
|
|
return Boolean(m.isIncoming());
|
2017-07-05 19:17:55 +00:00
|
|
|
});
|
|
|
|
|
2017-06-07 19:20:25 +00:00
|
|
|
var unreadCount = unreadMessages.length - read.length;
|
2017-07-28 22:12:51 +00:00
|
|
|
var promise = new Promise(function(resolve, reject) {
|
|
|
|
this.save({ unreadCount: unreadCount }).then(resolve, reject);
|
|
|
|
}.bind(this));
|
|
|
|
promises.push(promise);
|
2017-06-07 19:20:25 +00:00
|
|
|
|
2017-10-23 23:16:01 +00:00
|
|
|
// If a message has errors, we don't want to send anything out about it.
|
|
|
|
// read syncs - let's wait for a client that really understands the message
|
|
|
|
// to mark it read. we'll mark our local error read locally, though.
|
|
|
|
// read receipts - here we can run into infinite loops, where each time the
|
|
|
|
// conversation is viewed, another error message shows up for the contact
|
|
|
|
read = read.filter(function(item) {
|
|
|
|
return !item.hasErrors;
|
|
|
|
});
|
|
|
|
|
2017-06-07 19:20:25 +00:00
|
|
|
if (read.length && options.sendReadReceipts) {
|
|
|
|
console.log('Sending', read.length, 'read receipts');
|
2017-07-28 22:12:51 +00:00
|
|
|
promises.push(textsecure.messaging.syncReadMessages(read));
|
Feature: Blue check marks for read messages if opted in (#1489)
* Refactor delivery receipt event handler
* Rename the delivery receipt event
For less ambiguity with read receipts.
* Rename synced read event
For less ambiguity with read receipts from other Signal users.
* Add support for incoming receipt messages
Handle ReceiptMessages, which may include encrypted delivery receipts or read
receipts from recipients of our sent messages.
// FREEBIE
* Rename ReadReceipts to ReadSyncs
* Render read messages with blue double checks
* Send read receipts to senders of incoming messages
// FREEBIE
* Move ReadSyncs to their own file
// FREEBIE
* Fixup old comments on read receipts (now read syncs)
And some variable renaming for extra clarity.
// FREEBIE
* Add global setting for read receipts
Don't send read receipt messages unless the setting is enabled.
Don't process read receipts if the setting is disabled.
// FREEBIE
* Sync read receipt setting from mobile
Toggling this setting on your mobile device should sync it to Desktop. When
linking, use the setting in the provisioning message.
// FREEBIE
* Send receipt messages silently
Avoid generating phantom messages on ios
// FREEBIE
* Save recipients on the outgoing message models
For accurate tracking and display of sent/delivered/read state, even if group
membership changes later.
// FREEBIE
* Fix conversation type in profile key update handling
// FREEBIE
* Set recipients on synced sent messages
* Render saved recipients in message detail if available
For older messages, where we did not save the intended set of recipients at the
time of sending, fall back to the current group membership.
// FREEBIE
* Record who has been successfully sent to
// FREEBIE
* Record who a message has been delivered to
* Invert the not-clickable class
* Fix readReceipt setting sync when linking
* Render per recipient sent/delivered/read status
In the message detail view for outgoing messages, render each recipient's
individual sent/delivered/read status with respect to this message, as long as
there are no errors associated with the recipient (ie, safety number changes,
user not registered, etc...) since the error icon is displayed in that case.
*Messages sent before this change may not have per-recipient status lists
and will simply show no status icon.
// FREEBIE
* Add configuration sync request
Send these requests in a one-off fashion when:
1. We have just setup from a chrome app import
2. We have just upgraded to read-receipt support
// FREEBIE
* Expose sendRequestConfigurationSyncMessage
// FREEBIE
* Fix handling of incoming delivery receipts - union with array
FREEBIE
2017-10-04 22:28:43 +00:00
|
|
|
|
|
|
|
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));
|
|
|
|
});
|
|
|
|
}
|
2017-06-07 19:20:25 +00:00
|
|
|
}
|
2017-07-28 22:12:51 +00:00
|
|
|
|
|
|
|
return Promise.all(promises);
|
2017-06-07 19:20:25 +00:00
|
|
|
}.bind(this));
|
2015-03-11 19:06:19 +00:00
|
|
|
},
|
|
|
|
|
2017-09-11 16:50:35 +00:00
|
|
|
onChangeProfileKey: function() {
|
|
|
|
if (this.isPrivate()) {
|
|
|
|
this.getProfiles();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2017-05-27 00:05:49 +00:00
|
|
|
getProfiles: function() {
|
|
|
|
// request all conversation members' keys
|
|
|
|
var ids = [];
|
|
|
|
if (this.isPrivate()) {
|
|
|
|
ids = [this.id];
|
|
|
|
} else {
|
|
|
|
ids = this.get('members');
|
|
|
|
}
|
2017-06-16 01:23:29 +00:00
|
|
|
return Promise.all(_.map(ids, this.getProfile));
|
2017-05-27 00:05:49 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
getProfile: function(id) {
|
|
|
|
return textsecure.messaging.getProfile(id).then(function(profile) {
|
|
|
|
var identityKey = dcodeIO.ByteBuffer.wrap(profile.identityKey, 'base64').toArrayBuffer();
|
|
|
|
|
|
|
|
return textsecure.storage.protocol.saveIdentity(
|
2017-06-29 03:30:35 +00:00
|
|
|
id + '.1', identityKey, false
|
|
|
|
).then(function(changed) {
|
|
|
|
if (changed) {
|
|
|
|
// save identity will close all sessions except for .1, so we
|
|
|
|
// must close that one manually.
|
|
|
|
var address = new libsignal.SignalProtocolAddress(id, 1);
|
|
|
|
console.log('closing session for', address.toString());
|
|
|
|
var sessionCipher = new libsignal.SessionCipher(textsecure.storage.protocol, address);
|
|
|
|
return sessionCipher.closeOpenSessionForDevice();
|
|
|
|
}
|
2017-09-11 16:50:35 +00:00
|
|
|
}).then(function() {
|
|
|
|
var c = ConversationController.get(id);
|
|
|
|
return Promise.all([
|
|
|
|
c.setProfileName(profile.name),
|
|
|
|
c.setProfileAvatar(profile.avatar)
|
|
|
|
]).then(function() {
|
|
|
|
// success
|
|
|
|
return new Promise(function(resolve, reject) {
|
|
|
|
c.save().then(resolve, reject);
|
|
|
|
});
|
|
|
|
}, function(e) {
|
|
|
|
// fail
|
|
|
|
if (e.name === 'ProfileDecryptError') {
|
|
|
|
// probably the profile key has changed.
|
2017-09-18 22:33:08 +00:00
|
|
|
console.log(
|
|
|
|
'decryptProfile error:',
|
|
|
|
id,
|
|
|
|
profile,
|
2017-10-18 18:58:44 +00:00
|
|
|
e && e.stack ? e.stack : e
|
2017-09-18 22:33:08 +00:00
|
|
|
);
|
2017-09-11 16:50:35 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}.bind(this));
|
|
|
|
}.bind(this)).catch(function(error) {
|
2017-08-10 16:30:08 +00:00
|
|
|
console.log(
|
|
|
|
'getProfile error:',
|
|
|
|
error && error.stack ? error.stack : error
|
|
|
|
);
|
2017-05-27 00:05:49 +00:00
|
|
|
});
|
|
|
|
},
|
2017-09-11 16:50:35 +00:00
|
|
|
setProfileName: function(encryptedName) {
|
|
|
|
var key = this.get('profileKey');
|
|
|
|
if (!key) { return; }
|
|
|
|
|
2017-09-18 22:33:08 +00:00
|
|
|
try {
|
|
|
|
// decode
|
|
|
|
var data = dcodeIO.ByteBuffer.wrap(encryptedName, 'base64').toArrayBuffer();
|
2017-09-11 16:50:35 +00:00
|
|
|
|
2017-09-18 22:33:08 +00:00
|
|
|
// decrypt
|
|
|
|
return textsecure.crypto.decryptProfileName(data, key).then(function(decrypted) {
|
2017-09-11 16:50:35 +00:00
|
|
|
|
2017-09-18 22:33:08 +00:00
|
|
|
// encode
|
|
|
|
var name = dcodeIO.ByteBuffer.wrap(decrypted).toString('utf8');
|
2017-09-11 16:50:35 +00:00
|
|
|
|
2017-09-18 22:33:08 +00:00
|
|
|
// set
|
|
|
|
this.set({profileName: name});
|
|
|
|
}.bind(this));
|
|
|
|
}
|
|
|
|
catch (e) {
|
|
|
|
return Promise.reject(e);
|
|
|
|
}
|
2017-09-11 16:50:35 +00:00
|
|
|
},
|
|
|
|
setProfileAvatar: function(avatarPath) {
|
|
|
|
if (!avatarPath) { return; }
|
|
|
|
return textsecure.messaging.getAvatar(avatarPath).then(function(avatar) {
|
|
|
|
var key = this.get('profileKey');
|
|
|
|
if (!key) { return; }
|
|
|
|
// decrypt
|
|
|
|
return textsecure.crypto.decryptProfile(avatar, key).then(function(decrypted) {
|
|
|
|
// set
|
|
|
|
this.set({
|
|
|
|
profileAvatar: {
|
|
|
|
data: decrypted,
|
|
|
|
contentType: 'image/jpeg',
|
|
|
|
size: decrypted.byteLength
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}.bind(this));
|
|
|
|
}.bind(this));
|
|
|
|
},
|
|
|
|
setProfileKey: function(key) {
|
|
|
|
return new Promise(function(resolve, reject) {
|
|
|
|
if (!constantTimeEqualArrayBuffers(this.get('profileKey'), key)) {
|
|
|
|
this.save({profileKey: key}).then(resolve, reject);
|
|
|
|
} else {
|
|
|
|
resolve();
|
|
|
|
}
|
|
|
|
}.bind(this));
|
|
|
|
},
|
2017-05-27 00:05:49 +00:00
|
|
|
|
2015-07-07 23:03:12 +00:00
|
|
|
fetchMessages: function() {
|
2017-07-15 00:05:23 +00:00
|
|
|
if (!this.id) {
|
|
|
|
return Promise.reject('This conversation has no id!');
|
|
|
|
}
|
2017-05-19 21:06:56 +00:00
|
|
|
return this.messageCollection.fetchConversation(this.id, null, this.get('unreadCount'));
|
2014-12-12 03:41:40 +00:00
|
|
|
},
|
|
|
|
|
2017-06-15 19:27:41 +00:00
|
|
|
hasMember: function(number) {
|
|
|
|
return _.contains(this.get('members'), number);
|
|
|
|
},
|
2015-02-25 00:02:33 +00:00
|
|
|
fetchContacts: function(options) {
|
2017-09-01 16:10:41 +00:00
|
|
|
if (this.isPrivate()) {
|
|
|
|
this.contactCollection.reset([this]);
|
|
|
|
return Promise.resolve();
|
|
|
|
} else {
|
|
|
|
var members = this.get('members') || [];
|
|
|
|
var promises = members.map(function(number) {
|
|
|
|
return ConversationController.getOrCreateAndWait(number, 'private');
|
|
|
|
});
|
|
|
|
|
|
|
|
return Promise.all(promises).then(function(contacts) {
|
|
|
|
_.forEach(contacts, function(contact) {
|
|
|
|
this.listenTo(contact, 'change:verified', this.onMemberVerifiedChange);
|
|
|
|
}.bind(this));
|
|
|
|
|
|
|
|
this.contactCollection.reset(contacts);
|
|
|
|
}.bind(this));
|
|
|
|
}
|
2015-02-25 00:02:33 +00:00
|
|
|
},
|
|
|
|
|
2014-12-02 23:47:28 +00:00
|
|
|
destroyMessages: function() {
|
2016-01-27 20:42:27 +00:00
|
|
|
this.messageCollection.fetch({
|
|
|
|
index: {
|
|
|
|
// 'conversation' index on [conversationId, received_at]
|
|
|
|
name : 'conversation',
|
|
|
|
lower : [this.id],
|
|
|
|
upper : [this.id, Number.MAX_VALUE],
|
|
|
|
}
|
|
|
|
}).then(function() {
|
|
|
|
var models = this.messageCollection.models;
|
|
|
|
this.messageCollection.reset([]);
|
|
|
|
_.each(models, function(message) { message.destroy(); });
|
2016-03-25 17:39:36 +00:00
|
|
|
this.save({lastMessage: null, timestamp: null}); // archive
|
2016-01-27 20:42:27 +00:00
|
|
|
}.bind(this));
|
2015-01-24 20:36:04 +00:00
|
|
|
},
|
|
|
|
|
2016-03-18 20:09:45 +00:00
|
|
|
getName: function() {
|
|
|
|
if (this.isPrivate()) {
|
|
|
|
return this.get('name');
|
|
|
|
} else {
|
|
|
|
return this.get('name') || 'Unknown group';
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2015-01-24 20:36:04 +00:00
|
|
|
getTitle: function() {
|
2015-03-19 20:49:09 +00:00
|
|
|
if (this.isPrivate()) {
|
2015-12-04 23:09:53 +00:00
|
|
|
return this.get('name') || this.getNumber();
|
2015-03-19 20:49:09 +00:00
|
|
|
} else {
|
|
|
|
return this.get('name') || 'Unknown group';
|
|
|
|
}
|
2015-02-04 19:23:00 +00:00
|
|
|
},
|
|
|
|
|
2017-09-11 16:50:35 +00:00
|
|
|
getProfileName: function() {
|
|
|
|
if (this.isPrivate() && !this.get('name')) {
|
|
|
|
return this.get('profileName');
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2015-02-04 19:23:00 +00:00
|
|
|
getNumber: function() {
|
2015-12-04 23:09:53 +00:00
|
|
|
if (!this.isPrivate()) {
|
2015-02-04 19:23:00 +00:00
|
|
|
return '';
|
|
|
|
}
|
2015-12-04 23:09:53 +00:00
|
|
|
var number = this.id;
|
|
|
|
try {
|
|
|
|
var parsedNumber = libphonenumber.parse(number);
|
|
|
|
var regionCode = libphonenumber.getRegionCodeForNumber(parsedNumber);
|
|
|
|
if (regionCode === storage.get('regionCode')) {
|
|
|
|
return libphonenumber.format(parsedNumber, libphonenumber.PhoneNumberFormat.NATIONAL);
|
|
|
|
} else {
|
|
|
|
return libphonenumber.format(parsedNumber, libphonenumber.PhoneNumberFormat.INTERNATIONAL);
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
return number;
|
|
|
|
}
|
2015-02-25 00:02:33 +00:00
|
|
|
},
|
2015-02-24 00:23:22 +00:00
|
|
|
|
2015-02-25 00:02:33 +00:00
|
|
|
isPrivate: function() {
|
|
|
|
return this.get('type') === 'private';
|
2015-03-17 22:06:21 +00:00
|
|
|
},
|
|
|
|
|
2015-03-18 00:10:18 +00:00
|
|
|
revokeAvatarUrl: function() {
|
2015-03-17 22:06:21 +00:00
|
|
|
if (this.avatarUrl) {
|
|
|
|
URL.revokeObjectURL(this.avatarUrl);
|
|
|
|
this.avatarUrl = null;
|
|
|
|
}
|
2015-03-18 00:10:18 +00:00
|
|
|
},
|
|
|
|
|
2015-06-09 19:03:28 +00:00
|
|
|
updateAvatarUrl: function(silent) {
|
2015-03-18 00:10:18 +00:00
|
|
|
this.revokeAvatarUrl();
|
2017-09-11 16:50:35 +00:00
|
|
|
var avatar = this.get('avatar') || this.get('profileAvatar');
|
2015-03-17 22:06:21 +00:00
|
|
|
if (avatar) {
|
|
|
|
this.avatarUrl = URL.createObjectURL(
|
|
|
|
new Blob([avatar.data], {type: avatar.contentType})
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
this.avatarUrl = null;
|
|
|
|
}
|
2015-06-09 19:03:28 +00:00
|
|
|
if (!silent) {
|
|
|
|
this.trigger('change');
|
|
|
|
}
|
2015-03-17 22:06:21 +00:00
|
|
|
},
|
2016-09-11 22:03:05 +00:00
|
|
|
getColor: function() {
|
2016-03-18 20:09:45 +00:00
|
|
|
var title = this.get('name');
|
2016-09-03 21:36:56 +00:00
|
|
|
var color = this.get('color');
|
|
|
|
if (!color) {
|
|
|
|
if (this.isPrivate()) {
|
|
|
|
if (title) {
|
|
|
|
color = COLORS[Math.abs(this.hashCode()) % 15];
|
|
|
|
} else {
|
|
|
|
color = 'grey';
|
|
|
|
}
|
2016-03-21 20:02:34 +00:00
|
|
|
} else {
|
2016-09-03 21:36:56 +00:00
|
|
|
color = 'default';
|
2016-03-21 20:02:34 +00:00
|
|
|
}
|
2016-03-18 20:09:45 +00:00
|
|
|
}
|
2016-09-11 22:03:05 +00:00
|
|
|
return color;
|
|
|
|
},
|
|
|
|
getAvatar: function() {
|
|
|
|
if (this.avatarUrl === undefined) {
|
|
|
|
this.updateAvatarUrl(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
var title = this.get('name');
|
|
|
|
var color = this.getColor();
|
2016-03-18 20:09:45 +00:00
|
|
|
|
2015-06-19 00:05:00 +00:00
|
|
|
if (this.avatarUrl) {
|
2016-03-18 20:09:45 +00:00
|
|
|
return { url: this.avatarUrl, color: color };
|
2015-06-19 00:05:00 +00:00
|
|
|
} else if (this.isPrivate()) {
|
2016-03-21 22:37:53 +00:00
|
|
|
return {
|
|
|
|
color: color,
|
|
|
|
content: title ? title.trim()[0] : '#'
|
|
|
|
};
|
2015-06-19 00:05:00 +00:00
|
|
|
} else {
|
2017-03-08 00:54:46 +00:00
|
|
|
return { url: 'images/group_default.png', color: color };
|
2015-06-19 00:05:00 +00:00
|
|
|
}
|
2015-02-24 00:23:22 +00:00
|
|
|
},
|
|
|
|
|
2015-09-22 22:52:33 +00:00
|
|
|
getNotificationIcon: function() {
|
|
|
|
return new Promise(function(resolve) {
|
|
|
|
var avatar = this.getAvatar();
|
|
|
|
if (avatar.url) {
|
|
|
|
resolve(avatar.url);
|
|
|
|
} else {
|
|
|
|
resolve(new Whisper.IdenticonSVGView(avatar).getDataUrl());
|
|
|
|
}
|
|
|
|
}.bind(this));
|
|
|
|
},
|
|
|
|
|
2015-11-07 22:11:13 +00:00
|
|
|
notify: function(message) {
|
|
|
|
if (!message.isIncoming()) {
|
2017-08-04 01:12:08 +00:00
|
|
|
return Promise.resolve();
|
2015-11-07 22:11:13 +00:00
|
|
|
}
|
|
|
|
var conversationId = this.id;
|
2017-08-04 01:12:08 +00:00
|
|
|
|
2017-09-29 16:15:28 +00:00
|
|
|
return ConversationController.getOrCreateAndWait(message.get('source'), 'private')
|
2017-09-01 16:10:41 +00:00
|
|
|
.then(function(sender) {
|
|
|
|
return sender.getNotificationIcon().then(function(iconUrl) {
|
2017-08-04 01:12:08 +00:00
|
|
|
console.log('adding notification');
|
|
|
|
Whisper.Notifications.add({
|
|
|
|
title : sender.getTitle(),
|
|
|
|
message : message.getNotificationText(),
|
|
|
|
iconUrl : iconUrl,
|
|
|
|
imageUrl : message.getImageUrl(),
|
|
|
|
conversationId : conversationId,
|
|
|
|
messageId : message.id
|
|
|
|
});
|
2017-09-01 16:10:41 +00:00
|
|
|
});
|
|
|
|
});
|
2015-11-07 22:11:13 +00:00
|
|
|
},
|
2015-06-19 00:05:00 +00:00
|
|
|
hashCode: function() {
|
|
|
|
if (this.hash === undefined) {
|
|
|
|
var string = this.getTitle() || '';
|
|
|
|
if (string.length === 0) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
var hash = 0;
|
|
|
|
for (var i = 0; i < string.length; i++) {
|
|
|
|
hash = ((hash<<5)-hash) + string.charCodeAt(i);
|
|
|
|
hash = hash & hash; // Convert to 32bit integer
|
|
|
|
}
|
|
|
|
|
|
|
|
this.hash = hash;
|
|
|
|
}
|
|
|
|
return this.hash;
|
2014-11-16 23:30:40 +00:00
|
|
|
}
|
2014-05-17 04:48:46 +00:00
|
|
|
});
|
|
|
|
|
2014-11-13 22:35:37 +00:00
|
|
|
Whisper.ConversationCollection = Backbone.Collection.extend({
|
|
|
|
database: Whisper.Database,
|
|
|
|
storeName: 'conversations',
|
2015-02-08 02:18:53 +00:00
|
|
|
model: Whisper.Conversation,
|
2014-10-18 14:08:57 +00:00
|
|
|
|
|
|
|
comparator: function(m) {
|
|
|
|
return -m.get('timestamp');
|
|
|
|
},
|
|
|
|
|
2014-11-13 22:35:37 +00:00
|
|
|
destroyAll: function () {
|
|
|
|
return Promise.all(this.models.map(function(m) {
|
|
|
|
return new Promise(function(resolve, reject) {
|
|
|
|
m.destroy().then(resolve).fail(reject);
|
|
|
|
});
|
|
|
|
}));
|
2014-12-20 01:15:57 +00:00
|
|
|
},
|
|
|
|
|
2015-10-15 19:10:03 +00:00
|
|
|
search: function(query) {
|
|
|
|
query = query.trim().toLowerCase();
|
|
|
|
if (query.length > 0) {
|
2015-12-09 19:43:26 +00:00
|
|
|
query = query.replace(/[-.\(\)]*/g,'').replace(/^\+(\d*)$/, '$1');
|
2015-10-15 19:10:03 +00:00
|
|
|
var lastCharCode = query.charCodeAt(query.length - 1);
|
|
|
|
var nextChar = String.fromCharCode(lastCharCode + 1);
|
|
|
|
var upper = query.slice(0, -1) + nextChar;
|
|
|
|
return new Promise(function(resolve) {
|
|
|
|
this.fetch({
|
|
|
|
index: {
|
|
|
|
name: 'search', // 'search' index on tokens array
|
|
|
|
lower: query,
|
2015-12-31 22:05:41 +00:00
|
|
|
upper: upper,
|
|
|
|
excludeUpper: true
|
2015-10-15 19:10:03 +00:00
|
|
|
}
|
|
|
|
}).always(resolve);
|
|
|
|
}.bind(this));
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2015-11-28 00:11:42 +00:00
|
|
|
fetchAlphabetical: function() {
|
|
|
|
return new Promise(function(resolve) {
|
|
|
|
this.fetch({
|
|
|
|
index: {
|
|
|
|
name: 'search', // 'search' index on tokens array
|
|
|
|
},
|
|
|
|
limit: 100
|
|
|
|
}).always(resolve);
|
|
|
|
}.bind(this));
|
|
|
|
},
|
|
|
|
|
2014-12-20 01:15:57 +00:00
|
|
|
fetchGroups: function(number) {
|
2016-04-11 22:06:06 +00:00
|
|
|
return new Promise(function(resolve) {
|
|
|
|
this.fetch({
|
|
|
|
index: {
|
|
|
|
name: 'group',
|
|
|
|
only: number
|
|
|
|
}
|
|
|
|
}).always(resolve);
|
|
|
|
}.bind(this));
|
2014-11-13 00:48:28 +00:00
|
|
|
}
|
2014-11-13 22:35:37 +00:00
|
|
|
});
|
2016-09-11 22:03:05 +00:00
|
|
|
|
|
|
|
Whisper.Conversation.COLORS = COLORS.concat(['grey', 'default']).join(' ');
|
2017-05-07 21:18:22 +00:00
|
|
|
|
|
|
|
// Special collection for fetching all the groups a certain number appears in
|
|
|
|
Whisper.GroupCollection = Backbone.Collection.extend({
|
|
|
|
database: Whisper.Database,
|
|
|
|
storeName: 'conversations',
|
|
|
|
model: Whisper.Conversation,
|
|
|
|
fetchGroups: function(number) {
|
|
|
|
return new Promise(function(resolve) {
|
|
|
|
this.fetch({
|
|
|
|
index: {
|
|
|
|
name: 'group',
|
|
|
|
only: number
|
|
|
|
}
|
|
|
|
}).always(resolve);
|
|
|
|
}.bind(this));
|
|
|
|
}
|
|
|
|
});
|
2014-05-17 04:48:46 +00:00
|
|
|
})();
|