get some initial mailing list love into the code base
authorBryan Clark <clarkbw@gnome.org>
Wed, 28 Jan 2009 23:20:21 -0800
changeset 35 c16b02358f0632e6904e1711b6a111f3da6ca696
parent 34 2ef3084bf0aee665f70421fc20ab90f787d4e19e
child 36 9e7e4a9a4a91c528b9951b097011b328e6d8fc5d
push id1
push userroot
push dateWed, 08 Apr 2009 01:46:05 +0000
get some initial mailing list love into the code base
client/bubbles.xml
client/cloda.js
client/index.xhtml
client/messages.xml
client/searchResults.css
server/python/junius/model.py
--- a/client/bubbles.xml
+++ b/client/bubbles.xml
@@ -20,16 +20,23 @@
         },
         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");
         },
+        addDiscussion: function(aDiscussion) {
+          var node = $("<div/>").addClass("bubble").attr("type", "discussion")[0];
+          ElementXBL.prototype.addBinding.call(node, "bubbles.xml#constraint-discussion");
+          node.setDiscussion(aDiscussion);
+          $(this._nConstraints).append(node);
+          console.log("Discussion constraint", aDiscussion, "added");        
+        },
         getContacts: function() {
           var contacts = [];
           $(this._nConstraints).children().map(function (index, item) {
             if (item.getType() == "contact") {
               contacts.push(item.getContact());
             }
           });
           return contacts;
@@ -38,16 +45,28 @@
           var tagNames = [];
           $(this._nConstraints).children().map(function (index, item) {
             if (item.getType() == "tag") {
               tagNames.push(item.getTagName());
             }
           });
           return tagNames;
         },
+        getDiscussions: function() {
+          var discussions = [];
+          $(this._nConstraints).children().map(function (index, item) {
+            if (item.getType() == "discussion") {
+              discussions.push(item.getDiscussion());
+            }
+          });
+          return discussions;
+        },
+        clear: function() {
+          $(this._nConstraints).empty();
+        },
         xblBindingAttached: function () {
           this._nConstraints = this.shadowTree.getElementById("holder");
         },
       })
     ]]></xbl:implementation>
   </xbl:binding>
 
   <xbl:binding id="constraint-contact">
@@ -102,9 +121,34 @@
         },
         setTagName: function(aTagName) {
           this.tagname = aTagName;
           this.shadowTree.getElementById("tagname").textContent = aTagName;
         }
       })
     ]]></xbl:implementation>
   </xbl:binding>
+  <!-- Discussion -->
+  <xbl:binding id="constraint-discussion">
+    <xbl:template>
+      <span id="discussion"></span>
+    </xbl:template>
+    <xbl:implementation><![CDATA[
+      ({
+        getType: function() { return "discussion"; },
+        _discussion: null,
+        getDiscussion: function() {
+          return this._discussion;
+        },
+        _getAttr : function(attr) {
+          return this._discussion[attr];
+        },
+        getName: function() {
+          return this._getAttr("name");
+        },
+        setDiscussion: function(aDiscussion) {
+          this._discussion = aDiscussion;
+          this.shadowTree.getElementById("discussion").textContent = this.getName();
+        }
+      })
+    ]]></xbl:implementation>
+  </xbl:binding>
 </xbl:xbl>
--- a/client/cloda.js
+++ b/client/cloda.js
@@ -276,17 +276,17 @@ var MAX_TIMESTAMP = 4000000000;
 var Gloda = {
   dbContacts: $.couch.db("contacts"),
   dbMessages: $.couch.db("messages"),
 
   _init: function () {
 
   },
 
-  queryByStuff: function(aInvolvedContactIds, aTagNames, aCallback, aCallbackThis) {
+  queryByStuff: function(aInvolvedContactIds, aTagNames, aDiscussionIds, aCallback, aCallbackThis) {
     // -- for each involved person, get the set of conversations they're in
     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]
         };
@@ -295,16 +295,24 @@ var Gloda = {
     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));
     }
