Conversation view. Closes #3808

This commit is contained in:
Eric Gregory 2011-11-01 15:49:06 -07:00
parent 7bda84d331
commit 43a5b6152b
8 changed files with 233 additions and 168 deletions

View file

@ -16,19 +16,21 @@ public class MainWindow : Gtk.Window {
private MessageViewer message_viewer = new MessageViewer();
private Geary.EngineAccount? account = null;
private Geary.Folder? current_folder = null;
private Geary.Conversations? current_conversations = null;
private bool second_list_pass_required = false;
private int window_width;
private int window_height;
private bool window_maximized;
private Gtk.HPaned folder_paned = new Gtk.HPaned();
private Gtk.HPaned messages_paned = new Gtk.HPaned();
private Cancellable cancellable = new Cancellable();
private Cancellable cancellable_folder = new Cancellable();
private Cancellable cancellable_message = new Cancellable();
public MainWindow() {
title = GearyApplication.NAME;
message_list_view = new MessageListView(message_list_store);
message_list_view.message_selected.connect(on_message_selected);
message_list_view.conversation_selected.connect(on_conversation_selected);
folder_list_view = new FolderListView(folder_list_store);
folder_list_view.folder_selected.connect(on_folder_selected);
@ -136,12 +138,12 @@ public class MainWindow : Gtk.Window {
// message viewer
Gtk.ScrolledWindow message_viewer_scrolled = new Gtk.ScrolledWindow(null, null);
message_viewer_scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC);
message_viewer_scrolled.add_with_viewport(message_viewer);
message_viewer_scrolled.add(message_viewer);
// three-pane display: message list left of current message on bottom separated by
// grippable
messages_paned.pack1(message_list_scrolled, false, false);
messages_paned.pack2(message_viewer_scrolled, true, false);
messages_paned.pack2(message_viewer_scrolled, true, true);
// three-pane display: folder list on left and messages on right separated by grippable
folder_paned.pack1(folder_list_scrolled, false, false);
@ -166,61 +168,77 @@ public class MainWindow : Gtk.Window {
}
private async void do_select_folder(Geary.Folder folder) throws Error {
cancel();
cancel_folder();
message_list_store.clear();
if (current_folder != null) {
current_folder.messages_appended.disconnect(on_folder_messages_appended);
yield current_folder.close_async();
}
current_folder = folder;
current_folder.messages_appended.connect(on_folder_messages_appended);
yield current_folder.open_async(true, cancellable);
yield current_folder.open_async(true, cancellable_folder);
current_conversations = new Geary.Conversations(current_folder,
MessageListStore.REQUIRED_FIELDS);
current_conversations.monitor_new_messages(cancellable_folder);
current_conversations.scan_started.connect(on_scan_started);
current_conversations.scan_error.connect(on_scan_error);
current_conversations.scan_completed.connect(on_scan_completed);
current_conversations.conversations_added.connect(on_conversations_added);
current_conversations.conversation_appended.connect(on_conversation_appended);
current_conversations.updated_placeholders.connect(on_updated_placeholders);
// Do a quick-list of the messages (which should return what's in the local store) if
// supported by the Folder, followed by a complete list if needed
second_list_pass_required =
current_folder.get_supported_list_flags().is_all_set(Geary.Folder.ListFlags.FAST);
current_folder.lazy_list_email(-1, FETCH_EMAIL_CHUNK_COUNT, MessageListStore.REQUIRED_FIELDS,
current_folder.get_supported_list_flags() & Geary.Folder.ListFlags.FAST,
on_list_email_ready, cancellable);
// Load all conversations from the DB.
current_conversations.lazy_load(-1, -1, Geary.Folder.ListFlags.FAST, cancellable_folder);
}
private void on_list_email_ready(Gee.List<Geary.Email>? email, Error? err) {
if (email != null && email.size > 0) {
int low = int.MAX;
int high = int.MIN;
foreach (Geary.Email envelope in email) {
if (envelope.location.position < low)
low = envelope.location.position;
if (envelope.location.position > high)
high = envelope.location.position;
if (!message_list_store.has_envelope(envelope))
message_list_store.append_envelope(envelope);
}
debug("Listed %d emails from position %d to %d", email.size, low, high);
}
public void on_scan_started(int low, int count) {
debug("on scan started");
}
public void on_scan_error(Error err) {
debug("Scan error: %s", err.message);
}
public void on_scan_completed() {
debug("on scan completed");
if (err != null) {
debug("Error while listing email: %s", err.message);
// TODO: Better error handling here
return;
do_fetch_previews.begin(cancellable_message);
}
public void on_conversations_added(Gee.Collection<Geary.Conversation> conversations) {
debug("on conversation added");
foreach (Geary.Conversation c in conversations) {
if (!message_list_store.has_conversation(c))
message_list_store.append_conversation(c);
}
// end of list, go get the previews for them
if (email == null)
do_fetch_previews.begin(cancellable);
}
public void on_conversation_appended(Geary.Conversation conversation,
Gee.Collection<Geary.Email> email) {
message_list_store.update_conversation(conversation);
}
public void on_updated_placeholders(Geary.Conversation conversation,
Gee.Collection<Geary.Email> email) {
message_list_store.update_conversation(conversation);
}
private async void do_fetch_previews(Cancellable? cancellable) throws Error {
int count = message_list_store.get_count();
for (int ctr = 0; ctr < count; ctr++) {
Geary.Email? email = message_list_store.get_message_at_index(ctr);
Geary.Email? email = message_list_store.get_newest_message_at_index(ctr);
if (email == null)
continue;
Geary.Email? body = yield current_folder.fetch_email_async(email.id,
Geary.Email.Field.HEADER | Geary.Email.Field.BODY | Geary.Email.Field.ENVELOPE |
Geary.Email.Field.PROPERTIES, cancellable);
@ -231,8 +249,8 @@ public class MainWindow : Gtk.Window {
if (second_list_pass_required) {
second_list_pass_required = false;
debug("Doing second list pass now");
current_folder.lazy_list_email(-1, FETCH_EMAIL_CHUNK_COUNT, MessageListStore.REQUIRED_FIELDS,
Geary.Folder.ListFlags.NONE, on_list_email_ready, cancellable);
current_conversations.lazy_load(-1, FETCH_EMAIL_CHUNK_COUNT, Geary.Folder.ListFlags.NONE,
cancellable);
}
}
@ -244,25 +262,26 @@ public class MainWindow : Gtk.Window {
}
}
private void on_message_selected(Geary.Email? email) {
if (email != null)
do_select_message.begin(email, on_select_message_completed);
private void on_conversation_selected(Geary.Conversation? conversation) {
if (conversation != null)
do_select_message.begin(conversation, on_select_message_completed);
}
private async void do_select_message(Geary.Email email) throws Error {
private async void do_select_message(Geary.Conversation conversation) throws Error {
if (current_folder == null) {
debug("Message %s selected with no folder selected", email.to_string());
debug("Conversation selected with no folder selected");
return;
}
debug("Fetching email %s", email.to_string());
Geary.Email full_email = yield current_folder.fetch_email_async(email.id,
MessageViewer.REQUIRED_FIELDS, cancellable);
cancel_message();
message_viewer.clear();
message_viewer.add_message(full_email);
foreach (Geary.Email email in conversation.get_pool_sorted(compare_email)) {
Geary.Email full_email = yield current_folder.fetch_email_async(email.id,
MessageViewer.REQUIRED_FIELDS, cancellable_message);
message_viewer.add_message(full_email);
}
}
private void on_select_message_completed(Object? source, AsyncResult result) {
@ -298,22 +317,6 @@ public class MainWindow : Gtk.Window {
}
}
private void on_folder_messages_appended() {
int high = message_list_store.get_highest_folder_position();
if (high < 0) {
debug("Unable to find highest message position in %s", current_folder.to_string());
return;
}
debug("Message(s) appended to %s, fetching email at %d and above", current_folder.to_string(),
high + 1);
// Want to get the one *after* the highest position in the message list
current_folder.lazy_list_email(high + 1, -1, MessageListStore.REQUIRED_FIELDS,
Geary.Folder.ListFlags.NONE, on_list_email_ready, cancellable);
}
private async void search_folders_for_children(Gee.Collection<Geary.Folder> folders) {
Gee.ArrayList<Geary.Folder> accumulator = new Gee.ArrayList<Geary.Folder>();
foreach (Geary.Folder folder in folders) {
@ -330,9 +333,17 @@ public class MainWindow : Gtk.Window {
on_folders_added_removed(accumulator, null);
}
private void cancel() {
Cancellable old_cancellable = cancellable;
cancellable = new Cancellable();
private void cancel_folder() {
Cancellable old_cancellable = cancellable_folder;
cancellable_folder = new Cancellable();
cancel_message();
old_cancellable.cancel();
}
private void cancel_message() {
Cancellable old_cancellable = cancellable_message;
cancellable_message = new Cancellable();
old_cancellable.cancel();
}

View file

@ -5,6 +5,7 @@
*/
public class MessageListStore : Gtk.TreeStore {
public const Geary.Email.Field REQUIRED_FIELDS =
Geary.Email.Field.ENVELOPE | Geary.Email.Field.PROPERTIES;
@ -23,7 +24,7 @@ public class MessageListStore : Gtk.TreeStore {
public static Type[] get_types() {
return {
typeof (FormattedMessageData), // MESSAGE_DATA
typeof (Geary.Email) // MESSAGE_OBJECT
typeof (Geary.Conversation) // MESSAGE_OBJECT
};
}
@ -47,39 +48,53 @@ public class MessageListStore : Gtk.TreeStore {
}
// The Email should've been fetched with REQUIRED_FIELDS.
public void append_envelope(Geary.Email envelope) {
assert(envelope.fields.fulfills(REQUIRED_FIELDS));
public void append_conversation(Geary.Conversation conversation) {
Gtk.TreeIter iter;
append(out iter, null);
set(iter,
Column.MESSAGE_DATA, new FormattedMessageData.from_email(envelope),
Column.MESSAGE_OBJECT, envelope
);
Gee.SortedSet<Geary.Email>? pool = conversation.get_pool_sorted(compare_email);
envelope.location.position_deleted.connect(on_email_position_deleted);
if (pool != null)
set(iter,
Column.MESSAGE_DATA, new FormattedMessageData.from_email(pool.first()),
Column.MESSAGE_OBJECT, conversation
);
}
// The Email should've been fetched with REQUIRED_FIELDS.
public bool has_envelope(Geary.Email envelope) {
assert(envelope.fields.fulfills(REQUIRED_FIELDS));
public void update_conversation(Geary.Conversation conversation) {
Gtk.TreeIter iter;
if (!find_conversation(conversation, out iter)) {
// Unknown conversation, attempt to append it.
append_conversation(conversation);
return;
}
Gee.SortedSet<Geary.Email>? pool = conversation.get_pool_sorted(compare_email);
// Update the preview.
set(iter, Column.MESSAGE_DATA, new FormattedMessageData.from_email(pool.first()));
}
public bool has_conversation(Geary.Conversation conversation) {
int count = get_count();
for (int ctr = 0; ctr < count; ctr++) {
Geary.Email? email = get_message_at_index(ctr);
if (email == null)
break;
if (email.location.position == envelope.location.position)
if (conversation == get_conversation_at_index(ctr))
return true;
}
return false;
}
public Geary.Email? get_message_at_index(int index) {
return get_message_at(new Gtk.TreePath.from_indices(index, -1));
public Geary.Conversation? get_conversation_at_index(int index) {
return get_conversation_at(new Gtk.TreePath.from_indices(index, -1));
}
public Geary.Email? get_newest_message_at_index(int index) {
Geary.Conversation? c = get_conversation_at_index(index);
Gee.SortedSet<Geary.Email>? pool = c.get_pool_sorted(compare_email);
return pool != null ? pool.first() : null;
}
public void set_preview_at_index(int index, Geary.Email email) {
@ -97,77 +112,43 @@ public class MessageListStore : Gtk.TreeStore {
return iter_n_children(null);
}
public Geary.Email? get_message_at(Gtk.TreePath path) {
Gtk.TreeIter iter;
public Geary.Conversation? get_conversation_at(Gtk.TreePath path) {
Gtk.TreeIter iter;
if (!get_iter(out iter, path))
return null;
Geary.Email email;
get(iter, Column.MESSAGE_OBJECT, out email);
Geary.Conversation? conversation;
get(iter, Column.MESSAGE_OBJECT, out conversation);
return email;
return conversation;
}
// Returns -1 if the list is empty.
public int get_highest_folder_position() {
Gtk.TreeIter iter;
if (!get_iter_first(out iter))
return -1;
int high = int.MIN;
// TODO: It would be more efficient to maintain highest and lowest values in a table or
// as items are added and removed; this will do for now.
do {
Geary.Email email;
get(iter, Column.MESSAGE_OBJECT, out email);
if (email.location.position > high)
high = email.location.position;
} while (iter_next(ref iter));
return high;
}
private bool remove_at_position(int position) {
Gtk.TreeIter iter;
if (!get_iter_first(out iter))
return false;
do {
Geary.Email email;
get(iter, Column.MESSAGE_OBJECT, out email);
if (email.location.position == position) {
remove(iter);
email.location.position_deleted.disconnect(on_email_position_deleted);
return true;
}
} while (iter_next(ref iter));
private bool find_conversation(Geary.Conversation conversation, out Gtk.TreeIter iter) {
iter = Gtk.TreeIter();
int count = get_count();
for (int ctr = 0; ctr < count; ctr++) {
if (conversation == get_conversation_at_index(ctr))
return get_iter(out iter, new Gtk.TreePath.from_indices(ctr, -1));
}
return false;
}
private int sort_by_date(Gtk.TreeModel model, Gtk.TreeIter aiter, Gtk.TreeIter biter) {
Geary.Email aenvelope;
get(aiter, Column.MESSAGE_OBJECT, out aenvelope);
Geary.Conversation a, b;
Geary.Email benvelope;
get(biter, Column.MESSAGE_OBJECT, out benvelope);
get(aiter, Column.MESSAGE_OBJECT, out a);
get(biter, Column.MESSAGE_OBJECT, out b);
int diff = aenvelope.date.value.compare(benvelope.date.value);
if (diff != 0)
return diff;
Gee.SortedSet<Geary.Email>? apool = a.get_pool_sorted(compare_email);
Gee.SortedSet<Geary.Email>? bpool = b.get_pool_sorted(compare_email);
// stabilize sort by using the mail's position, which is always unique in a folder
return aenvelope.location.position - benvelope.location.position;
}
private void on_email_position_deleted(int position) {
if (!remove_at_position(position))
debug("on_email_position_deleted: unable to find email at position %d", position);
if (apool == null || apool.first() == null)
return -1;
else if (bpool == null || bpool.first() == null)
return 1;
return compare_email(apool.first(), bpool.first());
}
}

View file

@ -5,7 +5,7 @@
*/
public class MessageListView : Gtk.TreeView {
public signal void message_selected(Geary.Email? email);
public signal void conversation_selected(Geary.Conversation? conversation);
public MessageListView(MessageListStore store) {
set_model(store);
@ -48,14 +48,14 @@ public class MessageListView : Gtk.TreeView {
Gtk.TreeModel model;
Gtk.TreePath? path = get_selection().get_selected_rows(out model).nth_data(0);
if (path == null) {
message_selected(null);
conversation_selected(null);
return;
}
Geary.Email? email = get_store().get_message_at(path);
if (email != null)
message_selected(email);
Geary.Conversation? conversation = get_store().get_conversation_at(path);
if (conversation != null)
conversation_selected(conversation);
}
}

View file

@ -4,7 +4,7 @@
* (version 2.1 or later). See the COPYING file in this distribution.
*/
public class MessageViewer : Gtk.EventBox {
public class MessageViewer : Gtk.Viewport {
public const Geary.Email.Field REQUIRED_FIELDS =
Geary.Email.Field.HEADER
| Geary.Email.Field.BODY
@ -15,6 +15,7 @@ public class MessageViewer : Gtk.EventBox {
private const int HEADER_COL_SPACING = 10;
private const int HEADER_ROW_SPACING = 3;
private const int MESSAGE_BOX_MARGIN = 10;
// List of emails corresponding with VBox.
private Gee.LinkedList<Geary.Email> messages = new Gee.LinkedList<Geary.Email>();
@ -48,7 +49,7 @@ public class MessageViewer : Gtk.EventBox {
border-color: #cccccc;
border-style: solid;
border-width: 1;
-GtkWidget-separator-height: 1;
-GtkWidget-separator-height: 2;
}
""";
@ -77,12 +78,10 @@ public class MessageViewer : Gtk.EventBox {
}
// Only include to string if it's not just this account.
// TODO: <ultiple accounts.
// TODO: multiple accounts.
string to = "";
if (email.to != null) {
Geary.RFC822.MailboxAddresses addr = new Geary.RFC822.MailboxAddresses.
from_rfc822_string(email.to.to_string());
if (!(addr.get_all().size == 1 && addr.get_all().get(0).address == username))
if (!(email.to.get_all().size == 1 && email.to.get_all().get(0).address == username))
to = email.to.to_string();
}
@ -119,7 +118,12 @@ public class MessageViewer : Gtk.EventBox {
debug("Could not get message text. %s", err.message);
}
message_box.pack_start(container, false, false);
Gtk.EventBox box = new Gtk.EventBox();
box.add(container);
box.margin = MESSAGE_BOX_MARGIN;
message_box.pack_end(box, false, false);
message_box.show_all();
add_style();
}
@ -163,7 +167,8 @@ public class MessageViewer : Gtk.EventBox {
Gdk.RGBA color = Gdk.RGBA();
color.parse(sample_view.style.base[0].to_string());
override_background_color(Gtk.StateFlags.NORMAL, color);
foreach (Gtk.Widget w in message_box.get_children())
w.override_background_color(Gtk.StateFlags.NORMAL, color);
}
}

View file

@ -0,0 +1,14 @@
/* Copyright 2011 Yorba Foundation
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
public int compare_email(Geary.Email aenvelope, Geary.Email benvelope) {
int diff = aenvelope.date.value.compare(benvelope.date.value);
if (diff != 0)
return diff;
// stabilize sort by using the mail's position, which is always unique in a folder
return aenvelope.location.position - benvelope.location.position;
}

View file

@ -19,7 +19,8 @@ client_src = [
'ui/message-list-view.vala',
'ui/message-viewer.vala',
'util/util-keyring.vala'
'util/util-keyring.vala',
'util/util-email.vala'
]
gsettings_schemas = [

View file

@ -45,23 +45,35 @@ public abstract class Geary.Conversation : Object {
*/
public abstract Gee.Collection<Geary.ConversationNode>? get_replies(Geary.ConversationNode node);
/**
* Returns all ConversationNodes in the conversation, which can then be sorted by the caller's
* own requirements.
*
/**
* Returns all emails in the conversation.
* Only returns nodes that have an e-mail.
*/
public virtual Gee.Collection<Geary.ConversationNode>? get_pool() {
Gee.HashSet<ConversationNode> pool = new Gee.HashSet<ConversationNode>();
public virtual Gee.Set<Geary.Email>? get_pool() {
Gee.HashSet<Email> pool = new Gee.HashSet<Email>();
gather(pool, get_origin());
return (pool.size > 0) ? pool : null;
}
private void gather(Gee.Set<ConversationNode> pool, ConversationNode? current) {
/**
* Returns all emails in the conversation, sorted by compare_func.
* Only returns nodes that have an e-mail.
*/
public virtual Gee.SortedSet<Geary.Email>? get_pool_sorted(CompareFunc<Geary.Email>?
compare_func = null) {
Gee.TreeSet<Email> pool = new Gee.TreeSet<Email>(compare_func);
gather(pool, get_origin());
return (pool.size > 0) ? pool : null;
}
private void gather(Gee.Set<Email> pool, ConversationNode? current) {
if (current == null)
return;
pool.add(current);
if (current.get_email() != null)
pool.add(current.get_email());
Gee.Collection<Geary.ConversationNode>? children = get_replies(current);
if (children != null) {

View file

@ -166,6 +166,8 @@ public class Geary.Conversations : Object {
private Gee.Map<Geary.RFC822.MessageID, Node> id_map = new Gee.HashMap<Geary.RFC822.MessageID,
Node>(Geary.Hashable.hash_func, Geary.Equalable.equal_func);
private Gee.Set<ImplConversation> conversations = new Gee.HashSet<ImplConversation>();
private bool monitor_new = false;
private Cancellable? cancellable_monitor = null;
public virtual signal void scan_started(int low, int count) {
}
@ -196,6 +198,9 @@ public class Geary.Conversations : Object {
// Manually detach all the weak refs in the Conversation objects
foreach (ImplConversation conversation in conversations)
conversation.owner = null;
if (monitor_new)
folder.messages_appended.disconnect(on_folder_messages_appended);
}
protected virtual void notify_scan_started(int low, int count) {
@ -247,6 +252,16 @@ public class Geary.Conversations : Object {
folder.lazy_list_email(low, count, required_fields, flags, on_email_listed, cancellable);
}
public bool monitor_new_messages(Cancellable? cancellable = null) {
if (monitor_new)
return false;
monitor_new = true;
cancellable_monitor = cancellable;
folder.messages_appended.connect(on_folder_messages_appended);
return true;
}
private void on_email_listed(Gee.List<Geary.Email>? emails, Error? err) {
if (err != null)
notify_scan_error(err);
@ -340,7 +355,7 @@ public class Geary.Conversations : Object {
RFC822.MessageID ancestor_id = ancestors[ctr];
if (seen.contains(ancestor_id)) {
warning("Loop detected in conversation: %s seen twice", ancestor_id.to_string());
message("Loop detected in conversation: %s seen twice", ancestor_id.to_string());
continue;
}
@ -453,5 +468,31 @@ public class Geary.Conversations : Object {
}
}
}
private void on_folder_messages_appended() {
// Find highest position.
// TODO: optimize.
int high = -1;
foreach (Conversation c in conversations)
foreach (Email e in c.get_pool())
if (e.location.position > high)
high = e.location.position;
if (high < 0) {
debug("Unable to find highest message position in %s", folder.to_string());
return;
}
debug("Message(s) appended to %s, fetching email at %d and above", folder.to_string(),
high + 1);
// Want to get the one *after* the highest position in the list
try {
lazy_load(high + 1, -1, Folder.ListFlags.NONE, cancellable_monitor);
} catch (Error e) {
warning("Error getting new mail: %s", e.message);
}
}
}