[geary/wip/721828-undo-2: 1/5] Going an alternate route



commit 955eef1cc8c92414b4787938dc8e705edfe30a4c
Author: Jim Nelson <jim yorba org>
Date:   Thu Jan 29 18:57:21 2015 -0800

    Going an alternate route
    
    Revokable operations are postponed until an event occurs which
    forces them to be committed, or the user revokes them.

 src/CMakeLists.txt                                 |    4 +-
 src/client/application/geary-controller.vala       |   12 +-
 src/engine/api/geary-revokable.vala                |   90 +++++++++++++---
 .../imap-engine/imap-engine-minimal-folder.vala    |   46 +++++++-
 .../imap-engine/imap-engine-replay-queue.vala      |   21 +++--
 .../imap-engine/imap-engine-revokable-move.vala    |  107 +++++++++-----------
 .../imap-engine-send-replay-operation.vala         |    8 +-
 ...ail.vala => imap-engine-move-email-commit.vala} |   70 ++++---------
 .../replay-ops/imap-engine-move-email-prepare.vala |   65 ++++++++++++
 .../replay-ops/imap-engine-move-email-revoke.vala  |   55 ++++++++++
 10 files changed, 329 insertions(+), 149 deletions(-)
---
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 652be39..6a991b3 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -213,7 +213,9 @@ engine/imap-engine/replay-ops/imap-engine-fetch-email.vala
 engine/imap-engine/replay-ops/imap-engine-list-email-by-id.vala
 engine/imap-engine/replay-ops/imap-engine-list-email-by-sparse-id.vala
 engine/imap-engine/replay-ops/imap-engine-mark-email.vala
-engine/imap-engine/replay-ops/imap-engine-move-email.vala
+engine/imap-engine/replay-ops/imap-engine-move-email-commit.vala
+engine/imap-engine/replay-ops/imap-engine-move-email-prepare.vala
+engine/imap-engine/replay-ops/imap-engine-move-email-revoke.vala
 engine/imap-engine/replay-ops/imap-engine-remove-email.vala
 engine/imap-engine/replay-ops/imap-engine-replay-append.vala
 engine/imap-engine/replay-ops/imap-engine-replay-disconnect.vala
