[geary/mjog/account-command-stacks: 23/25] Re-select folder, conversation and email on EmailCommand undo



commit 2d47e4802b2bc6b5168788f6b743da49c60f7e91
Author: Michael Gratton <mike vee net>
Date:   Tue Nov 5 10:46:23 2019 +1100

    Re-select folder, conversation and email on EmailCommand undo
    
    When an EmailCommand is undone, select the folder, conversation and if
    relevant scroll to the email in question so as to provide better
    context.
    
    This isn't 100% bulletproof, but is about 80% of the way there. The
    remainer requires some major Engine rework to decouple local and remote
    actions.

 po/POTFILES.in                                     |   1 +
 src/client/application/geary-application.vala      |   6 +-
 src/client/components/main-window.vala             | 230 ++++++++++++++++-----
 .../conversation-list/conversation-list-view.vala  |  23 +--
 .../conversation-viewer/conversation-list-box.vala | 127 +++++++++---
 .../conversation-viewer/conversation-viewer.vala   |  13 +-
 src/engine/app/app-conversation-monitor.vala       |  29 ++-
 .../conversation-monitor/app-load-operation.vala   |  51 +++++
 src/engine/meson.build                             |   1 +
 9 files changed, 383 insertions(+), 98 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 993c0673..502e679a 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -158,6 +158,7 @@ src/engine/app/conversation-monitor/app-external-append-operation.vala
 src/engine/app/conversation-monitor/app-fill-window-operation.vala
 src/engine/app/conversation-monitor/app-insert-operation.vala
 src/engine/app/conversation-monitor/app-local-search-operation.vala
+src/engine/app/conversation-monitor/app-load-operation.vala
 src/engine/app/conversation-monitor/app-remove-operation.vala
 src/engine/app/conversation-monitor/app-reseed-operation.vala
 src/engine/app/conversation-monitor/app-terminate-operation.vala
