[geary/wip/730682-refine-convo-list] Update action state based on conversation state, not folder state.



commit d8a9b3a5b68ed33745a3b0d5c52f5e3523d647bf
Author: Michael James Gratton <mike vee net>
Date:   Sun Dec 31 19:09:05 2017 +1100

    Update action state based on conversation state, not folder state.
    
    * src/client/application/geary-controller.vala (GearyController): Expose
      new query_supported_operations() method, rework
      enable_context_dependent_buttons_async() to use that.
    
    * src/client/components/main-window.vala (MainWindow): When the selected
      or marked conversations changes, query what folder operations are
      supported for the set and update action state accordingly.
    
    * src/client/conversation-list/conversation-list.vala
      (ConversationListBox): Replace item_marked signal with items_marked, so
      if multiple items are marked the same time, only one supported
      operations query gets run.
    
    * src/engine/api/geary-folder.vala (Folder): Add get_support_types()
      method to return a folder's supported types.o
    
    * src/engine/app/app-email-store.vala
      (EmailStore::get_supported_operations_async): Never return null

 src/client/application/geary-controller.vala       |   54 ++++++---
 src/client/components/main-window.vala             |  129 +++++++++++++++++---
 .../conversation-list/conversation-list.vala       |   53 +++++++-
 src/engine/api/geary-folder.vala                   |   28 ++++-
 src/engine/app/app-email-store.vala                |   83 +++++++------
 5 files changed, 269 insertions(+), 78 deletions(-)
