Bug 1079941: implement LoopContacts.search to allow searching for contacts by query and use that to find out if a contact who's trying to call you is blocked. r=abr a=lmandel
authorMike de Boer <mdeboer@mozilla.com>
Thu, 16 Oct 2014 16:35:10 +0200
changeset 225738 bda95894a692
parent 225737 a4e22c4da890
child 225739 8c42ccaf8aa1
push id3995
push userrjesup@wgate.com
push date2014-10-20 00:58 +0000
treeherdermozilla-beta@8c42ccaf8aa1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersabr, lmandel
bugs1079941
milestone34.0
Bug 1079941: implement LoopContacts.search to allow searching for contacts by query and use that to find out if a contact who's trying to call you is blocked. r=abr a=lmandel
browser/components/loop/LoopContacts.jsm
browser/components/loop/MozLoopService.jsm
browser/components/loop/content/js/contacts.js
browser/components/loop/content/js/contacts.jsx
browser/components/loop/test/mochitest/browser_LoopContacts.js
--- a/browser/components/loop/LoopContacts.jsm
+++ b/browser/components/loop/LoopContacts.jsm
@@ -785,26 +785,95 @@ let LoopContactsInternal = Object.freeze
       return;
     }
     this._importServices[options.service].startImport(options, callback,
                                                       LoopContacts, windowRef);
   },
 
   /**
    * Search through the data store for contacts that match a certain (sub-)string.
+   * NB: The current implementation is very simple, naive if you will; we fetch
+   *     _all_ the contacts via `getAll()` and iterate over all of them to find
+   *     the contacts matching the supplied query (brute-force search in
+   *     exponential time).
    *
-   * @param {String}   query    Needle to search for in our haystack of contacts
+   * @param {Object}   query    Needle to search for in our haystack of contacts
    * @param {Function} callback Function that will be invoked once the operation
    *                            finished. The first argument passed will be an
    *                            `Error` object or `null`. The second argument will
    *                            be an `Array` of contact objects, if successfull.
+   *
+   * Example:
+   *   LoopContacts.search({
+   *     q: "foo@bar.com",
+   *     field: "email" // 'email' is the default.
+   *   }, function(err, contacts) {
+   *     if (err) {
+   *       throw err;
+   *     }
+   *     console.dir(contacts);
+   *   });
    */
   search: function(query, callback) {
-    //TODO in bug 1037114.
-    callback(new Error("Not implemented yet!"));
+    if (!("q" in query) || !query.q) {
+      callback(new Error("Nothing to search for. 'q' is required."));
+      return;
+    }
+    if (!("field" in query)) {
+      query.field = "email";
+    }
+    let queryValue = query.q;
+    if (query.field == "tel") {
+      queryValue = queryValue.replace(/[\D]+/g, "");
+    }
+
+    const checkForMatch = function(fieldValue) {
+      if (typeof fieldValue == "string") {
+        if (query.field == "tel") {
+          return fieldValue.replace(/[\D]+/g, "").endsWith(queryValue);
+        }
+        return fieldValue == queryValue;
+      }
+      if (typeof fieldValue == "number" || typeof fieldValue == "boolean") {
+        return fieldValue == queryValue;
+      }
+      if ("value" in fieldValue) {
+        return checkForMatch(fieldValue.value);
+      }
+      return false;
+    };
+
+    let foundContacts = [];
+    this.getAll((err, contacts) => {
+      if (err) {
+        callback(err);
+        return;
+      }
+
+      for (let contact of contacts) {
+        let matchWith = contact[query.field];
+        if (!matchWith) {
+          continue;
+        }
+
+        // Many fields are defined as Arrays.
+        if (Array.isArray(matchWith)) {
+          for (let fieldValue of matchWith) {
+            if (checkForMatch(fieldValue)) {
+              foundContacts.push(contact);
+              break;
+            }
+          }
+        } else if (checkForMatch(matchWith)) {
+          foundContacts.push(contact);
+        }
+      }
+
+      callback(null, foundContacts);
+    });
   }
 });
 
 /**
  * Public Loop Contacts API.
  *
  * LoopContacts implements the EventEmitter interface by exposing three methods -
  * `on`, `once` and `off` - to subscribe to events.
--- a/browser/components/loop/MozLoopService.jsm
+++ b/browser/components/loop/MozLoopService.jsm
@@ -18,16 +18,18 @@ const MAX_SOFT_START_TICKET_NUMBER = 167
 const LOOP_SESSION_TYPE = {
   GUEST: 1,
   FXA: 2,
 };
 
 // See LOG_LEVELS in Console.jsm. Common examples: "All", "Info", "Warn", & "Error".
 const PREF_LOG_LEVEL = "loop.debug.loglevel";
 
+const EMAIL_OR_PHONE_RE = /^(:?\S+@\S+|\+\d+)$/;
+
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
 Cu.import("resource://gre/modules/osfile.jsm", this);
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/FxAccountsOAuthClient.jsm");
 Cu.importGlobalProperties(["URL"]);
 
@@ -51,16 +53,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/FxAccountsProfileClient.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "HawkClient",
                                   "resource://services-common/hawkclient.js");
 
 XPCOMUtils.defineLazyModuleGetter(this, "deriveHawkCredentials",
                                   "resource://services-common/hawkrequest.js");
 
+XPCOMUtils.defineLazyModuleGetter(this, "LoopContacts",
+                                  "resource:///modules/loop/LoopContacts.jsm");
+
 XPCOMUtils.defineLazyModuleGetter(this, "LoopStorage",
                                   "resource:///modules/loop/LoopStorage.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "MozLoopPushHandler",
                                   "resource:///modules/loop/MozLoopPushHandler.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
                                    "@mozilla.org/uuid-generator;1",
@@ -781,27 +786,56 @@ let MozLoopServiceInternal = {
       log.warn("Error parsing calls info", err);
     }
   },
 
   /**
    * Starts a call, saves the call data, and opens a chat window.
    *
    * @param {Object} callData The data associated with the call including an id.
-   * @param {Boolean} conversationType Whether or not the call is "incoming"
-   *                                   or "outgoing"
+   * @param {String} conversationType Whether or not the call is "incoming"
+   *                                  or "outgoing"
    */
   _startCall: function(callData, conversationType) {
-    this.callsData.inUse = true;
-    this.callsData.data = callData;
-    this.openChatWindow(
-      null,
-      // No title, let the page set that, to avoid flickering.
-      "",
-      "about:loopconversation#" + conversationType + "/" + callData.callId);
+    const openChat = () => {
+      this.callsData.inUse = true;
+      this.callsData.data = callData;
+
+      this.openChatWindow(
+        null,
+        // No title, let the page set that, to avoid flickering.
+        "",
+        "about:loopconversation#" + conversationType + "/" + callData.callId);
+    };
+
+    if (conversationType == "incoming" && ("callerId" in callData) &&
+        EMAIL_OR_PHONE_RE.test(callData.callerId)) {
+      LoopContacts.search({
+        q: callData.callerId,
+        field: callData.callerId.contains("@") ? "email" : "tel"
+      }, (err, contacts) => {
+        if (err) {
+          // Database error, helas!
+          openChat();
+          return;
+        }
+
+        for (let contact of contacts) {
+          if (contact.blocked) {
+            // Blocked! Send a busy signal back to the caller.
+            this._returnBusy(callData);
+            return;
+          }
+        }
+
+        openChat();
+      })
+    } else {
+      openChat();
+    }
   },
 
   /**
    * Starts a direct call to the contact addresses.
    *
    * @param {Object} contact The contact to call
    * @param {String} callType The type of call, e.g. "audio-video" or "audio-only"
    * @return true if the call is opened, false if it is not opened (i.e. busy)
--- a/browser/components/loop/content/js/contacts.js
+++ b/browser/components/loop/content/js/contacts.js
@@ -81,22 +81,25 @@ loop.contacts = (function(_, mozL10n) {
 
       let blockAction = this.props.blocked ? "unblock" : "block";
       let blockLabel = this.props.blocked ? "unblock_contact_menu_button"
                                           : "block_contact_menu_button";
 
       return (
         React.DOM.ul({className: cx({ "dropdown-menu": true,
                             "dropdown-menu-up": this.state.openDirUp })}, 
-          React.DOM.li({className: cx({ "dropdown-menu-item": true }), 
-              onClick: this.onItemClick, 'data-action': "video-call"}, 
+          React.DOM.li({className: cx({ "dropdown-menu-item": true,
+                              "disabled": this.props.blocked }), 
+              onClick: this.onItemClick, 
+              'data-action': "video-call"}, 
             React.DOM.i({className: "icon icon-video-call"}), 
             mozL10n.get("video_call_menu_button")
           ), 
-          React.DOM.li({className: cx({ "dropdown-menu-item": true }), 
+          React.DOM.li({className: cx({ "dropdown-menu-item": true,
+                              "disabled": this.props.blocked }), 
               onClick: this.onItemClick, 'data-action': "audio-call"}, 
             React.DOM.i({className: "icon icon-audio-call"}), 
             mozL10n.get("audio_call_menu_button")
           ), 
           React.DOM.li({className: cx({ "dropdown-menu-item": true,
                               "disabled": !this.props.canEdit }), 
               onClick: this.onItemClick, 'data-action': "edit"}, 
             React.DOM.i({className: "icon icon-edit"}), 
@@ -383,20 +386,24 @@ loop.contacts = (function(_, mozL10n) {
           // Invoke the API named like the action.
           navigator.mozLoop.contacts[actionName](contact._guid, err => {
             if (err) {
               throw err;
             }
           });
           break;
         case "video-call":
-          navigator.mozLoop.startDirectCall(contact, CALL_TYPES.AUDIO_VIDEO);
+          if (!contact.blocked) {
+            navigator.mozLoop.startDirectCall(contact, CALL_TYPES.AUDIO_VIDEO);
+          }
           break;
         case "audio-call":
-          navigator.mozLoop.startDirectCall(contact, CALL_TYPES.AUDIO_ONLY);
+          if (!contact.blocked) {
+            navigator.mozLoop.startDirectCall(contact, CALL_TYPES.AUDIO_ONLY);
+          }
           break;
         default:
           console.error("Unrecognized action: " + actionName);
           break;
       }
     },
 
     sortContacts: function(contact1, contact2) {
--- a/browser/components/loop/content/js/contacts.jsx
+++ b/browser/components/loop/content/js/contacts.jsx
@@ -81,22 +81,25 @@ loop.contacts = (function(_, mozL10n) {
 
       let blockAction = this.props.blocked ? "unblock" : "block";
       let blockLabel = this.props.blocked ? "unblock_contact_menu_button"
                                           : "block_contact_menu_button";
 
       return (
         <ul className={cx({ "dropdown-menu": true,
                             "dropdown-menu-up": this.state.openDirUp })}>
-          <li className={cx({ "dropdown-menu-item": true })}
-              onClick={this.onItemClick} data-action="video-call">
+          <li className={cx({ "dropdown-menu-item": true,
+                              "disabled": this.props.blocked })}
+              onClick={this.onItemClick}
+              data-action="video-call">
             <i className="icon icon-video-call" />
             {mozL10n.get("video_call_menu_button")}
           </li>
-          <li className={cx({ "dropdown-menu-item": true })}
+          <li className={cx({ "dropdown-menu-item": true,
+                              "disabled": this.props.blocked })}
               onClick={this.onItemClick} data-action="audio-call">
             <i className="icon icon-audio-call" />
             {mozL10n.get("audio_call_menu_button")}
           </li>
           <li className={cx({ "dropdown-menu-item": true,
                               "disabled": !this.props.canEdit })}
               onClick={this.onItemClick} data-action="edit">
             <i className="icon icon-edit" />
@@ -383,20 +386,24 @@ loop.contacts = (function(_, mozL10n) {
           // Invoke the API named like the action.
           navigator.mozLoop.contacts[actionName](contact._guid, err => {
             if (err) {
               throw err;
             }
           });
           break;
         case "video-call":
-          navigator.mozLoop.startDirectCall(contact, CALL_TYPES.AUDIO_VIDEO);
+          if (!contact.blocked) {
+            navigator.mozLoop.startDirectCall(contact, CALL_TYPES.AUDIO_VIDEO);
+          }
           break;
         case "audio-call":
-          navigator.mozLoop.startDirectCall(contact, CALL_TYPES.AUDIO_ONLY);
+          if (!contact.blocked) {
+            navigator.mozLoop.startDirectCall(contact, CALL_TYPES.AUDIO_ONLY);
+          }
           break;
         default:
           console.error("Unrecognized action: " + actionName);
           break;
       }
     },
 
     sortContacts: function(contact1, contact2) {
--- a/browser/components/loop/test/mochitest/browser_LoopContacts.js
+++ b/browser/components/loop/test/mochitest/browser_LoopContacts.js
@@ -11,27 +11,37 @@ XPCOMUtils.defineLazyServiceGetter(this,
 const kContacts = [{
   id: 1,
   name: ["Ally Avocado"],
   email: [{
     "pref": true,
     "type": ["work"],
     "value": "ally@mail.com"
   }],
+  tel: [{
+    "pref": true,
+    "type": ["mobile"],
+    "value": "+31-6-12345678"
+  }],
   category: ["google"],
   published: 1406798311748,
   updated: 1406798311748
 },{
   id: 2,
   name: ["Bob Banana"],
   email: [{
     "pref": true,
     "type": ["work"],
     "value": "bob@gmail.com"
   }],
+  tel: [{
+    "pref": true,
+    "type": ["mobile"],
+    "value": "+1-214-5551234"
+  }],
   category: ["local"],
   published: 1406798311748,
   updated: 1406798311748
 }, {
   id: 3,
   name: ["Caitlin Cantaloupe"],
   email: [{
     "pref": true,
@@ -420,16 +430,55 @@ add_task(function* () {
   let contacts = yield promiseLoadContacts();
   for (let i = 0, l = contacts.length; i < l; ++i) {
     compareContacts(contacts[i], kContacts[i]);
   }
 
   LoopStorage.switchDatabase();
   Assert.equal(LoopStorage.databaseName, "default", "The active partition should have changed");
 
-  LoopContacts.getAll(function(err, contacts) {
-    Assert.equal(err, null, "There shouldn't be an error");
+  contacts = yield LoopContacts.promise("getAll");
+  for (let i = 0, l = contacts.length; i < l; ++i) {
+    compareContacts(contacts[i], kContacts[i]);
+  }
+});
+
+// Test searching for contacts.
+add_task(function* () {
+  yield promiseLoadContacts();
+
+  let contacts = yield LoopContacts.promise("search", {
+    q: "bob@gmail.com"
+  });
+  Assert.equal(contacts.length, 1, "There should be one contact found");
+  compareContacts(contacts[0], kContacts[1]);
+
+  // Test searching by name.
+  contacts = yield LoopContacts.promise("search", {
+    q: "Ally Avocado",
+    field: "name"
+  });
+  Assert.equal(contacts.length, 1, "There should be one contact found");
+  compareContacts(contacts[0], kContacts[0]);
 
-    for (let i = 0, l = contacts.length; i < l; ++i) {
-      compareContacts(contacts[i], kContacts[i]);
-    }
+  // Test searching for multiple contacts.
+  contacts = yield LoopContacts.promise("search", {
+    q: "google",
+    field: "category"
+  });
+  Assert.equal(contacts.length, 2, "There should be two contacts found");
+
+  // Test searching for telephone numbers.
+  contacts = yield LoopContacts.promise("search", {
+    q: "+31612345678",
+    field: "tel"
   });
+  Assert.equal(contacts.length, 1, "There should be one contact found");
+  compareContacts(contacts[0], kContacts[0]);
+
+  // Test searching for telephone numbers without prefixes.
+  contacts = yield LoopContacts.promise("search", {
+    q: "5551234",
+    field: "tel"
+  });
+  Assert.equal(contacts.length, 1, "There should be one contact found");
+  compareContacts(contacts[0], kContacts[1]);
 });