1 /* 2 * ***** BEGIN LICENSE BLOCK ***** 3 * Zimbra Collaboration Suite Web Client 4 * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. 5 * 6 * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at: https://www.zimbra.com/license 9 * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15 10 * have been added to cover use of software over a computer network and provide for limited attribution 11 * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B. 12 * 13 * Software distributed under the License is distributed on an "AS IS" basis, 14 * WITHOUT WARRANTY OF ANY KIND, either express or implied. 15 * See the License for the specific language governing rights and limitations under the License. 16 * The Original Code is Zimbra Open Source Web Client. 17 * The Initial Developer of the Original Code is Zimbra, Inc. All rights to the Original Code were 18 * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015. 19 * 20 * All portions of the code are Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved. 21 * ***** END LICENSE BLOCK ***** 22 */ 23 24 /** 25 * @overview 26 * This file contains the contact list class. 27 * 28 */ 29 30 /** 31 * Create a new, empty contact list. 32 * @class 33 * This class represents a list of contacts. In general, the list is the result of a 34 * search. It may be the result of a <code><GetContactsRequest></code>, which returns all of the user's 35 * local contacts. That list is considered to be canonical. 36 * <p> 37 * Loading of all local contacts has been optimized by delaying the creation of {@link ZmContact} objects until 38 * they are needed. That has a big impact on IE, and not much on Firefox. Loading a subset 39 * of attributes did not have much impact on load time, probably because a large majority 40 * of contacts contain only those minimal fields.</p> 41 * 42 * @author Conrad Damon 43 * 44 * @param {ZmSearch} search the search that generated this list 45 * @param {Boolean} isGal if <code>true</code>, this is a list of GAL contacts 46 * @param {constant} type the item type 47 * 48 * @extends ZmList 49 */ 50 ZmContactList = function(search, isGal, type) { 51 52 if (arguments.length == 0) { return; } 53 type = type || ZmItem.CONTACT; 54 ZmList.call(this, type, search); 55 56 this.isGal = (isGal === true); 57 this.isCanonical = false; 58 this.isLoaded = false; 59 60 this._app = appCtxt.getApp(ZmApp.CONTACTS); 61 if (!this._app) { 62 this._emailToContact = this._phoneToContact = {}; 63 return; 64 } 65 this._emailToContact = this._app._byEmail; 66 this._phoneToContact = this._app._byPhone; 67 68 this._alwaysUpdateHashes = true; // Should we update the phone & IM fast-lookup hashes even when account features don't require it? (bug #60411) 69 }; 70 71 ZmContactList.prototype = new ZmList; 72 ZmContactList.prototype.constructor = ZmContactList; 73 74 ZmContactList.prototype.isZmContactList = true; 75 ZmContactList.prototype.toString = function() { return "ZmContactList"; }; 76 77 78 79 80 // Constants 81 82 // Support for loading user's local contacts from a large string 83 84 ZmContactList.URL = "/Contacts"; // REST URL for loading user's local contacts 85 ZmContactList.URL_ARGS = { fmt: 'cf', t: 2, all: 'all' }; // arguments for the URL above 86 ZmContactList.CONTACT_SPLIT_CHAR = '\u001E'; // char for splitting string into contacts 87 ZmContactList.FIELD_SPLIT_CHAR = '\u001D'; // char for splitting contact into fields 88 // fields that belong to a contact rather than its attrs 89 ZmContactList.IS_CONTACT_FIELD = {"id":true, "l":true, "d":true, "fileAsStr":true, "rev":true}; 90 91 92 93 /** 94 * @private 95 */ 96 ZmContactList.prototype.addLoadedCallback = 97 function(callback) { 98 if (this.isLoaded) { 99 callback.run(); 100 return; 101 } 102 if (!this._loadedCallbacks) { 103 this._loadedCallbacks = []; 104 } 105 this._loadedCallbacks.push(callback); 106 }; 107 108 /** 109 * @private 110 */ 111 ZmContactList.prototype._finishLoading = 112 function() { 113 DBG.timePt("done loading " + this.size() + " contacts"); 114 this.isLoaded = true; 115 if (this._loadedCallbacks) { 116 var callback; 117 while (callback = this._loadedCallbacks.shift()) { 118 callback.run(); 119 } 120 } 121 }; 122 123 /** 124 * Retrieves the contacts from the back end, and parses the response. The list is then sorted. 125 * This method is used only by the canonical list of contacts, in order to load their content. 126 * <p> 127 * Loading a minimal set of attributes did not result in a significant performance gain. 128 * </p> 129 * 130 * @private 131 */ 132 ZmContactList.prototype.load = 133 function(callback, errorCallback, accountName) { 134 // only the canonical list gets loaded 135 this.isCanonical = true; 136 var respCallback = new AjxCallback(this, this._handleResponseLoad, [callback]); 137 DBG.timePt("requesting contact list", true); 138 if(appCtxt.isExternalAccount()) { 139 //Do not make a call in case of external user 140 //The rest url constructed wont exist in case of external user 141 if (callback) { 142 callback.run(); 143 } 144 return; 145 } 146 var args = ZmContactList.URL_ARGS; 147 148 // bug 74609: suppress overzealous caching by IE 149 if (AjxEnv.isIE) { 150 args = AjxUtil.hashCopy(args); 151 args.sid = ZmCsfeCommand.getSessionId(); 152 } 153 154 var params = {asyncMode:true, noBusyOverlay:true, callback:respCallback, errorCallback:errorCallback, offlineCallback:callback}; 155 params.restUri = AjxUtil.formatUrl({ 156 path:["/home/", (accountName || appCtxt.getUsername()), 157 ZmContactList.URL].join(""), 158 qsArgs: args, qsReset:true 159 }); 160 DBG.println(AjxDebug.DBG1, "loading contacts from " + params.restUri); 161 appCtxt.getAppController().sendRequest(params); 162 163 ZmContactList.addDlFolder(); 164 165 }; 166 167 /** 168 * @private 169 */ 170 ZmContactList.prototype._handleResponseLoad = 171 function(callback, result) { 172 DBG.timePt("got contact list"); 173 var text = result.getResponse(); 174 if (text && typeof text !== 'string'){ 175 text = text._data; 176 } 177 var derefList = []; 178 if (text) { 179 var contacts = text.split(ZmContactList.CONTACT_SPLIT_CHAR); 180 var derefBatchCmd = new ZmBatchCommand(true, null, true); 181 for (var i = 0, len = contacts.length; i < len; i++) { 182 var fields = contacts[i].split(ZmContactList.FIELD_SPLIT_CHAR); 183 var contact = {}, attrs = {}; 184 var groupMembers = []; 185 var foundDeref = false; 186 for (var j = 0, len1 = fields.length; j < len1; j += 2) { 187 if (ZmContactList.IS_CONTACT_FIELD[fields[j]]) { 188 contact[fields[j]] = fields[j + 1]; 189 } else { 190 var value = fields[j+1]; 191 switch (fields[j]) { 192 case ZmContact.F_memberC: 193 groupMembers.push({type: ZmContact.GROUP_CONTACT_REF, value: value}); 194 foundDeref = true; //load shared contacts 195 break; 196 case ZmContact.F_memberG: 197 groupMembers.push({type: ZmContact.GROUP_GAL_REF, value: value}); 198 foundDeref = true; 199 break; 200 case ZmContact.F_memberI: 201 groupMembers.push({type: ZmContact.GROUP_INLINE_REF, value: value}); 202 foundDeref = true; 203 break; 204 default: 205 attrs[fields[j]] = value; 206 } 207 } 208 } 209 if (attrs[ZmContact.F_type] === "group") { //set only for group. 210 attrs[ZmContact.F_groups] = groupMembers; 211 } 212 if (foundDeref) { 213 //batch group members for deref loading 214 var dummy = new ZmContact(contact["id"], this); 215 derefBatchCmd.add(new AjxCallback(dummy, dummy.load, [null, null, derefBatchCmd, true])); 216 } 217 contact._attrs = attrs; 218 this._addContact(contact); 219 } 220 derefBatchCmd.run(); 221 } 222 223 this._finishLoading(); 224 225 if (callback) { 226 callback.run(); 227 } 228 }; 229 230 /** 231 * @static 232 */ 233 ZmContactList.addDlFolder = 234 function() { 235 236 if (!appCtxt.get(ZmSetting.DLS_FOLDER_ENABLED)) { 237 return; 238 } 239 240 var dlsFolder = appCtxt.getById(ZmOrganizer.ID_DLS); 241 242 var root = appCtxt.getById(ZmOrganizer.ID_ROOT); 243 if (!root) { return; } 244 245 if (dlsFolder && root.getById(ZmOrganizer.ID_DLS)) { 246 //somehow (after a refresh block, can be reprod using $set:refresh. ZmClientCmdHandler.prototype.execute_refresh) the DLs folder object is removed from under the root (but still cached in appCtxt). So making sure it's there. 247 return; 248 } 249 250 if (!dlsFolder) { 251 var params = { 252 id: ZmOrganizer.ID_DLS, 253 name: ZmMsg.distributionLists, 254 parent: root, 255 tree: root.tree, 256 type: ZmOrganizer.ADDRBOOK, 257 numTotal: null, //we don't know how many 258 noTooltip: true //so don't show tooltip 259 }; 260 261 dlsFolder = new ZmAddrBook(params); 262 root.children.add(dlsFolder); 263 dlsFolder._isDL = true; 264 } 265 else { 266 //the dls folder object exists but no longer as a child of the root. 267 dlsFolder.parent = root; 268 root.children.add(dlsFolder); //any better way to do this? 269 } 270 271 }; 272 273 ZmContactList.prototype.add = 274 function(item, index) { 275 if (!item.id || !this._idHash[item.id]) { 276 this._vector.add(item, index); 277 if (item.id) { 278 this._idHash[item.id] = item; 279 } 280 this._updateHashes(item, true); 281 } 282 }; 283 284 ZmContactList.prototype.cache = 285 function(offset, newList) { 286 var getId = function(){ 287 return this.id; 288 } 289 var exists = function(obj) { 290 return this._vector.containsLike(obj, getId); 291 } 292 var unique = newList.sub(exists, this); 293 294 this.getVector().merge(offset, unique); 295 // reparent each item within new list, and add it to ID hash 296 var list = unique.getArray(); 297 for (var i = 0; i < list.length; i++) { 298 var item = list[i]; 299 item.list = this; 300 if (item.id) { 301 this._idHash[item.id] = item; 302 } 303 } 304 }; 305 306 /** 307 * @private 308 */ 309 ZmContactList.prototype._addContact = 310 function(contact) { 311 312 // note that we don't create a ZmContact here (optimization) 313 contact.list = this; 314 this._updateHashes(contact, true); 315 var fn = [], fl = []; 316 if (contact._attrs[ZmContact.F_firstName]) { fn.push(contact._attrs[ZmContact.F_firstName]); } 317 if (contact._attrs[ZmContact.F_middleName]) { fn.push(contact._attrs[ZmContact.F_middleName]); } 318 if (contact._attrs[ZmContact.F_lastName]) { fn.push(contact._attrs[ZmContact.F_lastName]); } 319 if (fn.length) { 320 contact._attrs[ZmContact.X_fullName] = fn.join(" "); 321 } 322 if (contact._attrs[ZmContact.F_firstName]) { fl.push(contact._attrs[ZmContact.F_firstName]); } 323 if (contact._attrs[ZmContact.F_lastName]) { fl.push(contact._attrs[ZmContact.F_lastName]); } 324 contact._attrs[ZmContact.X_firstLast] = fl.join(" "); 325 326 this.add(contact); 327 }; 328 329 /** 330 * Converts an anonymous contact object (contained by the JS returned by load request) 331 * into a ZmContact, and updates the containing list if it is the canonical one. 332 * 333 * @param {Object} contact a contact 334 * @param {int} idx the index of contact in canonical list 335 * 336 * @private 337 */ 338 ZmContactList.prototype._realizeContact = 339 function(contact, idx) { 340 341 if (contact instanceof ZmContact) { return contact; } 342 if (contact && contact.type == ZmItem.CONTACT) { return contact; } // instanceof often fails in new window 343 344 var args = {list:this}; 345 var obj = eval(ZmList.ITEM_CLASS[this.type]); 346 var realContact = obj && obj.createFromDom(contact, args); 347 348 if (this.isCanonical) { 349 var a = this.getArray(); 350 idx = idx || this.getIndexById(contact.id); 351 a[idx] = realContact; 352 this._updateHashes(realContact, true); 353 this._idHash[contact.id] = realContact; 354 } 355 356 return realContact; 357 }; 358 359 /** 360 * Finds the array index for the contact with the given ID. 361 * 362 * @param {int} id the contact ID 363 * @return {int} the index 364 * @private 365 */ 366 ZmContactList.prototype.getIndexById = 367 function(id) { 368 var a = this.getArray(); 369 for (var i = 0; i < a.length; i++) { 370 if (a[i].id == id) { 371 return i; 372 } 373 } 374 return null; 375 }; 376 377 /** 378 * Override in order to make sure the contacts have been realized. We don't 379 * call realizeContact() since this is not the canonical list. 380 * 381 * @param {int} offset the starting index 382 * @param {int} limit the size of sublist 383 * @return {AjxVector} a vector of {@link ZmContact} objects 384 */ 385 ZmContactList.prototype.getSubList = 386 function(offset, limit, folderId) { 387 if (folderId && this.isCanonical) { 388 // only collect those contacts that belong to the given folderId if provided 389 var newlist = []; 390 var sublist = this.getArray(); 391 var offsetCount = 0; 392 this.setHasMore(false); 393 394 for (var i = 0; i < sublist.length; i++) { 395 sublist[i] = this._realizeContact(sublist[i], i); 396 var folder = appCtxt.getById(sublist[i].folderId); 397 if (folder && folder.nId == ZmOrganizer.normalizeId(folderId)) { 398 if (offsetCount >= offset) { 399 if (newlist.length == limit) { 400 this.setHasMore(true); 401 break; 402 } 403 newlist.push(sublist[i]); 404 } 405 offsetCount++; 406 } 407 } 408 409 return AjxVector.fromArray(newlist); 410 } else { 411 var vec = ZmList.prototype.getSubList.call(this, offset, limit); 412 if (vec) { 413 var a = vec.getArray(); 414 for (var i = 0; i < a.length; i++) { 415 a[i] = this._realizeContact(a[i], offset + i); 416 } 417 } 418 419 return vec; 420 } 421 }; 422 423 /** 424 * Override in order to make sure the contact has been realized. Canonical list only. 425 * 426 * @param {int} id the contact ID 427 * @return {ZmContact} the contact or <code>null</code> if not found 428 */ 429 ZmContactList.prototype.getById = 430 function(id) { 431 if (!id || !this.isCanonical) return null; 432 433 var contact = this._idHash[id]; 434 return contact ? this._realizeContact(contact) : null; 435 }; 436 437 /** 438 * Gets the contact with the given address, if any (canonical list only). 439 * 440 * @param {String} address an email address 441 * @return {ZmContact} the contact or <code>null</code> if not found 442 */ 443 ZmContactList.prototype.getContactByEmail = 444 function(address) { 445 if (!address || !this.isCanonical) return null; 446 447 var contact = this._emailToContact[address.toLowerCase()]; 448 if (contact) { 449 contact = this._realizeContact(contact); 450 contact._lookupEmail = address; // so caller knows which address matched 451 return contact; 452 } else { 453 return null; 454 } 455 }; 456 457 /** 458 * Gets information about the contact with the given phone number, if any (canonical list only). 459 * 460 * @param {String} phone the phone number 461 * @return {Hash} an object with <code>contact</code> = the contact & <code>field</code> = the field with the matching phone number 462 */ 463 ZmContactList.prototype.getContactByPhone = 464 function(phone) { 465 if (!phone || !this.isCanonical) return null; 466 467 var digits = this._getPhoneDigits(phone); 468 var data = this._phoneToContact[digits]; 469 if (data) { 470 data.contact = this._realizeContact(data.contact); 471 return data; 472 } else { 473 return null; 474 } 475 }; 476 477 /** 478 * Moves a list of items to the given folder. 479 * <p> 480 * This method calls the base class for normal "moves" UNLESS we're dealing w/ 481 * shared items (or folder) in which case we must send a CREATE request for the 482 * given folder to the server followed by a hard delete of the shared contact. 483 * </p> 484 * 485 * @param {Hash} params a hash of parameters 486 * @param {Array} params.items a list of items to move 487 * @param {ZmFolder} params.folder the destination folder 488 * @param {Hash} params.attrs the additional attrs for SOAP command 489 * @param {Boolean} params.outOfTrash if <code>true</code>, we are moving contacts out of trash 490 */ 491 ZmContactList.prototype.moveItems = 492 function(params) { 493 494 params = Dwt.getParams(arguments, ["items", "folder", "attrs", "outOfTrash"]); 495 params.items = AjxUtil.toArray(params.items); 496 497 var moveBatchCmd = new ZmBatchCommand(true, null, true); 498 var loadBatchCmd = new ZmBatchCommand(true, null, true); 499 var softMove = []; 500 501 // if the folder we're moving contacts to is a shared folder, then dont bother 502 // checking whether each item is shared or not 503 if (params.items[0] && params.items[0] instanceof ZmItem) { 504 for (var i = 0; i < params.items.length; i++) { 505 var contact = params.items[i]; 506 507 if (contact.isReadOnly()) { continue; } 508 509 softMove.push(contact); 510 } 511 } else { 512 softMove = params.items; 513 } 514 515 // for "soft" moves, handle moving out of Trash differently 516 if (softMove.length > 0) { 517 var params1 = AjxUtil.hashCopy(params); 518 params1.attrs = params.attrs || {}; 519 var toFolder = params.folder; 520 params1.attrs.l = toFolder.isRemote() ? toFolder.getRemoteId() : toFolder.id; 521 params1.action = "move"; 522 params1.accountName = appCtxt.multiAccounts && appCtxt.accountList.mainAccount.name; 523 if (params1.folder.id == ZmFolder.ID_TRASH) { 524 params1.actionTextKey = 'actionTrash'; 525 // bug: 47389 avoid moving to local account's Trash folder. 526 params1.accountName = appCtxt.multiAccounts && params.items[0].getAccount().name; 527 } else { 528 params1.actionTextKey = 'actionMove'; 529 params1.actionArg = toFolder.getName(false, false, true); 530 } 531 params1.callback = params.outOfTrash && new AjxCallback(this, this._handleResponseMoveItems, params); 532 533 this._itemAction(params1); 534 } 535 }; 536 537 /** 538 * @private 539 */ 540 ZmContactList.prototype._handleResponseMoveBatchCmd = 541 function(result) { 542 var resp = result.getResponse().BatchResponse.ContactActionResponse; 543 // XXX: b/c the server does not return notifications for actions done on 544 // shares, we manually notify - TEMP UNTIL WE GET BETTER SERVER SUPPORT 545 var ids = resp[0].action.id.split(","); 546 for (var i = 0; i < ids.length; i++) { 547 var contact = appCtxt.cacheGet(ids[i]); 548 if (contact && contact.isShared()) { 549 contact.notifyDelete(); 550 appCtxt.cacheRemove(ids[i]); 551 } 552 } 553 }; 554 555 /** 556 * @private 557 */ 558 ZmContactList.prototype._handleResponseLoadMove = 559 function(moveBatchCmd, params) { 560 var deleteCmd = new AjxCallback(this, this._itemAction, [params]); 561 moveBatchCmd.add(deleteCmd); 562 563 var respCallback = new AjxCallback(this, this._handleResponseMoveBatchCmd); 564 moveBatchCmd.run(respCallback); 565 }; 566 567 /** 568 * @private 569 */ 570 ZmContactList.prototype._handleResponseBatchLoad = 571 function(batchCmd, folder, result, contact) { 572 batchCmd.add(this._getCopyCmd(contact, folder)); 573 }; 574 575 /** 576 * @private 577 */ 578 ZmContactList.prototype._getCopyCmd = 579 function(contact, folder) { 580 var temp = new ZmContact(null, this); 581 for (var j in contact.attr) { 582 temp.attr[j] = contact.attr[j]; 583 } 584 temp.attr[ZmContact.F_folderId] = folder.id; 585 586 return new AjxCallback(temp, temp.create, [temp.attr]); 587 }; 588 589 /** 590 * Deletes contacts after checking that this is not a GAL list. 591 * 592 * @param {Hash} params a hash of parameters 593 * @param {Array} params.items the list of items to delete 594 * @param {Boolean} params.hardDelete if <code>true</code>, force physical removal of items 595 * @param {Object} params.attrs the additional attrs for SOAP command 596 */ 597 ZmContactList.prototype.deleteItems = 598 function(params) { 599 if (this.isGal) { 600 if (ZmContactList.deleteGalItemsAllowed(params.items)) { 601 this._deleteDls(params.items); 602 return; 603 } 604 DBG.println(AjxDebug.DBG1, "Cannot delete GAL contacts that are not DLs"); 605 return; 606 } 607 ZmList.prototype.deleteItems.call(this, params); 608 }; 609 610 ZmContactList.deleteGalItemsAllowed = 611 function(items) { 612 var deleteDomainsAllowed = appCtxt.createDistListAllowedDomainsMap; 613 if (items.length == 0) { 614 return false; //need a special case since we don't want to enable the "delete" button for 0 items. 615 } 616 for (var i = 0; i < items.length; i++) { 617 var contact = items[i]; 618 var email = contact.getEmail(); 619 var domain = email.split("@")[1]; 620 var isDL = contact && contact.isDistributionList(); 621 //see bug 71368 and also bug 79672 - the !contact.dlInfo is in case somehow dlInfo is missing - so unfortunately if that happens (can't repro) - let's not allow to delete since we do not know if it's an owner 622 if (!isDL || !deleteDomainsAllowed[domain] || !contact.dlInfo || !contact.dlInfo.isOwner) { 623 return false; 624 } 625 } 626 return true; 627 }; 628 629 ZmContactList.prototype._deleteDls = 630 function(items, confirmDelete) { 631 632 if (!confirmDelete) { 633 var callback = this._deleteDls.bind(this, items, true); 634 this._popupDeleteWarningDialog(callback, false, items.length); 635 return; 636 } 637 638 var reqs = []; 639 for (var i = 0; i < items.length; i++) { 640 var contact = items[i]; 641 var email = contact.getEmail(); 642 reqs.push({ 643 _jsns: "urn:zimbraAccount", 644 dl: {by: "name", 645 _content: contact.getEmail() 646 }, 647 action: { 648 op: "delete" 649 } 650 }); 651 } 652 var jsonObj = { 653 BatchRequest: { 654 _jsns: "urn:zimbra", 655 DistributionListActionRequest: reqs 656 } 657 }; 658 var respCallback = this._deleteDlsResponseHandler.bind(this, items); 659 appCtxt.getAppController().sendRequest({jsonObj: jsonObj, asyncMode: true, callback: respCallback}); 660 661 }; 662 663 ZmContactList.prototype._deleteDlsResponseHandler = 664 function(items) { 665 if (appCtxt.getCurrentView().isZmGroupView) { 666 //this is the case we were editing the DL (different than viewing it in the DL list, in which case it's the contactListController). 667 //so we now need to pop up the view. 668 this.controller.popView(); 669 } 670 671 appCtxt.setStatusMsg(items.length == 1 ? ZmMsg.dlDeleted : ZmMsg.dlsDeleted); 672 673 for (var i = 0; i < items.length; i++) { 674 var item = items[i]; 675 item.clearDlInfo(); 676 item._notify(ZmEvent.E_DELETE); 677 } 678 }; 679 680 681 682 /** 683 * Sets the is GAL flag. 684 * 685 * @param {Boolean} isGal <code>true</code> if contact list is GAL 686 */ 687 ZmContactList.prototype.setIsGal = 688 function(isGal) { 689 this.isGal = isGal; 690 }; 691 692 ZmContactList.prototype.notifyCreate = 693 function(node) { 694 var obj = eval(ZmList.ITEM_CLASS[this.type]); 695 if (obj) { 696 var item = obj.createFromDom(node, {list:this}); 697 var index = this._sortIndex(item); 698 // only add if it sorts into this list 699 var listSize = this.size(); 700 var visible = false; 701 if (index < listSize || listSize == 0 || (index==listSize && !this._hasMore)) { 702 this.add(item, index); 703 this.createLocal(item); 704 visible = true; 705 } 706 this._notify(ZmEvent.E_CREATE, {items: [item], sortIndex: index, visible: visible}); 707 } 708 }; 709 710 /** 711 * Moves the items. 712 * 713 * @param {Array} items an array of {@link ZmContact} objects 714 * @param {String} folderId the folder id 715 */ 716 ZmContactList.prototype.moveLocal = 717 function(items, folderId) { 718 // don't remove any contacts from the canonical list 719 if (!this.isCanonical) 720 ZmList.prototype.moveLocal.call(this, items, folderId); 721 if (folderId == ZmFolder.ID_TRASH) { 722 for (var i = 0; i < items.length; i++) { 723 this._updateHashes(items[i], false); 724 } 725 } 726 }; 727 728 /** 729 * Deletes the items. 730 * 731 * @param {Array} items an array of {@link ZmContact} objects 732 */ 733 ZmContactList.prototype.deleteLocal = 734 function(items) { 735 ZmList.prototype.deleteLocal.call(this, items); 736 for (var i = 0; i < items.length; i++) { 737 this._updateHashes(items[i], false); 738 } 739 }; 740 741 /** 742 * Handle modified contact. 743 * 744 * @private 745 */ 746 ZmContactList.prototype.modifyLocal = 747 function(item, details) { 748 if (details) { 749 // notify item's list 750 this._evt.items = details.items = [item]; 751 this._evt.item = details.contact; //somehow this was set to something obsolete. What a mess. Also note that item is Object while details.contact is ZmContact 752 this._notify(ZmEvent.E_MODIFY, details); 753 } 754 755 var contact = details.contact; 756 if (this.isCanonical || contact.attr[ZmContact.F_email] != details.oldAttr[ZmContact.F_email]) { 757 // Remove traces of old contact - NOTE: we pass in null for the ID on 758 // PURPOSE to avoid overwriting the existing cached contact 759 var oldContact = new ZmContact(null, this); 760 oldContact.id = details.contact.id; 761 oldContact.attr = details.oldAttr; 762 this._updateHashes(oldContact, false); 763 764 // add new contact to hashes 765 this._updateHashes(contact, true); 766 } 767 768 // place in correct position in list 769 if (details.fileAsChanged) { 770 this.remove(contact); 771 var index = this._sortIndex(contact); 772 var listSize = this.size(); 773 if (index < listSize || listSize == 0 || (index == listSize && !this._hasMore)) { 774 this.add(contact, index); 775 } 776 } 777 778 // reset addrbook property 779 if (contact.addrbook && (contact.addrbook.id != contact.folderId)) { 780 contact.addrbook = appCtxt.getById(contact.folderId); 781 } 782 }; 783 784 /** 785 * Creates the item local. 786 * 787 * @param {ZmContact} item the item 788 */ 789 ZmContactList.prototype.createLocal = 790 function(item) { 791 this._updateHashes(item, true); 792 }; 793 794 /** 795 * @private 796 */ 797 ZmContactList.prototype._updateHashes = 798 function(contact, doAdd) { 799 800 this._app.updateCache(contact, doAdd); 801 802 // Update email hash. 803 for (var index = 0; index < ZmContact.EMAIL_FIELDS.length; index++) { 804 var field = ZmContact.EMAIL_FIELDS[index]; 805 for (var i = 1; true; i++) { 806 var aname = ZmContact.getAttributeName(field, i); 807 var avalue = ZmContact.getAttr(contact, aname); 808 if (!avalue) break; 809 if (doAdd) { 810 this._emailToContact[avalue.toLowerCase()] = contact; 811 } else { 812 delete this._emailToContact[avalue.toLowerCase()]; 813 } 814 } 815 } 816 817 // Update phone hash. 818 if (appCtxt.get(ZmSetting.VOICE_ENABLED) || this._alwaysUpdateHashes) { 819 for (var index = 0; index < ZmContact.PHONE_FIELDS.length; index++) { 820 var field = ZmContact.PHONE_FIELDS[index]; 821 for (var i = 1; true; i++) { 822 var aname = ZmContact.getAttributeName(field, i); 823 var avalue = ZmContact.getAttr(contact, aname); 824 if (!avalue) break; 825 var digits = this._getPhoneDigits(avalue); 826 if (digits) { 827 if (doAdd) { 828 this._phoneToContact[avalue] = {contact: contact, field: aname}; 829 } else { 830 delete this._phoneToContact[avalue]; 831 } 832 } 833 } 834 } 835 } 836 }; 837 838 /** 839 * Strips all non-digit characters from a phone number. 840 * 841 * @private 842 */ 843 ZmContactList.prototype._getPhoneDigits = 844 function(phone) { 845 return phone.replace(/[^\d]/g, ''); 846 }; 847 848 /** 849 * Returns the position at which the given contact should be inserted in this list. 850 * 851 * @private 852 */ 853 ZmContactList.prototype._sortIndex = 854 function(contact) { 855 var a = this._vector.getArray(); 856 for (var i = 0; i < a.length; i++) { 857 if (ZmContact.compareByFileAs(a[i], contact) > 0) { 858 return i; 859 } 860 } 861 return a.length; 862 }; 863 864 /** 865 * Gets the list ID hash 866 * @return idHash {Ojbect} list ID hash 867 */ 868 ZmContactList.prototype.getIdHash = 869 function() { 870 return this._idHash; 871 } 872 873 /** 874 * @private 875 */ 876 ZmContactList.prototype._handleResponseModifyItem = 877 function(item, result) { 878 // NOTE: we overload and do nothing b/c base class does more than we want 879 // (since everything is handled by notifications) 880 }; 881