diff --git a/src/client/application/geary-controller.vala b/src/client/application/geary-controller.vala
index 12e84be..76f5fcf 100644
--- a/src/client/application/geary-controller.vala
+++ b/src/client/application/geary-controller.vala
@@ -2476,28 +2476,28 @@ public class GearyController : Geary.BaseObject {
     private void save_revokable(Geary.Revokable? new_revokable, string? description) {
         // disconnect old revokable
         if (revokable != null)
-            revokable.notify[Geary.Revokable.PROP_CAN_REVOKE].disconnect(on_can_revoke_changed);
+            revokable.notify[Geary.Revokable.PROP_VALID].disconnect(on_revokable_valid_changed);
         
         // store new revokable
         revokable = new_revokable;
         
         // connect to new revokable
         if (revokable != null)
-            revokable.notify[Geary.Revokable.PROP_CAN_REVOKE].connect(on_can_revoke_changed);
+            revokable.notify[Geary.Revokable.PROP_VALID].connect(on_revokable_valid_changed);
         
         Gtk.Action undo_action = GearyApplication.instance.get_action(ACTION_UNDO);
-        undo_action.sensitive = revokable != null && revokable.can_revoke;
+        undo_action.sensitive = revokable != null && revokable.valid;
         undo_action.tooltip = (revokable != null && description != null) ? description : _("Undo (Ctrl+Z)");
     }
     
-    private void on_can_revoke_changed() {
+    private void on_revokable_valid_changed() {
         // remove revokable if it goes invalid
-        if (revokable != null && !revokable.can_revoke)
+        if (revokable != null && !revokable.valid)
             save_revokable(null, null);
     }
     
     private void on_revoke() {
-        if (revokable != null && revokable.can_revoke)
+        if (revokable != null && revokable.valid)
             revokable.revoke_async.begin(null, on_revoke_completed);
     }
     
diff --git a/src/engine/api/geary-revokable.vala b/src/engine/api/geary-revokable.vala
index 51b6f95..dbf938d 100644
--- a/src/engine/api/geary-revokable.vala
+++ b/src/engine/api/geary-revokable.vala
@@ -10,26 +10,27 @@
  */
 
 public abstract class Geary.Revokable : BaseObject {
-    public const string PROP_CAN_REVOKE = "can-revoke";
-    public const string PROP_IS_REVOKING = "is-revoking";
+    public const string PROP_VALID = "valid";
+    public const string PROP_IN_PROCESS = "in-process";
     
     /**
-     * Indicates if { link revoke_async} is a valid operation for this { link Revokable}.
+     * Indicates if { link revoke_async} or { link commit_async} are valid operations for this
+     * { link Revokable}.
      *
-     * Due to later operations or notifications, it's possible for the Revokable to go invalid.
-     * In some circumstances, this may be that it cannot fully revoke the original operation, in
-     * others it may be that it can't revoke any part of the original operation, depending on the
-     * nature of the operation.
+     * Due to later operations or notifications, it's possible for the Revokable to go invalid
+     * after being issued to the caller.
      */
-    public bool can_revoke { get; protected set; default = true; }
+    public bool valid { get; protected set; default = true; }
     
     /**
-     * Indicates a { link revoke_async} operation is underway.
+     * Indicates a { link revoke_async} or { link commit_async} operation is underway.
      *
-     * Only one revoke operation can occur at a time.  If this is true when revoke_async() is
-     * called, it will throw an Error.
+     * Only one operation can occur at a time, and when complete the { link Revokable} will be
+     * invalid.
+     *
+     * @see valid
      */
-    public bool is_revoking { get; protected set; default = false; }
+    public bool in_process { get; protected set; default = false; }
     
     protected Revokable() {
     }
@@ -37,13 +38,68 @@ public abstract class Geary.Revokable : BaseObject {
     /**
      * Revoke (undo) the operation.
      *
-     * Returns false if the operation failed and is no longer revokable.
-     *
      * If the call throws an Error that does not necessarily mean the { link Revokable} is
-     * invalid.  Check the return value or { link can_revoke}.
+     * invalid.  Check { link valid}.
+     *
+     * @throws EngineError.ALREADY_OPEN if { link in_process} is true.
+     */
+    public virtual async void revoke_async(Cancellable? cancellable = null) throws Error {
+        if (in_process)
+            throw new EngineError.ALREADY_OPEN("Already revoking or committing operation");
+        
+        in_process = true;
+        try {
+            yield internal_revoke_async(cancellable);
+        } finally {
+            in_process = false;
+        }
+    }
+    
+    /**
+     * The child class's implementation of { link revoke_async}.
+     *
+     * The default implementation of { link revoke_async} deals with state issues
+     * ({ link in_process}, throwing the appropriate Error, etc.)  Child classes can override this
+     * method and only worry about the revoke operation itself.
+     *
+     * This call *must* set { link valid} before exiting.
+     */
+    protected abstract async void internal_revoke_async(Cancellable? cancellable) throws Error;
+    
+    /**
+     * Commits (completes) the operation immediately.
+     *
+     * Some { link Revokable} operations work by delaying the operation until time has passed or
+     * some situation occurs which requires the operation to complete.  This call forces the
+     * operation to complete immediately rather than delay it for later.
+     *
+     * Even if the operation "actually" commits and is not delayed, calling commit_async() will
+     * make this Revokable invalid.
+     *
+     * @throws EngineError.ALREADY_OPEN if { link is_revoking} or { link is_committing} is true
+     * when called.
+     */
+    public virtual async void commit_async(Cancellable? cancellable = null) throws Error {
+        if (in_process)
+            throw new EngineError.ALREADY_OPEN("Already revoking or committing operation");
+        
+        in_process = true;
+        try {
+            yield internal_commit_async(cancellable);
+        } finally {
+            in_process = false;
+        }
+    }
+    
+    /**
+     * The child class's implementation of { link commit_async}.
+     *
+     * The default implementation of { link commit_async} deals with state issues
+     * ({ link in_process}, throwing the appropriate Error, etc.)  Child classes can override this
+     * method and only worry about the revoke operation itself.
      *
-     * @throws EngineError.ALREADY_OPEN if { link is_revoking} is true when called.
+     * This call *must* set { link valid} before exiting.
      */
-    public abstract async bool revoke_async(Cancellable? cancellable = null) throws Error;
+    protected abstract async void internal_commit_async(Cancellable? cancellable) throws Error;
 }
 
diff --git a/src/engine/imap-engine/imap-engine-minimal-folder.vala 
b/src/engine/imap-engine/imap-engine-minimal-folder.vala
index e073cce..21d9c59 100644
--- a/src/engine/imap-engine/imap-engine-minimal-folder.vala
+++ b/src/engine/imap-engine/imap-engine-minimal-folder.vala
@@ -52,6 +52,15 @@ private class Geary.ImapEngine.MinimalFolder : Geary.Folder, Geary.FolderSupport
     private Nonblocking.Mutex open_mutex = new Nonblocking.Mutex();
     private Nonblocking.Mutex close_mutex = new Nonblocking.Mutex();
     
+    /**
+     * Called when the folder is closing (and not reestablishing a connection) and will be flushing
+     * the replay queue.  Subscribers may add ReplayOperations to the list, which will be enqueued
+     * before the queue is flushed.
+     *
+     * Note that this is ''not'' fired if the queue is not being flushed.
+     */
+    public signal void closing(Gee.List<ReplayOperation> final_ops);
+    
     public MinimalFolder(GenericAccount account, Imap.Account remote, ImapDB.Account local,
         ImapDB.Folder local_folder, SpecialFolderType special_folder_type) {
         _account = account;
@@ -79,6 +88,10 @@ private class Geary.ImapEngine.MinimalFolder : Geary.Folder, Geary.FolderSupport
         local_folder.email_complete.disconnect(on_email_complete);
     }
     
+    protected virtual void notify_closing(Gee.List<ReplayOperation> final_ops) {
+        closing(final_ops);
+    }
+    
     /*
      * These signal notifiers are marked public (note this is a private class) so the various
      * ReplayOperations can directly fire the associated signals while within the queue.
@@ -814,6 +827,16 @@ private class Geary.ImapEngine.MinimalFolder : Geary.Folder, Geary.FolderSupport
         // That said, only flush, close, and destroy the ReplayQueue if fully closing and not
         // preparing for a connection reestablishment
         if (open_count <= 0) {
+            // if closing and flushing the queue, give Revokables a chance to schedule their
+            // commit operations before going down
+            if (flush_pending) {
+                Gee.List<ReplayOperation> final_ops = new Gee.ArrayList<ReplayOperation>();
+                notify_closing(final_ops);
+                
+                foreach (ReplayOperation op in final_ops)
+                    replay_queue.schedule(op);
+            }
+            
             // Close the replay queues; if a "clean" close, flush pending operations so everything
             // gets a chance to run; if forced close, drop everything outstanding
             try {
@@ -1381,16 +1404,27 @@ private class Geary.ImapEngine.MinimalFolder : Geary.Folder, Geary.FolderSupport
         if (destination.equal_to(path))
             return null;
         
-        MoveEmail move = new MoveEmail(this, (Gee.List<ImapDB.EmailIdentifier>) to_move, destination);
-        replay_queue.schedule(move);
+        MoveEmailPrepare prepare = new MoveEmailPrepare(this, (Gee.List<ImapDB.EmailIdentifier>) to_move,
+            cancellable);
+        replay_queue.schedule(prepare);
         
-        yield move.wait_for_ready_async(cancellable);
+        yield prepare.wait_for_ready_async(cancellable);
         
-        // If no COPYUIDs returned, can't revoke this operation
-        if (move.destination_uids.size == 0)
+        if (prepare.prepared_for_move == null || prepare.prepared_for_move.size == 0)
             return null;
         
-        return new RevokableMove(_account, path, destination, move.destination_uids);
+        return new RevokableMove(_account, this, destination, prepare.prepared_for_move);
+    }
+    
+    public void schedule_op(ReplayOperation op) throws Error {
+        check_open("schedule_op");
+        
+        replay_queue.schedule(op);
+    }
+    
+    public async void exec_op_async(ReplayOperation op, Cancellable? cancellable) throws Error {
+        schedule_op(op);
+        yield op.wait_for_ready_async(cancellable);
     }
     
     private void on_email_flags_changed(Gee.Map<Geary.EmailIdentifier, Geary.EmailFlags> changed) {
diff --git a/src/engine/imap-engine/imap-engine-replay-queue.vala 
b/src/engine/imap-engine/imap-engine-replay-queue.vala
index af43bad..a2e3b2b 100644
--- a/src/engine/imap-engine/imap-engine-replay-queue.vala
+++ b/src/engine/imap-engine/imap-engine-replay-queue.vala
@@ -9,6 +9,12 @@ private class Geary.ImapEngine.ReplayQueue : Geary.BaseObject {
     // see as high as 250ms
     private const int NOTIFICATION_QUEUE_WAIT_MSEC = 1000;
     
+    private enum State {
+        OPEN,
+        CLOSING,
+        CLOSED
+    }
+    
     private class CloseReplayQueue : ReplayOperation {
         public CloseReplayQueue() {
             // LOCAL_AND_REMOTE to make sure this operation is flushed all the way down the pipe
@@ -57,8 +63,7 @@ private class Geary.ImapEngine.ReplayQueue : Geary.BaseObject {
     private Gee.ArrayList<ReplayOperation> notification_queue = new Gee.ArrayList<ReplayOperation>();
     private Scheduler.Scheduled? notification_timer = null;
     private int64 next_submission_number = 0;
-    
-    private bool is_closed = false;
+    private State state = State.OPEN;
     
     public virtual signal void scheduled(ReplayOperation op) {
         Logging.debug(Logging.Flag.REPLAY, "[%s] ReplayQueue::scheduled: %s %s", to_string(),
@@ -143,7 +148,7 @@ private class Geary.ImapEngine.ReplayQueue : Geary.BaseObject {
      */
     public bool schedule(ReplayOperation op) {
         // ReplayClose is allowed past the velvet ropes even as the hoi palloi is turned away
-        if (is_closed && !(op is CloseReplayQueue)) {
+        if (state != State.OPEN && !(op is CloseReplayQueue)) {
             debug("Unable to schedule replay operation %s on %s: replay queue closed", op.to_string(),
                 to_string());
             
@@ -188,7 +193,7 @@ private class Geary.ImapEngine.ReplayQueue : Geary.BaseObject {
      * Returns false if the operation was not schedule (queue already closed).
      */
     public bool schedule_server_notification(ReplayOperation op) {
-        if (is_closed) {
+        if (state != State.OPEN) {
             debug("Unable to schedule notification operation %s on %s: replay queue closed", op.to_string(),
                 to_string());
             
@@ -293,7 +298,7 @@ private class Geary.ImapEngine.ReplayQueue : Geary.BaseObject {
      * A ReplayQueue cannot be re-opened.
      */
     public async void close_async(bool flush_pending, Cancellable? cancellable = null) throws Error {
-        if (is_closed)
+        if (state != State.OPEN)
             return;
         
         // cancel notification queue timeout
@@ -306,8 +311,7 @@ private class Geary.ImapEngine.ReplayQueue : Geary.BaseObject {
         
         // mark as closed now to prevent further scheduling ... ReplayClose gets special
         // consideration in schedule()
-        is_closed = true;
-        
+        state = State.CLOSING;
         closing();
         
         // if not flushing pending, clear out all waiting operations, backing out any that need to
@@ -322,6 +326,7 @@ private class Geary.ImapEngine.ReplayQueue : Geary.BaseObject {
         
         yield close_op.wait_for_ready_async(cancellable);
         
+        state = State.CLOSED;
         closed();
     }
     
@@ -472,7 +477,7 @@ private class Geary.ImapEngine.ReplayQueue : Geary.BaseObject {
             
             // wait until the remote folder is opened (or throws an exception, in which case closed)
             try {
-                if (!is_close_op && folder_opened)
+                if (!is_close_op && folder_opened && state == State.OPEN)
                     yield owner.wait_for_open_async();
             } catch (Error remote_err) {
                 debug("Folder %s closed or failed to open, remote replay queue closing: %s",
diff --git a/src/engine/imap-engine/imap-engine-revokable-move.vala 
b/src/engine/imap-engine/imap-engine-revokable-move.vala
index f288faa..3c5ce8d 100644
--- a/src/engine/imap-engine/imap-engine-revokable-move.vala
+++ b/src/engine/imap-engine/imap-engine-revokable-move.vala
@@ -6,79 +6,65 @@
 
 private class Geary.ImapEngine.RevokableMove : Revokable {
     private GenericAccount account;
-    private FolderPath original_source;
-    private FolderPath original_dest;
-    private Gee.Set<Imap.UID> destination_uids;
+    private ImapEngine.MinimalFolder source;
+    private FolderPath destination;
+    private Gee.Set<ImapDB.EmailIdentifier> move_ids;
     
-    public RevokableMove(GenericAccount account, FolderPath original_source, FolderPath original_dest,
-        Gee.Set<Imap.UID> destination_uids) {
+    public RevokableMove(GenericAccount account, ImapEngine.MinimalFolder source, FolderPath destination,
+        Gee.Set<ImapDB.EmailIdentifier> move_ids) {
         this.account = account;
-        this.original_source = original_source;
-        this.original_dest = original_dest;
-        this.destination_uids = destination_uids;
+        this.source = source;
+        this.destination = destination;
+        this.move_ids = move_ids;
         
         account.folders_available_unavailable.connect(on_folders_available_unavailable);
-        account.email_removed.connect(on_folder_email_removed);
+        source.email_removed.connect(on_source_email_removed);
+        source.closing.connect(on_source_closing);
     }
     
     ~RevokableMove() {
         account.folders_available_unavailable.disconnect(on_folders_available_unavailable);
-        account.email_removed.disconnect(on_folder_email_removed);
+        source.email_removed.disconnect(on_source_email_removed);
+        source.closing.disconnect(on_source_closing);
+        
+        // if still valid, schedule operation so its executed
+        if (valid) {
+            debug("Freeing revokable, scheduling move %d emails from %s to %s", move_ids.size,
+                source.path.to_string(), destination.to_string());
+            
+            try {
+                source.schedule_op(new MoveEmailCommit(source, move_ids, destination, null));
+            } catch (Error err) {
+                debug("Move from %s to %s failed: %s", source.path.to_string(), destination.to_string(),
+                    err.message);
+            }
+        }
     }
     
-    public override async bool revoke_async(Cancellable? cancellable) throws Error {
-        if (is_revoking)
-            throw new EngineError.ALREADY_OPEN("Already revoking operation");
-        
-        is_revoking = true;
+    protected override async void internal_revoke_async(Cancellable? cancellable) throws Error {
         try {
-            return yield internal_revoke_async(cancellable);
+            yield source.exec_op_async(new MoveEmailRevoke(source, move_ids, cancellable),
+                cancellable);
         } finally {
-            is_revoking = false;
+            valid = false;
         }
     }
     
-    private async bool internal_revoke_async(Cancellable? cancellable) throws Error {
-        // at this point, it's a one-shot deal: any error from here on out, or success, revoke
-        // is spent
-        can_revoke = false;
-        
-        // Use a detached Folder object, which bypasses synchronization on the destination folder
-        // for "quick" operations
-        Imap.Folder dest_folder = yield account.fetch_detached_folder_async(original_dest, cancellable);
-        yield dest_folder.open_async(cancellable);
-        
-        // open, revoke, close, ensuring the close and signal disconnect are performed in all cases
+    protected override async void internal_commit_async(Cancellable? cancellable) throws Error {
         try {
-            // watch out for messages detected as gone when folder is opened
-            if (destination_uids.size > 0) {
-                Gee.List<Imap.MessageSet> msg_sets = Imap.MessageSet.uid_sparse(destination_uids);
-                
-                // copy the moved email back to its source
-                foreach (Imap.MessageSet msg_set in msg_sets)
-                    yield dest_folder.copy_email_async(msg_set, original_source, cancellable);
-                
-                // remove it from the original destination in one fell swoop
-                yield dest_folder.remove_email_async(msg_sets, cancellable);
-            }
+            yield source.exec_op_async(new MoveEmailCommit(source, move_ids, destination, cancellable),
+                cancellable);
         } finally {
-            // note that the Cancellable is not used
-            try {
-                yield dest_folder.close_async(null);
-            } catch (Error err) {
-                // ignored
-            }
+            valid = false;
         }
-        
-        return can_revoke;
     }
     
     private void on_folders_available_unavailable(Gee.List<Folder>? available, Gee.List<Folder>? 
unavailable) {
-        // look for either of the original folders going away
+        // look for either of the folders going away
         if (unavailable != null) {
             foreach (Folder folder in unavailable) {
-                if (folder.path.equal_to(original_source) || folder.path.equal_to(original_dest)) {
-                    can_revoke = false;
+                if (folder.path.equal_to(source.path) || folder.path.equal_to(destination)) {
+                    valid = false;
                     
                     break;
                 }
@@ -86,21 +72,20 @@ private class Geary.ImapEngine.RevokableMove : Revokable {
         }
     }
     
-    private void on_folder_email_removed(Folder folder, Gee.Collection<EmailIdentifier> ids) {
+    private void on_source_email_removed(Gee.Collection<EmailIdentifier> ids) {
         // one-way switch, and only interested in destination folder activity
-        if (!can_revoke || !folder.path.equal_to(original_dest))
+        if (!valid)
             return;
         
-        // convert generic identifiers to UIDs
-        Gee.HashSet<Imap.UID> removed_uids = traverse<EmailIdentifier>(ids)
-            .cast_object<ImapDB.EmailIdentifier>()
-            .filter(id => id.uid == null)
-            .map<Imap.UID>(id => id.uid)
-            .to_hash_set();
+        foreach (EmailIdentifier id in ids)
+            move_ids.remove((ImapDB.EmailIdentifier) id);
         
-        // otherwise, ability to revoke is best-effort
-        destination_uids.remove_all(removed_uids);
-        can_revoke = destination_uids.size > 0;
+        valid = move_ids.size > 0;
+    }
+    
+    private void on_source_closing(Gee.List<ReplayOperation> final_ops) {
+        if (valid)
+            final_ops.add(new MoveEmailCommit(source, move_ids, destination, null));
     }
 }
 
diff --git a/src/engine/imap-engine/imap-engine-send-replay-operation.vala 
b/src/engine/imap-engine/imap-engine-send-replay-operation.vala
index 5ae0dce..50449ce 100644
--- a/src/engine/imap-engine/imap-engine-send-replay-operation.vala
+++ b/src/engine/imap-engine/imap-engine-send-replay-operation.vala
@@ -5,11 +5,15 @@
  */
 
 private abstract class Geary.ImapEngine.SendReplayOperation : Geary.ImapEngine.ReplayOperation {
-    public SendReplayOperation(string name, ReplayOperation.OnError on_remote_error = OnError.THROW) {
+    protected SendReplayOperation(string name, ReplayOperation.OnError on_remote_error = OnError.THROW) {
         base (name, ReplayOperation.Scope.LOCAL_AND_REMOTE, on_remote_error);
     }
     
-    public SendReplayOperation.only_remote(string name, ReplayOperation.OnError on_remote_error = 
OnError.THROW) {
+    protected SendReplayOperation.only_local(string name, ReplayOperation.OnError on_remote_error = 
OnError.THROW) {
+        base (name, ReplayOperation.Scope.LOCAL_ONLY, on_remote_error);
+    }
+    
+    protected SendReplayOperation.only_remote(string name, ReplayOperation.OnError on_remote_error = 
OnError.THROW) {
         base (name, ReplayOperation.Scope.REMOTE_ONLY, on_remote_error);
     }
     
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-commit.vala
similarity index 54%
rename from src/engine/imap-engine/replay-ops/imap-engine-move-email.vala
rename to src/engine/imap-engine/replay-ops/imap-engine-move-email-commit.vala
index e4b19db..2d46ae0 100644
--- a/src/engine/imap-engine/replay-ops/imap-engine-move-email.vala
+++ b/src/engine/imap-engine/replay-ops/imap-engine-move-email-commit.vala
@@ -4,70 +4,43 @@
  * (version 2.1 or later).  See the COPYING file in this distribution.
  */
 
-private class Geary.ImapEngine.MoveEmail : Geary.ImapEngine.SendReplayOperation {
-    public Gee.Set<Imap.UID> destination_uids = new Gee.HashSet<Imap.UID>();
-    
+private class Geary.ImapEngine.MoveEmailCommit : Geary.ImapEngine.SendReplayOperation {
     private MinimalFolder engine;
     private Gee.List<ImapDB.EmailIdentifier> to_move = new Gee.ArrayList<ImapDB.EmailIdentifier>();
     private Geary.FolderPath destination;
     private Cancellable? cancellable;
-    private Gee.Set<ImapDB.EmailIdentifier>? moved_ids = null;
-    private int original_count = 0;
     private Gee.List<Imap.MessageSet>? remaining_msg_sets = null;
-
-    public MoveEmail(MinimalFolder engine, Gee.List<ImapDB.EmailIdentifier> to_move, 
-        Geary.FolderPath destination, Cancellable? cancellable = null) {
-        base("MoveEmail", OnError.RETRY);
-
+    
+    public MoveEmailCommit(MinimalFolder engine, Gee.Collection<ImapDB.EmailIdentifier> to_move,
+        Geary.FolderPath destination, Cancellable? cancellable) {
+        base.only_remote("MoveEmailCommit", OnError.RETRY);
+        
         this.engine = engine;
-
+        
         this.to_move.add_all(to_move);
         this.destination = destination;
         this.cancellable = cancellable;
     }
     
     public override void notify_remote_removed_ids(Gee.Collection<ImapDB.EmailIdentifier> ids) {
-        // don't bother updating on server or backing out locally
-        if (moved_ids != null)
-            moved_ids.remove_all(ids);
+        to_move.remove_all(ids);
     }
     
     public override async ReplayOperation.Status replay_local_async() throws Error {
-        if (to_move.size <= 0)
-            return ReplayOperation.Status.COMPLETED;
-        
-        int remote_count;
-        int last_seen_remote_count;
-        original_count = engine.get_remote_counts(out remote_count, out last_seen_remote_count);
-        
-        // as this value is only used for reporting, offer best-possible service
-        if (original_count < 0)
-            original_count = to_move.size;
-        
-        moved_ids = yield engine.local_folder.mark_removed_async(to_move, true, cancellable);
-        if (moved_ids == null || moved_ids.size == 0)
-            return ReplayOperation.Status.COMPLETED;
-        
-        engine.replay_notify_email_removed(moved_ids);
-        
-        engine.replay_notify_email_count_changed(Numeric.int_floor(original_count - to_move.size, 0),
-            Geary.Folder.CountChangeReason.REMOVED);
-        
         return ReplayOperation.Status.CONTINUE;
     }
     
     public override void get_ids_to_be_remote_removed(Gee.Collection<ImapDB.EmailIdentifier> ids) {
-        if (moved_ids != null)
-            ids.add_all(moved_ids);
+        ids.add_all(to_move);
     }
     
     public override async ReplayOperation.Status replay_remote_async() throws Error {
-        if (moved_ids.size == 0)
+        if (to_move.size == 0)
             return ReplayOperation.Status.COMPLETED;
         
         // Remaining MessageSets are persisted in case of network retries
         if (remaining_msg_sets == null)
-            remaining_msg_sets = Imap.MessageSet.uid_sparse(ImapDB.EmailIdentifier.to_uids(moved_ids));
+            remaining_msg_sets = Imap.MessageSet.uid_sparse(ImapDB.EmailIdentifier.to_uids(to_move));
         
         if (remaining_msg_sets == null || remaining_msg_sets.size == 0)
             return ReplayOperation.Status.COMPLETED;
@@ -81,11 +54,7 @@ private class Geary.ImapEngine.MoveEmail : Geary.ImapEngine.SendReplayOperation
             
             Imap.MessageSet msg_set = iter.get();
             
-            Gee.Map<Imap.UID, Imap.UID>? src_dst_uids = yield engine.remote_folder.copy_email_async(
-                msg_set, destination, null);
-            if (src_dst_uids != null)
-                destination_uids.add_all(src_dst_uids.values);
-            
+            yield engine.remote_folder.copy_email_async(msg_set, destination, null);
             yield engine.remote_folder.remove_email_async(msg_set.to_list(), null);
             
             // completed successfully, remove from list in case of retry
@@ -94,14 +63,19 @@ private class Geary.ImapEngine.MoveEmail : Geary.ImapEngine.SendReplayOperation
         
         return ReplayOperation.Status.COMPLETED;
     }
-
+    
     public override async void backout_local_async() throws Error {
-        yield engine.local_folder.mark_removed_async(moved_ids, false, cancellable);
+        if (to_move.size == 0)
+            return;
+        
+        yield engine.local_folder.mark_removed_async(to_move, false, cancellable);
+        
+        int count = engine.get_remote_counts(null, null);
         
-        engine.replay_notify_email_inserted(moved_ids);
-        engine.replay_notify_email_count_changed(original_count, Geary.Folder.CountChangeReason.INSERTED);
+        engine.replay_notify_email_inserted(to_move);
+        engine.replay_notify_email_count_changed(count + to_move.size, Folder.CountChangeReason.INSERTED);
     }
-
+    
     public override string describe_state() {
         return "%d email IDs to %s".printf(to_move.size, destination.to_string());
     }
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-move-email-prepare.vala 
b/src/engine/imap-engine/replay-ops/imap-engine-move-email-prepare.vala
new file mode 100644
index 0000000..7e0db69
--- /dev/null
+++ b/src/engine/imap-engine/replay-ops/imap-engine-move-email-prepare.vala
@@ -0,0 +1,65 @@
+/* Copyright 2012-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.ImapEngine.MoveEmailPrepare : Geary.ImapEngine.SendReplayOperation {
+    public Gee.Set<ImapDB.EmailIdentifier>? prepared_for_move = null;
+    
+    private MinimalFolder engine;
+    private Cancellable? cancellable;
+    private Gee.List<ImapDB.EmailIdentifier> to_move = new Gee.ArrayList<ImapDB.EmailIdentifier>();
+    
+    public MoveEmailPrepare(MinimalFolder engine, Gee.Collection<ImapDB.EmailIdentifier> to_move,
+        Cancellable? cancellable) {
+        base.only_local("MoveEmailPrepare", OnError.RETRY);
+        
+        this.engine = engine;
+        this.to_move.add_all(to_move);
+        this.cancellable = cancellable;
+    }
+    
+    public override void notify_remote_removed_ids(Gee.Collection<ImapDB.EmailIdentifier> ids) {
+        if (prepared_for_move != null)
+            prepared_for_move.remove_all(ids);
+    }
+    
+    public override async ReplayOperation.Status replay_local_async() throws Error {
+        if (to_move.size <= 0)
+            return ReplayOperation.Status.COMPLETED;
+        
+        int count = engine.get_remote_counts(null, null);
+        
+        // as this value is only used for reporting, offer best-possible service
+        if (count < 0)
+            count = to_move.size;
+        
+        prepared_for_move = yield engine.local_folder.mark_removed_async(to_move, true, cancellable);
+        if (prepared_for_move == null || prepared_for_move.size == 0)
+            return ReplayOperation.Status.COMPLETED;
+        
+        engine.replay_notify_email_removed(prepared_for_move);
+        
+        engine.replay_notify_email_count_changed(
+            Numeric.int_floor(count - prepared_for_move.size, 0),
+            Folder.CountChangeReason.REMOVED);
+        
+        return ReplayOperation.Status.COMPLETED;
+    }
+    
+    public override void get_ids_to_be_remote_removed(Gee.Collection<ImapDB.EmailIdentifier> ids) {
+    }
+    
+    public override async ReplayOperation.Status replay_remote_async() throws Error {
+        return ReplayOperation.Status.COMPLETED;
+    }
+    
+    public override async void backout_local_async() throws Error {
+    }
+    
+    public override string describe_state() {
+        return "%d email IDs".printf(prepared_for_move.size);
+    }
+}
+
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-move-email-revoke.vala 
b/src/engine/imap-engine/replay-ops/imap-engine-move-email-revoke.vala
new file mode 100644
index 0000000..62df914
--- /dev/null
+++ b/src/engine/imap-engine/replay-ops/imap-engine-move-email-revoke.vala
@@ -0,0 +1,55 @@
+/* Copyright 2012-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.ImapEngine.MoveEmailRevoke : Geary.ImapEngine.SendReplayOperation {
+    private MinimalFolder engine;
+    private Gee.List<ImapDB.EmailIdentifier> to_revoke = new Gee.ArrayList<ImapDB.EmailIdentifier>();
+    private Cancellable? cancellable;
+    
+    public MoveEmailRevoke(MinimalFolder engine, Gee.Collection<ImapDB.EmailIdentifier> to_revoke,
+        Cancellable? cancellable) {
+        base.only_local("MoveEmailRevoke", OnError.RETRY);
+        
+        this.engine = engine;
+        
+        this.to_revoke.add_all(to_revoke);
+        this.cancellable = cancellable;
+    }
+    
+    public override void notify_remote_removed_ids(Gee.Collection<ImapDB.EmailIdentifier> ids) {
+        to_revoke.remove_all(ids);
+    }
+    
+    public override async ReplayOperation.Status replay_local_async() throws Error {
+        if (to_revoke.size == 0)
+            return ReplayOperation.Status.COMPLETED;
+        
+        yield engine.local_folder.mark_removed_async(to_revoke, false, cancellable);
+        
+        int count = engine.get_remote_counts(null, null);
+        
+        engine.replay_notify_email_inserted(to_revoke);
+        engine.replay_notify_email_count_changed(count + to_revoke.size,
+            Geary.Folder.CountChangeReason.INSERTED);
+        
+        return ReplayOperation.Status.COMPLETED;
+    }
+    
+    public override void get_ids_to_be_remote_removed(Gee.Collection<ImapDB.EmailIdentifier> ids) {
+    }
+    
+    public override async ReplayOperation.Status replay_remote_async() throws Error {
+        return ReplayOperation.Status.COMPLETED;
+    }
+    
+    public override async void backout_local_async() throws Error {
+    }
+    
+    public override string describe_state() {
+        return "%d email IDs".printf(to_revoke.size);
+    }
+}
+


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