auto-complete on tags and constrain by them.
authorAndrew Sutherland <asutherland@asutherland.org>
Wed, 14 Jan 2009 04:16:51 -0800
changeset 26 215ac5fef05b56737aec6c460331e4498a66b601
parent 25 71b48845ac495f2c72b99ae8d4e353269dbbaac0
child 27 a3d0d7963b9d8af60d92b576e5014a5d82657452
push id1
push userroot
push dateWed, 08 Apr 2009 01:46:05 +0000
auto-complete on tags and constrain by them.
client/autocomplete.xml
client/bubbles.xml
client/cloda-completers.js
client/cloda.js
client/index.xhtml
client/messages.xml
server/python/junius/model.py
--- 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);