[geary/wip/721828-undo] Working on getting client notified of message undo before round-trip



commit babc2ef7f3342069269c7c12da35894cde09454f
Author: Jim Nelson <jim yorba org>
Date:   Tue Dec 23 17:52:10 2014 -0800

    Working on getting client notified of message undo before round-trip
    
    Waiting to synchronize with server is too slow; this partial work is
    to get the client notified that the message has returned to the folder
    before we have actually synchronized with the server.

 src/CMakeLists.txt                                 |    1 +
 src/engine/abstract/geary-abstract-account.vala    |   10 +++++
 src/engine/abstract/geary-abstract-folder.vala     |    8 ++++
 src/engine/api/geary-account.vala                  |   20 +++++++++++
 src/engine/api/geary-folder.vala                   |   27 ++++++++++++++
 src/engine/app/app-conversation-monitor.vala       |   37 ++++++++++++++++++++
 .../app-predict-insert-operation.vala              |   19 ++++++++++
 .../imap-engine/imap-engine-generic-account.vala   |    4 ++
 .../imap-engine/imap-engine-minimal-folder.vala    |    2 +-
 .../imap-engine/imap-engine-revokable-move.vala    |   23 +++++++++---
 .../replay-ops/imap-engine-move-email.vala         |   18 ++++++++-
 src/engine/imap/api/imap-folder.vala               |    2 +-
 12 files changed, 161 insertions(+), 10 deletions(-)
---
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 3ce824a..3b70b55 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -57,6 +57,7 @@ engine/app/conversation-monitor/app-external-append-operation.vala
 engine/app/conversation-monitor/app-fill-window-operation.vala
 engine/app/conversation-monitor/app-local-load-operation.vala
 engine/app/conversation-monitor/app-local-search-operation.vala
+engine/app/conversation-monitor/app-predict-insert-operation.vala
 engine/app/conversation-monitor/app-remove-operation.vala
 engine/app/conversation-monitor/app-reseed-operation.vala
 engine/app/conversation-monitor/app-terminate-operation.vala
diff --git a/src/engine/abstract/geary-abstract-account.vala b/src/engine/abstract/geary-abstract-account.vala
index 1d590bd..f861aba 100644
--- a/src/engine/abstract/geary-abstract-account.vala
+++ b/src/engine/abstract/geary-abstract-account.vala
@@ -63,6 +63,16 @@ public abstract class Geary.AbstractAccount : BaseObject, Geary.Account {
         email_flags_changed(folder, flag_map);
     }
     
+    protected virtual void notify_predict_email_inserted(Geary.Folder folder,
+        Gee.Collection<Geary.EmailIdentifier> ids) {
+        predict_email_inserted(folder, ids);
+    }
+    
+    protected virtual void notify_unpredict_email_inserted(Geary.Folder folder,
+        Gee.Collection<Geary.EmailIdentifier> ids) {
+        unpredict_email_inserted(folder, ids);
+    }
+    
     protected virtual void notify_opened() {
         opened();
     }
diff --git a/src/engine/abstract/geary-abstract-folder.vala b/src/engine/abstract/geary-abstract-folder.vala
index aeed7cd..0080fc1 100644
--- a/src/engine/abstract/geary-abstract-folder.vala
+++ b/src/engine/abstract/geary-abstract-folder.vala
@@ -56,6 +56,14 @@ public abstract class Geary.AbstractFolder : BaseObject, Geary.Folder {
         email_count_changed(new_count, reason);
     }
     
