[geary/wip/730682-refine-convo-list: 13/37] Initial implementation of refined conversation list ux.



commit 98a4e40786d93c7c2531b720a2bddc9d2c6ebda5
Author: Michael James Gratton <mike vee net>
Date:   Sat Oct 14 05:40:42 2017 +1100

    Initial implementation of refined conversation list ux.
    
    * src/client/conversation-list/conversation-list.vala (ConversationList):
      New widget for displaying a list of conversations.
    
    * src/client/conversation-list/conversation-list-model.vala
      (ConversationListModel): New list model for concersations that uses an
      underlying sorted GLib.ListModel as a backing store for conversations
      obtained from a ConversationMonitor.
    
    * src/client/conversation-list/conversation-list-item.vala
      (ConversationListItem): New class for displaying an individual
      conversation in the list as a row. Copy a bunch of code over from
      FormattedConversationData.
    
    * src/client/components/main-window.vala (MainWindow): Add new
      ConversationList widget and display it in place of the old
      ConversationListView, but keep the older one around for the moment so
      as to not heinously break compilation.
    
    * ui/conversation-list-item.ui: New ui definition for
      ConversationListItem.
    
    * ui/geary.css: Style new list item widgets.

 po/POTFILES.in                                     |    4 +
 src/CMakeLists.txt                                 |    3 +
 src/client/components/main-window.vala             |   11 +-
 .../conversation-list/conversation-list-item.vala  |  230 ++++++++++++++++++++
 .../conversation-list/conversation-list-model.vala |   69 ++++++
 .../conversation-list/conversation-list.vala       |   42 ++++
 ui/CMakeLists.txt                                  |    1 +
 ui/conversation-list-item.ui                       |  158 ++++++++++++++
 ui/geary.css                                       |   31 +++
 9 files changed, 548 insertions(+), 1 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 002f320..e71383e 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -50,7 +50,10 @@ src/client/composer/contact-list-store.vala
 src/client/composer/contact-list-store-cache.vala
 src/client/composer/email-entry.vala
 src/client/composer/spell-check-popover.vala
+src/client/conversation-list/conversation-list.vala
 src/client/conversation-list/conversation-list-cell-renderer.vala
+src/client/conversation-list/conversation-list-item.vala
+src/client/conversation-list/conversation-list-model.vala
 src/client/conversation-list/conversation-list-store.vala
 src/client/conversation-list/conversation-list-view.vala
 src/client/conversation-list/formatted-conversation-data.vala
@@ -403,6 +406,7 @@ ui/composer-widget.ui
 ui/conversation-email.ui
 ui/conversation-email-attachment-view.ui
 ui/conversation-email-menus.ui
+ui/conversation-list-item.ui
 ui/conversation-message-menus.ui
 ui/conversation-message.ui
 ui/conversation-viewer.ui
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 681dd57..7b9b6f6 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -368,6 +368,9 @@ client/composer/email-entry.vala
 client/composer/spell-check-popover.vala
 
 client/conversation-list/conversation-list-cell-renderer.vala
+client/conversation-list/conversation-list.vala
+client/conversation-list/conversation-list-item.vala
+client/conversation-list/conversation-list-model.vala
 client/conversation-list/conversation-list-store.vala
 client/conversation-list/conversation-list-view.vala
 client/conversation-list/formatted-conversation-data.vala
