[geary/wip/730682-refine-convo-list: 13/37] Initial implementation of refined conversation list ux.
- From: Michael Gratton <mjog src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [geary/wip/730682-refine-convo-list: 13/37] Initial implementation of refined conversation list ux.
- Date: Mon, 11 Dec 2017 21:13:41 +0000 (UTC)
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]