+    if (aDiscussionIds && aDiscussionIds.length) {
+      constraints = constraints.concat(aDiscussionIds.map(function (discussion) {
+        return {
+          view: "by_mailing_list/by_list_id",
+          startkey: [discussion.id, 0], endkey: [discussion.id, 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
@@ -47,16 +47,58 @@
           constraints.addTagName(tagName);
           console.log("added tag name", tagName);
         }
         else
           return;
         query.updateConstraints(constraints);
       });
       console.log("constrainer registered");
+
+      ru = new RainUtil();
+      ru.addStaticMailbox("Inbox","inbox");
+      ru.addStaticMailbox("Drafts","drafts");
+      ru.addStaticMailbox("Archives","archives");
+      ru.addStaticMailbox("Sent","sent");
+      ru.addStaticMailbox("Trash","trash");
+      ru.addStaticMailbox("Junk","junk");
+      
+      ru.addDynamicMailbox("People","people-by-frecency");
+      ru.addDynamicMailbox("Search","searches");
+      ru.addDynamicMailbox("Discussion","discussion");
+      
+      
+      var dbMessages = $.couch.db("messages");
+      dbMessages.view("by_header_id/by_mailing_list", {
+        include_docs: false,
+        group : true,
+        success: function(json) {
+          var parent = $("#discussion");
+          json.rows.forEach(function(row) {
+            var li = $(document.createElement("li")).addClass("discussion-list");
+            parent.append(li
+                          .append($(document.createElement("a"))
+                                            .addClass("discussion-list")
+                                            .attr("title", row.key["name"] + " (" + row.value + ")")
+                                            .attr("href","#/discussion/" + row.key["id"])
+                                            .mousedown(function() { 
+                                                          var c = document.getElementById("constraints"); 
+                                                          c.clear(); 
+                                                          c.addDiscussion(row.key);
+                                                          var query = document.getElementById("query");
+                                                          query.updateConstraints(c);
+                                                        }
+                                                      )
+                                            .text(row.key["name"] + " (" + row.value + ")")
+                                            )
+                                  );
+          });
+        }
+      });
+      
     }
 
     $(function() {
       window.setTimeout(funkyInit, 1000);
       //DocumentXBL.prototype.loadBindingDocument.call(document, "messages.xml");
       /*
       var dbMessages = $.couch.db("messages");
       dbMessages.view("by_timestamp/by_timestamp", {
@@ -71,18 +113,54 @@
             node.setMessage(row.doc);
             //fDocumentXBL_addBindings(node);
             return node;
           });
         }
       })
       */
     });
-  ]]></script>
+
+  function RainUtil() { }
+  RainUtil.prototype = {
+    addStaticMailbox: function(aTitle, aQuery) {
+      var a = document.createElement("a");
+      $("#static-mailboxes").append($(a).addClass("sMailbox").attr("href","#" + aQuery).text(aTitle));
+    },
+    addDynamicMailbox: function(aTitle, aQuery) {
+      $("#dynamic-mailboxes").append(this._dynamicMailbox(aTitle,aQuery));
+    },
+    _dynamicMailbox: function(aTitle,aQuery) {
+      var d = document.createElement("div"), 
+          dt = document.createElement("div"),
+          span = document.createElement("span"),
+          ul = document.createElement("ul"),
+          li = document.createElement("li");
+      return $(d).addClass("dMailbox").append(
+              $(dt).addClass("title").text(aTitle).append(
+                $(span).addClass("toggle")
+                       .text("-")
+                       .click(function() { $(ul).slideToggle(); })),
+                        $(ul).addClass("results").attr("id", aQuery));
+    },
+  
+  };
+  ]]></script>  
 </head>
 <body>
   <div id="autocomplete"></div>
   <div id="constraints"/>
   <div id="query"/>
-  <div id="conversations">
+
+  <div id="mc">
+    <div id="left">
+      <div id="static-mailboxes">
+      </div>
+      <div id="dynamic-mailboxes">
+      </div>
+    </div>
+    <div id="right">
+      <div id="conversations">
+      </div>
+    </div>
   </div>
 </body>
 </html>