diff --git a/src/client/components/main-window.vala b/src/client/components/main-window.vala
index 4161fa3..dc706b6 100644
--- a/src/client/components/main-window.vala
+++ b/src/client/components/main-window.vala
@@ -30,6 +30,7 @@ public class MainWindow : Gtk.ApplicationWindow {
     public MainToolbar main_toolbar { get; private set; }
     public SearchBar search_bar { get; private set; default = new SearchBar(); }
     public ConversationListView conversation_list_view  { get; private set; }
+    public ConversationList conversation_list  { get; private set; }
     public ConversationViewer conversation_viewer { get; private set; default = new ConversationViewer(); }
     public StatusBar status_bar { get; private set; default = new StatusBar(); }
     private MonitoredSpinner spinner = new MonitoredSpinner();
@@ -64,6 +65,8 @@ public class MainWindow : Gtk.ApplicationWindow {
     public MainWindow(GearyApplication application) {
         Object(application: application);
 
+        this.conversation_list = new ConversationList(application.config);
+
         load_config(application.config);
         restore_saved_window_state();
 
@@ -202,7 +205,11 @@ public class MainWindow : Gtk.ApplicationWindow {
         // Folder list
         this.folder_list_scrolled.add(this.folder_list);
         // Conversation list
-        this.conversation_list_scrolled.add(this.conversation_list_view);
+        this.conversation_list_scrolled.set_policy(
+            Gtk.PolicyType.NEVER,
+            Gtk.PolicyType.AUTOMATIC
+        );
+        this.conversation_list_scrolled.add(this.conversation_list);
         // Conversation viewer
         this.conversations_paned.pack2(this.conversation_viewer, true, true);
 
@@ -285,6 +292,8 @@ public class MainWindow : Gtk.ApplicationWindow {
             this.progress_monitor.add(new_model.preview_monitor);
             this.progress_monitor.add(conversations.progress_monitor);
             this.conversation_list_view.set_model(new_model);
+
+            this.conversation_list.set_model(conversations);
         }
 
         if (old_model != null) {
diff --git a/src/client/conversation-list/conversation-list-item.vala 
b/src/client/conversation-list/conversation-list-item.vala
new file mode 100644
index 0000000..0a46152
--- /dev/null
+++ b/src/client/conversation-list/conversation-list-item.vala
@@ -0,0 +1,230 @@
+/*
+ * Copyright 2017 Michael Gratton <mike vee net>
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later).  See the COPYING file in this distribution.
+ */
+
+/**
+ * A Gtk.ListBoxRow child that displays a conversation in the list.
+ */
+[GtkTemplate (ui = "/org/gnome/Geary/conversation-list-item.ui")]
+public class ConversationListItem : Gtk.Grid {
+
+    private const string STARRED_CLASS = "geary-starred";
+    private const string UNREAD_CLASS = "geary-unread";
+
+    // Translators: This stands in place for the user's name in the
+    // list of participants in a conversation. Should be short,
+    // ideally.
+    private const string ME = _("Me");
+
+    private class ParticipantDisplay : Geary.BaseObject, Gee.Hashable<ParticipantDisplay> {
+
+        public Geary.RFC822.MailboxAddress address;
+        public bool is_unread;
+
+        public ParticipantDisplay(Geary.RFC822.MailboxAddress address, bool is_unread) {
+            this.address = address;
+            this.is_unread = is_unread;
+        }
+
+        public string get_full_markup(Gee.List<Geary.RFC822.MailboxAddress> account_mailboxes) {
+            return get_as_markup((address in account_mailboxes) ? ME : address.get_short_address());
+        }
+
+        public string get_short_markup(Gee.List<Geary.RFC822.MailboxAddress> account_mailboxes) {
+            if (address in account_mailboxes)
+                return get_as_markup(ME);
+
+            string short_address = address.get_short_address().strip();
+
+            if (", " in short_address) {
+                // assume address is in Last, First format
+                string[] tokens = short_address.split(", ", 2);
+                short_address = tokens[1].strip();
+                if (Geary.String.is_empty(short_address))
+                    return get_full_markup(account_mailboxes);
+            }
+
+            // use first name as delimited by a space
+            string[] tokens = short_address.split(" ", 2);
+            if (tokens.length < 1)
+                return get_full_markup(account_mailboxes);
+
+            string first_name = tokens[0].strip();
+            if (Geary.String.is_empty_or_whitespace(first_name))
+                return get_full_markup(account_mailboxes);
+
+            return get_as_markup(first_name);
+        }
+
+        private string get_as_markup(string participant) {
+            return "%s%s%s".printf(
+                is_unread ? "<b>" : "", Geary.HTML.escape_markup(participant), is_unread ? "</b>" : "");
+        }
+
+        public bool equal_to(ParticipantDisplay other) {
+            return address.equal_to(other.address);
+        }
+
+        public uint hash() {
+            return address.hash();
+        }
+    }
+
+
+    [GtkChild]
+    private Gtk.Button star_button;
+
+    [GtkChild]
+    private Gtk.Button unstar_button;
+
+    [GtkChild]
+    private Gtk.Label participants;
+
+    [GtkChild]
+    private Gtk.Label subject;
+
+    [GtkChild]
+    private Gtk.Label preview;
+
+    [GtkChild]
+    private Gtk.Label date;
+
+    [GtkChild]
+    private Gtk.Label count;
+
+    private Configuration config;
+
+    public ConversationListItem(Geary.App.Conversation conversation,
+                                Gee.List<Geary.RFC822.MailboxAddress> account_addresses,
+                                bool use_to,
+                                Configuration config) {
+        // XXX should add a hook here to update the date when the
+        // preview pref and clock format changes
+        this.config = config;
+
+        Gtk.StyleContext style = get_style_context();
+
+        if (conversation.is_flagged()) {
+            style.add_class(STARRED_CLASS);
+            this.star_button.hide();
+            this.unstar_button.show();
+        } else {
+            style.remove_class(STARRED_CLASS);
+            this.star_button.show();
+            this.unstar_button.hide();
+        }
+
+        if (conversation.is_unread()) {
+            style.add_class(UNREAD_CLASS);
+        } else {
+            style.remove_class(UNREAD_CLASS);
+        }
+
+        string participants = get_participants_markup(conversation, use_to, account_addresses);
+        this.participants.set_markup(participants);
+        this.participants.set_tooltip_markup(participants);
+
+        // Use the latest message in the conversation by sender's date
+        // for extracting preview text for use here
+        Geary.Email? preview_message = conversation.get_latest_recv_email(
+            Geary.App.Conversation.Location.ANYWHERE
+        );
+
+        string subject_markup = conversation.is_unread() ? "<b>%s</b>" : "%s";
+        subject_markup = Markup.printf_escaped(
+            subject_markup,
+            Geary.String.reduce_whitespace(EmailUtil.strip_subject_prefixes(preview_message))
+        );
+        this.subject.set_markup(subject_markup);
+        if (preview_message.subject != null) {
+            this.subject.set_tooltip_text(
+                Geary.String.reduce_whitespace(preview_message.subject.to_string())
+            );
+        }
+
+        string preview_text = "long long long long preview";
+        if (config.display_preview) {
+            // XXX load & format preview here
+            // preview_text = XXXX;
+            preview.set_text(preview_text);
+            preview.show();
+        }
+
+        // conversation list store sorts by date-received, so
+        // display that instead of sender's Date:
+        string date_text = "";
+        string date_tooltip = "";
+        Geary.Email? latest_message = conversation.get_latest_recv_email(
+            Geary.App.Conversation.Location.IN_FOLDER_OUT_OF_FOLDER
+        );
+        if (latest_message != null && latest_message.properties != null) {
+            date_text = Date.pretty_print(
+                latest_message.properties.date_received, config.clock_format
+            );
+            date_tooltip = Date.pretty_print_verbose(
+                latest_message.properties.date_received, config.clock_format
+            );
+        }
+        this.date.set_text(date_text);
+        this.date.set_tooltip_text(date_tooltip);
+
+        uint count = conversation.get_count();
+        this.count.set_text("%u".printf(count));
+        if (count <= 1) {
+            this.count.hide();
+        }
+    }
+
+    private static string get_participants_markup(Geary.App.Conversation conversation,
+                                                  bool use_to,
+                                                  Gee.List<Geary.RFC822.MailboxAddress> 
account_owner_emails) {
+        // Build chronological list of AuthorDisplay records, setting
+        // to unread if any message by that author is unread
+        Gee.ArrayList<ParticipantDisplay> list = new Gee.ArrayList<ParticipantDisplay>();
+        foreach (Geary.Email message in 
conversation.get_emails(Geary.App.Conversation.Ordering.RECV_DATE_ASCENDING)) {
+            // only display if something to display
+            Geary.RFC822.MailboxAddresses? addresses = use_to ? message.to : message.from;
+            if (addresses == null || addresses.size < 1)
+                continue;
+
+            foreach (Geary.RFC822.MailboxAddress address in addresses) {
+                ParticipantDisplay participant_display = new ParticipantDisplay(address,
+                    message.email_flags.is_unread());
+
+                // if not present, add in chronological order
+                int existing_index = list.index_of(participant_display);
+                if (existing_index < 0) {
+                    list.add(participant_display);
+
+                    continue;
+                }
+
+                // if present and this message is unread but the prior were read,
+                // this author is now unread
+                if (message.email_flags.is_unread() && !list[existing_index].is_unread)
+                    list[existing_index].is_unread = true;
+            }
+        }
+
+        StringBuilder builder = new StringBuilder();
+        if (list.size == 1) {
+            // if only one participant, use full name
+            builder.append(list[0].get_full_markup(account_owner_emails));
+        } else {
+            bool first = true;
+            foreach (ParticipantDisplay participant in list) {
+                if (!first)
+                    builder.append(", ");
+
+                builder.append(participant.get_short_markup(account_owner_emails));
+                first = false;
+            }
+        }
+
+        return builder.str;
+    }
+
+}
diff --git a/src/client/conversation-list/conversation-list-model.vala 
b/src/client/conversation-list/conversation-list-model.vala
new file mode 100644
index 0000000..f936ca9
--- /dev/null
+++ b/src/client/conversation-list/conversation-list-model.vala
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2017 Michael Gratton <mike vee net>
+ * Copyright 2016 Software Freedom Conservancy Inc.
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later).  See the COPYING file in this distribution.
+ */
+
+/**
+ * A GLib.ListModel of sorted {@link Geary.App.Conversation}s.
+ *
+ * Conversations are sorted by {@link
+ * Geary.EmailProperties.date_received} (IMAP's INTERNALDATE) rather
+ * than the Date: header, as that ensures newly received email sort to
+ * the top where the user expects to see them.  The ConversationViewer
+ * sorts by the Date: header, as that presents better to the user.
+ */
+public class ConversationListModel : Geary.BaseObject, GLib.ListModel {
+
+
+    private Geary.App.ConversationMonitor monitor;
+
+    // Can't just derive from this directly since it's a compact class
+    private ListStore conversations = new ListStore(typeof(Geary.App.Conversation));
+
+
+    public Object? get_item(uint position) {
+        return this.conversations.get_item(position);
+    }
+
+    public uint get_n_items() {
+        return this.conversations.get_n_items();
+    }
+
+    public Type get_item_type() {
+        return this.conversations.get_item_type();
+    }
+
+
+    public ConversationListModel(Geary.App.ConversationMonitor monitor) {
+        this.monitor = monitor;
+
+        //monitor.scan_completed.connect(on_scan_completed);
+        monitor.conversations_added.connect(on_conversations_added);
+        //monitor.conversations_removed.connect(on_conversation_removed);
+        //monitor.conversation_appended.connect(on_conversation_appended);
+        //monitor.conversation_trimmed.connect(on_conversation_trimmed);
+        //monitor.email_flags_changed.connect(on_email_flags_changed);
+
+        // add all existing monitor
+        on_conversations_added(monitor.get_conversations());
+
+        this.conversations.items_changed.connect((position, removed, added) => {
+                this.items_changed(position, removed, added);
+            });
+    }
+
+    private void on_conversations_added(Gee.Collection<Geary.App.Conversation> monitor) {
+        foreach (Geary.App.Conversation conversation in monitor) {
+            this.conversations.insert_sorted(
+                conversation,
+                (a, b) => {
+                    return - compare_conversation_ascending(a as Geary.App.Conversation,
+                                                            b as Geary.App.Conversation); }
+            );
+        }
+    }
+
+}
diff --git a/src/client/conversation-list/conversation-list.vala 
b/src/client/conversation-list/conversation-list.vala
new file mode 100644
index 0000000..20edc8d
--- /dev/null
+++ b/src/client/conversation-list/conversation-list.vala
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2017 Michael Gratton <mike vee net>
+ * Copyright 2016 Software Freedom Conservancy Inc.
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later).  See the COPYING file in this distribution.
+ */
+
+/**
+ * A Gtk.ListBox that displays a list of conversations.
+ */
+public class ConversationList : Gtk.ListBox {
+
+
+    private const string CLASS = "geary-conversation-list";
+
+    private Configuration config;
+
+
+    public ConversationList(Configuration config) {
+        this.config = config;
+        get_style_context().add_class(CLASS);
+        set_activate_on_single_click(true);
+        set_selection_mode(Gtk.SelectionMode.MULTIPLE);
+    }
+
+    public void set_model(Geary.App.ConversationMonitor monitor) {
+        Geary.Folder displayed = monitor.folder;
+        Gee.List<Geary.RFC822.MailboxAddress> account_addresses = 
displayed.account.information.get_all_mailboxes();
+        bool use_to = (displayed != null) && displayed.special_folder_type.is_outgoing();
+        bind_model(
+            new ConversationListModel(monitor),
+            (convo) => {
+                return new ConversationListItem(convo as Geary.App.Conversation,
+                                                account_addresses,
+                                                use_to,
+                                                this.config);
+            }
+        );
+    }
+
+}
diff --git a/ui/CMakeLists.txt b/ui/CMakeLists.txt
index 4513df6..e8aaa76 100644
--- a/ui/CMakeLists.txt
+++ b/ui/CMakeLists.txt
@@ -15,6 +15,7 @@ set(RESOURCE_LIST
   STRIPBLANKS "conversation-email.ui"
   STRIPBLANKS "conversation-email-attachment-view.ui"
   STRIPBLANKS "conversation-email-menus.ui"
+  STRIPBLANKS "conversation-list-item.ui"
   STRIPBLANKS "conversation-message.ui"
   STRIPBLANKS "conversation-message-menus.ui"
   STRIPBLANKS "conversation-viewer.ui"
diff --git a/ui/conversation-list-item.ui b/ui/conversation-list-item.ui
new file mode 100644
index 0000000..b4966f5
--- /dev/null
+++ b/ui/conversation-list-item.ui
@@ -0,0 +1,158 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.20.0 -->
+<interface>
+  <requires lib="gtk+" version="3.20"/>
+  <template class="ConversationListItem" parent="GtkGrid">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <child>
+      <object class="GtkLabel" id="participants">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="halign">start</property>
+        <property name="valign">start</property>
+        <property name="margin_bottom">2</property>
+        <property name="hexpand">True</property>
+        <property name="label" translatable="yes">Participants</property>
+        <property name="ellipsize">end</property>
+        <property name="single_line_mode">True</property>
+        <property name="xalign">0</property>
+      </object>
+      <packing>
+        <property name="left_attach">1</property>
+        <property name="top_attach">0</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="subject">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="halign">start</property>
+        <property name="hexpand">True</property>
+        <property name="label" translatable="yes">Subject</property>
+        <property name="ellipsize">end</property>
+        <property name="single_line_mode">True</property>
+        <property name="xalign">0</property>
+      </object>
+      <packing>
+        <property name="left_attach">1</property>
+        <property name="top_attach">1</property>
+        <property name="width">2</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkGrid">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="vexpand">True</property>
+        <child>
+          <object class="GtkButton" id="star_button">
+            <property name="visible">True</property>
+            <property name="sensitive">False</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <property name="tooltip_text" translatable="yes" comments="Note: The application will never show 
this button at the same time as unstar_button, one will always be hidden.">Mark this message as 
starred</property>
+            <property name="valign">center</property>
+            <property name="vexpand">True</property>
+            <property name="action_name">eml.star</property>
+            <property name="relief">none</property>
+            <child>
+              <object class="GtkImage">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="icon_name">non-starred-symbolic</property>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="left_attach">0</property>
+            <property name="top_attach">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkButton" id="unstar_button">
+            <property name="sensitive">False</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <property name="tooltip_text" translatable="yes" comments="Note: The application will never show 
this button at the same time as star_button, one will always be hidden.">Mark this message as not 
starred</property>
+            <property name="valign">center</property>
+            <property name="vexpand">True</property>
+            <property name="action_name">eml.unstar</property>
+            <property name="relief">none</property>
+            <child>
+              <object class="GtkImage">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="icon_name">starred-symbolic</property>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="left_attach">0</property>
+            <property name="top_attach">0</property>
+          </packing>
+        </child>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">0</property>
+        <property name="height">3</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="preview">
+        <property name="can_focus">False</property>
+        <property name="halign">start</property>
+        <property name="hexpand">True</property>
+        <property name="label" translatable="yes" comments="Placeholder for preview text used while loading 
the preview for a message in the conversation list.">…</property>
+        <property name="ellipsize">end</property>
+        <property name="single_line_mode">True</property>
+        <property name="xalign">0</property>
+        <style>
+          <class name="dim-label"/>
+        </style>
+      </object>
+      <packing>
+        <property name="left_attach">1</property>
+        <property name="top_attach">2</property>
+        <property name="width">2</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="count">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="halign">end</property>
+        <property name="valign">center</property>
+        <property name="vexpand">True</property>
+        <property name="label" translatable="yes">0</property>
+        <style>
+          <class name="geary-count"/>
+        </style>
+      </object>
+      <packing>
+        <property name="left_attach">3</property>
+        <property name="top_attach">1</property>
+        <property name="height">2</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="date">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="halign">end</property>
+        <property name="valign">start</property>
+        <property name="margin_left">6</property>
+        <property name="label" translatable="yes">Date</property>
+      </object>
+      <packing>
+        <property name="left_attach">2</property>
+        <property name="top_attach">0</property>
+        <property name="width">2</property>
+      </packing>
+    </child>
+    <style>
+      <class name="geary-conversation-list-item"/>
+    </style>
+  </template>
+</interface>
diff --git a/ui/geary.css b/ui/geary.css
index d92ca39..97e2695 100644
--- a/ui/geary.css
+++ b/ui/geary.css
@@ -61,6 +61,37 @@ row.geary-folder-popover-list-row > label {
   color: @theme_text_color;
 }
 
+/* ConversationList */
+
+list.geary-conversation-list > row {
+  margin: 0;
+  border: 0;
+  padding: 0;
+}
+
+/* ConversationListItem */
+
+/* XXX RTL */
+grid.geary-conversation-list-item {
+  border-left: 4px solid transparent;
+  margin: 0px;
+  padding: 12px;
+  padding-left: 0;
+  transition: border 4s;
+}
+
+grid.geary-conversation-list-item.geary-unread {
+  border-color: @theme_selected_bg_color;
+  transition: border 0.25s;
+}
+
+grid.geary-conversation-list-item label.geary-count {
+  padding: 2px 6px;
+  border-radius: 2px;
+  color: @theme_selected_fg_color;
+  background: @theme_selected_bg_color;
+}
+
 /* ConversationListBox */
 
 .conversation-listbox {


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]