Commit 47c7675cd47d8444d3c04401bc5bb7e8482f1afd

Authored by OpenWapp Developer
1 parent 99160ca025

Async load of contacts and groups

Showing 11 changed files with 412 additions and 381 deletions

app/scripts/collections/contacts.js View file @ 47c7675
... ... @@ -4,58 +4,24 @@
4 4 'underscore',
5 5 'models/contact',
6 6 'zeptojs',
7   - 'utils/phonenumber',
8   - 'storage/dbmanager',
9   - 'storage/auth'
10   -], function (Backbone, global, _, Contact, $, PhoneNumber, DbManager,
11   - authStorage) {
  7 + 'storage/dbmanager'
  8 +], function (Backbone, global, _, Contact, $, DbManager) {
12 9 'use strict';
13 10  
14 11 var Contacts = Backbone.Collection.extend({
  12 +
15 13 model: Contact,
16 14  
17 15 comparator: 'displayName',
18 16  
19 17 initialize: function () {
20   - this.sentContactNumbers = [];
21   - this.registeredToListenContacts = false;
22   - this.userMsisdn = null;
23   - this._getUserMsisdn();
24   - },
  18 + this.listenTo(global.rtc,
  19 + 'availability:available', this._updateAvailability);
25 20  
26   - _getUserMsisdn: function () {
27   - var _this = this;
28   - if (global.auth.get('msisdn')) {
29   - _this.userMsisdn = global.auth.get('msisdn');
30   - _this._initContacts();
31   - } else {
32   - authStorage.load(function (userId, password, msisdn) {
33   - if (msisdn) {
34   - _this.userMsisdn = msisdn;
35   - _this._initContacts();
36   - }
37   - else {
38   - console.log('Error cannot retreive user msisdn, ' +
39   - 'probably first user connection');
40   - // Wait for msisdn change
41   - _this.listenTo(global.auth, 'change:msisdn', function () {
42   - _this.userMsisdn = global.auth.get('msisdn');
43   - _this._initContacts();
44   - });
45   - }
46   - });
47   - }
  21 + this.listenTo(global.rtc,
  22 + 'availability:unavailable', this._updateAvailability);
48 23 },
49 24  
50   - _initContacts: function () {
51   - PhoneNumber.setBaseNumber(this.userMsisdn);
52   - this._fetchContactsFromStorage();
53   - this.listenTo(global.rtc, 'availability:available',
54   - this._updateAvailability);
55   - this.listenTo(global.rtc, 'availability:unavailable',
56   - this._updateAvailability);
57   - },
58   -
59 25 _updateAvailability: function (from, content) {
60 26 var contact = this.findWhere({ phone: from.msisdn });
61 27 if (contact) {
62 28  
63 29  
64 30  
65 31  
66 32  
67 33  
68 34  
69 35  
... ... @@ -63,80 +29,56 @@
63 29 }
64 30 },
65 31  
66   - /**
67   - * Current contacts are the sum of contacts into the OpenWapp storage of
68   - * contacts and those from recent conversations.
69   - */
70   - _fetchContactsFromStorage: function () {
  32 + findOrCreate: function (phone, displayName, callback) {
71 33 var _this = this;
72   - DbManager.read({
73   - loadWithCursor: true,
74   - store: DbManager.dbContactsStore,
75   - sortedBy: 'displayName',
76   - callback: function (err, cursor) {
77   - console.log('[contacts] Fetching users from storage.');
  34 + var isNew = false;
  35 + var contact = global.contacts.findWhere({phone: phone});
78 36  
79   - if (cursor) {
80   - _this.addNewContact(cursor.value);
81   - /* jshint es5:true */
82   - cursor.continue();
83   - /* jshint es5:false */
84   - }
85   - else if (global.historyCollection.finishedLoading) {
86   - _this._fetchContactsFromRecentConversations();
87   - }
88   - else {
89   - global.historyCollection.once('history:loaded',
90   - _this._fetchContactsFromRecentConversations.bind(_this));
91   - }
92   - }
93   - });
94   - },
  37 + // The contact is not cached
  38 + if (!contact) {
95 39  
96   - // TODO: Consider listening for new additions to the history collection
97   - // to add the owner to the contact list automatically.
98   - _fetchContactsFromRecentConversations: function () {
99   - console.log('[contacts] Fetching from recent conversations.');
  40 + this._loadFromStorage(phone, function (err, contact) {
100 41  
101   - var msisdn,
102   - conversations = global.historyCollection.models;
  42 + // The contacts is not persisted yet
  43 + if (!contact) {
  44 + isNew = true;
  45 + contact = _this.add({
  46 + id: phone,
  47 + phone: phone,
  48 + displayName: displayName || '+' + phone,
  49 + subject: displayName
  50 + }).get(phone);
  51 + _this.saveToStorage(contact);
  52 + }
103 53  
104   - for (var i = 0, l = conversations.length; i < l; i++) {
105   - msisdn = conversations[i].get('id');
106   - if (!this.findWhere({ id: msisdn })) {
107   - console.log('[contacts] Adding not found contact', msisdn,
108   - 'from a recent conversation');
109   - this.addNewContact({ id: msisdn, phone: msisdn });
110   - }
  54 + callback(null, {
  55 + isNew: isNew,
  56 + contact: contact
  57 + });
  58 + });
111 59 }
112   -
113   - this.isLoaded = true;
114   - this.trigger('complete', this.models);
115   - },
116   -
117   - findAndCreateContact: function (phone, displayName) {
118   - var contact = global.contacts.findWhere({phone: phone});
119   - var isNew = false;
120   - if (!contact) {
121   - isNew = true;
122   - contact = this.addNewContact({
123   - id: phone,
124   - phone: phone,
125   - displayName: displayName || '+' + phone,
126   - subject: displayName
  60 + else {
  61 + callback(null, {
  62 + isNew: isNew,
  63 + contact: contact
127 64 });
128 65 }
129   - return {
130   - contact: contact,
131   - isNew: isNew
132   - };
133 66 },
134 67  
135   - addNewContact: function (contactProperties) {
136   - var contact = new Contact(contactProperties);
137   - this.add(contact);
138   - contact.saveToStorage();
139   - return contact;
  68 + _loadFromStorage: function (phone, callback) {
  69 + var _this = this;
  70 + DbManager.read({
  71 + store: DbManager.dbContactsStore,
  72 + value: phone,
  73 + callback: function (err, items) {
  74 + console.log('[contacts] Loading contact', phone, 'from storage.');
  75 + var contact = items && items[0] || null;
  76 + if (contact) {
  77 + contact = _this.add(contact).get(phone);
  78 + }
  79 + callback(err, contact);
  80 + }
  81 + });
140 82 },
141 83  
142 84 saveToStorage: function (contact) {
... ... @@ -189,8 +131,12 @@
189 131 return global.auth.get('screenName');
190 132 }
191 133  
192   - var contact = global.contacts.findAndCreateContact(phone).contact;
193   - return contact.get('displayName');
  134 + var contact = this.get(phone);
  135 + if (contact) {
  136 + return contact.get('displayName');
  137 + }
  138 +
  139 + return '+' + phone;
194 140 }
195 141 });
196 142  
app/scripts/collections/history.js View file @ 47c7675
... ... @@ -6,13 +6,13 @@
6 6 'models/conversation',
7 7 'models/message',
8 8 'utils/phonenumber'
9   -], function (Backbone, global, _, AsyncStorage, ConversationModel, MessageModel,
  9 +], function (Backbone, global, _, AsyncStorage, Conversation, MessageModel,
10 10 PhoneNumber) {
11 11 'use strict';
12 12  
13 13 return Backbone.Collection.extend({
14 14  
15   - model: ConversationModel,
  15 + model: Conversation,
16 16  
17 17 initialize: function () {
18 18 this.listenTo(global.rtc, 'message:text', this._onTextReceived);
... ... @@ -131,9 +131,9 @@
131 131  
132 132 _onParticipantNotification: function (from, meta, content) {
133 133 content = content; // shut jslint up
134   - var convInstance =
135   - this.findAndCreateConversation(from.msisdn);
136   - convInstance.updateParticipantList();
  134 + this.findOrCreate(from.msisdn, null, function (err, result) {
  135 + result.conversation.updateParticipantList();
  136 + });
137 137 },
138 138  
139 139 _onPictureNotification: function (from, meta, content) {
140 140  
141 141  
142 142  
143 143  
144 144  
... ... @@ -143,28 +143,29 @@
143 143 },
144 144  
145 145 _addIncomingMessage: function (message) {
146   - // lookup (or create) ConversationModel
  146 + // lookup (or create) Conversation
147 147 var from = message.get('from');
148   - var convInstance =
149   - this.findAndCreateConversation(from.msisdn);
150   - var contact = global.contacts.findAndCreateContact(from.msisdn).contact;
  148 + this.findOrCreate(from.msisdn, null, function (err, result) {
  149 + var conversation = result.conversation;
  150 + var contact = conversation.get('contact');
151 151  
152   - if (from.displayName) {
153   - if (!contact.get('displayName')) {
154   - contact.set('displayName', from.displayName);
  152 + if (from.displayName) {
  153 + if (!contact.get('displayName')) {
  154 + contact.set('displayName', from.displayName);
  155 + }
  156 + if (!contact.get('isGroup')) {
  157 + conversation.set('title', contact.get('displayName'));
  158 + }
155 159 }
156   - if (!contact.get('isGroup')) {
157   - convInstance.set('title', contact.get('displayName'));
158   - }
159   - }
160 160  
161   - var isRead = (!!global.router.currentView &&
162   - global.router.currentView.model === convInstance);
163   - convInstance.set({
164   - isOnline: false,
165   - isRead: isRead
  161 + var isRead = (!!global.router.currentView &&
  162 + global.router.currentView.model === conversation);
  163 + conversation.set({
  164 + isOnline: false,
  165 + isRead: isRead
  166 + });
  167 + conversation.get('messages').add(message);
166 168 });
167   - convInstance.get('messages').add(message);
168 169 },
169 170  
170 171 saveConversationList: function (callback) {
... ... @@ -173,7 +174,7 @@
173 174 });
174 175 var _this = this;
175 176 AsyncStorage.setItem('conversations', list, function () {
176   - console.log('Saved conversation list');
  177 + console.log('[history] Saved conversation list');
177 178 _this.trigger('history:save');
178 179 if (callback) { callback(); }
179 180 });
180 181  
181 182  
182 183  
183 184  
184 185  
185 186  
186 187  
187 188  
188 189  
189 190  
190 191  
191 192  
192 193  
193 194  
194 195  
195 196  
196 197  
197 198  
198 199  
199 200  
200 201  
201 202  
202 203  
203 204  
204 205  
205 206  
206 207  
207 208  
... ... @@ -190,152 +191,233 @@
190 191 }
191 192  
192 193 if (this.finishedLoading) { // Only load on first use
  194 + console.log('[history] All conversations are already loaded.');
193 195 return;
194 196 }
195 197  
  198 + console.log('[history] Loading conversations...');
  199 +
196 200 var _this = this;
197 201 AsyncStorage.getItem('conversations', function (list) {
198 202 if (!list || !list.length) {
199 203 _this.finishedLoading = true;
200 204 _this.trigger('history:loaded');
201   - return _this._loadGroups();
  205 + return _this._syncGroups();
202 206 }
203 207  
204 208 var sync = waitFor(list.length, function () {
205 209 _this.finishedLoading = true;
206 210 _this.trigger('history:loaded');
207   - _this._loadGroups();
  211 + _this._syncGroups();
208 212 });
209 213  
210 214 list.forEach(function (id) {
211   - console.log('[history] About to load conversation ', id);
212   - ConversationModel.loadFromStorage(id, sync);
  215 + _this._loadFromStorage(id, sync);
213 216 });
214 217 });
215 218 },
216 219  
217   - _loadGroups: function () {
  220 + _loadFromStorage: function (id, callback) {
  221 + var key = 'conv:' + id;
  222 + var _this = this;
  223 + console.log('[history] Loading conversation:', id);
  224 + AsyncStorage.getItem(key, function (conversation) {
  225 + if (conversation) {
  226 + console.log('[history] Conversation:', id, 'loaded!');
  227 + conversation = _this.add(conversation).get(id);
  228 + global.contacts.findOrCreate(conversation.get('id'), null,
  229 + function (err, result) {
  230 + conversation.set('contact', result.contact);
  231 + callback(null, conversation);
  232 + }
  233 + );
  234 + }
  235 + else {
  236 + console.log('[history] Conversation:', id, 'not found.');
  237 + callback(null, null);
  238 + }
  239 + });
  240 + },
218 241  
  242 + _syncGroups: function () {
  243 +
219 244 // Stall until logged
220 245 if (!global.client.isOnline) {
221   - this.listenToOnce(global.auth, 'login:success', this._loadGroups);
  246 + console.log('[history] Load groups stalled until login.');
  247 + this.listenToOnce(global.auth, 'login:success', this._syncGroups);
222 248 return;
223 249 }
224 250  
225   - // Stall until contacts are loaded
226   - if (!global.contacts.isLoaded) {
227   - this.listenToOnce(global.contacts, 'complete', this._loadGroups);
228   - return;
229   - }
  251 + console.log('[history] Syncing groups...');
230 252  
231 253 var _this = this;
232 254 global.client.getGroups(function (err, groups) {
  255 + groups = groups || [];
  256 + var groupsLength = groups.length;
233 257  
234   - // Process each contact, chaining a callback calling itself
235   - // when either the contact is synchronized or a timeout passes.
236   - function syncPicturelessGroupsInSerie() {
  258 + function syncGroup(group, callback) {
  259 + _this.findOrCreate(group.gid,
  260 + { noSaveList: true },
  261 + function (err, result) {
  262 + if (result.isNew) {
  263 + var conversation = result.conversation;
  264 + setTimeout(function () {
  265 + conversation.get('contact').set('subject', group.subject);
  266 + });
  267 + _this._fetchPicture(conversation);
  268 + }
  269 + callback();
  270 + }
  271 + );
  272 + }
237 273  
238   - var picturelessGroup, timeout = null;
239   -
240   - function nextOne() {
241   - clearTimeout(timeout);
242   - setTimeout(syncPicturelessGroupsInSerie, 1000);
  274 + function processGroups(offset) {
  275 + var index = offset;
  276 + if (index === groupsLength) {
  277 + _this.saveConversationList();
  278 + _this.trigger('history:synced');
243 279 }
244   -
245   - if (picturelessGroups.length) {
246   - picturelessGroup = picturelessGroups.pop();
247   - picturelessGroup.syncWithServer();
248   - picturelessGroup.once('synchronized:server', nextOne);
249   - timeout = setTimeout(function () {
250   - picturelessGroup.off('synchronized:server', nextOne);
251   - nextOne();
252   - }, 1000);
  280 + else {
  281 + /* jshint validthis: true */
  282 + syncGroup(groups[index], processGroups.bind(this, index + 1));
253 283 }
254 284 }
255 285  
256   - function processGroups(list, offset) {
257   - var start = Date.now(),
258   - tooMuchTime,
259   - MAX_LOOP_TIME = 17;
260   - var result, conversation, group;
261   - for (var i = offset, l = list.length; i < l && !tooMuchTime; i++) {
262   - group = list[i];
263   - result = global.contacts
264   - .findAndCreateContact(group.gid, group.subject);
  286 + if (err) { console.error('Error retreiving groups.'); }
  287 + processGroups(0);
  288 + });
  289 + },
265 290  
266   - if (result.isNew || !result.contact.get('photo')) {
267   - picturelessGroups.push(result.contact);
268   - }
  291 + _fetchPicture: function (conversation) {
  292 + var _this = this;
269 293  
270   - conversation =
271   - global.historyCollection
272   - .findAndCreateConversation(group.gid, { noSaveList: true });
273   - conversation.saveToStorage();
274   -
275   - tooMuchTime = Date.now() - start >= MAX_LOOP_TIME;
276   - }
277   - if (i < l) {
278   - setTimeout(processGroups, 0, list, i);
279   - }
280   - else {
281   - global.historyCollection.saveConversationList();
282   - _this.trigger('history:groups');
283   - syncPicturelessGroupsInSerie();
284   - }
  294 + function _realFetch() {
  295 + var task;
  296 + if (_this._pictureQueue.length > 0) {
  297 + task = _this._pictureQueue.shift();
  298 + task(_realFetch);
285 299 }
  300 + }
286 301  
287   - groups = groups || [];
288   - var picturelessGroups = [];
289   - if (err) { console.error('Error retreiving groups.'); }
290   - processGroups(groups, 0);
  302 + this._pictureQueue = this._pictureQueue || [];
  303 + this._pictureQueue.push(function fetchTask(next) {
  304 + conversation.get('contact').updatePhoto(next);
291 305 });
  306 +
  307 + if (this._pictureQueue.length === 1) {
  308 + _realFetch();
  309 + }
  310 +
292 311 },
293 312  
294   - /* Look up a conversation in the history. If it's not there, create one
295   - and add it to the history.
296   - */
297   - findAndCreateConversation: function (identifier, options) {
  313 + findOrCreate: function (id, options, callback) {
298 314 options = options || {};
299 315 var noSaveList = options.noSaveList || false;
300   - var c = this.findWhere({id : identifier});
301   - if (c) {
302   - return c;
303   - }
  316 + var conversationSubject = options.subject || null;
304 317  
305   - c = new ConversationModel({
306   - id : identifier,
307   - title: this._getConversationTitle(identifier)
308   - });
309   - this.listenTo(c.get('messages'), 'add', this._purgeOldMessages);
310   - this.add(c);
311   - // TODO: See inbox. Look for 'updateInbox'
312   - if (this._addConversation) {
313   - this._addConversation(c);
  318 + var _this = this;
  319 + var isNew = false;
  320 + var conversation = this.findWhere({id : id});
  321 +
  322 + // The conversation is not cached
  323 + if (!conversation) {
  324 +
  325 + this._loadFromStorage(id, function (err, conversation) {
  326 +
  327 + // TODO: I don't like this here. It should be a separated method
  328 + // called when loading a contact from storage or when created in
  329 + // this method.
  330 + function postLoad(conversation) {
  331 + _this.listenTo(
  332 + conversation.get('messages'), 'add', _this.purgeOldMessages);
  333 +
  334 + if (!noSaveList) {
  335 + _this.saveConversationList();
  336 + }
  337 + }
  338 +
  339 + // The conversation is not persisted yet
  340 + if (!conversation) {
  341 + isNew = true;
  342 +
  343 + // Asssociate the proper contact
  344 + global.contacts.findOrCreate(id, null,
  345 + function (err, result) {
  346 + conversation = _this.add({
  347 + id: id,
  348 + title: conversationSubject || _this._getConversationTitle(id)
  349 + }).get(id);
  350 + conversation.set('contact', result.contact);
  351 + _this.saveToStorage(conversation);
  352 + postLoad(conversation);
  353 + callback(null, {
  354 + isNew: isNew,
  355 + conversation: conversation
  356 + });
  357 + }
  358 + );
  359 + }
  360 + else {
  361 + postLoad(conversation);
  362 + callback(null, {
  363 + isNew: isNew,
  364 + conversation: conversation
  365 + });
  366 + }
  367 +
  368 + });
314 369 }
315   - if (!noSaveList) { this.saveConversationList(); }
316   - return c;
  370 + else {
  371 + callback(null, {
  372 + isNew: isNew,
  373 + conversation: conversation
  374 + });
  375 + }
317 376 },
318 377  
  378 + saveToStorage: function (conversation) {
  379 + var key = this._getStorageKey(conversation);
  380 + console.log('[history] Saving conversation', key);
  381 + AsyncStorage.setItem(key, this._serialize(conversation));
  382 + },
  383 +
  384 + _getStorageKey: function (conversation) {
  385 + return 'conv:' + conversation.get('id');
  386 + },
  387 +
  388 + _serialize: function (conversation) {
  389 + var attr = _.clone(conversation.attributes);
  390 + delete attr.messages;
  391 + delete attr.contact;
  392 + return attr;
  393 + },
  394 +
319 395 removeConversation: function (identifier) {
320   - var c = this.findWhere({id : identifier});
321   - var messages = c ? c.get('messages') : [];
  396 + var conversation = this.findWhere({id : identifier});
  397 + var contact = conversation.get('contact');
  398 +
  399 + var messages = conversation ? conversation.get('messages') : [];
322 400 messages.forEach(function (message) {
323 401 messages.remove(message);
324 402 });
325 403  
326 404 // Remove from history collection
327   - this.remove(c);
  405 + this.remove(conversation);
328 406  
329 407 // Remove the conversation from the AsyncStorage
330   - c.removeFromStorage();
  408 + this.removeFromStorage(conversation);
331 409  
332 410 // Remove from contacts only if it is a group
333   - var contact = global.contacts.findWhere({id: identifier});
334 411 if (contact && contact.get('isGroup')) {
335 412 global.contacts.removeFromStorage(contact);
336 413 global.contacts.remove(contact);
337 414 }
338 415 this.saveConversationList();
  416 + },
  417 +
  418 + removeFromStorage: function (conversation) {
  419 + var key = this._getStorageKey(conversation);
  420 + AsyncStorage.removeItem(key);
339 421 },
340 422  
341 423 _purgeOldMessages: function () {
app/scripts/models/auth.js View file @ 47c7675
... ... @@ -2,8 +2,9 @@
2 2 'backbone',
3 3 'global',
4 4 'storage/auth',
5   - 'utils/connectivity'
6   -], function (Backbone, global, authStorage, connectivity) {
  5 + 'utils/connectivity',
  6 + 'utils/phonenumber'
  7 +], function (Backbone, global, authStorage, connectivity, PhoneNumber) {
7 8 'use strict';
8 9  
9 10 var Auth = Backbone.Model.extend({
... ... @@ -44,7 +45,12 @@
44 45 _this.set('status', profile.status || null);
45 46 _this.set('screenName', profile.screenName || null);
46 47 }
47   - if (msisdn) { _this.set('msisdn', msisdn); }
  48 + if (msisdn) {
  49 + // TODO: Here there is a race condition we should avoid.
  50 + // This should be recovered before allowing adding participants.
  51 + PhoneNumber.setBaseNumber(msisdn);
  52 + _this.set('msisdn', msisdn);
  53 + }
48 54 if (userId && password) {
49 55 _this._tryToLogin(userId, password, msisdn);
50 56 } else {
app/scripts/models/contact.js View file @ 47c7675
... ... @@ -54,7 +54,9 @@
54 54 this.set('isGroup', this.get('id') && this.get('id').indexOf('-') >= 0);
55 55 this.set('admin',
56 56 this.get('isGroup') && (this.get('id').split('-')[0]));
57   - this.on('change:_photo', this.saveToStorage);
  57 + this.on('change', function () {
  58 + setTimeout(this.saveToStorage.bind(this));
  59 + });
58 60 },
59 61  
60 62 syncAllAndSave: function () {
app/scripts/models/conversation.js View file @ 47c7675
... ... @@ -34,12 +34,16 @@
34 34 this.listenTo(this.get('messages'), 'reset', this.updateLastMessage);
35 35 this.listenTo(this.get('messages'), 'add', this.updateLastMessage);
36 36 this.listenTo(this.get('messages'), 'add', this._onAddMessage);
  37 + this.on('change:contact', function (model, newContact) {
  38 + var previousContact = this.previous('contact');
  39 + if (previousContact) {
  40 + this.stopListening(previousContact);
  41 + }
37 42  
38   - var contact = this._lookupContact();
39   - if (!contact) {
40   - // try it again later
41   - this.listenTo(global.contacts, 'add', this._lookupContact);
42   - }
  43 + this.listenTo(newContact, 'change', this._onContactChanged);
  44 + this._onContactChanged(newContact);
  45 + });
  46 + this.on('change', this.saveToStorage);
43 47  
44 48 this.set('isGroup', this.get('id').indexOf('-') >= 0);
45 49 },
... ... @@ -71,28 +75,6 @@
71 75 }
72 76 },
73 77  
74   - saveToStorage: function (callback) {
75   - var key = this.getStorageKey();
76   - var _this = this;
77   - AsyncStorage.setItem(key, this._serialize(), function () {
78   - console.log('Saved conversation', key,
79   - _this.get('date').toLocaleString());
80   - _this.trigger('conversation:save', _this);
81   - if (callback) { callback(); }
82   - });
83   - },
84   -
85   - removeFromStorage: function (callback) {
86   - var key = this.getStorageKey();
87   - var _this = this;
88   - AsyncStorage.removeItem(key, function () {
89   - console.log('Removed conversation', key,
90   - _this.get('date').toLocaleString());
91   - _this.trigger('conversation:remove', _this);
92   - if (callback) { callback(); }
93   - });
94   - },
95   -
96 78 loadMessagesFromStorage: function (callback) {
97 79 var _this = this;
98 80 var messages = [];
... ... @@ -120,16 +102,6 @@
120 102 });
121 103 },
122 104  
123   - getStorageKey: function () {
124   - return 'conv:' + this.get('id');
125   - },
126   -
127   - _serialize: function () {
128   - var attr = _.clone(this.attributes);
129   - delete attr.messages;
130   - return attr;
131   - },
132   -
133 105 _onAddMessage: function (message) {
134 106 var isLastMessage = function (messages, message) {
135 107 // TODO: we should be able to pass 'true', as optional parameter to
136 108  
... ... @@ -185,39 +157,14 @@
185 157 }
186 158 },
187 159  
188   - _lookupContact: function () {
189   - var contact = global.contacts.findWhere({phone: this.get('id')});
190   - if (contact) {
191   - // set the initial conversation name
192   - this._onContactChanged(contact);
193   - // listen for changes
194   - this.listenTo(contact, 'change', this._onContactChanged);
195   - this.stopListening(global.contacts, 'add', this._lookupContact);
196   - }
197   - return contact;
  160 + saveToStorage: function () {
  161 + global.historyCollection.saveToStorage(this);
198 162 },
199 163  
200 164 updateParticipantList: function () {
201 165 this.trigger('dirty:participants');
202 166 }
203 167  
204   - }, {
205   - // static methods and vars
206   - loadFromStorage: function (id, callback) {
207   - var key = 'conv:' + id;
208   - AsyncStorage.getItem(key, function (value) {
209   - console.log('[conversation] Loading conversation', key,
210   - (value ? 'OK' : 'FAILED'));
211   -
212   - if (!value) {
213   - callback(null);
214   - } else {
215   - var c = global.historyCollection.findAndCreateConversation(id);
216   - c.set(value);
217   - callback(c);
218   - }
219   - });
220   - }
221 168 });
222 169  
223 170 return Conversation;
app/scripts/router.js View file @ 47c7675
... ... @@ -128,34 +128,51 @@
128 128 },
129 129  
130 130 conversation: function (identifier, scrollTop) {
131   - console.log('Creating conversation ' + identifier);
  131 + console.log('[router] Going to conversation:', identifier);
132 132 // here we can cache/reuse views if needed
133   - var c = global.historyCollection.findAndCreateConversation(identifier);
134   - c.loadMessagesFromStorage();
135   - c.set('isRead', true);
136   - c.saveToStorage();
137   - var cView = new ConversationView({model: c, scrollTop: scrollTop});
138   - this.show(cView);
  133 + var _this = this;
  134 + global.historyCollection.findOrCreate(identifier, null,
  135 + function (err, result) {
  136 + var conversation = result.conversation;
  137 + conversation.loadMessagesFromStorage();
  138 + conversation.set('isRead', true);
  139 + conversation.saveToStorage();
  140 + var cView = new ConversationView({
  141 + model: conversation,
  142 + scrollTop: scrollTop
  143 + });
  144 + _this.show(cView);
  145 + }
  146 + );
139 147 },
140 148  
141 149 sendLocation: function (conversationId) {
142   - var c = global.historyCollection.findAndCreateConversation(
143   - conversationId);
144   - this.show(new ComposeLocationView({conversation: c}));
  150 + var _this = this;
  151 + global.historyCollection.findOrCreate(conversationId, null,
  152 + function (err, result) {
  153 + var conversation = result.conversation;
  154 + _this.show(new ComposeLocationView({conversation: conversation}));
  155 + }
  156 + );
145 157 },
146 158  
147 159 _viewer: function (conversationId, messageId, scroll, ViewClass) {
148   - var c = global.historyCollection.findAndCreateConversation(
149   - conversationId);
150   - var message = c.get('messages').find(function (x) {
151   - return x.get('_id') === parseInt(messageId, 10);
152   - });
  160 + var _this = this;
  161 + global.historyCollection.findOrCreate(conversationId, null,
  162 + function (err, result) {
  163 + var conversation = result.conversation;
153 164  
154   - this.show(new ViewClass({
155   - model: message,
156   - conversation: c,
157   - scrollTop: scroll
158   - }));
  165 + var message = conversation.get('messages').find(function (x) {
  166 + return x.get('_id') === parseInt(messageId, 10);
  167 + });
  168 +
  169 + _this.show(new ViewClass({
  170 + model: message,
  171 + conversation: conversation,
  172 + scrollTop: scroll
  173 + }));
  174 + }
  175 + );
159 176 },
160 177  
161 178 locationViewer: function (conversationId, messageId, scroll) {
... ... @@ -163,11 +180,16 @@
163 180 },
164 181  
165 182 sendImage: function (conversationId) {
166   - console.log('router sendImage');
167   - var c = global.historyCollection.findAndCreateConversation(
168   - conversationId);
169   - var message = new Message();
170   - this.show(new ComposeImageView({conversation: c, model: message}));
  183 + var _this = this;
  184 + global.historyCollection.findOrCreate(conversationId, null,
  185 + function (err, result) {
  186 + var message = new Message();
  187 + _this.show(new ComposeImageView({
  188 + conversation: result.conversation,
  189 + model: message
  190 + }));
  191 + }
  192 + );
171 193 },
172 194  
173 195 show: function (view) {
app/scripts/templates/inbox.hbs View file @ 47c7675
... ... @@ -26,6 +26,9 @@
26 26 <section class="conversation-list" id="conversations">
27 27 <ul>
28 28 </ul>
  29 + <div id='contacts-centinel'>
  30 + <p>Centinela</p>
  31 + </div>
29 32 </section>
30 33  
31 34 {{!
app/scripts/views/conversation.js View file @ 47c7675
... ... @@ -158,21 +158,24 @@
158 158  
159 159 var names = [];
160 160 phoneList.forEach(function (phone, index) {
161   - var result = global.contacts.findAndCreateContact(phone);
162   - var contact = result.contact;
163   - if (result.isNew) {
164   - //Make the sync in different times
165   - var wait = 300 * index + 200;
166   - setTimeout(function () {
167   - contact.syncAllAndSave();
168   - contact.once('synchronized:webcontacts', function () {
  161 + global.contacts.findOrCreate(phone, undefined,
  162 + function (err, result) {
  163 + var contact = result.contact;
  164 + if (result.isNew) {
  165 + //Make the sync in different times
  166 + var wait = 300 * index + 200;
  167 + setTimeout(function () {
  168 + contact.syncAllAndSave();
  169 + contact.once('synchronized:webcontacts', function () {
  170 + names.push(contact.get('displayName'));
  171 + });
  172 + }, wait);
  173 + }
  174 + else {
169 175 names.push(contact.get('displayName'));
170   - });
171   - }, wait);
172   - }
173   - else {
174   - names.push(contact.get('displayName'));
175   - }
  176 + }
  177 + }
  178 + );
176 179 });
177 180  
178 181 // Let's fill with participants once we have some loaded
app/scripts/views/group-profile.js View file @ 47c7675
... ... @@ -290,24 +290,29 @@
290 290 participants.forEach(function (participant, index) {
291 291 if (global.auth.isMe(participant)) { return; }
292 292  
293   - var result =
294   - global.contacts.findAndCreateContact(participant);
295   - var con = result.contact;
296   - _this._addParticipant(result.contact);
  293 + global.contacts.findOrCreate(participant, null,
  294 + function (err, result) {
  295 + var contact = result.contact;
  296 + _this._addParticipant(contact);
297 297  
298   - //Make the sync in different times
299   - var wait = 300 * index + 200;
300   - setTimeout(function () {
301   - con.syncAllAndSave();
302   - }, wait);
  298 + // Make the sync in different times
  299 + var wait = 300 * index + 200;
  300 + setTimeout(function () {
  301 + contact.syncAllAndSave();
  302 + }, wait);
  303 + }
  304 + );
  305 +
303 306 });
304 307 },
305 308  
306 309 _addParticipant: function (contact) {
307 310 this.$el
308 311 .find('ul.participants')
309   - .append(new ContactView({ model: contact,
310   - showControls: this.showControls }).render().el);
  312 + .append(new ContactView({
  313 + model: contact,
  314 + showControls: this.showControls
  315 + }).render().el);
311 316 },
312 317  
313 318 _closeProfile: function (evt) {
app/scripts/views/inbox.js View file @ 47c7675
... ... @@ -48,8 +48,26 @@
48 48  
49 49 global.auth.checkCredentials();
50 50 }
  51 + this._contactListBuffer = document.createDocumentFragment();
51 52 },
52 53  
  54 + _checkCentinelVisibility: function () {
  55 + var view = this.$el.find('.page-wrapper')[0];
  56 + var viewTop = view.offsetTop + view.scrollTop;
  57 + var viewBottom = viewTop + view.clientHeight;
  58 + var centinelTop = this._centinel.offsetTop;
  59 + var centinelBottom = centinelTop + this._centinel.offsetHeight;
  60 + var isVisible = viewTop < centinelBottom && centinelBottom <= viewBottom;
  61 + if (!this._centinelWasVisible && isVisible) {
  62 + this.trigger('centinel:appeared');
  63 + }
  64 + else if (this._centinelWasVisible && !isVisible) {
  65 + this.trigger('centinel:dissapeared');
  66 + }
  67 + this._centinelWasVisible = isVisible;
  68 + return isVisible;
  69 + },
  70 +
53 71 _goToValidate: function () {
54 72 global.router.navigate(
55 73 'validate/' + localStorage.getItem('phoneAndCC'), { trigger: true });
56 74  
... ... @@ -73,12 +91,10 @@
73 91 });
74 92 },
75 93  
76   - // The sections for Today, Yesterday, Before are all initially
77   - // hidden. If a conversation exists in a section it is visible.
78 94 _populate: function () {
79 95 this._clearMiniConversations();
80 96 this.model.sort();
81   - var conversations = global.historyCollection.models;
  97 + var conversations = this.model.models;
82 98 var section = this.$el.find('#conversations').first();
83 99 var list = section.find('ul').first();
84 100 if (conversations.length !== 0) {
85 101  
86 102  
87 103  
88 104  
89 105  
90 106  
91 107  
92 108  
... ... @@ -87,46 +103,55 @@
87 103 }
88 104  
89 105 for (var i = 0; i < conversations.length; i++) {
90   - this.listenTo(
91   - conversations[i],
92   - 'message:added',
93   - this._onMessagePromoteConversation
94   - );
95 106 var mc = new MiniConversationView({
96 107 el: $('<li>'),
97 108 model: conversations[i]
98 109 });
99   -
100 110 mc.render();
  111 +
  112 + this.listenTo(
  113 + conversations[i],
  114 + 'message:added',
  115 + this._onMessagePromoteConversation.bind(this, mc)
  116 + );
  117 +
101 118 list.append(mc.$el);
102 119 this.miniConversationViews.push(mc);
103 120 }
104 121 },
105 122  
106 123 _addConversation: function (c) {
107   - this.listenTo(
108   - c,
109   - 'message:added',
110   - this._onMessagePromoteConversation
111   - );
112 124 var mc = new MiniConversationView({
113 125 el: $('<li>'),
114 126 model: c
115 127 });
116 128 mc.render();
117   - var list = this.$el.find('#conversations ul').first();
118   - list.prepend(mc.$el);
119   - this.miniConversationViews.splice(0, 0, mc);
  129 +
  130 + this.listenTo(
  131 + c,