diff --git a/src/client/application/geary-application.vala b/src/client/application/geary-application.vala
index 76d34e15..4fb97a0e 100644
--- a/src/client/application/geary-application.vala
+++ b/src/client/application/geary-application.vala
@@ -535,7 +535,11 @@ public class GearyApplication : Gtk.Application {
     public async void show_email(Geary.Folder? folder,
                                  Geary.EmailIdentifier id) {
         yield this.present();
-        yield this.controller.main_window.show_email(folder, id, true);
+        this.controller.main_window.show_email.begin(
+            folder,
+            Geary.Collection.single(id),
+            true
+        );
     }
 
     public async void show_folder(Geary.Folder? folder) {
diff --git a/src/client/components/main-window.vala b/src/client/components/main-window.vala
index 3dc5ac57..20eb7088 100644
--- a/src/client/components/main-window.vala
+++ b/src/client/components/main-window.vala
@@ -465,15 +465,77 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface {
         update_headerbar();
     }
 
+    /** Selects the given account, folder and conversations. */
+    public async void show_conversations(Geary.Folder location,
+                                         Gee.Collection<Geary.App.Conversation> to_show,
+                                         bool is_interactive) {
+        yield select_folder(location, is_interactive);
+        // The folder may have changed again by the type the async
+        // call returns, so only continue if still current
+        if (this.selected_folder == location) {
+            // Since conversation ids don't persist between
+            // conversation monitor instances, need to load
+            // conversations based on their messages.
+            var latest_email = new Gee.HashSet<Geary.EmailIdentifier>();
+            foreach (var stale in to_show) {
+                Geary.Email? first = stale.get_latest_recv_email(IN_FOLDER);
+                if (first != null) {
+                    latest_email.add(first.id);
+                }
+            }
+            var loaded = yield load_conversations_for_email(
+                location, latest_email
+            );
+            if (!loaded.is_empty) {
+                yield select_conversations(
+                    loaded,
+                    Gee.Collection.empty<Geary.EmailIdentifier>(),
+                    is_interactive
+                );
+            }
+        }
+    }
+
     /** Selects the given account, folder and email. */
-    public async void show_email(Geary.Folder folder,
-                                 Geary.EmailIdentifier id,
+    public async void show_email(Geary.Folder location,
+                                 Gee.Collection<Geary.EmailIdentifier> to_show,
                                  bool is_interactive) {
-        yield select_folder(folder, is_interactive);
-        Geary.App.Conversation? conversation =
-            this.conversations.get_by_email_identifier(id);
-        if (conversation != null) {
-            this.conversation_list_view.select_conversation(conversation);
+        yield select_folder(location, is_interactive);
+        // The folder may have changed again by the type the async
+        // call returns, so only continue if still current
+        if (this.selected_folder == location) {
+            var loaded = yield load_conversations_for_email(location, to_show);
+
+            if (loaded.size == 1) {
+                // A single conversation was loaded, so ensure we
+                // scroll to the email in the conversation.
+                Geary.App.Conversation target = Geary.Collection.get_first(loaded);
+                ConversationListBox? current_list =
+                    this.conversation_viewer.current_list;
+                if (current_list != null &&
+                    current_list.conversation == target) {
+                    // The target conversation is already loaded, just
+                    // scroll to the messages.
+                    //
+                    // XXX this is actually racy, since the view may
+                    // still be in the middle of loading the messages
+                    // obtained from the conversation monitor when
+                    // this call is made.
+                    current_list.scroll_to_messages(to_show);
+                } else {
+                    // The target conversation is not loaded, select
+                    // it and scroll to the messages.
+                    yield select_conversations(loaded, to_show, is_interactive);
+                }
+            } else if (!loaded.is_empty) {
+                // Multiple conversations found, just select those
+                yield select_conversations(
+                    loaded,
+                    Gee.Collection.empty<Geary.EmailIdentifier>(),
+                    is_interactive
+                );
+            } else {
+            }
         }
     }
 
@@ -961,6 +1023,42 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface {
         return (dialog.run() == Gtk.ResponseType.OK);
     }
 
+
+    private async Gee.Collection<Geary.App.Conversation>
+        load_conversations_for_email(
+            Geary.Folder location,
+            Gee.Collection<Geary.EmailIdentifier> to_load) {
+        bool was_loaded = false;
+        // Can't assume the conversation monitor is valid, so check
+        // it first.
+        if (this.conversations != null &&
+            this.conversations.base_folder == location) {
+            try {
+                yield this.conversations.load_email(to_load, this.folder_open);
+                was_loaded = true;
+            } catch (GLib.Error err) {
+                debug("Error loading conversations to show them: %s",
+                      err.message);
+            }
+        }
+
+        // Conversation monitor may have changed since resuming from
+        // the last async statement, so check it's still valid again.
+        var loaded = new Gee.HashSet<Geary.App.Conversation>();
+        if (was_loaded &&
+            this.conversations != null &&
+            this.conversations.base_folder == location) {
+            foreach (var id in to_load) {
+                Geary.App.Conversation? conversation =
+                    this.conversations.get_by_email_identifier(id);
+                if (conversation != null) {
+                    loaded.add(conversation);
+                }
+            }
+        }
+        return loaded;
+    }
+
     private inline void handle_error(Geary.AccountInformation? account,
                                      GLib.Error error) {
         Geary.ProblemReport? report = (account != null)
@@ -1025,6 +1123,63 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface {
         }
     }
 
+    private async void select_conversations(Gee.Collection<Geary.App.Conversation> to_select,
+                                            Gee.Collection<Geary.EmailIdentifier> scroll_to,
+                                            bool is_interactive) {
+        bool start_mark_timer = (
+            this.previous_selection_was_interactive && is_interactive
+        );
+        this.previous_selection_was_interactive = is_interactive;
+
+        // Ensure that the conversations are selected in the UI if
+        // this was called by something other than the selection
+        // changed callback. That will check to ensure that we're not
+        // setting it again.
+        this.conversation_list_view.select_conversations(to_select);
+
+        this.main_toolbar.selected_conversations = to_select.size;
+        if (this.selected_folder != null && !this.has_composer) {
+            switch(to_select.size) {
+            case 0:
+                update_conversation_actions(NONE);
+                this.conversation_viewer.show_none_selected();
+                break;
+
+            case 1:
+                update_conversation_actions(SINGLE);
+                Geary.App.Conversation convo = Geary.Collection.get_first(to_select);
+
+                // 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.
+                Application.Controller.AccountContext? context = this.context;
+                if (context != null && convo.get_count() > 0) {
+                    try {
+                        yield this.conversation_viewer.load_conversation(
+                            convo,
+                            scroll_to,
+                            context.emails,
+                            context.contacts,
+                            start_mark_timer
+                        );
+                    } catch (GLib.IOError.CANCELLED err) {
+                        // All good
+                    } catch (GLib.Error err) {
+                        handle_error(convo.base_folder.account.information, err);
+                    }
+                }
+                break;
+
+            default:
+                update_conversation_actions(MULTIPLE);
+                this.conversation_viewer.show_multiple_selected();
+                break;
+            }
+        }
+    }
+
     private async void open_conversation_monitor(Geary.App.ConversationMonitor to_open,
                                                  GLib.Cancellable cancellable) {
         to_open.scan_completed.connect(on_scan_completed);
@@ -1100,54 +1255,7 @@ 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.selected_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
-                );
-
-                // 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.
-                Application.Controller.AccountContext? context = this.context;
-                if (context != null && convo.get_count() > 0) {
-                    this.conversation_viewer.load_conversation.begin(
-                        convo,
-                        context.emails,
-                        context.contacts,
-                        this.previous_selection_was_interactive,
-                        (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;
-            }
-        }
-
-        this.previous_selection_was_interactive = true;
+        this.select_conversations.begin(selected, Gee.Collection.empty(), true);
     }
 
     private void on_conversation_count_changed() {
@@ -1487,6 +1595,18 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface {
     }
 
     private void on_command_undo(Application.Command command) {
+        Application.EmailCommand? email = command as Application.EmailCommand;
+        if (email != null) {
+            if (email.conversations.size > 1) {
+                this.show_conversations.begin(
+                    email.location, email.conversations, false
+                );
+            } else {
+                this.show_email.begin(
+                    email.location, email.email, false
+                );
+            }
+        }
         if (command.undone_label != null) {
             Components.InAppNotification ian =
                 new Components.InAppNotification(command.undone_label);
diff --git a/src/client/conversation-list/conversation-list-view.vala 
b/src/client/conversation-list/conversation-list-view.vala
index d1df162f..0be2d5ec 100644
--- a/src/client/conversation-list/conversation-list-view.vala
+++ b/src/client/conversation-list/conversation-list-view.vala
@@ -496,18 +496,17 @@ public class ConversationListView : Gtk.TreeView, Geary.BaseInterface {
         scheduled_update_visible_conversations = Geary.Scheduler.on_idle(update_visible_conversations);
     }
 
-    public void select_conversation(Geary.App.Conversation conversation) {
-        Gtk.TreePath path = get_model().get_path_for_conversation(conversation);
-        if (path != null)
-            set_cursor(path, null, false);
-    }
-
-    public void select_conversations(Gee.Set<Geary.App.Conversation> conversations) {
-        Gtk.TreeSelection selection = get_selection();
-        foreach (Geary.App.Conversation conversation in conversations) {
-            Gtk.TreePath path = get_model().get_path_for_conversation(conversation);
-            if (path != null)
-                selection.select_path(path);
+    public void select_conversations(Gee.Collection<Geary.App.Conversation> new_selection) {
+        if (this.selected.size != new_selection.size ||
+            !this.selected.contains_all(new_selection)) {
+            Gtk.TreeSelection selection = get_selection();
+            selection.unselect_all();
+            foreach (var conversation in new_selection) {
+                Gtk.TreePath path = get_model().get_path_for_conversation(conversation);
+                if (path != null) {
+                    selection.select_path(path);
+                }
+            }
         }
     }
 
diff --git a/src/client/conversation-viewer/conversation-list-box.vala 
b/src/client/conversation-viewer/conversation-list-box.vala
index 7c56823c..b4034909 100644
--- a/src/client/conversation-viewer/conversation-list-box.vala
+++ b/src/client/conversation-viewer/conversation-list-box.vala
@@ -121,7 +121,8 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface {
         /**
          * Loads search term matches for this list's emails.
          */
-        public async void highlight_matching_email(Geary.SearchQuery query)
+        public async void highlight_matching_email(Geary.SearchQuery query,
+                                                   bool enable_scroll)
             throws GLib.Error {
             cancel();
 
@@ -164,8 +165,8 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface {
                             first = row;
                         }
                     }
-                    if (first != null) {
-                        this.list.scroll_to(first);
+                    if (first != null && enable_scroll) {
+                        this.list.scroll_to_row(first);
                     }
 
                     // Now expand them all
@@ -529,6 +530,7 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface {
     // The id of the draft referred to by the current composer.
     private Geary.EmailIdentifier? draft_id = null;
 
+    private bool suppress_mark_timer;
     private Geary.TimeoutManager mark_read_timer;
 
     private GLib.SimpleActionGroup email_actions = new GLib.SimpleActionGroup();
@@ -605,6 +607,7 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface {
      * Constructs a new conversation list box instance.
      */
     public ConversationListBox(Geary.App.Conversation conversation,
+                               bool suppress_mark_timer,
                                Geary.App.EmailStore email_store,
                                Application.ContactStore contacts,
                                Configuration config,
@@ -617,6 +620,7 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface {
 
         this.search = new SearchManager(this, conversation);
 
+        this.suppress_mark_timer = suppress_mark_timer;
         this.mark_read_timer = new Geary.TimeoutManager.milliseconds(
             MARK_READ_TIMEOUT_MSEC, this.check_mark_read
         );
@@ -651,8 +655,8 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface {
         base.destroy();
     }
 
-    public async void load_conversation(Geary.SearchQuery? query,
-                                        bool start_mark_timer)
+    public async void load_conversation(Gee.Collection<Geary.EmailIdentifier> scroll_to,
+                                        Geary.SearchQuery? query)
         throws GLib.Error {
         set_sort_func(null);
 
@@ -668,17 +672,44 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface {
         Geary.Email? first_interesting = null;
         Gee.LinkedList<Geary.Email> post_interesting =
             new Gee.LinkedList<Geary.Email>();
-        foreach (Geary.Email email in all_email) {
-            if (first_interesting == null) {
-                if (is_interesting(email)) {
-                    first_interesting = email;
+
+        if (!scroll_to.is_empty) {
+            var valid_scroll_to = Geary.traverse(scroll_to).filter(
+                id => this.conversation.contains_email_by_id(id)
+            ).to_array_list();
+            valid_scroll_to.sort((a, b) => a.natural_sort_comparator(b));
+            var first_scroll = Geary.Collection.get_first(valid_scroll_to);
+
+            if (first_scroll != null) {
+                foreach (Geary.Email email in all_email) {
+                    if (first_interesting == null) {
+                        if (email.id == first_scroll) {
+                            first_interesting = email;
+                        } else {
+                            // Inserted reversed so most recent uninteresting
+                            // rows are added first.
+                            uninteresting.insert(0, email);
+                        }
+                    } else {
+                        post_interesting.add(email);
+                    }
+                }
+            }
+        }
+
+        if (first_interesting == null) {
+            foreach (Geary.Email email in all_email) {
+                if (first_interesting == null) {
+                    if (is_interesting(email)) {
+                        first_interesting = email;
+                    } else {
+                        // Inserted reversed so most recent uninteresting
+                        // rows are added first.
+                        uninteresting.insert(0, email);
+                    }
                 } else {
-                    // Inserted reversed so most recent uninteresting
-                    // rows are added first.
-                    uninteresting.insert(0, email);
+                    post_interesting.add(email);
                 }
-            } else {
-                post_interesting.add(email);
             }
         }
 
@@ -700,12 +731,8 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface {
         yield interesting_row.view.load_contacts();
         yield interesting_row.expand();
         this.finish_loading.begin(
-            query, uninteresting, post_interesting
+            query, scroll_to.is_empty, uninteresting, post_interesting
         );
-
-        if (start_mark_timer) {
-            this.mark_read_timer.start();
-        }
     }
 
     /** Cancels loading the current conversation, if still in progress */
@@ -713,6 +740,54 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface {
         this.cancellable.cancel();
     }
 
+    /** Scrolls to the closest message in the current conversation. */
+    public void scroll_to_messages(Gee.Collection<Geary.EmailIdentifier> targets) {
+        // Get the currently displayed email, allowing for some
+        // padding at the top
+        Gtk.ListBoxRow? current_child = get_row_at_y(32);
+
+        // Find the row currently at the top of the viewport
+        EmailRow? current = null;
+        if (current_child != null) {
+            int pos = current_child.get_index();
+            do {
+                current = current_child as EmailRow;
+                current_child = get_row_at_index(--pos);
+            } while (current == null && pos > 0);
+        }
+
+        EmailRow? best = null;
+        // Find the message closest to the current message, preferring
+        // an earlier one. If there's no current message, the list is
+        // empty and we don't have anything to scroll to anyway.
+        if (current != null) {
+            uint closest_distance = uint.MAX;
+            foreach (var id in targets) {
+                EmailRow? target = this.email_rows[id];
+                if (target != null) {
+                    uint distance = (
+                        current.get_index() - target.get_index()
+                    ).abs();
+                    if (distance < closest_distance ||
+                        (distance == closest_distance &&
+                         Geary.Email.compare_sent_date_ascending(
+                             target.email, best.email
+                         ) < 0)) {
+                        debug("XXX have new best row....");
+                        closest_distance = distance;
+                        best = target;
+                    }
+
+                }
+            }
+        }
+
+        if (best != null) {
+            scroll_to_row(best);
+            best.expand.begin();
+        }
+    }
+
     /**
      * Returns the email view to be replied to, if any.
      *
@@ -772,7 +847,7 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface {
         row.enable_should_scroll();
         // Use row param rather than row var from closure to avoid a
         // circular ref.
-        row.should_scroll.connect((row) => { scroll_to(row); });
+        row.should_scroll.connect((row) => { scroll_to_row(row); });
         add(row);
         this.has_composer = true;
 
@@ -862,6 +937,7 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface {
     }
 
     private async void finish_loading(Geary.SearchQuery? query,
+                                      bool enable_query_scroll,
                                       Gee.LinkedList<Geary.Email> to_insert,
                                       Gee.LinkedList<Geary.Email> to_append)
         throws GLib.Error {
@@ -917,7 +993,9 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface {
             // XXX this sucks for large conversations because it can take
             // a long time for the load to complete and hence for
             // matches to show up.
-            yield this.search.highlight_matching_email(query);
+            yield this.search.highlight_matching_email(
+                query, enable_query_scroll
+            );
         }
     }
 
@@ -1024,7 +1102,7 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface {
         }
     }
 
-    private void scroll_to(ConversationRow row) {
+    private void scroll_to_row(ConversationRow row) {
         Gtk.Allocation? alloc = null;
         row.get_allocation(out alloc);
         int y = 0;
@@ -1203,7 +1281,10 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface {
                                               GLib.ParamSpec param) {
         ConversationEmail? view = obj as ConversationEmail;
         if (view != null && view.message_body_state == COMPLETED) {
-            this.mark_read_timer.start();
+            if (!this.suppress_mark_timer) {
+                this.mark_read_timer.start();
+            }
+            this.suppress_mark_timer = false;
         }
     }
 
diff --git a/src/client/conversation-viewer/conversation-viewer.vala 
b/src/client/conversation-viewer/conversation-viewer.vala
index 078086fe..d1386ab6 100644
--- a/src/client/conversation-viewer/conversation-viewer.vala
+++ b/src/client/conversation-viewer/conversation-viewer.vala
@@ -232,7 +232,8 @@ public class ConversationViewer : Gtk.Stack, Geary.BaseInterface {
      * Shows a conversation in the viewer.
      */
     public async void load_conversation(Geary.App.Conversation conversation,
-                                        Geary.App.EmailStore emails,
+                                        Gee.Collection<Geary.EmailIdentifier> scroll_to,
+                                        Geary.App.EmailStore store,
                                         Application.ContactStore contacts,
                                         bool start_mark_timer)
         throws GLib.Error {
@@ -240,7 +241,8 @@ public class ConversationViewer : Gtk.Stack, Geary.BaseInterface {
 
         ConversationListBox new_list = new ConversationListBox(
             conversation,
-            emails,
+            !start_mark_timer,
+            store,
             contacts,
             this.config,
             this.conversation_scroller.get_vadjustment()
@@ -281,7 +283,7 @@ public class ConversationViewer : Gtk.Stack, Geary.BaseInterface {
             }
         }
 
-        yield new_list.load_conversation(query, start_mark_timer);
+        yield new_list.load_conversation(scroll_to, query);
     }
 
     // Add a new conversation list to the UI
@@ -380,7 +382,7 @@ public class ConversationViewer : Gtk.Stack, Geary.BaseInterface {
                     cancellable
                 );
                 if (query != null) {
-                    yield list.search.highlight_matching_email(query);
+                    yield list.search.highlight_matching_email(query, true);
                 }
             } catch (GLib.Error err) {
                 warning("Error updating find results: %s", err.message);
@@ -433,7 +435,8 @@ public class ConversationViewer : Gtk.Stack, Geary.BaseInterface {
                     Geary.SearchQuery? search_query = search_folder.search_query;
                     if (search_query != null) {
                         this.current_list.search.highlight_matching_email.begin(
-                            search_query
+                            search_query,
+                            true
                         );
                     }
                 }
diff --git a/src/engine/app/app-conversation-monitor.vala b/src/engine/app/app-conversation-monitor.vala
index 8184da6b..a4de164d 100644
--- a/src/engine/app/app-conversation-monitor.vala
+++ b/src/engine/app/app-conversation-monitor.vala
@@ -46,8 +46,11 @@ public class Geary.App.ConversationMonitor : BaseObject {
      * These fields will be retrieved regardless of the Field
      * parameter passed to the constructor.
      */
-    public const Geary.Email.Field REQUIRED_FIELDS = Geary.Email.Field.REFERENCES |
-        Geary.Email.Field.FLAGS | Geary.Email.Field.DATE;
+    public const Geary.Email.Field REQUIRED_FIELDS = (
+        Geary.Email.Field.REFERENCES |
+        Geary.Email.Field.FLAGS |
+        Geary.Email.Field.DATE
+    );
 
 
     private struct ProcessJobContext {
@@ -378,6 +381,28 @@ public class Geary.App.ConversationMonitor : BaseObject {
         return is_closing;
     }
 
+    /** Ensures the given email are loaded in the monitor. */
+    public async void load_email(Gee.Collection<Geary.EmailIdentifier> to_load,
+                                 GLib.Cancellable? cancellable)
+        throws GLib.Error {
+        if (!this.is_monitoring) {
+            throw new EngineError.OPEN_REQUIRED("Monitor is not open");
+        }
+
+        var remaining = traverse(to_load).filter(
+            id => this.conversations.get_by_email_identifier(id) == null
+        ).to_array_list();
+
+        if (!remaining.is_empty) {
+            remaining.sort((a, b) => a.natural_sort_comparator(b));
+            var op = new LoadOperation(
+                this, remaining[0], this.operation_cancellable
+            );
+            this.queue.add(op);
+            yield op.wait_until_complete(cancellable);
+        }
+    }
+
     /**
      * Returns the conversation containing the given email, if any.
      */
diff --git a/src/engine/app/conversation-monitor/app-load-operation.vala 
b/src/engine/app/conversation-monitor/app-load-operation.vala
new file mode 100644
index 00000000..057171fd
--- /dev/null
+++ b/src/engine/app/conversation-monitor/app-load-operation.vala
@@ -0,0 +1,51 @@
+/*
+ * 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.
+ */
+
+/**
+ * Loads a specific email all the way to the end of the vector.
+ */
+private class Geary.App.LoadOperation : ConversationOperation {
+
+
+    private EmailIdentifier to_load;
+    private Nonblocking.Spinlock completed = new Nonblocking.Spinlock();
+
+
+    public LoadOperation(ConversationMonitor monitor,
+                         EmailIdentifier to_load,
+                         GLib.Cancellable cancellable) {
+        base(monitor);
+        this.to_load = to_load;
+        this.completed = new Nonblocking.Spinlock(cancellable);
+    }
+
+    public override async void execute_async()
+        throws GLib.Error {
+        Geary.EmailIdentifier? lowest_known = this.monitor.window_lowest;
+        if (lowest_known == null ||
+            this.to_load.natural_sort_comparator(lowest_known) < 0) {
+            // XXX the further back to_load is, the more expensive
+            // this will be.
+            debug("Loading messages into %s",
+                  this.monitor.base_folder.to_string());
+            yield this.monitor.load_by_id_async(
+                this.to_load, int.MAX, Folder.ListFlags.OLDEST_TO_NEWEST
+            );
+        } else {
+            debug("Not loading messages in %s",
+                  this.monitor.base_folder.to_string());
+        }
+
+        this.completed.notify();
+    }
+
+    public async void wait_until_complete(GLib.Cancellable cancellable)
+        throws GLib.Error {
+        yield this.completed.wait_async(cancellable);
+    }
+
+}
diff --git a/src/engine/meson.build b/src/engine/meson.build
index fe34d99e..7515ee72 100644
--- a/src/engine/meson.build
+++ b/src/engine/meson.build
@@ -55,6 +55,7 @@ geary_engine_vala_sources = files(
   'app/conversation-monitor/app-external-append-operation.vala',
   'app/conversation-monitor/app-fill-window-operation.vala',
   'app/conversation-monitor/app-insert-operation.vala',
+  'app/conversation-monitor/app-load-operation.vala',
   'app/conversation-monitor/app-local-search-operation.vala',
   'app/conversation-monitor/app-remove-operation.vala',
   'app/conversation-monitor/app-reseed-operation.vala',


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