+    internal virtual void notify_predict_email_inserted(Gee.Collection<Geary.EmailIdentifier> ids) {
+        predict_email_inserted(ids);
+    }
+    
+    internal virtual void notify_unpredict_email_inserted(Gee.Collection<Geary.EmailIdentifier> ids) {
+        unpredict_email_inserted(ids);
+    }
+    
     internal virtual void notify_email_flags_changed(Gee.Map<Geary.EmailIdentifier,
         Geary.EmailFlags> flag_map) {
         email_flags_changed(flag_map);
diff --git a/src/engine/api/geary-account.vala b/src/engine/api/geary-account.vala
index 152508e..c15843e 100644
--- a/src/engine/api/geary-account.vala
+++ b/src/engine/api/geary-account.vala
@@ -109,6 +109,20 @@ public interface Geary.Account : BaseObject {
         Gee.Map<Geary.EmailIdentifier, Geary.EmailFlags> map);
     
     /**
+     * Fired when email is predicted to be inserted into the { link Folder}.
+     *
+     * @see Folder.predict_email_inserted
+     */
+    public signal void predict_email_inserted(Geary.Folder folder, Gee.Collection<Geary.EmailIdentifier> 
ids);
+    
+    /**
+     * Fired when email predicted to be inserted into the { link Folder} will not be arriving.
+     *
+     * @see Folder.unpredict_email_inserted
+     */
+    public signal void unpredict_email_inserted(Geary.Folder folder, Gee.Collection<Geary.EmailIdentifier> 
ids);
+    
+    /**
      * Signal notification method for subclasses to use.
      */
     protected abstract void notify_opened();
@@ -178,6 +192,12 @@ public interface Geary.Account : BaseObject {
     protected abstract void notify_email_flags_changed(Geary.Folder folder,
         Gee.Map<Geary.EmailIdentifier, Geary.EmailFlags> flag_map);
     
+    protected abstract void notify_predict_email_inserted(Geary.Folder folder,
+        Gee.Collection<Geary.EmailIdentifier> ids);
+    
+    protected abstract void notify_unpredict_email_inserted(Geary.Folder folder,
+        Gee.Collection<Geary.EmailIdentifier> ids);
+    
     /**
      * A utility method to sort a Gee.Collection of { link Folder}s by their { link FolderPath}s
      * to ensure they comport with { link folders_available_unavailable} and
diff --git a/src/engine/api/geary-folder.vala b/src/engine/api/geary-folder.vala
index 69c6d30..3fb411f 100644
--- a/src/engine/api/geary-folder.vala
+++ b/src/engine/api/geary-folder.vala
@@ -259,6 +259,29 @@ public interface Geary.Folder : BaseObject {
     public signal void email_count_changed(int new_count, CountChangeReason reason);
     
     /**
+     * Fired when an operation on another { link Folder} predicts that email will be inserted
+     * into this Folder.
+     *
+     * It should ''not'' be assumed that the email identifiers can be loaded from this Folder,
+     * especially since this can be fired when the Folder is closed.  Use
+     * { link Account.local_fetch_email_async} to fetch whatever portion of the email is locally
+     * available without regards to folder association.
+     *
+     * Since it's unknown what the predicted email's ordering will be on the remote folder, there
+     * is no predict_email_appended.
+     */
+    public signal void predict_email_inserted(Gee.Collection<Geary.EmailIdentifier> ids);
+    
+    /**
+     * Fired when an operation on another { link Folder} predicted that email will be inserted into
+     * this Folder, but that prediction is now incorrect.
+     *
+     * This most often occurs because the original prediction was made ''before'' the remote
+     * operation took place, and then the operation failed.
+     */
+    public signal void unpredict_email_inserted(Gee.Collection<Geary.EmailIdentifier> ids);
+    
+    /**
      * Fired when the supplied email flags have changed, whether due to local action or reported by
      * the server.
      */
@@ -304,6 +327,10 @@ public interface Geary.Folder : BaseObject {
     
     protected abstract void notify_email_count_changed(int new_count, CountChangeReason reason);
     
+    protected abstract void notify_predict_email_inserted(Gee.Collection<Geary.EmailIdentifier> ids);
+    
+    protected abstract void notify_unpredict_email_inserted(Gee.Collection<Geary.EmailIdentifier> ids);
+    
     protected abstract void notify_email_flags_changed(Gee.Map<Geary.EmailIdentifier,
         Geary.EmailFlags> flag_map);
     
diff --git a/src/engine/app/app-conversation-monitor.vala b/src/engine/app/app-conversation-monitor.vala
index 14bf509..bdd9da2 100644
--- a/src/engine/app/app-conversation-monitor.vala
+++ b/src/engine/app/app-conversation-monitor.vala
@@ -286,6 +286,7 @@ public class Geary.App.ConversationMonitor : BaseObject {
         folder.opened.connect(on_folder_opened);
         folder.account.email_flags_changed.connect(on_account_email_flags_changed);
         folder.account.email_locally_complete.connect(on_account_email_locally_complete);
+        folder.account.predict_email_inserted.connect(on_account_predict_email_inserted);
         // TODO: handle removed email
         
         try {
@@ -299,6 +300,7 @@ public class Geary.App.ConversationMonitor : BaseObject {
             folder.opened.disconnect(on_folder_opened);
             folder.account.email_flags_changed.disconnect(on_account_email_flags_changed);
             folder.account.email_locally_complete.disconnect(on_account_email_locally_complete);
+            folder.account.predict_email_inserted.disconnect(on_account_predict_email_inserted);
             
             throw err;
         }
@@ -348,6 +350,7 @@ public class Geary.App.ConversationMonitor : BaseObject {
         folder.opened.disconnect(on_folder_opened);
         folder.account.email_flags_changed.disconnect(on_account_email_flags_changed);
         folder.account.email_locally_complete.disconnect(on_account_email_locally_complete);
+        folder.account.predict_email_inserted.disconnect(on_account_predict_email_inserted);
         
         Error? close_err = null;
         if (close_folder) {
@@ -630,6 +633,18 @@ public class Geary.App.ConversationMonitor : BaseObject {
         operation_queue.add(new ExternalAppendOperation(this, folder, complete_ids));
     }
     
+    private void on_account_predict_email_inserted(Geary.Folder folder,
+        Gee.Collection<Geary.EmailIdentifier> complete_ids) {
+        debug("PREDICTED: %s %d", folder.to_string(), complete_ids.size);
+        
+        // only load in predicted emails going to monitored folder; don't attempt to merge in
+        // all messages predicted anywhere in the account
+        if (this.folder != folder)
+            return;
+        
+        operation_queue.add(new PredictInsertOperation(this, complete_ids));
+    }
+    
     internal async void append_emails_async(Gee.Collection<Geary.EmailIdentifier> appended_ids) {
         debug("%d message(s) appended to %s, fetching to add to conversations...", appended_ids.size,
             folder.to_string());
@@ -677,6 +692,28 @@ public class Geary.App.ConversationMonitor : BaseObject {
         yield external_load_by_sparse_id(folder, appended_ids, Geary.Folder.ListFlags.NONE, null);
     }
     
+    internal async void predict_emails_async(Gee.Collection<Geary.EmailIdentifier> ids) {
+        debug("Loading predicted local email: %d", ids.size);
+        Gee.ArrayList<Geary.Email> emails = new Gee.ArrayList<Geary.Email>();
+        foreach (Geary.EmailIdentifier id in ids) {
+            try {
+                emails.add(yield folder.account.local_fetch_email_async(id, required_fields));
+            } catch (Error err) {
+                // Ignore error; INCOMPLETE and NOT_FOUND are just part of the fuzziness of
+                // prediction
+                debug("Unable to locally fetch predicted email in %s: %s", folder.to_string(),
+                    err.message);
+            }
+        }
+        
+        debug("Loaded predicted local email: %d", emails.size);
+        
+        if (emails.size == 0)
+            return;
+        
+        yield process_email_async(emails, new ProcessJobContext(false));
+    }
+    
     private void on_account_email_flags_changed(Geary.Folder folder,
         Gee.Map<Geary.EmailIdentifier, Geary.EmailFlags> map) {
         foreach (Geary.EmailIdentifier id in map.keys) {
diff --git a/src/engine/app/conversation-monitor/app-predict-insert-operation.vala 
b/src/engine/app/conversation-monitor/app-predict-insert-operation.vala
new file mode 100644
index 0000000..4036a52
--- /dev/null
+++ b/src/engine/app/conversation-monitor/app-predict-insert-operation.vala
@@ -0,0 +1,19 @@
+/* Copyright 2014 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.
+ */
+
+private class Geary.App.PredictInsertOperation : ConversationOperation {
+    private Gee.Collection<Geary.EmailIdentifier> predicted_ids;
+    
+    public PredictInsertOperation(ConversationMonitor monitor,
+        Gee.Collection<Geary.EmailIdentifier> predicted_ids) {
+        base(monitor);
+        this.predicted_ids = predicted_ids;
+    }
+    
+    public override async void execute_async() {
+        yield monitor.predict_emails_async(predicted_ids);
+    }
+}
diff --git a/src/engine/imap-engine/imap-engine-generic-account.vala 
b/src/engine/imap-engine/imap-engine-generic-account.vala
index 50f6001..20ccbe0 100644
--- a/src/engine/imap-engine/imap-engine-generic-account.vala
+++ b/src/engine/imap-engine/imap-engine-generic-account.vala
@@ -60,6 +60,8 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount {
                 folder.email_removed.connect(notify_email_removed);
                 folder.email_locally_complete.connect(notify_email_locally_complete);
                 folder.email_flags_changed.connect(notify_email_flags_changed);
+                folder.predict_email_inserted.connect(notify_predict_email_inserted);
+                folder.unpredict_email_inserted.connect(notify_unpredict_email_inserted);
             }
         }
         if (unavailable != null) {
@@ -69,6 +71,8 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount {
                 folder.email_removed.disconnect(notify_email_removed);
                 folder.email_locally_complete.disconnect(notify_email_locally_complete);
                 folder.email_flags_changed.disconnect(notify_email_flags_changed);
+                folder.predict_email_inserted.disconnect(notify_predict_email_inserted);
+                folder.unpredict_email_inserted.disconnect(notify_unpredict_email_inserted);
             }
         }
     }
diff --git a/src/engine/imap-engine/imap-engine-minimal-folder.vala 
b/src/engine/imap-engine/imap-engine-minimal-folder.vala
index 32e2936..c3956de 100644
--- a/src/engine/imap-engine/imap-engine-minimal-folder.vala
+++ b/src/engine/imap-engine/imap-engine-minimal-folder.vala
@@ -1271,7 +1271,7 @@ private class Geary.ImapEngine.MinimalFolder : Geary.AbstractFolder, Geary.Folde
         replay_queue.schedule(move);
         yield move.wait_for_ready_async(cancellable);
         
-        return new RevokableMove(_account, path, destination, move.destination_uids);
+        return new RevokableMove(_account, path, destination, move.destination_ids);
     }
     
     private void on_email_flags_changed(Gee.Map<Geary.EmailIdentifier, Geary.EmailFlags> changed) {
diff --git a/src/engine/imap-engine/imap-engine-revokable-move.vala 
b/src/engine/imap-engine/imap-engine-revokable-move.vala
index 646e4bf..f670116 100644
--- a/src/engine/imap-engine/imap-engine-revokable-move.vala
+++ b/src/engine/imap-engine/imap-engine-revokable-move.vala
@@ -8,14 +8,18 @@ private class Geary.ImapEngine.RevokableMove : Revokable {
     private GenericAccount account;
     private FolderPath original_source;
     private FolderPath original_dest;
-    private Gee.Set<Imap.UID> uids;
+    private Gee.Set<ImapDB.EmailIdentifier> destination_ids;
     
+    /**
+     * Supplied EmailIdentifiers *must* be loaded with UIDs of the messages on the *destination*
+     * folder.  Do *not* merely stuff in here the EmailIdentifier from the source folder.
+     */
     public RevokableMove(GenericAccount account, FolderPath original_source, FolderPath original_dest,
-        Gee.Set<Imap.UID> uids) {
+        Gee.Set<ImapDB.EmailIdentifier> destination_ids) {
         this.account = account;
         this.original_source = original_source;
         this.original_dest = original_dest;
-        this.uids = uids;
+        this.destination_ids = destination_ids;
         
         account.folders_available_unavailable.connect(on_folders_available_unavailable);
         account.email_removed.connect(on_folder_email_removed);
@@ -56,8 +60,10 @@ private class Geary.ImapEngine.RevokableMove : Revokable {
             yield dest_folder.open_async(Geary.Folder.OpenFlags.NO_DELAY, cancellable);
             
             // watch out for messages detected as gone when folder is opened
-            if (can_revoke && !yield dest_folder.revoke_move_async(uids, original_source, cancellable))
+            if (can_revoke && !yield dest_folder.revoke_move_async(destination_ids, original_source,
+                cancellable)) {
                 can_revoke = false;
+            }
         } finally {
             // note that the Cancellable is not used
             try {
@@ -99,8 +105,13 @@ private class Geary.ImapEngine.RevokableMove : Revokable {
             .to_hash_set();
         
         // otherwise, ability to revoke is best-effort
-        uids.remove_all(removed_uids);
-        can_revoke = uids.size > 0;
+        Gee.Iterator<ImapDB.EmailIdentifier> iter = destination_ids.iterator();
+        while (iter.next()) {
+            if (removed_uids.contains(iter.get().uid))
+                iter.remove();
+        }
+        
+        can_revoke = destination_ids.size > 0;
     }
 }
 
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-move-email.vala 
b/src/engine/imap-engine/replay-ops/imap-engine-move-email.vala
index a93a742..30b19a4 100644
--- a/src/engine/imap-engine/replay-ops/imap-engine-move-email.vala
+++ b/src/engine/imap-engine/replay-ops/imap-engine-move-email.vala
@@ -5,7 +5,7 @@
  */
 
 private class Geary.ImapEngine.MoveEmail : Geary.ImapEngine.SendReplayOperation {
-    public Gee.Set<Imap.UID>? destination_uids { get; private set; default = null; }
+    public Gee.Set<ImapDb.EmailIdentifier>? destination_ids { get; private set; default = null; }
     
     private MinimalFolder engine;
     private Gee.List<ImapDB.EmailIdentifier> to_move = new Gee.ArrayList<ImapDB.EmailIdentifier>();
@@ -52,6 +52,13 @@ private class Geary.ImapEngine.MoveEmail : Geary.ImapEngine.SendReplayOperation
         engine.notify_email_count_changed(Numeric.int_floor(original_count - to_move.size, 0),
             Geary.Folder.CountChangeReason.REMOVED);
         
+        MinimalFolder? dest_folder = (yield engine.account.fetch_folder_async(destination, cancellable))
+            as MinimalFolder;
+        if (dest_folder != null) {
+            debug("NOTIFY PREDICTED: %d", moved_ids.size);
+            dest_folder.notify_predict_email_inserted(moved_ids);
+        }
+        
         return ReplayOperation.Status.CONTINUE;
     }
     
@@ -69,7 +76,9 @@ private class Geary.ImapEngine.MoveEmail : Geary.ImapEngine.SendReplayOperation
         if (cancellable != null && cancellable.is_cancelled())
             throw new IOError.CANCELLED("Move email to %s cancelled", engine.remote_folder.to_string());
         
-        Gee.Set<Imap.UID> acc_uids = new Gee.HashSet<Imap.UID>();
+        // TODO: Need to generate fully-valid ImapDB.EmailIdentifiers for the destination folder
+        // (with message_ids and UIDs properly associated)
+        Gee.Set<ImapDB.EmailIdentifier> acc_ids = new Gee.HashSet<ImapDB.EmailIdentifier>();
         
         Gee.List<Imap.MessageSet> msg_sets = Imap.MessageSet.uid_sparse(
             ImapDB.EmailIdentifier.to_uids(moved_ids));
@@ -93,6 +102,11 @@ private class Geary.ImapEngine.MoveEmail : Geary.ImapEngine.SendReplayOperation
         
         engine.notify_email_inserted(moved_ids);
         engine.notify_email_count_changed(original_count, Geary.Folder.CountChangeReason.INSERTED);
+        
+        MinimalFolder? dest_folder = (yield engine.account.fetch_folder_async(destination, cancellable))
+            as MinimalFolder;
+        if (dest_folder != null)
+            dest_folder.notify_unpredict_email_inserted(moved_ids);
     }
 
     public override string describe_state() {
diff --git a/src/engine/imap/api/imap-folder.vala b/src/engine/imap/api/imap-folder.vala
index f40ae46..d5bde97 100644
--- a/src/engine/imap/api/imap-folder.vala
+++ b/src/engine/imap/api/imap-folder.vala
@@ -675,7 +675,7 @@ private class Geary.Imap.Folder : BaseObject {
         yield exec_commands_async(cmds, null, null, cancellable);
     }
     
-    public async Gee.Set<UID>? copy_email_async(MessageSet msg_set, Geary.FolderPath destination,
+    public async Gee.List<UID>? copy_email_async(MessageSet msg_set, Geary.FolderPath destination,
         Cancellable? cancellable) throws Error {
         check_open();
         


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