[geary/mjog/account-command-stacks: 8/27] Move conversation selection and action updates to main window
- From: Michael Gratton <mjog src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [geary/mjog/account-command-stacks: 8/27] Move conversation selection and action updates to main window
- Date: Sat, 26 Oct 2019 05:34:54 +0000 (UTC)
commit 500d758e81ad2ad6a3bd9612d7b105ad30c5f8e1
Author: Michael Gratton <mike vee net>
Date: Sat Oct 5 19:21:01 2019 +1000
Move conversation selection and action updates to main window
Move the remaining main window action code from Application.Controller
to MainWindow.
src/client/application/application-controller.vala | 237 ---------------------
src/client/components/main-window.vala | 187 ++++++++++++++--
2 files changed, 175 insertions(+), 249 deletions(-)
---
diff --git a/src/client/application/application-controller.vala
b/src/client/application/application-controller.vala
index 4621f670..bc99ff0a 100644
--- a/src/client/application/application-controller.vala
+++ b/src/client/application/application-controller.vala
@@ -165,9 +165,6 @@ public class Application.Controller : Geary.BaseObject {
private Cancellable cancellable_folder = new Cancellable();
private Cancellable cancellable_search = new Cancellable();
private Cancellable cancellable_open_account = new Cancellable();
- private Cancellable cancellable_context_dependent_buttons = new Cancellable();
- private Gee.Set<Geary.App.Conversation> selected_conversations = new
Gee.HashSet<Geary.App.Conversation>();
- private Geary.App.Conversation? last_deleted_conversation = null;
private Gee.LinkedList<ComposerWidget> composer_widgets = new Gee.LinkedList<ComposerWidget>();
private uint select_folder_timeout_id = 0;
private int64 next_folder_select_allowed_usec = 0;
@@ -249,12 +246,9 @@ public class Application.Controller : Geary.BaseObject {
main_window.retry_service_problem.connect(on_retry_service_problem);
main_window.notify["has-toplevel-focus"].connect(on_has_toplevel_focus);
- enable_message_buttons(false);
-
engine.account_available.connect(on_account_available);
// Connect to various UI signals.
- main_window.conversation_list_view.conversations_selected.connect(on_conversations_selected);
main_window.conversation_list_view.conversation_activated.connect(on_conversation_activated);
main_window.conversation_list_view.visible_conversations_changed.connect(on_visible_conversations_changed);
main_window.folder_list.folder_selected.connect(on_folder_selected);
@@ -370,11 +364,9 @@ public class Application.Controller : Geary.BaseObject {
this.application.engine.account_available.disconnect(on_account_available);
// Release folder and conversations in the main window
- on_conversations_selected(new Gee.HashSet<Geary.App.Conversation>());
on_folder_selected(null);
// Disconnect from various UI signals.
- this.main_window.conversation_list_view.conversations_selected.disconnect(on_conversations_selected);
this.main_window.conversation_list_view.conversation_activated.disconnect(on_conversation_activated);
this.main_window.conversation_list_view.visible_conversations_changed.disconnect(on_visible_conversations_changed);
this.main_window.folder_list.folder_selected.disconnect(on_folder_selected);
@@ -464,9 +456,6 @@ public class Application.Controller : Geary.BaseObject {
this.current_account = null;
- this.selected_conversations = new Gee.HashSet<Geary.App.Conversation>();
- this.last_deleted_conversation = null;
-
this.pending_mailtos.clear();
this.composer_widgets.clear();
this.waiting_to_close.clear();
@@ -968,17 +957,6 @@ public class Application.Controller : Geary.BaseObject {
return is_descendent;
}
- // Update widgets and such to match capabilities of the current folder ... sensitivity is handled
- // by other utility methods
- private void update_ui() {
- this.main_window.main_toolbar.selected_conversations =
- this.selected_conversations.size;
- this.main_window.main_toolbar.update_trash_button(
- !this.main_window.is_shift_down &&
- current_folder_supports_trash()
- );
- }
-
private void on_folder_selected(Geary.Folder? folder) {
debug("Folder %s selected", folder != null ? folder.to_string() : "(null)");
if (folder == null) {
@@ -987,10 +965,6 @@ public class Application.Controller : Geary.BaseObject {
main_window.main_toolbar.folder = null;
this.main_window.folder_selected(null, null);
} else if (folder != this.current_folder) {
- this.main_window.conversation_viewer.show_loading();
- get_window_action(MainWindow.ACTION_FIND_IN_CONVERSATION).set_enabled(false);
- enable_message_buttons(false);
-
// To prevent the user from selecting folders too quickly,
// we prevent additional selection changes to occur until
// after a timeout has expired from the last one
@@ -1069,8 +1043,6 @@ public class Application.Controller : Geary.BaseObject {
main_window.main_toolbar.copy_folder_menu.enable_disable_folder(current_folder, false);
main_window.main_toolbar.move_folder_menu.enable_disable_folder(current_folder, false);
- update_ui();
-
this.main_window.folder_selected(folder, this.cancellable_folder);
clear_new_messages("do_select_folder", null);
@@ -1088,64 +1060,6 @@ public class Application.Controller : Geary.BaseObject {
}
}
- private void on_conversations_selected(Gee.Set<Geary.App.Conversation> selected) {
- this.selected_conversations = selected;
- get_window_action(MainWindow.ACTION_FIND_IN_CONVERSATION).set_enabled(false);
- ConversationViewer viewer = this.main_window.conversation_viewer;
- if (this.current_folder != null && !this.main_window.has_composer) {
- switch(selected.size) {
- case 0:
- enable_message_buttons(false);
- viewer.show_none_selected();
- break;
-
- case 1:
- // Cancel existing avatar loads before loading new
- // convo since that will start loading more avatars
- Geary.App.Conversation convo = Geary.Collection.get_first(
- selected
- );
-
- AccountContext? context = this.accounts.get(
- convo.base_folder.account.information
- );
-
- // It's possible for a conversation with zero email to
- // be selected, when it has just evaporated after its
- // last email was removed but the conversation monitor
- // hasn't signalled its removal yet. In this case,
- // just don't load it since it will soon disappear.
- if (context != null && convo.get_count() > 0) {
- viewer.load_conversation.begin(
- convo,
- context.emails,
- context.contacts,
- (obj, ret) => {
- try {
- viewer.load_conversation.end(ret);
- enable_message_buttons(true);
- get_window_action(
- MainWindow.ACTION_FIND_IN_CONVERSATION
- ).set_enabled(true);
- } catch (GLib.IOError.CANCELLED err) {
- // All good
- } catch (Error err) {
- debug("Unable to load conversation: %s",
- err.message);
- }
- }
- );
- }
- break;
-
- default:
- enable_multiple_message_buttons();
- viewer.show_multiple_selected();
- break;
- }
- }
- }
-
private void on_conversation_activated(Geary.App.Conversation activated) {
// Currently activating a conversation is only available for drafts folders.
if (current_folder == null || current_folder.special_folder_type !=
@@ -1336,13 +1250,6 @@ public class Application.Controller : Geary.BaseObject {
old_cancellable.cancel();
}
- private void cancel_context_dependent_buttons() {
- Cancellable old_cancellable = cancellable_context_dependent_buttons;
- cancellable_context_dependent_buttons = new Cancellable();
-
- old_cancellable.cancel();
- }
-
// We need to include the second parameter, or valac doesn't recognize the function as matching
// GearyApplication.exiting's signature.
private bool on_application_exiting(GearyApplication sender, bool panicked) {
@@ -1358,61 +1265,6 @@ public class Application.Controller : Geary.BaseObject {
clear_new_messages("on_has_toplevel_focus", null);
}
- // latest_sent_only uses Email's Date: field, which corresponds to
- // how they're sorted in the ConversationViewer, not whether they
- // are in the sent folder.
- private Gee.Collection<Geary.EmailIdentifier> get_conversation_email_ids(
- Gee.Collection<Geary.App.Conversation> conversations,
- bool latest_sent_only) {
-
- Gee.Collection<Geary.EmailIdentifier> ids =
- new Gee.ArrayList<Geary.EmailIdentifier>();
-
- // Blacklist the Outbox unless that's currently selected since
- // we don't want any operations to apply to messages there
- // normally.
- Gee.Collection<Geary.FolderPath>? blacklist = null;
- if (this.current_folder != null &&
- this.current_folder.special_folder_type != Geary.SpecialFolderType.OUTBOX) {
- Geary.Folder? outbox = this.current_account.get_special_folder(
- Geary.SpecialFolderType.OUTBOX
- );
-
- blacklist = new Gee.ArrayList<Geary.FolderPath>();
- blacklist.add(outbox.path);
- }
-
- foreach(Geary.App.Conversation conversation in conversations) {
- if (latest_sent_only) {
- Geary.Email? latest = conversation.get_latest_sent_email(
- Geary.App.Conversation.Location.IN_FOLDER_OUT_OF_FOLDER,
- blacklist
- );
- if (latest != null) {
- ids.add(latest.id);
- }
- } else {
- Geary.traverse<Geary.Email>(
- conversation.get_emails(
- Geary.App.Conversation.Ordering.NONE,
- Geary.App.Conversation.Location.ANYWHERE,
- blacklist
- )
- ).map<Geary.EmailIdentifier>(e => e.id)
- .add_all_to(ids);
- }
- }
-
- return ids;
- }
-
- private Gee.Collection<Geary.EmailIdentifier>
- get_selected_email_ids(bool latest_sent_only) {
- return get_conversation_email_ids(
- this.selected_conversations, latest_sent_only
- );
- }
-
private void on_visible_conversations_changed(Gee.Set<Geary.App.Conversation> visible) {
clear_new_messages("on_visible_conversations_changed", visible);
}
@@ -1856,12 +1708,6 @@ public class Application.Controller : Geary.BaseObject {
}
}
- private bool current_folder_supports_trash() {
- return (current_folder != null && current_folder.special_folder_type != Geary.SpecialFolderType.TRASH
- && !current_folder.properties.is_local_only && current_account != null
- && (current_folder as Geary.FolderSupport.Move) != null);
- }
-
private void on_sent(Geary.Account account, Geary.RFC822.Message sent) {
// Translators: The label for an in-app notification. The
// string substitution is a list of recipients of the email.
@@ -1874,77 +1720,6 @@ public class Application.Controller : Geary.BaseObject {
this.plugin_manager.notifications.email_sent(account, sent);
}
- private SimpleAction get_window_action(string action_name) {
- return (SimpleAction) this.main_window.lookup_action(action_name);
- }
-
- // Disables all single-message buttons and enables all multi-message buttons.
- public void enable_multiple_message_buttons() {
- main_window.main_toolbar.selected_conversations = this.selected_conversations.size;
-
- // Single message only buttons.
- get_window_action(MainWindow.ACTION_REPLY_TO_MESSAGE).set_enabled(false);
- get_window_action(MainWindow.ACTION_REPLY_ALL_MESSAGE).set_enabled(false);
- get_window_action(MainWindow.ACTION_FORWARD_MESSAGE).set_enabled(false);
-
- // Mutliple message buttons.
- get_window_action(MainWindow.ACTION_MOVE_MENU).set_enabled(current_folder is
Geary.FolderSupport.Move);
- get_window_action(MainWindow.ACTION_ARCHIVE_CONVERSATION).set_enabled(current_folder is
Geary.FolderSupport.Archive);
- get_window_action(MainWindow.ACTION_TRASH_CONVERSATION).set_enabled(current_folder_supports_trash());
- get_window_action(MainWindow.ACTION_DELETE_CONVERSATION).set_enabled(current_folder is
Geary.FolderSupport.Remove);
-
- cancel_context_dependent_buttons();
- enable_context_dependent_buttons_async.begin(true, cancellable_context_dependent_buttons);
- }
-
- // Enables or disables the message buttons on the toolbar.
- public void enable_message_buttons(bool sensitive) {
- main_window.main_toolbar.selected_conversations = this.selected_conversations.size;
-
- // No reply/forward in drafts folder.
- bool respond_sensitive = sensitive;
- if (current_folder != null && current_folder.special_folder_type == Geary.SpecialFolderType.DRAFTS)
- respond_sensitive = false;
-
- get_window_action(MainWindow.ACTION_REPLY_TO_MESSAGE).set_enabled(respond_sensitive);
- get_window_action(MainWindow.ACTION_REPLY_ALL_MESSAGE).set_enabled(respond_sensitive);
- get_window_action(MainWindow.ACTION_FORWARD_MESSAGE).set_enabled(respond_sensitive);
- get_window_action(MainWindow.ACTION_MOVE_MENU).set_enabled(sensitive && (current_folder is
Geary.FolderSupport.Move));
- get_window_action(MainWindow.ACTION_ARCHIVE_CONVERSATION).set_enabled(sensitive && (current_folder
is Geary.FolderSupport.Archive));
- get_window_action(MainWindow.ACTION_TRASH_CONVERSATION).set_enabled(sensitive &&
current_folder_supports_trash());
- get_window_action(MainWindow.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);
- }
-
- private async void enable_context_dependent_buttons_async(bool sensitive, Cancellable? cancellable) {
- Gee.MultiMap<Geary.EmailIdentifier, Type>? selected_operations = null;
- try {
- if (current_folder != null) {
- Geary.App.EmailStore? store = get_email_store_for_folder(current_folder);
- if (store != null) {
- selected_operations = yield store
- .get_supported_operations_async(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(MainWindow.ACTION_SHOW_MARK_MENU).set_enabled(sensitive &&
(typeof(Geary.FolderSupport.Mark) in supported_operations));
- get_window_action(MainWindow.ACTION_COPY_MENU).set_enabled(sensitive &&
(supported_operations.contains(typeof(Geary.FolderSupport.Copy))));
- }
-
// Returns a list of composer windows for an account, or null if none.
public Gee.List<ComposerWidget>? get_composer_widgets_for_account(Geary.AccountInformation account) {
Gee.LinkedList<ComposerWidget> ret = Geary.traverse<ComposerWidget>(composer_widgets)
@@ -1988,18 +1763,6 @@ public class Application.Controller : Geary.BaseObject {
}
}
- /**
- * Returns a read-only set of currently selected conversations.
- */
- public Gee.Set<Geary.App.Conversation> get_selected_conversations() {
- return selected_conversations.read_only_view;
- }
-
- private inline Geary.App.EmailStore? get_email_store_for_folder(Geary.Folder target) {
- AccountContext? context = this.accounts.get(target.account.information);
- return (context != null) ? context.emails : null;
- }
-
private bool should_add_folder(Gee.Collection<Geary.Folder>? all,
Geary.Folder folder) {
// if folder is openable, add it
diff --git a/src/client/components/main-window.vala b/src/client/components/main-window.vala
index 74a75602..1a446c34 100644
--- a/src/client/components/main-window.vala
+++ b/src/client/components/main-window.vala
@@ -153,6 +153,8 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface {
);
}
+ private enum ConversationCount { NONE, SINGLE, MULTIPLE; }
+
public new GearyApplication application {
get { return (GearyApplication) base.get_application(); }
@@ -203,6 +205,10 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface {
private MonitoredSpinner spinner = new MonitoredSpinner();
private Geary.AggregateProgressMonitor progress_monitor = new Geary.AggregateProgressMonitor();
+
+ private GLib.Cancellable action_update_cancellable = new GLib.Cancellable();
+
+
private Geary.TimeoutManager update_ui_timeout;
private int64 update_ui_last = 0;
@@ -275,6 +281,8 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface {
this.commands.redone.connect(on_command_execute);
update_command_actions();
+ update_conversation_actions(NONE);
+
this.application.engine.account_available.connect(on_account_available);
this.application.engine.account_unavailable.connect(on_account_unavailable);
@@ -536,6 +544,7 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface {
private void setup_layout(Configuration config) {
this.conversation_list_view = new ConversationListView(this);
this.conversation_list_view.load_more.connect(on_load_more);
+ this.conversation_list_view.conversations_selected.connect(on_conversations_selected);
this.conversation_viewer = new ConversationViewer(
this.application.config
@@ -670,18 +679,26 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface {
public void folder_selected(Geary.Folder? folder,
GLib.Cancellable? cancellable) {
- if (this.current_folder != null) {
- this.progress_monitor.remove(this.current_folder.opening_monitor);
- this.current_folder.properties.notify.disconnect(update_headerbar);
- close_conversation_monitor();
- }
+ if (this.current_folder != folder) {
+ if (this.current_folder != null) {
+ this.progress_monitor.remove(this.current_folder.opening_monitor);
+ this.current_folder.properties.notify.disconnect(update_headerbar);
+ close_conversation_monitor();
+ }
+
+ this.current_folder = folder;
- this.current_folder = folder;
+ this.conversation_viewer.show_loading();
+ update_conversation_actions(NONE);
+ this.main_toolbar.update_trash_button(
+ !this.is_shift_down && current_folder_supports_trash()
+ );
- if (folder != null) {
- this.progress_monitor.add(folder.opening_monitor);
- folder.properties.notify.connect(update_headerbar);
- open_conversation_monitor.begin(cancellable);
+ if (folder != null) {
+ this.progress_monitor.add(folder.opening_monitor);
+ folder.properties.notify.connect(update_headerbar);
+ open_conversation_monitor.begin(cancellable);
+ }
}
update_headerbar();
@@ -838,6 +855,58 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface {
}
}
+ private void on_conversations_selected(Gee.Set<Geary.App.Conversation> selected) {
+ this.main_toolbar.selected_conversations = selected.size;
+ if (this.current_folder != null && !this.has_composer) {
+ switch(selected.size) {
+ case 0:
+ update_conversation_actions(NONE);
+ this.conversation_viewer.show_none_selected();
+ break;
+
+ case 1:
+ Geary.App.Conversation convo = Geary.Collection.get_first(
+ selected
+ );
+
+ Application.Controller.AccountContext? context =
+ this.application.controller.get_context_for_account(
+ convo.base_folder.account.information
+ );
+
+ // It's possible for a conversation with zero email to
+ // be selected, when it has just evaporated after its
+ // last email was removed but the conversation monitor
+ // hasn't signalled its removal yet. In this case,
+ // just don't load it since it will soon disappear.
+ if (context != null && convo.get_count() > 0) {
+ this.conversation_viewer.load_conversation.begin(
+ convo,
+ context.emails,
+ context.contacts,
+ (obj, ret) => {
+ try {
+ this.conversation_viewer.load_conversation.end(ret);
+ update_conversation_actions(SINGLE);
+ } catch (GLib.IOError.CANCELLED err) {
+ // All good
+ } catch (Error err) {
+ debug("Unable to load conversation: %s",
+ err.message);
+ }
+ }
+ );
+ }
+ break;
+
+ default:
+ update_conversation_actions(MULTIPLE);
+ this.conversation_viewer.show_multiple_selected();
+ break;
+ }
+ }
+ }
+
private void on_conversation_count_changed() {
// Only update the UI if we don't currently have a composer,
// so we don't clobber it
@@ -849,7 +918,7 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface {
} else {
this.conversation_viewer.show_empty_folder();
}
- this.application.controller.enable_message_buttons(false);
+ update_conversation_actions(NONE);
} else {
// When not doing autoselect, we never get
// conversations_selected firing from the convo list,
@@ -857,7 +926,7 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface {
if (!this.application.config.autoselect &&
this.conversation_list_view.get_selection().count_selected_rows() == 0) {
this.conversation_viewer.show_none_selected();
- this.application.controller.enable_message_buttons(false);
+ update_conversation_actions(NONE);
}
}
}
@@ -955,6 +1024,100 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface {
this.info_bar_frame.set_visible(show_frame);
}
+ private void update_conversation_actions(ConversationCount count) {
+ bool sensitive = (count != NONE);
+ bool multiple = (count == MULTIPLE);
+
+ get_action(ACTION_FIND_IN_CONVERSATION).set_enabled(
+ sensitive && !multiple
+ );
+
+ bool reply_sensitive = (
+ sensitive &&
+ !multiple &&
+ this.current_folder != null &&
+ this.current_folder.special_folder_type != DRAFTS
+ );
+ get_action(ACTION_REPLY_TO_MESSAGE).set_enabled(reply_sensitive);
+ get_action(ACTION_REPLY_ALL_MESSAGE).set_enabled(reply_sensitive);
+ get_action(ACTION_FORWARD_MESSAGE).set_enabled(reply_sensitive);
+
+ bool move_enabled = (
+ sensitive && (current_folder is Geary.FolderSupport.Move)
+ );
+ this.main_toolbar.move_message_button.set_sensitive(move_enabled);
+ get_action(ACTION_MOVE_MENU).set_enabled(move_enabled);
+
+ bool copy_enabled = (
+ sensitive && (current_folder is Geary.FolderSupport.Copy)
+ );
+ this.main_toolbar.copy_message_button.set_sensitive(copy_enabled);
+ get_action(ACTION_COPY_MENU).set_enabled(move_enabled);
+
+ get_action(ACTION_ARCHIVE_CONVERSATION).set_enabled(
+ sensitive && (current_folder is Geary.FolderSupport.Archive)
+ );
+ get_action(ACTION_TRASH_CONVERSATION).set_enabled(
+ sensitive && current_folder_supports_trash()
+ );
+ get_action(ACTION_DELETE_CONVERSATION).set_enabled(
+ sensitive && (current_folder is Geary.FolderSupport.Remove)
+ );
+
+ this.update_context_dependent_actions.begin(sensitive);
+ }
+
+ private async void update_context_dependent_actions(bool sensitive) {
+ // Cancel any existing update that is running
+ this.action_update_cancellable.cancel();
+ GLib.Cancellable cancellable = new Cancellable();
+ this.action_update_cancellable = cancellable;
+
+ Gee.MultiMap<Geary.EmailIdentifier, Type>? selected_operations = null;
+ if (this.current_folder != null) {
+ Application.Controller.AccountContext? context =
+ this.application.controller.get_context_for_account(
+ this.current_folder.account.information
+ );
+ if (context != null) {
+ Gee.Collection<Geary.EmailIdentifier> ids =
+ new Gee.LinkedList<Geary.EmailIdentifier>();
+ foreach (Geary.App.Conversation convo in
+ this.conversation_list_view.get_selected_conversations()) {
+ ids.add_all(convo.get_email_ids());
+ }
+ try {
+ selected_operations = yield context.emails.get_supported_operations_async(
+ ids, cancellable
+ );
+ } catch (GLib.Error e) {
+ debug("Error checking for what operations are supported in the selected conversations:
%s",
+ e.message);
+ }
+ }
+ }
+
+ if (!cancellable.is_cancelled()) {
+ Gee.HashSet<Type> supported_operations = new Gee.HashSet<Type>();
+ if (selected_operations != null) {
+ supported_operations.add_all(selected_operations.get_values());
+ }
+
+ get_action(ACTION_SHOW_MARK_MENU).set_enabled(
+ sensitive &&
+ (typeof(Geary.FolderSupport.Mark) in supported_operations)
+ );
+ get_action(ACTION_COPY_MENU).set_enabled(
+ sensitive &&
+ (supported_operations.contains(typeof(Geary.FolderSupport.Copy)))
+ );
+ get_action(ACTION_MOVE_MENU).set_enabled(
+ sensitive &&
+ (supported_operations.contains(typeof(Geary.FolderSupport.Move)))
+ );
+ }
+ }
+
private inline void check_shift_event(Gdk.EventKey event) {
// FIXME: it's possible the user will press two shift keys. We want
// the shift key to report as released when they release ALL of them.
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]