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

This commit is contained in:
Mike de Boer 2014-10-16 16:35:10 +02:00
Родитель 8d32ac3d02
Коммит 7df061c788
5 изменённых файлов: 195 добавлений и 29 удалений

Просмотреть файл

@ -790,16 +790,85 @@ let LoopContactsInternal = Object.freeze({
/** /**
* Search through the data store for contacts that match a certain (sub-)string. * 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 * @param {Function} callback Function that will be invoked once the operation
* finished. The first argument passed will be an * finished. The first argument passed will be an
* `Error` object or `null`. The second argument will * `Error` object or `null`. The second argument will
* be an `Array` of contact objects, if successfull. * 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) { search: function(query, callback) {
//TODO in bug 1037114. if (!("q" in query) || !query.q) {
callback(new Error("Not implemented yet!")); 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);
});
} }
}); });

Просмотреть файл

@ -23,6 +23,8 @@ const LOOP_SESSION_TYPE = {
// See LOG_LEVELS in Console.jsm. Common examples: "All", "Info", "Warn", & "Error". // See LOG_LEVELS in Console.jsm. Common examples: "All", "Info", "Warn", & "Error".
const PREF_LOG_LEVEL = "loop.debug.loglevel"; 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/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Promise.jsm"); Cu.import("resource://gre/modules/Promise.jsm");
@ -56,6 +58,9 @@ XPCOMUtils.defineLazyModuleGetter(this, "HawkClient",
XPCOMUtils.defineLazyModuleGetter(this, "deriveHawkCredentials", XPCOMUtils.defineLazyModuleGetter(this, "deriveHawkCredentials",
"resource://services-common/hawkrequest.js"); "resource://services-common/hawkrequest.js");
XPCOMUtils.defineLazyModuleGetter(this, "LoopContacts",
"resource:///modules/loop/LoopContacts.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "LoopStorage", XPCOMUtils.defineLazyModuleGetter(this, "LoopStorage",
"resource:///modules/loop/LoopStorage.jsm"); "resource:///modules/loop/LoopStorage.jsm");
@ -786,17 +791,46 @@ let MozLoopServiceInternal = {
* Starts a call, saves the call data, and opens a chat window. * 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 {Object} callData The data associated with the call including an id.
* @param {Boolean} conversationType Whether or not the call is "incoming" * @param {String} conversationType Whether or not the call is "incoming"
* or "outgoing" * or "outgoing"
*/ */
_startCall: function(callData, conversationType) { _startCall: function(callData, conversationType) {
const openChat = () => {
this.callsData.inUse = true; this.callsData.inUse = true;
this.callsData.data = callData; this.callsData.data = callData;
this.openChatWindow( this.openChatWindow(
null, null,
// No title, let the page set that, to avoid flickering. // No title, let the page set that, to avoid flickering.
"", "",
"about:loopconversation#" + conversationType + "/" + callData.callId); "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();
}
}, },
/** /**

Просмотреть файл

@ -86,12 +86,15 @@ loop.contacts = (function(_, mozL10n) {
return ( return (
React.DOM.ul({className: cx({ "dropdown-menu": true, React.DOM.ul({className: cx({ "dropdown-menu": true,
"dropdown-menu-up": this.state.openDirUp })}, "dropdown-menu-up": this.state.openDirUp })},
React.DOM.li({className: cx({ "dropdown-menu-item": true }), React.DOM.li({className: cx({ "dropdown-menu-item": true,
onClick: this.onItemClick, 'data-action': "video-call"}, "disabled": this.props.blocked }),
onClick: this.onItemClick,
'data-action': "video-call"},
React.DOM.i({className: "icon icon-video-call"}), React.DOM.i({className: "icon icon-video-call"}),
mozL10n.get("video_call_menu_button") 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"}, onClick: this.onItemClick, 'data-action': "audio-call"},
React.DOM.i({className: "icon icon-audio-call"}), React.DOM.i({className: "icon icon-audio-call"}),
mozL10n.get("audio_call_menu_button") mozL10n.get("audio_call_menu_button")
@ -388,10 +391,14 @@ loop.contacts = (function(_, mozL10n) {
}); });
break; break;
case "video-call": case "video-call":
if (!contact.blocked) {
navigator.mozLoop.startDirectCall(contact, CALL_TYPES.AUDIO_VIDEO); navigator.mozLoop.startDirectCall(contact, CALL_TYPES.AUDIO_VIDEO);
}
break; break;
case "audio-call": case "audio-call":
if (!contact.blocked) {
navigator.mozLoop.startDirectCall(contact, CALL_TYPES.AUDIO_ONLY); navigator.mozLoop.startDirectCall(contact, CALL_TYPES.AUDIO_ONLY);
}
break; break;
default: default:
console.error("Unrecognized action: " + actionName); console.error("Unrecognized action: " + actionName);

Просмотреть файл

@ -86,12 +86,15 @@ loop.contacts = (function(_, mozL10n) {
return ( return (
<ul className={cx({ "dropdown-menu": true, <ul className={cx({ "dropdown-menu": true,
"dropdown-menu-up": this.state.openDirUp })}> "dropdown-menu-up": this.state.openDirUp })}>
<li className={cx({ "dropdown-menu-item": true })} <li className={cx({ "dropdown-menu-item": true,
onClick={this.onItemClick} data-action="video-call"> "disabled": this.props.blocked })}
onClick={this.onItemClick}
data-action="video-call">
<i className="icon icon-video-call" /> <i className="icon icon-video-call" />
{mozL10n.get("video_call_menu_button")} {mozL10n.get("video_call_menu_button")}
</li> </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"> onClick={this.onItemClick} data-action="audio-call">
<i className="icon icon-audio-call" /> <i className="icon icon-audio-call" />
{mozL10n.get("audio_call_menu_button")} {mozL10n.get("audio_call_menu_button")}
@ -388,10 +391,14 @@ loop.contacts = (function(_, mozL10n) {
}); });
break; break;
case "video-call": case "video-call":
if (!contact.blocked) {
navigator.mozLoop.startDirectCall(contact, CALL_TYPES.AUDIO_VIDEO); navigator.mozLoop.startDirectCall(contact, CALL_TYPES.AUDIO_VIDEO);
}
break; break;
case "audio-call": case "audio-call":
if (!contact.blocked) {
navigator.mozLoop.startDirectCall(contact, CALL_TYPES.AUDIO_ONLY); navigator.mozLoop.startDirectCall(contact, CALL_TYPES.AUDIO_ONLY);
}
break; break;
default: default:
console.error("Unrecognized action: " + actionName); console.error("Unrecognized action: " + actionName);

Просмотреть файл

@ -16,6 +16,11 @@ const kContacts = [{
"type": ["work"], "type": ["work"],
"value": "ally@mail.com" "value": "ally@mail.com"
}], }],
tel: [{
"pref": true,
"type": ["mobile"],
"value": "+31-6-12345678"
}],
category: ["google"], category: ["google"],
published: 1406798311748, published: 1406798311748,
updated: 1406798311748 updated: 1406798311748
@ -27,6 +32,11 @@ const kContacts = [{
"type": ["work"], "type": ["work"],
"value": "bob@gmail.com" "value": "bob@gmail.com"
}], }],
tel: [{
"pref": true,
"type": ["mobile"],
"value": "+1-214-5551234"
}],
category: ["local"], category: ["local"],
published: 1406798311748, published: 1406798311748,
updated: 1406798311748 updated: 1406798311748
@ -425,11 +435,50 @@ add_task(function* () {
LoopStorage.switchDatabase(); LoopStorage.switchDatabase();
Assert.equal(LoopStorage.databaseName, "default", "The active partition should have changed"); Assert.equal(LoopStorage.databaseName, "default", "The active partition should have changed");
LoopContacts.getAll(function(err, contacts) { contacts = yield LoopContacts.promise("getAll");
Assert.equal(err, null, "There shouldn't be an error");
for (let i = 0, l = contacts.length; i < l; ++i) { for (let i = 0, l = contacts.length; i < l; ++i) {
compareContacts(contacts[i], kContacts[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]);
// 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]);
}); });