Bug 741728 - Previous conversations should be collapsed by default. r=florian, ui-r=florian, a=Standard8.
authorMike Conley <mconley@mozilla.com>
Tue, 14 Aug 2012 10:10:26 -0400
changeset 12544 8ad0a8d8ffc9bfbc237c7fb54da56d809ef59a40
parent 12543 d30864d616a7ea3d7555ad73167ed478d92de724
child 12545 4577a7128713bbc4f497337683141ede1e3a68cd
push id637
push usermconley@mozilla.com
push dateTue, 14 Aug 2012 14:45:48 +0000
treeherdercomm-beta@4577a7128713 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersflorian, florian, Standard8
bugs741728
Bug 741728 - Previous conversations should be collapsed by default. r=florian, ui-r=florian, a=Standard8.
mail/components/im/content/chat-messenger-overlay.js
mail/components/im/content/chat-messenger-overlay.xul
mail/components/im/themes/chat.css
mail/themes/qute/mail/chat-aero.css
mail/themes/qute/mail/chat.css
mailnews/base/content/jsTreeView.js
--- a/mail/components/im/content/chat-messenger-overlay.js
+++ b/mail/components/im/content/chat-messenger-overlay.js
@@ -95,17 +95,17 @@ var chatTabType = {
 
     let item = document.getElementById("searchResultConv");
     item.log = aArgs.conv;
     if (aArgs.searchTerm)
       item.searchTerm = aArgs.searchTerm;
     else
       delete item.searchTerm;
     item.hidden = false;
-    if (item.selected)
+    if (item.getAttribute("selected"))
       chatHandler.onListItemSelected();
     else
       document.getElementById("contactlistbox").selectedItem = item;
   },
   openTab: function(aTab, aArgs) {
     if (!this.hasBeenOpened) {
       let convs = imServices.conversations.getUIConversations();
       if (convs.length != 0) {
@@ -384,48 +384,67 @@ var chatHandler = {
     if (today - aDate < kDayInMsecs)
       return bundle.getFormattedString("yesterday", [time]);
 
     let date = dts.FormatDate("", dts.dateFormatShort, aDate.getFullYear(),
                               aDate.getMonth() + 1, aDate.getDate());
     return bundle.getFormattedString("dateTime", [date, time]);
   },
 
-  _showLogList: function(aLogs) {
-    let listbox = document.getElementById("logList");
-    while (listbox.firstChild)
-      listbox.removeChild(listbox.firstChild);
-    let logs = [];
-    for (let log in fixIterator(aLogs))
-      logs.push(log);
-    logs.sort(function(log1, log2) log2.time - log1.time);
-    for each (let log in logs) {
-      if (log.format != "json")
-        continue;
-      let elt = document.createElement("listitem");
-      elt.setAttribute("label",
-                       this._makeFriendlyDate(new Date(log.time * 1000)));
-      elt.log = log;
-      listbox.appendChild(elt);
+  /**
+   * Display a list of logs into a tree, and optionally handle a default selection.
+   *
+   * @param aLogs An nsISimpleEnumerator of imILog.
+   * @param aShouldSelect Either a boolean (true means select the first log
+   * of the list, false or undefined means don't mess with the selection) or a log
+   * item that needs to be selected.
+   * @returns true if there's at least one log in the list, false if empty.
+   */
+  _showLogList: function(aLogs, aShouldSelect) {
+    let logTree = document.getElementById("logTree");
+    let treeView = this._treeView = new chatLogTreeView(logTree, aLogs);
+    if (!treeView._rowMap.length)
+      return false;
+    if (aShouldSelect) {
+      if (aShouldSelect === true) {
+        // Open the first group (index 0)
+        treeView.toggleOpenState(0);
+        // Select the first log of the first group (index 1)
+        logTree.view.selection.select(1);
+      }
+      else {
+        let logTime = aShouldSelect.time;
+        for (let index = 0; index < treeView._rowMap.length; ++index) {
+          if (!treeView._rowMap[index].children.some(function (i) i.log.time == logTime))
+            continue;
+          treeView.toggleOpenState(index);
+          ++index;
+          while (index < treeView._rowMap.length && treeView._rowMap[index].log.time != logTime)
+            ++index;
+          if (treeView._rowMap[index].log.time == logTime) {
+            logTree.view.selection.select(index);
+            logTree.treeBoxObject.ensureRowIsVisible(index);
+          }
+          return true;
+        }
+      }
     }
-    return logs;
+    return true;
   },
+
   onLogSelect: function() {
-    let selectedItem = document.getElementById("logList").selectedItem;
-    if (!selectedItem)
+    let selection = this._treeView.selection;
+    let currentIndex = selection.currentIndex;
+    // The current (focused) row may not be actually selected...
+    if (!selection.isSelected(currentIndex))
       return;
-    let log = selectedItem.log;
-    if (!log) {
-      let item = document.getElementById("contactlistbox").selectedItem;
-      if (item) {
-        document.getElementById("conversationsDeck").selectedPanel =
-          item.convView;
-      }
+
+    let log = this._treeView._rowMap[currentIndex].log;
+    if (!log)
       return;
-    }
 
     let list = document.getElementById("contactlistbox");
     if (list.selectedItem.getAttribute("id") != "searchResultConv")
       document.getElementById("goToConversation").hidden = false;
     this._showLog(log.getConversation(), log.path);
   },
 
   _contactObserver: {
@@ -449,18 +468,21 @@ var chatHandler = {
     if (aContact)
       aContact.addObserver(this._contactObserver);
     return aContact;
   },
   showCurrentConversation: function() {
     let item = document.getElementById("contactlistbox").selectedItem;
     if (!item)
       return;
-    if (item.localName == "imconv")
+    if (item.localName == "imconv") {
       document.getElementById("conversationsDeck").selectedPanel = item.convView;
+      document.getElementById("logTree").view.selection.clearSelection();
+      item.convView.focus();
+    }
     else if (item.localName == "imcontact")
       item.openConversation();
   },
   focusConversation: function(aUIConv) {
     let conv =
       document.getElementById("conversationsGroup").contactsById[aUIConv.id];
     document.getElementById("contactlistbox").selectedItem = conv;
     if (conv.convView)
@@ -532,28 +554,17 @@ var chatHandler = {
       cti.removeAttribute("statusMessageWithDash");
       cti.removeAttribute("statusMessage");
       cti.removeAttribute("status");
       cti.removeAttribute("statusTypeTooltiptext");
       cti.removeAttribute("statusTooltiptext");
       cti.removeAttribute("topicEditable");
       cti.removeAttribute("noTopic");
 
-      let logs = this._showLogList(imServices.logs.getSimilarLogs(log));
-      let time = log.time;
-      let list = document.getElementById("logList");
-      let logItem = list.firstChild;
-      while (logItem) {
-        if (logItem.log.time == time) {
-          list.selectedItem = logItem;
-          break;
-        }
-        logItem = logItem.nextSibling;
-      }
-
+      this._showLogList(imServices.logs.getSimilarLogs(log), log);
       this.observedContact = null;
     }
     else if (item.localName == "imconv") {
       let convDeck = document.getElementById("conversationsDeck");
       if (!item.convView) {
         let conv = document.createElement("imconversation");
         convDeck.appendChild(conv);
         conv.conv = item.conv;
@@ -594,21 +605,17 @@ var chatHandler = {
                 // change caused the imcontact to move.
                 // Return early to avoid flickering and changing the selected log.
 
       this.showContactInfo(contact);
       this.observedContact = contact;
 
       document.getElementById("contextPane").removeAttribute("chat");
 
-      let logs = this._showLogList(imServices.logs.getLogsForContact(contact));
-      let listbox = document.getElementById("logList");
-      if (logs.length)
-        listbox.selectedItem = listbox.firstChild;
-      else {
+      if (!this._showLogList(imServices.logs.getLogsForContact(contact), true)) {
         document.getElementById("conversationsDeck").selectedPanel =
           document.getElementById("logDisplay");
         document.getElementById("logDisplayDeck").selectedPanel =
           document.getElementById("noPreviousConvScreen");
       }
     }
     this.updateTitle();
   },
@@ -1011,9 +1018,129 @@ var chatHandler = {
       this.initAfterChatCore();
     else {
       this.ChatCore.init();
       this._addObserver("chat-core-initialized");
     }
   }
 };
 
+function chatLogTreeGroupItem(aTitle, aLogItems) {
+  this._title = aTitle;
+  this._children = aLogItems;
+  for each (let child in this._children)
+    child._parent = this;
+  this._open = false;
+}
+chatLogTreeGroupItem.prototype = {
+  getText: function() this._title,
+  get id() this._title,
+  get open() this._open,
+  get level() 0,
+  get _parent() null,
+  get children() this._children,
+  getProperties: function(aProps) {}
+};
+
+function chatLogTreeLogItem(aLog, aText) {
+  this.log = aLog;
+  this._text = aText;
+}
+chatLogTreeLogItem.prototype = {
+  getText: function() this._text,
+  get id() this.log.title,
+  get open() false,
+  get level() 1,
+  get children() [],
+  getProperties: function(aProps) {}
+};
+
+function chatLogTreeView(aTree, aLogs) {
+  this._tree = aTree;
+  this._logs = aLogs;
+  this._tree.view = this;
+  this._rebuild();
+}
+chatLogTreeView.prototype = {
+  __proto__: new PROTO_TREE_VIEW(),
+
+  _rebuild: function cLTV__rebuild() {
+    // Drop the old rowMap.
+    if (this._tree)
+      this._tree.rowCountChanged(0, -this._rowMap.length);
+    this._rowMap = [];
+
+    // The keys used in the 'groups' object should match string ids in
+    // messenger.properties, except 'other' that has a special handling.
+    let groups = {
+      today: [],
+      yesterday: [],
+      lastWeek: [],
+      twoWeeksAgo: [],
+      other: []
+    };
+
+    // Some date helpers...
+    const kDayInMsecs = 24 * 60 * 60 * 1000;
+    const kWeekInMsecs = 7 * kDayInMsecs;
+    const kTwoWeeksInMsecs = 2 * kWeekInMsecs;
+    let dts = Components.classes["@mozilla.org/intl/scriptabledateformat;1"]
+                        .getService(Ci.nsIScriptableDateFormat);
+    let formatDate = function(aDate) {
+      return dts.FormatDate("", dts.dateFormatShort, aDate.getFullYear(),
+                            aDate.getMonth() + 1, aDate.getDate());
+    };
+    let nowDate = new Date();
+    let todayDate = new Date(nowDate.getFullYear(), nowDate.getMonth(),
+                             nowDate.getDate());
+
+    // Build a chatLogTreeLogItem for each log, and put it in the right group.
+    let chatBundle = document.getElementById("chatBundle");
+    for each (let log in fixIterator(this._logs)) {
+      let logDate = new Date(log.time * 1000);
+      let timeFromToday = todayDate - logDate;
+      let title = dts.FormatTime("", dts.timeFormatNoSeconds,
+                                 logDate.getHours(), logDate.getMinutes(), 0);
+      if (timeFromToday > kDayInMsecs) {
+        title = chatBundle.getFormattedString("dateTime",
+                                              [formatDate(logDate), title]);
+      }
+      let group;
+      if (timeFromToday <= 0)
+        group = groups.today;
+      else if (timeFromToday <= kDayInMsecs)
+        group = groups.yesterday;
+      else if (timeFromToday <= kWeekInMsecs)
+        group = groups.lastWeek;
+      else if (timeFromToday <= kTwoWeeksInMsecs)
+        group = groups.twoWeeksAgo;
+      else
+        group = groups.other;
+      group.push(new chatLogTreeLogItem(log, title));
+    }
+
+    // Create a chatLogTreeGroupItem for each group.
+    let msgBundle = document.getElementById("bundle_messenger");
+    for each (let [groupId, group] in Iterator(groups)) {
+      if (!group.length)
+        continue;
+      group.sort(function(l1, l2) l2.log.time - l1.log.time);
+      let groupName;
+      if (groupId == "other") {
+        groupName = formatDate(new Date(group[0].log.time * 1000));
+        if (group.length > 1) {
+          let fromDate = new Date(group[group.length - 1].log.time * 1000);
+          groupName += " - " + formatDate(fromDate);
+        }
+      }
+      else {
+        groupName = msgBundle.getString(groupId);
+      }
+      this._rowMap.push(new chatLogTreeGroupItem(groupName, group));
+    }
+
+    // Finally, notify the tree.
+    if (this._tree)
+      this._tree.rowCountChanged(0, this._rowMap.length);
+  }
+};
+
 window.addEventListener("load", chatHandler.init.bind(chatHandler));
--- a/mail/components/im/content/chat-messenger-overlay.xul
+++ b/mail/components/im/content/chat-messenger-overlay.xul
@@ -15,17 +15,18 @@
   <!ENTITY % messengerDTD SYSTEM "chrome://messenger/locale/messenger.dtd">
   %messengerDTD;
   <!ENTITY % textcontextDTD SYSTEM "chrome://global/locale/textcontext.dtd">
   %textcontextDTD;
 ]>
 
 <overlay id="chat-messenger-overlay"
          xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
-
+  <script type="application/javascript"
+          src="chrome://messenger/content/jsTreeView.js"/>
   <script type="application/javascript"
           src="chrome://messenger/content/chat/chat-messenger-overlay.js"/>
   <script type="application/javascript"
           src="chrome://messenger/content/chat/imStatusSelector.js"/>
   <script type="application/javascript"
           src="chrome://messenger/content/chat/imContextMenu.js"/>
 
   <stringbundleset id="stringbundleset">
@@ -254,22 +255,29 @@
                   <textbox flex="1" readonly="true" class="plain" id="participantCount"/>
                 </hbox>
                 <listbox id="nicklist" class="conv-nicklist"
                          flex="1" seltype="multiple"
                          tooltip="buddyTooltip"
                          onclick="chatHandler.onNickClick(event);"
                          onkeypress="chatHandler.onNicklistKeyPress(event);"/>
               </vbox>
-              <splitter id="logsSplitter" class="conv-chat"/>
+              <splitter id="logsSplitter" class="conv-chat" collapse="after"/>
               <vbox flex="1" id="previousConversations">
                 <label class="conv-logs-header-label"
                        id="participantLabel"
                        value="&chat.previousConversations;"/>
-                <listbox flex="1" id="logList" onselect="chatHandler.onLogSelect();"/>
+                <tree id="logTree" flex="1" hidecolumnpicker="true" seltype="single"
+                      context="logTreeContext" onselect="chatHandler.onLogSelect();">
+                  <treecols>
+                    <treecol id="logCol" flex="1" primary="true" hideheader="true"
+                             crop="center" ignorecolumnpicker="true"/>
+                  </treecols>
+                  <treechildren/>
+                </tree>
               </vbox>
             </vbox>
           </vbox>
         </hbox>
       </notificationbox>
     </vbox>
   </tabpanels>
 </overlay>
--- a/mail/components/im/themes/chat.css
+++ b/mail/components/im/themes/chat.css
@@ -330,17 +330,17 @@ grippy {
   background: transparent;
 %endif
 }
 
 #conv-toolbar {
   border-style: none;
 }
 
-#logList {
+#logTree {
   margin: 0 0;
 }
 
 .conv-nicklist > .listitem-iconic > .listcell-iconic > .listcell-label {
   font-weight: bold;
   -moz-padding-start: 1px;
 %ifdef XP_MACOSX
   text-shadow: 0 1px 0 rgba(255, 255, 255, 0.4);
@@ -386,17 +386,17 @@ grippy {
 }
 
 .conv-textbox[focused="true"] {
   box-shadow: inset 0 0 2px 1px rgba(40, 120, 212, 0.7),
                     0 0 4px 1px rgb(40, 120, 212),
               inset 0 1px 2px rgba(0, 0, 0, 0.7);
 }
 
-.conv-nicklist, #logList {
+.conv-nicklist, #logTree {
   -moz-appearance: none;
   width: 250px;
   border: 0px;
 }
 %endif
 %ifdef XP_WIN
 .splitter.conv-chat {
   border-left: 1px solid rgba(0, 0, 0, 0.25);
--- a/mail/themes/qute/mail/chat-aero.css
+++ b/mail/themes/qute/mail/chat-aero.css
@@ -151,17 +151,16 @@
     border: 1px solid Highlight;
   }
 
   .listitem-iconic[selected] > .listcell-iconic > .listcell-label,
   #nicklist:focus > .listitem-iconic[inactive][selected] > .listcell-iconic > .listcell-label {
     color: -moz-dialogtext !important;
   }
 
-  #logList > listitem,
   #nicklist > listitem {
     border-width: 1px !important;
     outline: none !important;
   }
 }
 
 #button-add-buddy,
 #button-add-buddy[disabled],
--- a/mail/themes/qute/mail/chat.css
+++ b/mail/themes/qute/mail/chat.css
@@ -84,17 +84,17 @@
 .conv-top-info {
   background: transparent;
 }
 
 .userIcon {
   border-width: 0px;
 }
 
-#logList,
+#logTree,
 .conv-nicklist {
   -moz-appearance: none;
   border: none;
   margin: 0;
 }
 
 .conv-nicklist-header,
 .conv-logs-header-label {
--- a/mailnews/base/content/jsTreeView.js
+++ b/mailnews/base/content/jsTreeView.js
@@ -221,10 +221,18 @@ PROTO_TREE_VIEW.prototype = {
   _persistOpenMap: null,
 
   _restoreOpenStates: function jstv__restoreOpenStates() {
     // Note that as we iterate through here, .length may grow
     for (let i = 0; i < this._rowMap.length; i++) {
       if (this._persistOpenMap.indexOf(this._rowMap[i].id) != -1)
         this.toggleOpenState(i);
     }
+  },
+
+  QueryInterface: function QueryInterface(aIID) {
+    if (aIID.equals(Components.interfaces.nsITreeView) ||
+        aIID.equals(Components.interfaces.nsISupports))
+      return this;
+ 
+    throw Components.results.NS_ERROR_NO_INTERFACE;
   }
 };