Files
Signal-Desktop/js/conversation_controller.js
Scott Nonnenberg ce01eb7913 Log more info w/http errors, resilient to invalid group members (#1968)
* api.js: HttpError now preserves more of original error info

* Allow invalid conversation to be added to ConversationController

If a number we don't believe is valid comes in as part of a group, we
fall apart. We event prevent display of that conversation. Because we
have 'isValid()' protections in place for the places we create contacts
from user input in desktop (the search bar), we can temporarily add an
invalid contact to our collection (not saving it to the database) to
unblock this group scenario.

Still investigating what kind of phone number is valid in a mobile app
but not valid for us.

* Finish the debuggable error support

* Fix logging in the case of an invalid number
2018-01-12 16:19:26 -08:00

190 lines
6.5 KiB
JavaScript

/*global $, Whisper, Backbone, textsecure, extension*/
/*
* vim: ts=4:sw=4:expandtab
*/
// This script should only be included in background.html
(function () {
'use strict';
window.Whisper = window.Whisper || {};
var conversations = new Whisper.ConversationCollection();
var inboxCollection = new (Backbone.Collection.extend({
initialize: function() {
this.on('change:timestamp change:name change:number', this.sort);
this.listenTo(conversations, 'add change:active_at', this.addActive);
this.on('add remove change:unreadCount',
_.debounce(this.updateUnreadCount.bind(this), 1000)
);
this.startPruning();
this.collator = new Intl.Collator();
},
comparator: function(m1, m2) {
var timestamp1 = m1.get('timestamp');
var timestamp2 = m2.get('timestamp');
if (timestamp1 && !timestamp2) {
return -1;
}
if (timestamp2 && !timestamp1) {
return 1;
}
if (timestamp1 && timestamp2 && timestamp1 !== timestamp2) {
return timestamp2 - timestamp1;
}
var title1 = m1.getTitle().toLowerCase();
var title2 = m2.getTitle().toLowerCase();
return this.collator.compare(title1, title2);
},
addActive: function(model) {
if (model.get('active_at')) {
this.add(model);
} else {
this.remove(model);
}
},
updateUnreadCount: function() {
var newUnreadCount = _.reduce(
this.map(function(m) { return m.get('unreadCount'); }),
function(item, memo) {
return item + memo;
},
0
);
storage.put("unreadCount", newUnreadCount);
if (newUnreadCount > 0) {
window.setBadgeCount(newUnreadCount);
window.document.title = window.config.title + " (" + newUnreadCount + ")";
} else {
window.setBadgeCount(0);
window.document.title = window.config.title;
}
},
startPruning: function() {
var halfHour = 30 * 60 * 1000;
this.interval = setInterval(function() {
this.forEach(function(conversation) {
conversation.trigger('prune');
});
}.bind(this), halfHour);
}
}))();
window.getInboxCollection = function() {
return inboxCollection;
};
window.ConversationController = {
get: function(id) {
if (!this._initialFetchComplete) {
throw new Error('ConversationController.get() needs complete initial fetch');
}
return conversations.get(id);
},
// Needed for some model setup which happens during the initial fetch() call below
getUnsafe: function(id) {
return conversations.get(id);
},
createTemporary: function(attributes) {
return conversations.add(attributes);
},
getOrCreate: function(id, type) {
if (!this._initialFetchComplete) {
throw new Error('ConversationController.get() needs complete initial fetch');
}
var conversation = conversations.get(id);
if (conversation) {
return conversation;
}
conversation = conversations.add({
id: id,
type: type
});
conversation.initialPromise = new Promise(function(resolve, reject) {
if (!conversation.isValid()) {
var validationError = conversation.validationError || {};
console.log(
'Contact is not valid. Not saving, but adding to collection:',
conversation.idForLogging(),
validationError.stack
);
return resolve(conversation);
}
var deferred = conversation.save();
if (!deferred) {
console.log('Conversation save failed! ', id, type);
return reject(new Error('getOrCreate: Conversation save failed'));
}
deferred.then(function() {
resolve(conversation);
}, reject);
});
return conversation;
},
getOrCreateAndWait: function(id, type) {
return this._initialPromise.then(function() {
var conversation = this.getOrCreate(id, type);
if (conversation) {
return conversation.initialPromise.then(function() {
return conversation;
});
}
return Promise.reject(
new Error('getOrCreateAndWait: did not get conversation')
);
}.bind(this));
},
getAllGroupsInvolvingId: function(id) {
var groups = new Whisper.GroupCollection();
return groups.fetchGroups(id).then(function() {
return groups.map(function(group) {
return conversations.add(group);
});
});
},
loadPromise: function() {
return this._initialPromise;
},
reset: function() {
this._initialPromise = null;
conversations.reset([]);
},
load: function() {
console.log('ConversationController: starting initial fetch');
if (this._initialPromise) {
throw new Error('ConversationController.load() has already been called!');
}
this._initialPromise = new Promise(function(resolve, reject) {
conversations.fetch().then(function() {
console.log('ConversationController: done with initial fetch');
this._initialFetchComplete = true;
resolve();
}.bind(this), function(error) {
console.log(
'ConversationController: initial fetch failed',
error && error.stack ? error.stack : error
);
reject(error);
});
}.bind(this));
return this._initialPromise;
}
};
})();