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/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]);
});