---
diff --git a/src/client/application/geary-controller.vala b/src/client/application/geary-controller.vala
index 6d25c6f..3bb6b9f 100644
--- a/src/client/application/geary-controller.vala
+++ b/src/client/application/geary-controller.vala
@@ -1136,6 +1136,34 @@ public class GearyController : Geary.BaseObject {
     }
 
     /**
+     * Determines the common supported operations for a set of email.
+     */
+    internal async Gee.Set<Type> query_supported_operations(Gee.Collection<Geary.EmailIdentifier> ids,
+                                                            Cancellable? cancellable)
+        throws Error {
+        Gee.Set<Type>? supported = null;
+        if (ids.size > 1) {
+            Gee.MultiMap<Geary.EmailIdentifier,Type>? selected = null;
+            Geary.App.EmailStore? store = this.email_stores.get(this.current_account);
+            if (store != null) {
+                selected = yield store.get_supported_operations_async(
+                    ids, cancellable
+                );
+            }
+
+            supported = new Gee.HashSet<Type>();
+            if (selected != null) {
+                supported.add_all(selected.get_values());
+            }
+        } else {
+            // Heuristic: If there is only one email, assume it is in
+            // the current folder and return the folder's support
+            supported = this.current_folder.get_support_types();
+        }
+        return supported;
+    }
+
+    /**
      * Adds and removes flags from a set of emails.
      */
     internal async void mark_email(Gee.Collection<Geary.EmailIdentifier> ids,
@@ -2615,37 +2643,31 @@ public class GearyController : Geary.BaseObject {
         get_window_action(ACTION_DELETE_CONVERSATION).set_enabled(sensitive && (current_folder is 
Geary.FolderSupport.Remove));
 
         cancel_context_dependent_buttons();
-        enable_context_dependent_buttons_async.begin(sensitive, cancellable_context_dependent_buttons);
+        if (this.current_folder != null) {
+            enable_context_dependent_buttons_async.begin(sensitive, cancellable_context_dependent_buttons);
+        }
     }
 
     private async void enable_context_dependent_buttons_async(bool sensitive, Cancellable? cancellable) {
-        Gee.MultiMap<Geary.EmailIdentifier, Type>? selected_operations = null;
+        Gee.Set<Type>? supported = null;
         try {
-            if (current_folder != null) {
-                Geary.App.EmailStore? store = email_stores.get(current_folder.account);
-                if (store != null) {
-                    selected_operations = yield store
-                        .get_supported_operations_async(get_selected_email_ids(false), cancellable);
-                }
-            }
+            supported = yield query_supported_operations(
+                get_selected_email_ids(false), cancellable
+            );
         } catch (Error e) {
             debug("Error checking for what operations are supported in the selected conversations: %s",
                 e.message);
         }
-        
+
         // Exit here if the user has cancelled.
         if (cancellable != null && cancellable.is_cancelled())
             return;
-        
-        Gee.HashSet<Type> supported_operations = new Gee.HashSet<Type>();
-        if (selected_operations != null)
-            supported_operations.add_all(selected_operations.get_values());
 
         get_window_action(ACTION_SHOW_MARK_MENU).set_enabled(
-            sensitive && (typeof(Geary.FolderSupport.Mark) in supported_operations)
+            sensitive && (typeof(Geary.FolderSupport.Mark) in supported)
         );
         get_window_action(ACTION_COPY_MENU).set_enabled(
-            sensitive && (supported_operations.contains(typeof(Geary.FolderSupport.Copy)))
+            sensitive && (supported.contains(typeof(Geary.FolderSupport.Copy)))
         );
     }
 
diff --git a/src/client/components/main-window.vala b/src/client/components/main-window.vala
index 3d329c0..8585b6d 100644
--- a/src/client/components/main-window.vala
+++ b/src/client/components/main-window.vala
@@ -53,6 +53,32 @@ public class MainWindow : Gtk.ApplicationWindow {
     };
 
 
+    private class SupportedOperations {
+
+        internal bool supports_archive = false;
+        internal bool supports_copy = false;
+        internal bool supports_delete = false;
+        internal bool supports_mark = false;
+        internal bool supports_move = false;
+        internal bool supports_trash = false;
+
+
+        internal SupportedOperations(Geary.Folder base_folder, Gee.Set<Type>? supports) {
+            this.supports_archive = supports.contains(typeof(Geary.FolderSupport.Archive));
+            this.supports_copy = supports.contains(typeof(Geary.FolderSupport.Copy));
+            this.supports_delete = supports.contains(typeof(Geary.FolderSupport.Remove));
+            this.supports_mark = supports.contains(typeof(Geary.FolderSupport.Mark));
+            this.supports_move = supports.contains(typeof(Geary.FolderSupport.Move));
+            this.supports_trash = (
+                this.supports_move &&
+                base_folder.special_folder_type != Geary.SpecialFolderType.TRASH &&
+                !base_folder.properties.is_local_only
+            );
+        }
+
+    }
+
+
     public new GearyApplication application {
         get { return (GearyApplication) base.get_application(); }
         set { base.set_application(value); }
@@ -77,6 +103,7 @@ public class MainWindow : Gtk.ApplicationWindow {
     public Geary.Folder? current_folder { get; private set; default = null; }
     public Geary.App.ConversationMonitor? current_conversations { get; private set; default = null; }
     private Cancellable load_cancellable = new Cancellable();
+    private SupportedOperations? folder_operations = null;
 
     private ConversationActionBar conversation_list_actions =
         new ConversationActionBar();
@@ -122,7 +149,7 @@ public class MainWindow : Gtk.ApplicationWindow {
         this.conversation_list = new ConversationList(application.config);
         this.conversation_list.conversation_selection_changed.connect(on_conversation_selection_changed);
         this.conversation_list.conversation_activated.connect(on_conversation_activated);
-        this.conversation_list.item_marked.connect(on_conversation_item_marked);
+        this.conversation_list.items_marked.connect(on_conversation_items_marked);
         this.conversation_list.marked_conversations_evaporated.connect(on_selection_mode_disabled);
         this.conversation_list.selection_mode_enabled.connect(on_selection_mode_enabled);
         this.conversation_list.visible_conversations_changed.connect(on_visible_conversations_changed);
@@ -417,14 +444,79 @@ public class MainWindow : Gtk.ApplicationWindow {
             this.main_toolbar.folder = this.current_folder.get_display_name();
     }
 
-    private void update_conversation_actions(Geary.App.Conversation? target) {
-        bool is_unread = target.is_unread();
-        get_action(ACTION_MARK_READ).set_enabled(is_unread);
-        get_action(ACTION_MARK_UNREAD).set_enabled(!is_unread);
+    // Queries the supported actions for the currently highlighted
+    // conversations then updates them.
+    private void query_supported_actions() {
+        // Update actions up-front using folder defaults, even when
+        // actually doing a query, so the operations are vaguely correct.
+        update_conversation_actions(this.folder_operations);
+
+        Gee.Collection<Geary.EmailIdentifier> highlighted = get_highlighted_email();
+        if (!highlighted.is_empty && highlighted.size >= 1) {
+            this.application.controller.query_supported_operations.begin(
+                highlighted,
+                this.load_cancellable,
+                (obj, res) => {
+                    Gee.Set<Type>? supported = null;
+                    try {
+                        supported = this.application.controller.query_supported_operations.end(res);
+                    } catch (Error err) {
+                        debug("Error querying supported actions: %s", err.message);
+                    }
+                    update_conversation_actions(
+                        new SupportedOperations(this.current_folder, supported)
+                    );
+                });
+        }
+    }
+
+    // Updates conversation action enabled state based on those that
+    // are currently supported.
+    private void update_conversation_actions(SupportedOperations ops) {
+        Gee.Collection<Geary.App.Conversation> highlighted =
+            this.conversation_list.get_highlighted_conversations();
+        bool has_highlighted = !highlighted.is_empty;
+
+        get_action(ACTION_ARCHIVE).set_enabled(has_highlighted && ops.supports_archive);
+        get_action(ACTION_COPY).set_enabled(has_highlighted && ops.supports_copy);
+        get_action(ACTION_DELETE).set_enabled(has_highlighted && ops.supports_delete);
+        get_action(ACTION_JUNK).set_enabled(has_highlighted && ops.supports_move);
+        get_action(ACTION_MOVE).set_enabled(has_highlighted && ops.supports_move);
+        get_action(ACTION_RESTORE).set_enabled(has_highlighted && ops.supports_move);
+        get_action(ACTION_TRASH).set_enabled(has_highlighted && ops.supports_trash);
+
+        get_action(ACTION_SHOW_COPY).set_enabled(has_highlighted && ops.supports_copy);
+        get_action(ACTION_SHOW_MOVE).set_enabled(has_highlighted && ops.supports_move);
+
+        SimpleAction read = get_action(ACTION_MARK_READ);
+        SimpleAction unread = get_action(ACTION_MARK_UNREAD);
+        SimpleAction starred = get_action(ACTION_MARK_STARRED);
+        SimpleAction unstarred = get_action(ACTION_MARK_UNSTARRED);
+        if (has_highlighted && ops.supports_mark) {
+            bool has_read = false;
+            bool has_unread = false;
+            bool has_starred = false;
+
+            foreach (Geary.App.Conversation convo in highlighted) {
+                has_read |= convo.has_any_read_message();
+                has_unread |= convo.is_unread();
+                has_starred |= convo.is_flagged();
+
+                if (has_starred && has_unread && has_starred) {
+                    break;
+                }
+            }
 
-        bool is_starred = target.is_flagged();
-        get_action(ACTION_MARK_UNSTARRED).set_enabled(is_starred);
-        get_action(ACTION_MARK_STARRED).set_enabled(!is_starred);
+            read.set_enabled(has_unread);
+            unread.set_enabled(has_read);
+            starred.set_enabled(!has_starred);
+            unstarred.set_enabled(has_starred);
+        } else {
+            read.set_enabled(false);
+            unread.set_enabled(false);
+            starred.set_enabled(false);
+            unstarred.set_enabled(false);
+        }
     }
 
     private inline void check_shift_event(Gdk.EventKey event) {
@@ -455,6 +547,9 @@ public class MainWindow : Gtk.ApplicationWindow {
 
         folder.properties.notify.connect(update_headerbar);
         this.current_folder = folder;
+        this.folder_operations = new SupportedOperations(
+            folder, folder.get_support_types()
+        );
 
         // Set up a new conversation monitor for the folder
         Geary.App.ConversationMonitor monitor = new Geary.App.ConversationMonitor(
@@ -480,6 +575,7 @@ public class MainWindow : Gtk.ApplicationWindow {
         this.conversation_list_actions.update_location(this.current_folder);
         update_headerbar();
         set_selection_mode_enabled(false);
+        update_conversation_actions(this.folder_operations);
 
         this.progress_monitor.add(folder.opening_monitor);
         this.progress_monitor.add(monitor.progress_monitor);
@@ -551,6 +647,7 @@ public class MainWindow : Gtk.ApplicationWindow {
         this.main_toolbar.set_selection_mode_enabled(enabled);
         this.conversation_list.set_selection_mode_enabled(enabled);
         this.conversation_viewer.show_none_selected();
+        update_conversation_actions(this.folder_operations);
     }
 
     private void report_problem(Action action, Variant? param, Error? err = null) {
@@ -637,7 +734,7 @@ public class MainWindow : Gtk.ApplicationWindow {
 
     private void on_conversation_selection_changed(Geary.App.Conversation? selection) {
         show_conversation(selection);
-        update_conversation_actions(selection);
+        query_supported_actions();
     }
 
     private void on_conversation_activated(Geary.App.Conversation activated) {
@@ -647,22 +744,24 @@ public class MainWindow : Gtk.ApplicationWindow {
             // TODO: Determine how to map between conversations and drafts correctly.
             Geary.Email draft = activated.get_latest_recv_email(
                 Geary.App.Conversation.Location.IN_FOLDER
-                );
+            );
             this.application.controller.create_compose_widget(
                 ComposerWidget.ComposeType.NEW_MESSAGE, draft, null, null, true
             );
         }
     }
 
-    private void on_conversation_item_marked(ConversationListItem item, bool marked) {
-        if (marked) {
-            show_conversation(item.conversation);
+    private void on_conversation_items_marked(Gee.List<ConversationListItem> marked,
+                                              Gee.List<ConversationListItem> unmarked) {
+        if (!marked.is_empty) {
+            show_conversation(marked.last().conversation);
         } else {
             this.conversation_viewer.show_none_selected();
         }
         this.main_toolbar.update_selection_count(
             this.conversation_list.get_marked_items().size
         );
+        query_supported_actions();
     }
 
     private void on_initial_conversation_load() {
@@ -713,8 +812,8 @@ public class MainWindow : Gtk.ApplicationWindow {
     }
 
     private void on_conversation_flags_changed(Geary.App.Conversation changed) {
-        if (this.conversation_list.selected == changed) {
-            update_conversation_actions(changed);
+        if (this.conversation_list.is_highlighted(changed)) {
+            update_conversation_actions(this.folder_operations);
         }
     }
 
diff --git a/src/client/conversation-list/conversation-list.vala 
b/src/client/conversation-list/conversation-list.vala
index 522cc40..c84923c 100644
--- a/src/client/conversation-list/conversation-list.vala
+++ b/src/client/conversation-list/conversation-list.vala
@@ -51,6 +51,7 @@ public class ConversationList : Gtk.ListBox {
     private Gee.Map<Geary.App.Conversation,ConversationListItem> marked =
         new Gee.HashMap<Geary.App.Conversation,ConversationListItem>();
     private ConversationListItem? last_marked = null;
+    private bool is_marking = false;
     private Gee.Set<Geary.App.Conversation>? visible_conversations = null;
     private Geary.Scheduler.Scheduled? update_visible_scheduled = null;
     private bool enable_load_more = true;
@@ -86,9 +87,10 @@ public class ConversationList : Gtk.ListBox {
     public signal void marked_conversations_evaporated();
 
     /**
-     * Fired when a list item was marked as selected in selection mode.
+     * Fired when list items are marked or unmarked in selection mode.
      */
-    public signal void item_marked(ConversationListItem item, bool marked);
+    public signal void items_marked(Gee.List<ConversationListItem> marked,
+                                    Gee.List<ConversationListItem> unmarked);
 
 
     public ConversationList(Configuration config) {
@@ -247,14 +249,26 @@ public class ConversationList : Gtk.ListBox {
                     } else {
                         anchor = last_marked;
                     }
+
+                    this.is_marking = true;
+                    Gee.List<ConversationListItem> marked =
+                        new Gee.LinkedList<ConversationListItem>();
+                    Gee.List<ConversationListItem> unmarked =
+                        Gee.List.empty<ConversationListItem>();
+
                     int index = int.min(clicked.get_index(), anchor.get_index());
                     int end = index + (clicked.get_index() - anchor.get_index()).abs();
                     while (index <= end) {
                         ConversationListItem? row = get_item_at_index(index++);
                         if (row != null) {
                             row.set_marked(true);
+                            marked.add(row);
                         }
                     }
+
+                    items_marked(marked, unmarked);
+                    this.is_marking = false;
+
                     ret = Gdk.EVENT_STOP;
                 }
             }
@@ -279,13 +293,23 @@ public class ConversationList : Gtk.ListBox {
         if (enabled) {
             freeze_selection();
         } else {
+            this.is_marking = true;
+            Gee.List<ConversationListItem> marked =
+                Gee.List.empty<ConversationListItem>();
+            Gee.List<ConversationListItem> unmarked =
+                new Gee.LinkedList<ConversationListItem>();
+
             // Call to_array here to get a copy of the value
             // collection, since unmarking the items will cause the
             // underlying map to be modified
             foreach (ConversationListItem item in this.marked.values.to_array()) {
                 item.set_marked(false);
+                unmarked.add(item);
             }
-            this.marked.clear();
+
+            items_marked(marked, unmarked);
+            this.is_marking = false;
+
             thaw_selection();
         }
         this.is_selection_mode_enabled = enabled;
@@ -467,7 +491,7 @@ public class ConversationList : Gtk.ListBox {
         }
     }
 
-    private void on_item_marked(ConversationListItem item, bool marked) {
+    private void on_item_marked(ConversationListItem item, bool is_marked) {
         if (!this.is_selection_mode_enabled) {
             // Selection mode not enabled, so the item would have
             // been Ctrl-activated and we need to enable it
@@ -475,13 +499,30 @@ public class ConversationList : Gtk.ListBox {
             selection_mode_enabled();
         }
 
-        if (marked) {
+        if (is_marked) {
             this.marked.set(item.conversation, item);
             this.last_marked = item;
         } else {
             this.marked.remove(item.conversation);
         }
-        item_marked(item, marked);
+
+        // Only fire the event for a single item if we aren't doing a
+        // mass-marking elsewhere
+        if (!this.is_marking) {
+            Gee.List<ConversationListItem> marked =
+                Gee.List.empty<ConversationListItem>();
+            Gee.List<ConversationListItem> unmarked =
+                Gee.List.empty<ConversationListItem>();
+
+            if (is_marked) {
+                marked = new Gee.LinkedList<ConversationListItem>();
+                marked.add(item);
+            } else {
+                unmarked = new Gee.LinkedList<ConversationListItem>();
+                unmarked.add(item);
+            }
+            items_marked(marked, unmarked);
+        }
     }
 
 }
diff --git a/src/engine/api/geary-folder.vala b/src/engine/api/geary-folder.vala
index e33852d..7dde63e 100644
--- a/src/engine/api/geary-folder.vala
+++ b/src/engine/api/geary-folder.vala
@@ -412,7 +412,33 @@ public abstract class Geary.Folder : BaseObject {
         return (special_folder_type == Geary.SpecialFolderType.NONE)
             ? path.basename : special_folder_type.get_display_name();
     }
-    
+
+    /**
+     * Returns this folder's supported {@link FolderSupport} types.
+     */
+    public Gee.Set<Type> get_support_types() {
+        Gee.Set<Type>? ops = new Gee.HashSet<Type>();
+        if (this is Geary.FolderSupport.Archive) {
+            ops.add(typeof(Geary.FolderSupport.Archive));
+        }
+        if (this is Geary.FolderSupport.Copy) {
+            ops.add(typeof(Geary.FolderSupport.Copy));
+        }
+        if (this is Geary.FolderSupport.Create) {
+            ops.add(typeof(Geary.FolderSupport.Create));
+        }
+        if (this is Geary.FolderSupport.Mark) {
+            ops.add(typeof(Geary.FolderSupport.Mark));
+        }
+        if (this is Geary.FolderSupport.Move) {
+            ops.add(typeof(Geary.FolderSupport.Move));
+        }
+        if (this is Geary.FolderSupport.Remove) {
+            ops.add(typeof(Geary.FolderSupport.Remove));
+        }
+        return ops;
+    }
+
     /**
      * Returns the state of the Folder's connections to the local and remote stores.
      */
diff --git a/src/engine/app/app-email-store.vala b/src/engine/app/app-email-store.vala
index f74ede3..38d3467 100644
--- a/src/engine/app/app-email-store.vala
+++ b/src/engine/app/app-email-store.vala
@@ -10,57 +10,60 @@ public class Geary.App.EmailStore : BaseObject {
     public EmailStore(Geary.Account account) {
         this.account = account;
     }
-    
+
     /**
+     * Determines the supported operations for a set of email.
+     *
      * Return a map of EmailIdentifiers to the special Geary.FolderSupport
      * interfaces each one supports.  For example, if an EmailIdentifier comes
      * back mapped to typeof(Geary.FolderSupport.Mark), it can be marked via
      * mark_email_async().  If an EmailIdentifier doesn't appear in the
      * returned map, no operations are supported on it.
      */
-    public async Gee.MultiMap<Geary.EmailIdentifier, Type>? get_supported_operations_async(
-        Gee.Collection<Geary.EmailIdentifier> emails, Cancellable? cancellable = null) throws Error {
-        Gee.MultiMap<Geary.EmailIdentifier, Geary.FolderPath>? folders
-            = yield account.get_containing_folders_async(emails, cancellable);
-        if (folders == null)
-            return null;
-        
-        Gee.HashSet<Type> all_support = new Gee.HashSet<Type>();
-        all_support.add(typeof(Geary.FolderSupport.Archive));
-        all_support.add(typeof(Geary.FolderSupport.Copy));
-        all_support.add(typeof(Geary.FolderSupport.Create));
-        all_support.add(typeof(Geary.FolderSupport.Mark));
-        all_support.add(typeof(Geary.FolderSupport.Move));
-        all_support.add(typeof(Geary.FolderSupport.Remove));
-        
-        Gee.HashMultiMap<Geary.EmailIdentifier, Type> map
-            = new Gee.HashMultiMap<Geary.EmailIdentifier, Type>();
-        foreach (Geary.EmailIdentifier email in folders.get_keys()) {
-            Gee.HashSet<Type> support = new Gee.HashSet<Type>();
-            
-            foreach (Geary.FolderPath path in folders.get(email)) {
-                Geary.Folder folder;
-                try {
-                    folder = yield account.fetch_folder_async(path, cancellable);
-                } catch (Error e) {
-                    debug("Error getting a folder from path %s: %s", path.to_string(), e.message);
-                    continue;
-                }
-                
-                foreach (Type type in all_support) {
-                    if (folder.get_type().is_a(type))
-                        support.add(type);
+    public async Gee.MultiMap<Geary.EmailIdentifier,Type>
+        get_supported_operations_async(Gee.Collection<Geary.EmailIdentifier> emails,
+                                       Cancellable? cancellable = null)
+        throws Error {
+        Gee.HashMultiMap<Geary.EmailIdentifier,Type> supported =
+            new Gee.HashMultiMap<Geary.EmailIdentifier,Type>();
+        Gee.MultiMap<Geary.EmailIdentifier, Geary.FolderPath>? folders =
+            yield account.get_containing_folders_async(emails, cancellable);
+        if (folders != null) {
+            Gee.HashSet<Type> all_support = new Gee.HashSet<Type>();
+            all_support.add(typeof(Geary.FolderSupport.Archive));
+            all_support.add(typeof(Geary.FolderSupport.Copy));
+            all_support.add(typeof(Geary.FolderSupport.Create));
+            all_support.add(typeof(Geary.FolderSupport.Mark));
+            all_support.add(typeof(Geary.FolderSupport.Move));
+            all_support.add(typeof(Geary.FolderSupport.Remove));
+
+            foreach (Geary.EmailIdentifier email in folders.get_keys()) {
+                Gee.HashSet<Type> support = new Gee.HashSet<Type>();
+
+                foreach (Geary.FolderPath path in folders.get(email)) {
+                    Geary.Folder folder;
+                    try {
+                        folder = yield account.fetch_folder_async(path, cancellable);
+                    } catch (Error e) {
+                        debug("Error getting a folder from path %s: %s", path.to_string(), e.message);
+                        continue;
+                    }
+
+                    foreach (Type type in all_support) {
+                        if (folder.get_type().is_a(type))
+                            support.add(type);
+                    }
+                    if (support.contains_all(all_support))
+                        break;
                 }
-                if (support.contains_all(all_support))
-                    break;
+
+                Geary.Collection.multi_map_set_all<Geary.EmailIdentifier, Type>(supported, email, support);
             }
-            
-            Geary.Collection.multi_map_set_all<Geary.EmailIdentifier, Type>(map, email, support);
         }
-        
-        return (map.size > 0 ? map : null);
+
+        return supported;
     }
-    
+
     /**
      * Lists any set of EmailIdentifiers as if they were all in one folder.
      */


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