--- a/client/messages.xml
+++ b/client/messages.xml
@@ -18,16 +18,17 @@
             node.setConversation(conversation);
             return node;
           }));
         },
         updateConstraints: function(aConstraints) {
           console.log("updating constraints");
           Gloda.queryByStuff(aConstraints.getContacts(),
                              aConstraints.getTagNames(),
+                             aConstraints.getDiscussions(),
                              this.resultsAhoy, this);
           console.log("constraints updated");
         }
       })
     ]]></xbl:implementation>
     <xbl:handlers>
       <xbl:handler event="click"><![CDATA[
 
--- a/client/searchResults.css
+++ b/client/searchResults.css
@@ -27,17 +27,17 @@ conversation { padding: 1em 1px; margin:
                border-top: 1px solid transparent; border-bottom: 1px solid #ddd; 
                display: block;
                color: #555; background-color: #f5f6f7; /* default read */
               }
 conversation:focus { border: 1px dotted #111; padding: 1em 0px; }
 conversation[unread="true"]:focus { border: 1px dotted #111; padding: 1em 0px; }
 conversation:focus:last-child { border: 1px dotted #111; padding: 1em 0px; }
 conversation:focus:first-child { border: 1px dotted #111; padding: 1em 0px; }
-conversation:last-child { border-bottom: 1px solid transparent; }
+conversation:last-child { border-color: transparent; }
 conversation:first-child { border-top: 1px solid #ddd; }
 
 conversation > .target > .header,
 conversation > .target > .body,
 conversation > .target > .subject,
 conversation > .replies { margin-left: 24px; font-size: 95%; }
 
 conversation > .target { display: block; padding: 0.2em 0em; padding-right: 1em; }
@@ -102,8 +102,20 @@ conversation[unread="true"] > .target > 
 /* BUTTON TOOLBOX */
 search-toolbox { display: block; }
 button.archive { font-weight: bold; }
 button[type="menu"] { font-size: small; border: 1px solid #bed1df; background-color: #cee1ef; color: #666; }
 search-toolbox > .notifications { float: right; }
 .notifications div { padding: 0.3em 1em; -moz-border-radius: 0.3em; }
 .notifications .alert { background-color: #fcaf3e; color: #111; }
 .notifications .info { background-color: inherit; color: #666; }
+
+#mc { display: table; }
+#left { display: table-cell; width: 10em; padding: 0.2em 1em 1ex 1em; }
+#right { display: table-cell; border: 2px solid lightsteelblue; -moz-border-radius: 4px; }
+.sMailbox { display: block; margin: 0.2ex 0px; }
+#dynamic-mailboxes { margin: 1ex 0px; max-width: 10em; }
+.dMailbox { margin: 1ex 0px; padding: 0px; border: 1px solid lightsteelblue; -moz-border-radius: 0.5em; }
+.dMailbox .title { background-color: steelblue; color: white; padding: 0.2ex 0.4em; position: relative; }
+.dMailbox .title { -moz-border-radius-topright: 0.2em; -moz-border-radius-topleft: 0.2em; }
+.dMailbox .title .toggle { position: absolute; right: 0.5em; font-weight: bold; cursor: pointer; }
+.dMailbox .results { list-style: none; margin: 0px; padding: 0.4ex 0.2em; font-size: small; white-space: nowrap; overflow: hidden; }
+.dMailbox .results li { padding: 0.2ex 0px; }
--- a/server/python/junius/model.py
+++ b/server/python/junius/model.py
@@ -182,17 +182,44 @@ class Message(schema.Document):
     # -- 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);
         }''', include_docs=False)
+        
+    by_mailing_list = schema.View('by_header_id', '''\
+        function(doc) {
+          if (doc.headers && doc.headers["List-Id"]) {
+            var parts = doc.headers["List-Id"].match(/([\W\w]*)\s*<(.+)>.*/);
+            var keys = {"List-Id" : doc.headers["List-Id"],
+                         "id" : parts[2],
+                         "name" : parts[1] };
+            for each (var headerId in ["List-Post","List-Archive","List-Help",
+                                       "List-Subscribe","List-Unsubscribe"]) {
+              if (doc.headers[headerId])
+                keys[headerId] = doc.headers[headerId];
+            }
+            emit(keys, 1);
+          }
+        }''', '''\
+        function(keys, values, rereduce) {
+          return sum(values);
+        }''', include_docs=False, group=True, group_level=1)
 
+    by_list_id = schema.View('by_mailing_list', '''\
+        function(doc) {
+          if (doc.headers && doc.headers["List-Id"]) {
+            var parts = doc.headers["List-Id"].match(/[\W\w]\s*<(.+)>.*/);
+            emit([parts[1], doc.timestamp], doc.conversation_id);
+          }
+        }''', include_docs=True)    
+        
 DATABASES = {
     # the app database proper, no real data
     'junius': None,
     #
     'accounts': Account,
     'contacts': Contact,
     'messages': Message,
 }