diff options
Diffstat (limited to 'comm/mail/components/addrbook/content/abView-new.js')
-rw-r--r-- | comm/mail/components/addrbook/content/abView-new.js | 577 |
1 files changed, 577 insertions, 0 deletions
diff --git a/comm/mail/components/addrbook/content/abView-new.js b/comm/mail/components/addrbook/content/abView-new.js new file mode 100644 index 0000000000..cb3eca969c --- /dev/null +++ b/comm/mail/components/addrbook/content/abView-new.js @@ -0,0 +1,577 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* globals PROTO_TREE_VIEW */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +function ABView( + directory, + searchQuery, + searchString, + sortColumn, + sortDirection +) { + this.__proto__.__proto__ = new PROTO_TREE_VIEW(); + this.directory = directory; + this.searchString = searchString; + + let directories = directory ? [directory] : MailServices.ab.directories; + if (searchQuery) { + this._searchesInProgress = directories.length; + searchQuery = searchQuery.replace(/^\?+/, ""); + for (let dir of directories) { + dir.search(searchQuery, searchString, this); + } + } else { + for (let dir of directories) { + for (let card of dir.childCards) { + this._rowMap.push(new abViewCard(card, dir)); + } + } + } + this.sortBy(sortColumn, sortDirection); +} +ABView.nameFormat = Services.prefs.getIntPref( + "mail.addr_book.lastnamefirst", + 0 +); +ABView.NOT_SEARCHING = 0; +ABView.SEARCHING = 1; +ABView.SEARCH_COMPLETE = 2; +ABView.prototype = { + QueryInterface: ChromeUtils.generateQI([ + "nsITreeView", + "nsIAbDirSearchListener", + "nsIObserver", + "nsISupportsWeakReference", + ]), + + directory: null, + _notifications: [ + "addrbook-directory-deleted", + "addrbook-directory-invalidated", + "addrbook-contact-created", + "addrbook-contact-updated", + "addrbook-contact-deleted", + "addrbook-list-created", + "addrbook-list-updated", + "addrbook-list-deleted", + "addrbook-list-member-added", + "addrbook-list-member-removed", + ], + + sortColumn: "", + sortDirection: "", + collator: new Intl.Collator(undefined, { numeric: true }), + + deleteSelectedCards() { + let directoryMap = new Map(); + for (let i of this._tree.selectedIndices) { + let card = this.getCardFromRow(i); + let cardSet = directoryMap.get(card.directoryUID); + if (!cardSet) { + cardSet = new Set(); + directoryMap.set(card.directoryUID, cardSet); + } + cardSet.add(card); + } + + for (let [directoryUID, cardSet] of directoryMap) { + let directory; + if (this.directory && this.directory.isMailList) { + // Removes cards from the list instead of deleting them. + directory = this.directory; + } else { + directory = MailServices.ab.getDirectoryFromUID(directoryUID); + } + + cardSet = [...cardSet]; + directory.deleteCards(cardSet.filter(card => !card.isMailList)); + for (let card of cardSet.filter(card => card.isMailList)) { + MailServices.ab.deleteAddressBook(card.mailListURI); + } + } + }, + getCardFromRow(row) { + return this._rowMap[row] ? this._rowMap[row].card : null; + }, + getDirectoryFromRow(row) { + return this._rowMap[row] ? this._rowMap[row].directory : null; + }, + getIndexForUID(uid) { + return this._rowMap.findIndex(row => row.id == uid); + }, + sortBy(sortColumn, sortDirection, resort) { + let selectionExists = false; + if (this._tree) { + let { selectedIndices, currentIndex } = this._tree; + selectionExists = selectedIndices.length; + // Remember what was selected. + for (let i = 0; i < this._rowMap.length; i++) { + this._rowMap[i].wasSelected = selectedIndices.includes(i); + this._rowMap[i].wasCurrent = currentIndex == i; + } + } + + // Do the sort. + if (sortColumn == this.sortColumn && !resort) { + if (sortDirection == this.sortDirection) { + return; + } + this._rowMap.reverse(); + } else { + this._rowMap.sort((a, b) => { + let aText = a.getText(sortColumn); + let bText = b.getText(sortColumn); + if (sortDirection == "descending") { + return this.collator.compare(bText, aText); + } + return this.collator.compare(aText, bText); + }); + } + + // Restore what was selected. + if (this._tree) { + this._tree.reset(); + if (selectionExists) { + for (let i = 0; i < this._rowMap.length; i++) { + this._tree.toggleSelectionAtIndex( + i, + this._rowMap[i].wasSelected, + true + ); + } + // Can't do this until updating the selection is finished. + for (let i = 0; i < this._rowMap.length; i++) { + if (this._rowMap[i].wasCurrent) { + this._tree.currentIndex = i; + break; + } + } + this.selectionChanged(); + } + } + this.sortColumn = sortColumn; + this.sortDirection = sortDirection; + }, + get searchState() { + if (this._searchesInProgress === undefined) { + return ABView.NOT_SEARCHING; + } + return this._searchesInProgress ? ABView.SEARCHING : ABView.SEARCH_COMPLETE; + }, + + // nsITreeView + + selectionChanged() {}, + setTree(tree) { + this._tree = tree; + for (let topic of this._notifications) { + if (tree) { + Services.obs.addObserver(this, topic, true); + } else { + try { + Services.obs.removeObserver(this, topic); + } catch (ex) { + // `this` might not be a valid observer. + } + } + } + Services.prefs.addObserver("mail.addr_book.lastnamefirst", this, true); + }, + + // nsIAbDirSearchListener + + onSearchFoundCard(card) { + // Instead of duplicating the insertion code below, just call it. + this.observe(card, "addrbook-contact-created", this.directory?.UID); + }, + onSearchFinished(status, complete, secInfo, location) { + // Special handling for Bad Cert errors. + let offerCertException = false; + try { + // If code is not an NSS error, getErrorClass() will fail. + let nssErrorsService = Cc["@mozilla.org/nss_errors_service;1"].getService( + Ci.nsINSSErrorsService + ); + let errorClass = nssErrorsService.getErrorClass(status); + if (errorClass == Ci.nsINSSErrorsService.ERROR_CLASS_BAD_CERT) { + offerCertException = true; + } + } catch (ex) {} + + if (offerCertException) { + // Give the user the option of adding an exception for the bad cert. + let params = { + exceptionAdded: false, + securityInfo: secInfo, + prefetchCert: true, + location, + }; + window.browsingContext.topChromeWindow.openDialog( + "chrome://pippki/content/exceptionDialog.xhtml", + "", + "chrome,centerscreen,modal", + params + ); + // params.exceptionAdded will be set if the user added an exception. + } + + this._searchesInProgress--; + if (!this._searchesInProgress && this._tree) { + this._tree.dispatchEvent(new CustomEvent("searchstatechange")); + } + }, + + // nsIObserver + + observe(subject, topic, data) { + if (topic == "nsPref:changed") { + ABView.nameFormat = Services.prefs.getIntPref( + "mail.addr_book.lastnamefirst", + 0 + ); + for (let card of this._rowMap) { + delete card._getTextCache.GeneratedName; + } + if (this._tree) { + if (this.sortColumn == "GeneratedName") { + this.sortBy(this.sortColumn, this.sortDirection, true); + } else { + // Remember what was selected. + let { selectedIndices, currentIndex } = this._tree; + for (let i = 0; i < this._rowMap.length; i++) { + this._rowMap[i].wasSelected = selectedIndices.includes(i); + this._rowMap[i].wasCurrent = currentIndex == i; + } + + this._tree.reset(); + for (let i = 0; i < this._rowMap.length; i++) { + this._tree.toggleSelectionAtIndex( + i, + this._rowMap[i].wasSelected, + true + ); + } + // Can't do this until updating the selection is finished. + for (let i = 0; i < this._rowMap.length; i++) { + if (this._rowMap[i].wasCurrent) { + this._tree.currentIndex = i; + break; + } + } + } + } + return; + } + + if (this.directory && data && this.directory.UID != data) { + return; + } + + // If we make it here, we're in the root directory, or the right directory. + + switch (topic) { + case "addrbook-directory-deleted": { + if (this.directory) { + break; + } + + subject.QueryInterface(Ci.nsIAbDirectory); + let scrollPosition = this._tree?.getFirstVisibleIndex(); + for (let i = this._rowMap.length - 1; i >= 0; i--) { + if (this._rowMap[i].directory.UID == subject.UID) { + this._rowMap.splice(i, 1); + if (this._tree) { + this._tree.rowCountChanged(i, -1); + } + } + } + if (this._tree && scrollPosition !== null) { + this._tree.scrollToIndex(scrollPosition); + } + break; + } + case "addrbook-directory-invalidated": + subject.QueryInterface(Ci.nsIAbDirectory); + if (subject == this.directory) { + this._rowMap.length = 0; + for (let card of this.directory.childCards) { + this._rowMap.push(new abViewCard(card, this.directory)); + } + this.sortBy(this.sortColumn, this.sortDirection, true); + } + break; + case "addrbook-list-created": { + let parentDir = MailServices.ab.getDirectoryFromUID(data); + // `subject` is an nsIAbDirectory, make it the matching card instead. + subject.QueryInterface(Ci.nsIAbDirectory); + for (let card of parentDir.childCards) { + if (card.UID == subject.UID) { + subject = card; + break; + } + } + } + // Falls through. + case "addrbook-list-member-added": + case "addrbook-contact-created": + if (topic == "addrbook-list-member-added" && !this.directory) { + break; + } + + subject.QueryInterface(Ci.nsIAbCard); + let viewCard = new abViewCard(subject); + let sortText = viewCard.getText(this.sortColumn); + let addIndex = null; + for (let i = 0; addIndex === null && i < this._rowMap.length; i++) { + let comparison = this.collator.compare( + sortText, + this._rowMap[i].getText(this.sortColumn) + ); + if ( + (comparison < 0 && this.sortDirection == "ascending") || + (comparison >= 0 && this.sortDirection == "descending") + ) { + addIndex = i; + } + } + if (addIndex === null) { + addIndex = this._rowMap.length; + } + this._rowMap.splice(addIndex, 0, viewCard); + if (this._tree) { + this._tree.rowCountChanged(addIndex, 1); + } + break; + + case "addrbook-list-updated": { + let parentDir = this.directory; + if (!parentDir) { + parentDir = MailServices.ab.getDirectoryFromUID(data); + } + // `subject` is an nsIAbDirectory, make it the matching card instead. + subject.QueryInterface(Ci.nsIAbDirectory); + for (let card of parentDir.childCards) { + if (card.UID == subject.UID) { + subject = card; + break; + } + } + } + // Falls through. + case "addrbook-contact-updated": { + subject.QueryInterface(Ci.nsIAbCard); + let needsSort = false; + for (let i = this._rowMap.length - 1; i >= 0; i--) { + if ( + this._rowMap[i].card.equals(subject) && + this._rowMap[i].card.directoryUID == subject.directoryUID + ) { + this._rowMap.splice(i, 1, new abViewCard(subject)); + needsSort = true; + } + } + if (needsSort) { + this.sortBy(this.sortColumn, this.sortDirection, true); + } + break; + } + + case "addrbook-list-deleted": { + subject.QueryInterface(Ci.nsIAbDirectory); + let scrollPosition = this._tree?.getFirstVisibleIndex(); + for (let i = this._rowMap.length - 1; i >= 0; i--) { + if (this._rowMap[i].card.UID == subject.UID) { + this._rowMap.splice(i, 1); + if (this._tree) { + this._tree.rowCountChanged(i, -1); + } + } + } + if (this._tree && scrollPosition !== null) { + this._tree.scrollToIndex(scrollPosition); + } + break; + } + case "addrbook-list-member-removed": + if (!this.directory) { + break; + } + // Falls through. + case "addrbook-contact-deleted": { + subject.QueryInterface(Ci.nsIAbCard); + let scrollPosition = this._tree?.getFirstVisibleIndex(); + for (let i = this._rowMap.length - 1; i >= 0; i--) { + if ( + this._rowMap[i].card.equals(subject) && + this._rowMap[i].card.directoryUID == subject.directoryUID + ) { + this._rowMap.splice(i, 1); + if (this._tree) { + this._tree.rowCountChanged(i, -1); + } + } + } + if (this._tree && scrollPosition !== null) { + this._tree.scrollToIndex(scrollPosition); + } + break; + } + } + }, +}; + +/** + * Representation of a card, used as a table row in ABView. + * + * @param {nsIAbCard} card - contact or mailing list card for this row. + * @param {nsIAbDirectory} [directoryHint] - the directory containing card, + * if available (this is a performance optimization only). + */ +function abViewCard(card, directoryHint) { + this.card = card; + this._getTextCache = {}; + if (directoryHint) { + this._directory = directoryHint; + } else { + this._directory = MailServices.ab.getDirectoryFromUID( + this.card.directoryUID + ); + } +} +abViewCard.listFormatter = new Services.intl.ListFormat( + Services.appinfo.name == "xpcshell" ? "en-US" : undefined, + { type: "unit" } +); +abViewCard.prototype = { + _getText(columnID) { + try { + let { getProperty, supportsVCard, vCardProperties } = this.card; + + if (this.card.isMailList) { + if (columnID == "GeneratedName") { + return this.card.displayName; + } + if (["NickName", "Notes"].includes(columnID)) { + return getProperty(columnID, ""); + } + if (columnID == "addrbook") { + return MailServices.ab.getDirectoryFromUID(this.card.directoryUID) + .dirName; + } + return ""; + } + + switch (columnID) { + case "addrbook": + return this._directory.dirName; + case "GeneratedName": + return this.card.generateName(ABView.nameFormat); + case "EmailAddresses": + return abViewCard.listFormatter.format(this.card.emailAddresses); + case "PhoneNumbers": { + let phoneNumbers; + if (supportsVCard) { + phoneNumbers = vCardProperties.getAllValues("tel"); + } else { + phoneNumbers = [ + getProperty("WorkPhone", ""), + getProperty("HomePhone", ""), + getProperty("CellularNumber", ""), + getProperty("FaxNumber", ""), + getProperty("PagerNumber", ""), + ]; + } + return abViewCard.listFormatter.format(phoneNumbers.filter(Boolean)); + } + case "Addresses": { + let addresses; + if (supportsVCard) { + addresses = vCardProperties + .getAllValues("adr") + .map(v => v.join(" ").trim()); + } else { + addresses = [ + this.formatAddress("Work"), + this.formatAddress("Home"), + ]; + } + return abViewCard.listFormatter.format(addresses.filter(Boolean)); + } + case "JobTitle": + case "Title": + if (supportsVCard) { + return vCardProperties.getFirstValue("title"); + } + return getProperty("JobTitle", ""); + case "Department": + if (supportsVCard) { + let vCardValue = vCardProperties.getFirstValue("org"); + if (Array.isArray(vCardValue)) { + return vCardValue[1] || ""; + } + return ""; + } + return getProperty(columnID, ""); + case "Company": + case "Organization": + if (supportsVCard) { + let vCardValue = vCardProperties.getFirstValue("org"); + if (Array.isArray(vCardValue)) { + return vCardValue[0] || ""; + } + return vCardValue; + } + return getProperty("Company", ""); + default: + return getProperty(columnID, ""); + } + } catch (ex) { + return ""; + } + }, + getText(columnID) { + if (!(columnID in this._getTextCache)) { + this._getTextCache[columnID] = this._getText(columnID)?.trim() ?? ""; + } + return this._getTextCache[columnID]; + }, + get id() { + return this.card.UID; + }, + get open() { + return false; + }, + get level() { + return 0; + }, + get children() { + return []; + }, + getProperties() { + return ""; + }, + get directory() { + return this._directory; + }, + + /** + * Creates a string representation of an address from card properties. + * + * @param {"Work"|"Home"} prefix + * @returns {string} + */ + formatAddress(prefix) { + return Array.from( + ["Address", "Address2", "City", "State", "ZipCode", "Country"], + field => this.card.getProperty(`${prefix}${field}`, "") + ) + .join(" ") + .trim(); + }, +}; |