auto-complete on tags and constrain by them.
auto-complete on tags and constrain by them.
--- a/client/autocomplete.xml
+++ b/client/autocomplete.xml
@@ -19,28 +19,39 @@
<xbl:implementation><![CDATA[
({
SELECTED_CLASS: "auco_selected",
_autocompleters: [],
addCompleter: function (aCompleter) {
this._autocompleters.push(aCompleter);
},
_serial: 0,
+ resultsByType: null,
goFish: function (aText) {
var serial = this._serial++;
+ this.resultsByType = {};
this._autocompleters.forEach(function (completer) {
completer.complete(this, aText);
}, this);
},
- haveSomeResults: function(aText, aNodes) {
+ haveSomeResults: function(aText, aNodes, aCompleter, aPriority) {
// bail if this is not the reslt for what is currently typed
if (aText != this._entry.value)
return;
+ this.resultsByType[aCompleter.type] = [aPriority, aNodes];
+ var nodes = [];
+ var prioritizedResults = [];
+ for each (var tupe in this.resultsByType)
+ prioritizedResults.push(tupe);
+ prioritizedResults.sort(function (a,b) { return b[0] - a[0]; });
+ for (var i = 0; i < prioritizedResults.length; i++)
+ nodes = nodes.concat(prioritizedResults[i][1]);
+
this.clearResultsList();
- $(this._resultDiv).append(aNodes);
+ $(this._resultDiv).append(nodes);
this.showResultsList();
},
/* ===== Results List Stuff ===== */
_resultDiv: null,
resultsVisible: false,
showResultsList: function() {
var entryOffset = $(this._entry).offset();
$(this._resultDiv).css({
@@ -198,10 +209,34 @@
this._nEmails = this.shadowTree.getElementById("emails");
console.log("done attaching contact result");
},
})
]]></xbl:implementation>
<xbl:handlers>
</xbl:handlers>
</xbl:binding>
+ <!-- Tag -->
+ <xbl:binding id="tag-completion">
+ <xbl:template>
+ messages tagged <span id="tagname"/>
+ </xbl:template>
+ <xbl:implementation><![CDATA[
+ ({
+ type: "tag",
+ getType: function() {
+ return this.type;
+ },
+ tagname: null,
+ getTagName: function() {
+ return this.tagname;
+ },
+ setTagName: function(aTagName) {
+ this.tagname = aTagName;
+ this.shadowTree.getElementById("tagname").textContent = aTagName;
+ }
+ })
+ ]]></xbl:implementation>
+ <xbl:handlers>
+ </xbl:handlers>
+ </xbl:binding>
</xbl:xbl>
--- a/client/bubbles.xml
+++ b/client/bubbles.xml
@@ -13,21 +13,40 @@
_nConstraints: null,
addContact: function(aContact) {
var node = $("<div/>").addClass("bubble").attr("type", "contact")[0];
ElementXBL.prototype.addBinding.call(node, "bubbles.xml#constraint-contact");
node.setContact(aContact);
$(this._nConstraints).append(node);
console.log("Contact", aContact, "added");
},
+ addTagName: function(aTagName) {
+ var node = $("<div/>").addClass("bubble").attr("type", "tag")[0];
+ ElementXBL.prototype.addBinding.call(node, "bubbles.xml#constraint-tag");
+ node.setTagName(aTagName);
+ $(this._nConstraints).append(node);
+ console.log("Tag name constraint", aTagName, "added");
+ },
getContacts: function() {
- return $(this._nConstraints).children().map(function (index, item) {
- console.log("getting contact from", item);
- return item.getContact();
- }).get();
+ var contacts = [];
+ $(this._nConstraints).children().map(function (index, item) {
+ if (item.getType() == "contact") {
+ contacts.push(item.getContact());
+ }
+ });
+ return contacts;
+ },
+ getTagNames: function() {
+ var tagNames = [];
+ $(this._nConstraints).children().map(function (index, item) {
+ if (item.getType() == "tag") {
+ tagNames.push(item.getTagName());
+ }
+ });
+ return tagNames;
},
xblBindingAttached: function () {
this._nConstraints = this.shadowTree.getElementById("holder");
},
})
]]></xbl:implementation>
</xbl:binding>
@@ -41,16 +60,17 @@
.con_contactpic {
width: 18;
height: 18;
}
]]></xbl:style>
</xbl:resources>
<xbl:implementation><![CDATA[
({
+ getType: function() { return "contact"; },
contact: null,
getContact: function() {
return this.contact;
},
setContact: function(aContact) {
this.contact = aContact;
console.log("setContact", this);
this.shadowTree.getElementById("name").textContent = this.contact.name;
@@ -63,9 +83,28 @@
this.shadowTree.getElementById("picture").setAttribute("src",
"http://www.gravatar.com/avatar/" + hex_md5(bestEmail) +
".jpg?r=pg&d=identicon&s=18");
}
},
})
]]></xbl:implementation>
</xbl:binding>
+ <!-- Tag -->
+ <xbl:binding id="constraint-tag">
+ <xbl:template>
+ <span id="tagname"></span>
+ </xbl:template>
+ <xbl:implementation><![CDATA[
+ ({
+ getType: function() { return "tag"; },
+ tagname: null,
+ getTagName: function() {
+ return this.tagname;
+ },
+ setTagName: function(aTagName) {
+ this.tagname = aTagName;
+ this.shadowTree.getElementById("tagname").textContent = aTagName;
+ }
+ })
+ ]]></xbl:implementation>
+ </xbl:binding>
</xbl:xbl>
--- a/client/cloda-completers.js
+++ b/client/cloda-completers.js
@@ -1,9 +1,10 @@
var ContactCompleter = {
+ type: "contact",
complete: function(aAutocomplete, aText) {
console.log("Contact completer firing on", aText);
Gloda.dbContacts.view("contact_ids/by_suffix", {
startkey: aText,
endkey: aText + "\u9999",
include_docs: true,
limit: 10,
success: function(result) {
@@ -15,13 +16,37 @@ var ContactCompleter = {
ElementXBL.prototype.addBinding.call(node, "autocomplete.xml#contact-completion");
node.setContact(row.doc);
nodes.push(node);
seen[row.id] = true;
}
});
console.log("Want to tell dude about:", aAutocomplete);
- aAutocomplete.haveSomeResults(aText, nodes);
+ aAutocomplete.haveSomeResults(aText, nodes, ContactCompleter, 1);
}
});
}
-};
\ No newline at end of file
+};
+
+var TagCompleter = {
+ type: "tag",
+ complete: function(aAutocomplete, aText) {
+ console.log("Tag completer firing on", aText);
+ Gloda.dbMessages.view("tags/all_tags", {
+ success: function(result) {
+ var tagNames = result.rows[0].value;
+ console.log("Tag completer got tag names:", tagNames);
+ var nodes = [];
+ var textLength = aText.length;
+ tagNames.forEach(function (tagName) {
+ if (tagName.substring(0, textLength) == aText) {
+ var node = $("<div/>")[0];
+ ElementXBL.prototype.addBinding.call(node, "autocomplete.xml#tag-completion");
+ node.setTagName(tagName);
+ nodes.push(node);
+ }
+ });
+ aAutocomplete.haveSomeResults(aText, nodes, TagCompleter, 100);
+ }
+ });
+ }
+};
--- a/client/cloda.js
+++ b/client/cloda.js
@@ -276,24 +276,35 @@ var MAX_TIMESTAMP = 4000000000;
var Gloda = {
dbContacts: $.couch.db("contacts"),
dbMessages: $.couch.db("messages"),
_init: function () {
},
- queryByInvolved: function(aInvolvedContactIds, aCallback, aCallbackThis) {
+ queryByStuff: function(aInvolvedContactIds, aTagNames, aCallback, aCallbackThis) {
// -- for each involved person, get the set of conversations they're in
- var constraints = aInvolvedContactIds.map(function (contact) {
- return {
- view: "by_involves/by_involves",
- startkey: [contact._id, 0], endkey: [contact._id, MAX_TIMESTAMP]
- };
- }, this);
+ var constraints = [];
+ if (aInvolvedContactIds && aInvolvedContactIds.length) {
+ constraints = constraints.concat(aInvolvedContactIds.map(function (contact) {
+ return {
+ view: "by_involves/by_involves",
+ startkey: [contact._id, 0], endkey: [contact._id, MAX_TIMESTAMP]
+ };
+ }, this));
+ }
+ if (aTagNames && aTagNames.length) {
+ constraints = constraints.concat(aTagNames.map(function (tagName) {
+ return {
+ view: "by_tags/by_tags",
+ startkey: [tagName, 0], endkey: [tagName, MAX_TIMESTAMP]
+ };
+ }, this));
+ }
var query = new GlodaConvQuery();
query.queryForConversations(constraints, aCallback, aCallbackThis);
// -- intersect all those conversations
// -- (fetch the conversation meta-info)
// -- fetch the messages in the conversations
}
};
--- a/client/index.xhtml
+++ b/client/index.xhtml
@@ -18,33 +18,40 @@
<link rel="stylesheet" href="searchResults.css" type="text/css"></link>
<script src="cloda.js"></script>
<script src="cloda-completers.js"></script>
<script src="utils.js"></script>
<script type="text/javascript" charset="utf-8"><![CDATA[
function funkyInit() {
var autocompleter = document.getElementById("autocomplete");
console.log("autocompleter", autocompleter);
+ autocompleter.addCompleter(TagCompleter);
+ console.log("tag completer registered");
autocompleter.addCompleter(ContactCompleter);
console.log("contact completer registered");
var query = document.getElementById("query");
var constraints = document.getElementById("constraints");
autocompleter.addActionListener(function (item) {
console.log("!!!Action Listener!!!", item);
var itemType = item.getType();
console.log(".type:", itemType);
if (itemType == "contact") {
console.log("..contact case");
var contact = item.getContact();
- console.log("contact", contact);
constraints.addContact(contact);
console.log("added contact", contact);
}
+ else if (itemType == "tag") {
+ console.log("..tag case");
+ var tagName = item.getTagName();
+ constraints.addTagName(tagName);
+ console.log("added tag name", tagName);
+ }
else
return;
query.updateConstraints(constraints);
});
console.log("constrainer registered");
}
$(function() {
--- a/client/messages.xml
+++ b/client/messages.xml
@@ -16,17 +16,19 @@
var node = $("<div/>")[0];
ElementXBL.prototype.addBinding.call(node, "messages.xml#conversation");
node.setConversation(conversation);
return node;
}));
},
updateConstraints: function(aConstraints) {
console.log("updating constraints");
- Gloda.queryByInvolved(aConstraints.getContacts(), this.resultsAhoy, this);
+ Gloda.queryByStuff(aConstraints.getContacts(),
+ aConstraints.getTagNames(),
+ this.resultsAhoy, this);
console.log("constraints updated");
}
})
]]></xbl:implementation>
<xbl:handlers>
<xbl:handler event="click"><![CDATA[
]]></xbl:handler>
--- a/server/python/junius/model.py
+++ b/server/python/junius/model.py
@@ -141,23 +141,47 @@ class Message(schema.Document):
# (no ghosts!)
by_involves = schema.View('by_involves', '''\
function(doc) {
for each (var contact_id in doc.involves_contact_ids)
emit([contact_id, doc.timestamp], doc.conversation_id);
}''')
# -- user provided meta-info junk
- by_tags = schema.View('by_tags', '''\
+ tagmap_func = '''\
function(doc) {
if (doc.tags) {
for (var i = 0; i < doc.tags.length; i++)
emit([doc.tags[i], doc.timestamp], doc.conversation_id);
}
- }''')
+ }'''
+ by_tags = schema.View('by_tags', tagmap_func)
+
+ # by reusing tagmap_func, we are able to consume its output from the
+ # previous view without introducing additional storage needs
+ all_tags = schema.View('tags', tagmap_func, '''\
+ function(keys, values, rereduce) {
+ var keySet = {}, i, j;
+ if (!rereduce) {
+ for (i = 0; i < keys.length; i++)
+ keySet[keys[i][0][0]] = true;
+ }
+ else {
+ for (i = 0; i < values.length; i++) {
+ var inSet = values[i];
+ for (j = 0; j < inSet.length; j++)
+ keySet[inSet[j]] = true;
+ }
+ }
+ var out = [];
+ for (var key in keySet)
+ out.push(key);
+ out.sort();
+ return out;
+ }''', group=False, group_level=0)
# -- storage info views
# so, this key is theoretically just wildly expensive
# no ghosts!
by_storage = schema.View('by_storage', '''\
function(doc) {
if (doc.timestamp)
emit([doc.account_id, doc.storage_path, doc.storage_id], null);