[geary/wip/713592-drafts] Hook up to composer widget, expand functionality and instrumentation



commit fcb9f77f1a4c1090a363e0d0b390bcbd9121a3e0
Author: Jim Nelson <jim yorba org>
Date:   Thu Jan 8 16:34:25 2015 -0800

    Hook up to composer widget, expand functionality and instrumentation

 src/client/composer/composer-widget.vala |  259 +++++++++++++++++-------------
 src/engine/api/geary-email-flags.vala    |   12 ++
 src/engine/app/app-draft-manager.vala    |  199 +++++++++++++++++++----
 3 files changed, 321 insertions(+), 149 deletions(-)
---
diff --git a/src/client/composer/composer-widget.vala b/src/client/composer/composer-widget.vala
index 5a6e422..f7b94c9 100644
--- a/src/client/composer/composer-widget.vala
+++ b/src/client/composer/composer-widget.vala
@@ -112,7 +112,7 @@ public class ComposerWidget : Gtk.EventBox {
         </body></html>""";
     private const string CURSOR = "<span id=\"cursormarker\"></span>";
     
-    private const int DRAFT_TIMEOUT_SEC = 10;
+    private const int DRAFT_TIMEOUT_SEC = 2;
     
     public const string ATTACHMENT_KEYWORDS_SUFFIX = ".doc|.pdf|.xls|.ppt|.rtf|.pps";
     
@@ -237,12 +237,10 @@ public class ComposerWidget : Gtk.EventBox {
     private string reply_message_id = "";
     private bool top_posting = true;
     
-    private Geary.FolderSupport.Create? drafts_folder = null;
-    private Geary.EmailIdentifier? draft_id = null;
+    private Geary.App.DraftManager? draft_manager = null;
+    private Geary.EmailIdentifier? editing_draft_id = null;
+    private Geary.EmailFlags draft_flags = new Geary.EmailFlags.with(Geary.EmailFlags.DRAFT);
     private uint draft_save_timeout_id = 0;
-    private Cancellable cancellable_drafts = new Cancellable();
-    private Cancellable cancellable_save_draft = new Cancellable();
-    private bool in_draft_save = false;
     
     public WebKit.WebView editor;
     // We need to keep a reference to the edit-fixer in composer-window, so it doesn't get
@@ -443,7 +441,7 @@ public class ComposerWidget : Gtk.EventBox {
                     }
                     
                     if (is_referred_draft)
-                        draft_id = referred.id;
+                        editing_draft_id = referred.id;
                     
                     add_attachments(referred.attachments);
                 break;
@@ -558,12 +556,12 @@ public class ComposerWidget : Gtk.EventBox {
         chain.append(attachments_box);
         box.set_focus_chain(chain);
         
-        // If there's only one account, open the drafts folder.  If there's more than one account,
-        // the drafts folder will be opened by on_from_changed().
+        // If there's only one account, open the drafts manager.  If there's more than one account,
+        // the drafts manager will be opened by on_from_changed().
         if (!from_multiple.visible)
-            open_drafts_folder_async.begin(cancellable_drafts);
+            open_draft_manager_async.begin(null);
         
-        destroy.connect(() => { close_drafts_folder_async.begin(); });
+        destroy.connect(() => { close_draft_manager_async.begin(null); });
     }
     
     public ComposerWidget.from_mailto(Geary.Account account, string mailto) {
@@ -891,9 +889,10 @@ public class ComposerWidget : Gtk.EventBox {
     }
     
     private bool can_save() {
-        return (drafts_folder != null && drafts_folder.get_open_state() == Geary.Folder.OpenState.BOTH
-            && !drafts_folder.properties.create_never_returns_id && editor.can_undo()
-            && account.information.save_drafts);
+        return draft_manager != null
+            && draft_manager.is_open
+            && editor.can_undo()
+            && account.information.save_drafts;
     }
 
     public CloseStatus should_close() {
@@ -902,10 +901,7 @@ public class ComposerWidget : Gtk.EventBox {
         container.present();
         AlertDialog dialog;
         
-        if (drafts_folder == null && try_to_save) {
-            dialog = new ConfirmationDialog(container.top_window,
-                _("Do you want to discard the unsaved message?"), null, Stock._DISCARD);
-        } else if (try_to_save) {
+        if (try_to_save) {
             dialog = new TernaryConfirmationDialog(container.top_window,
                 _("Do you want to discard this message?"), null, Stock._KEEP, Stock._DISCARD,
                 Gtk.ResponseType.CLOSE);
@@ -919,13 +915,13 @@ public class ComposerWidget : Gtk.EventBox {
             return CloseStatus.CANCEL_CLOSE; // Cancel
         } else if (response == Gtk.ResponseType.OK) {
             if (try_to_save) {
-                save_and_exit.begin(); // Save
+                save_and_exit(); // Save
                 return CloseStatus.PENDING_CLOSE;
             } else {
                 return CloseStatus.DO_CLOSE;
             }
         } else {
-            delete_and_exit.begin(); // Discard
+            discard_and_exit(); // Discard
             return CloseStatus.PENDING_CLOSE;
         }
     }
@@ -1049,8 +1045,6 @@ public class ComposerWidget : Gtk.EventBox {
     
     // Used internally by on_send()
     private async void on_send_async() {
-        cancellable_save_draft.cancel();
-        
         container.vanish();
         
         linkify_document(editor.get_dom_document());
@@ -1062,83 +1056,157 @@ public class ComposerWidget : Gtk.EventBox {
             GLib.message("Error sending email: %s", e.message);
         }
         
-        yield delete_draft_async();
-        container.close_container(); // Only close window after draft is deleted; this closes the drafts 
folder.
+        Geary.Nonblocking.Semaphore? semaphore = discard_draft();
+        if (semaphore != null) {
+            try {
+                yield semaphore.wait_async();
+            } catch (Error err) {
+                // ignored
+            }
+        }
+        
+        // Only close window after draft is deleted; this closes the drafts folder.
+        container.close_container();
+    }
+    
+    private void on_draft_state_changed() {
+        switch (draft_manager.draft_state) {
+            case Geary.App.DraftManager.DraftState.STORED:
+                draft_save_text = DRAFT_SAVED_TEXT;
+            break;
+            
+            case Geary.App.DraftManager.DraftState.STORING:
+                draft_save_text = DRAFT_SAVING_TEXT;
+            break;
+            
+            case Geary.App.DraftManager.DraftState.NOT_STORED:
+                draft_save_text = "";
+            break;
+            
+            case Geary.App.DraftManager.DraftState.ERROR:
+                draft_save_text = DRAFT_ERROR_TEXT;
+            break;
+            
+            default:
+                assert_not_reached();
+        }
     }
     
-    private void on_drafts_opened(Geary.Folder.OpenState open_state, int count) {
-        if (open_state == Geary.Folder.OpenState.BOTH)
-            reset_draft_timer();
+    private void on_draft_manager_fatal(Error err) {
+        draft_save_text = DRAFT_ERROR_TEXT;
+    }
+    
+    private void connect_to_draft_manager() {
+        draft_manager.notify[Geary.App.DraftManager.PROP_DRAFT_STATE].connect(on_draft_state_changed);
+        draft_manager.fatal.connect(on_draft_manager_fatal);
+    }
+    
+    // This code is in a separate method due to https://bugzilla.gnome.org/show_bug.cgi?id=742621
+    // connect_to_draft_manager() is simply for symmetry.  When above bug is fixed, this code can
+    // be moved back into open/close methods
+    private void disconnect_from_draft_manager() {
+        draft_manager.notify[Geary.App.DraftManager.PROP_DRAFT_STATE].disconnect(on_draft_state_changed);
+        draft_manager.fatal.disconnect(on_draft_manager_fatal);
     }
     
     // Returns the drafts folder for the current From account.
-    private async void open_drafts_folder_async(Cancellable cancellable) throws Error {
-        yield close_drafts_folder_async(cancellable);
+    private async void open_draft_manager_async(Cancellable? cancellable) throws Error {
+        yield close_draft_manager_async(cancellable);
         
         if (!account.information.save_drafts)
             return;
         
-        Geary.FolderSupport.Create? folder = (yield account.get_required_special_folder_async(
-            Geary.SpecialFolderType.DRAFTS, cancellable)) as Geary.FolderSupport.Create;
+        draft_manager = new Geary.App.DraftManager(account);
+        try {
+            yield draft_manager.open_async(editing_draft_id, cancellable);
+        } catch (Error err) {
+            debug("Unable to open draft manager %s: %s", draft_manager.to_string(), err.message);
+            
+            draft_manager = null;
+            
+            throw err;
+        }
+        
+        // clear now, as it was only needed to open draft manager
+        editing_draft_id = null;
+        
+        connect_to_draft_manager();
+    }
+    
+    private async void close_draft_manager_async(Cancellable? cancellable) throws Error {
+        // clear status text
+        draft_save_text = "";
         
-        if (folder == null)
-            return; // No drafts folder.
+        // only clear editing_draft_id if associated with prior draft_manager, not due to this
+        // widget being initialized with it
+        if (draft_manager == null)
+            return;
         
-        yield folder.open_async(Geary.Folder.OpenFlags.FAST_OPEN | Geary.Folder.OpenFlags.NO_DELAY,
-            cancellable);
+        disconnect_from_draft_manager();
         
-        drafts_folder = folder;
-        drafts_folder.opened.connect(on_drafts_opened);
+        // drop ref even if close failed
+        try {
+            yield draft_manager.close_async(cancellable);
+        } finally {
+            draft_manager = null;
+            editing_draft_id = null;
+        }
     }
     
-    private async void close_drafts_folder_async(Cancellable? cancellable = null) throws Error {
-        if (drafts_folder == null)
+    // Resets the draft save timeout.
+    private void reset_draft_timer() {
+        draft_save_text = "";
+        cancel_draft_timer();
+        
+        if (can_save())
+            draft_save_timeout_id = Timeout.add_seconds(DRAFT_TIMEOUT_SEC, on_save_draft_timeout);
+    }
+    
+    // Cancels the draft save timeout
+    private void cancel_draft_timer() {
+        if (draft_save_timeout_id == 0)
             return;
         
-        // Close existing folder.
-        drafts_folder.opened.disconnect(on_drafts_opened);
-        yield drafts_folder.close_async(cancellable);
-        drafts_folder = null;
+        Source.remove(draft_save_timeout_id);
+        draft_save_timeout_id = 0;
     }
     
-    // Save to the draft folder, if available.
-    // Note that drafts are NOT "linkified."
     private bool on_save_draft_timeout() {
-        // since all control paths return false, this is not rescheduled by the event loop, so
-        // kill the timeout id
+        // this is not rescheduled by the event loop, so kill the timeout id
         draft_save_timeout_id = 0;
         
-        if (in_draft_save)
-            return false;
-        
-        in_draft_save = true;
-        save_async.begin(cancellable_save_draft, () => { in_draft_save = false; });
+        save_draft();
         
         return false;
     }
     
-    private async void save_async(Cancellable? cancellable) {
-        if (drafts_folder == null || !can_save())
-            return;
-        
+    // Note that drafts are NOT "linkified."
+    private Geary.Nonblocking.Semaphore? save_draft() {
+        // cancel timer in favor of just doing it now
         cancel_draft_timer();
         
-        draft_save_text = DRAFT_SAVING_TEXT;
+        try {
+            if (draft_manager != null)
+                return draft_manager.update(get_composed_email(null, true), draft_flags, null);
+        } catch (Error err) {
+            GLib.message("Unable to save draft: %s", err.message);
+        }
         
-        Geary.EmailFlags flags = new Geary.EmailFlags();
-        flags.add(Geary.EmailFlags.DRAFT);
+        return null;
+    }
+    
+    private Geary.Nonblocking.Semaphore? discard_draft() {
+        // cancel timer in favor of this operation
+        cancel_draft_timer();
         
         try {
-            // only save HTML drafts to avoid resetting the DOM (which happens when converting the
-            // HTML to flowed text)
-            draft_id = yield drafts_folder.create_email_async(new Geary.RFC822.Message.from_composed_email(
-                get_composed_email(null, true), null), flags, null, draft_id, cancellable);
-            
-            draft_save_text = DRAFT_SAVED_TEXT;
-        } catch (Error e) {
-            GLib.message("Error saving draft: %s", e.message);
-            draft_save_text = DRAFT_ERROR_TEXT;
+            if (draft_manager != null)
+                return draft_manager.discard();
+        } catch (Error err) {
+            GLib.message("Unable to discard draft: %s", err.message);
         }
+        
+        return null;
     }
     
     // Used while waiting for draft to save before closing widget.
@@ -1147,42 +1215,24 @@ public class ComposerWidget : Gtk.EventBox {
         cancel_draft_timer();
     }
     
-    private async void save_and_exit() {
+    private void save_and_exit() {
         make_gui_insensitive();
         
-        // Do the save.
-        yield save_async(null);
+        save_draft();
         
         container.close_container();
     }
     
-    private async void delete_and_exit() {
+    private void discard_and_exit() {
         make_gui_insensitive();
         
-        // Do the delete.
-        yield delete_draft_async();
+        discard_draft();
+        if (draft_manager != null)
+            draft_manager.discard_on_close = true;
         
         container.close_container();
     }
     
-    private async void delete_draft_async() {
-        if (drafts_folder == null || draft_id == null)
-            return;
-        
-        Geary.FolderSupport.Remove? removable_drafts = drafts_folder as Geary.FolderSupport.Remove;
-        if (removable_drafts == null) {
-            debug("Draft folder does not support remove.\n");
-            
-            return;
-        }
-        
-        try {
-            yield removable_drafts.remove_single_email_async(draft_id);
-        } catch (Error e) {
-            debug("Unable to delete draft: %s", e.message);
-        }
-    }
-    
     private void on_add_attachment_button_clicked() {
         AttachmentDialog dialog = null;
         do {
@@ -1903,27 +1953,6 @@ public class ComposerWidget : Gtk.EventBox {
         return false;
     }
     
-    // Resets the draft save timeout.
-    private void reset_draft_timer() {
-        if (!can_save())
-            return;
-        
-        draft_save_text = "";
-        cancel_draft_timer();
-        
-        if (drafts_folder != null)
-            draft_save_timeout_id = Timeout.add_seconds(DRAFT_TIMEOUT_SEC, on_save_draft_timeout);
-    }
-    
-    // Cancels the draft save timeout
-    private void cancel_draft_timer() {
-        if (draft_save_timeout_id == 0)
-            return;
-        
-        Source.remove(draft_save_timeout_id);
-        draft_save_timeout_id = 0;
-    }
-    
     private void update_actions() {
         // Undo/redo.
         actions.get_action(ACTION_UNDO).sensitive = editor.can_undo();
@@ -2056,10 +2085,10 @@ public class ComposerWidget : Gtk.EventBox {
         // if the Geary.Account didn't change and the drafts folder is open(ing), do nothing more;
         // need to check for the drafts folder because opening it in the case of multiple From:
         // is handled here alone, so need to open it if not already
-        if (!changed && drafts_folder != null)
+        if (!changed && draft_manager != null)
             return;
         
-        open_drafts_folder_async.begin(cancellable_drafts);
+        open_draft_manager_async.begin(null);
         reset_draft_timer();
     }
     
diff --git a/src/engine/api/geary-email-flags.vala b/src/engine/api/geary-email-flags.vala
index 2eddfec..b67c72e 100644
--- a/src/engine/api/geary-email-flags.vala
+++ b/src/engine/api/geary-email-flags.vala
@@ -41,6 +41,18 @@ public class Geary.EmailFlags : Geary.NamedFlags {
     public EmailFlags() {
     }
     
+    /**
+     * Create a new { link EmailFlags} container initialized with one or more flags.
+     */
+    public EmailFlags.with(Geary.NamedFlag flag1, ...) {
+        va_list args = va_list();
+        NamedFlag? flag = flag1;
+        
+        do {
+            add(flag);
+        } while((flag = args.arg()) != null);
+    }
+    
     // Convenience method to check if the unread flag is set.
     public inline bool is_unread() {
         return contains(UNREAD);
diff --git a/src/engine/app/app-draft-manager.vala b/src/engine/app/app-draft-manager.vala
index 2724686..2aad54c 100644
--- a/src/engine/app/app-draft-manager.vala
+++ b/src/engine/app/app-draft-manager.vala
@@ -13,6 +13,10 @@
  * asynchronous) methods for the composer to schedule remote operations without worrying about
  * synchronization, operation ordering, error-handling, and so forth.
  *
+ * If successive drafts are submitted for storage, drafts waiting in the queue (i.e. not yet sent
+ * to the server) are dropped without further consideration.  This prevents needless I/O with the
+ * server saving drafts that are only to be replaced by later versions.
+ *
  * Important: This object should be used ''per'' composed email and not to manage multiple emails
  * being composed to the same { link Account}.  DraftManager's internal state is solely for managing
  * the lifecycle of a single email being composed by the user.
@@ -24,6 +28,9 @@
 public class Geary.App.DraftManager : BaseObject {
     public const string PROP_IS_OPEN = "is-open";
     public const string PROP_DRAFT_STATE = "draft-state";
+    public const string PROP_VERSIONS_SAVED = "versions-saved";
+    public const string PROP_VERSIONS_DROPPED = "versions-dropped";
+    public const string PROP_DISCARD_ON_CLOSE = "discard-on-close";
     
     /**
      * Current saved state of the draft.
@@ -84,25 +91,60 @@ public class Geary.App.DraftManager : BaseObject {
      */
     public DraftState draft_state { get; private set; default = DraftState.NOT_STORED; }
     
+    /**
+     * The { link Geary.EmailIdentifier} of the last saved draft.
+     */
+    public Geary.EmailIdentifier? current_draft_id { get; private set; default = null; }
+    
+    /**
+     * The version number of the most recently saved draft.
+     *
+     * Even if an initial draft is supplied (with { link open_async}, this always starts at zero.
+     * It merely represents the number of times a draft was successfully saved.
+     *
+     * A { link discard} operation will reset this counter to zero.
+     */
+    public int versions_saved { get; private set; default = 0; }
+    
+    /**
+     * The number of drafts dropped as new ones are added to the queue.
+     *
+     * @see dropped
+     */
+    public int versions_dropped { get; private set; default = 0; }
+    
+    /**
+     * When set, the draft will be discarded when { link close_async} is called.
+     *
+     * In addition, when set all future { link update}s will result in the draft being dropped.
+     */
+    public bool discard_on_close { get; set; default = false; }
+    
     private Account account;
     private Folder? drafts_folder = null;
     private FolderSupport.Create? create_support = null;
     private FolderSupport.Remove? remove_support = null;
-    private EmailIdentifier? current_draft = null;
     private Nonblocking.Mailbox<Operation?> mailbox = new Nonblocking.Mailbox<Operation?>();
     private bool was_opened = false;
     private Error? fatal_err = null;
     
     /**
-     * Fired if an unrecoverable error occurs while processing drafts.
+     * Fired when a draft is successfully saved.
+     */
+    public signal void stored(Geary.ComposedEmail draft);
+    
+    /**
+     * Fired when a draft is discarded.
+     */
+    public signal void discarded();
+    
+    /**
+     * Fired when a draft is dropped.
      *
-     * The { link DraftManager} will be unable to process future drafts.
+     * This occurs when a draft is scheduled for { link update} while another draft is queued
+     * to be pushed to the server.  The queued draft is dropped in favor of the new one.
      */
-    public virtual signal void fatal(Error err) {
-        fatal_err = err;
-        
-        debug("%s: Irrecoverable failure: %s", to_string(), err.message);
-    }
+    public signal void dropped(Geary.ComposedEmail draft);
     
     /**
      * Fired when unable to save a draft but the { link DraftManager} remains open.
@@ -115,10 +157,31 @@ public class Geary.App.DraftManager : BaseObject {
         debug("%s: Unable to create draft: %s", to_string(), err.message);
     }
     
+    /**
+     * Fired if an unrecoverable error occurs while processing drafts.
+     *
+     * The { link DraftManager} will be unable to process future drafts.
+     */
+    public virtual signal void fatal(Error err) {
+        fatal_err = err;
+        
+        debug("%s: Irrecoverable failure: %s", to_string(), err.message);
+    }
+    
     public DraftManager(Geary.Account account) {
         this.account = account;
     }
     
+    protected void notify_stored(Geary.ComposedEmail draft) {
+        versions_saved++;
+        stored(draft);
+    }
+    
+    protected void notify_discarded() {
+        versions_saved = 0;
+        discarded();
+    }
+    
     /**
      * Open the { link DraftManager} and prepare it for handling composed messages.
      *
@@ -134,7 +197,7 @@ public class Geary.App.DraftManager : BaseObject {
      *
      * @see is_open
      */
-    public async void open_async(Geary.EmailIdentifier? initial_draft, Cancellable? cancellable = null)
+    public async void open_async(Geary.EmailIdentifier? initial_draft_id, Cancellable? cancellable = null)
         throws Error {
         if (is_open)
             throw new EngineError.ALREADY_OPEN("%s is already open", to_string());
@@ -143,14 +206,15 @@ public class Geary.App.DraftManager : BaseObject {
         
         was_opened = true;
         
-        current_draft = initial_draft;
-        if (current_draft != null)
+        current_draft_id = initial_draft_id;
+        if (current_draft_id != null)
             draft_state = DraftState.STORED;
         
         drafts_folder = account.get_special_folder(SpecialFolderType.DRAFTS);
         if (drafts_folder == null)
             throw new EngineError.NOT_FOUND("%s: No drafts folder found", to_string());
         
+        // if drafts folder doesn't support create and remove, call it quits
         create_support = drafts_folder as Geary.FolderSupport.Create;
         remove_support = drafts_folder as Geary.FolderSupport.Remove;
         if (create_support == null || remove_support == null) {
@@ -162,6 +226,19 @@ public class Geary.App.DraftManager : BaseObject {
         
         yield drafts_folder.open_async(Folder.OpenFlags.NONE, cancellable);
         
+        // if drafts folder doesn't return the identifier of newly created emails, then this object
+        // can't do it's work ... wait until open to check for this, to be absolutely sure
+        if (drafts_folder.properties.create_never_returns_id) {
+            try {
+                yield drafts_folder.close_async();
+            } catch (Error err) {
+                // ignore
+            }
+            
+            throw new EngineError.UNSUPPORTED("%s: Drafts folder %s does not return created mail ID",
+                to_string(), drafts_folder.to_string());
+        }
+        
         // start the operation message loop, which ensures commands are handled in orderly fashion
         operation_loop_async.begin();
         
@@ -170,8 +247,10 @@ public class Geary.App.DraftManager : BaseObject {
     }
     
     private void on_folder_closed(Folder.CloseReason reason) {
-        fatal(new EngineError.SERVER_UNAVAILABLE("%s: Unexpected drafts folder closed (%s)",
-            to_string(), reason.to_string()));
+        if (reason == Folder.CloseReason.FOLDER_CLOSED) {
+            fatal(new EngineError.SERVER_UNAVAILABLE("%s: Unexpected drafts folder closed (%s)",
+                to_string(), reason.to_string()));
+        }
     }
     
     /**
@@ -191,21 +270,38 @@ public class Geary.App.DraftManager : BaseObject {
         
         // don't flush a CLOSE down the pipe if failed, the operation loop is closed for business
         if (fatal_err == null) {
+            // if discarding on close, do so now
+            if (discard_on_close) {
+                // don't use discard(), which checks if open, but submit_push() directly,
+                // which doesn't
+                submit_push(null, null, null);
+            }
+            
             // flush pending I/O
             Nonblocking.Semaphore semaphore = new Nonblocking.Semaphore(cancellable);
             mailbox.send(new Operation(OperationType.CLOSE, null, null, null, semaphore));
             
             // wait for close to complete
-            yield semaphore.wait_async(cancellable);
+            try {
+                yield semaphore.wait_async(cancellable);
+            } catch (Error err) {
+                if (err is IOError.CANCELLED)
+                    throw err;
+                
+                // fall through
+            }
         }
         
         // Disconnect before closing, as signal handler is for unexpected closes
         drafts_folder.closed.disconnect(on_folder_closed);
         
-        yield drafts_folder.close_async(cancellable);
-        drafts_folder = null;
-        create_support = null;
-        remove_support = null;
+        try {
+            yield drafts_folder.close_async(cancellable);
+        } finally {
+            drafts_folder = null;
+            create_support = null;
+            remove_support = null;
+        }
     }
     
     private void check_open() throws EngineError {
@@ -218,10 +314,15 @@ public class Geary.App.DraftManager : BaseObject {
      *
      * See { link FolderSupport.Create.create_email_async} for more information on the flags and
      * date_received arguments.
+     *
+     * @returns A { link Semaphore} that is notified when the operation completes (with or without
+     * error)
      */
-    public void update(Geary.ComposedEmail draft, Geary.EmailFlags? flags, DateTime? date_received)
-        throws Error {
-        submit_push(draft, flags, date_received);
+    public Geary.Nonblocking.Semaphore? update(Geary.ComposedEmail draft, Geary.EmailFlags? flags,
+        DateTime? date_received) throws Error {
+        check_open();
+        
+        return submit_push(draft, flags, date_received);
     }
     
     /**
@@ -232,22 +333,44 @@ public class Geary.App.DraftManager : BaseObject {
      *
      * Note: Replaced drafts are deleted, but on some services (i.e. Gmail) those deleted messages
      * are actually moved to the Trash.  This call does not currently solve that problem.
+     *
+     * @returns A { link Semaphore} that is notified when the operation completes (with or without
+     * error)
      */
-    public void discard() throws Error {
-        submit_push(null, null, null);
+    public Geary.Nonblocking.Semaphore? discard() throws Error {
+        check_open();
+        
+        return submit_push(null, null, null);
     }
     
-    private void submit_push(ComposedEmail? draft, EmailFlags? flags, DateTime? date_received)
-        throws Error {
-        check_open();
+    // Note that this call doesn't check_open(), important when used within close_async()
+    private Nonblocking.Semaphore? submit_push(ComposedEmail? draft, EmailFlags? flags,
+        DateTime? date_received) {
+        // no drafts are pushed when discarding on close
+        if (draft != null && discard_on_close) {
+            versions_dropped++;
+            dropped(draft);
+            
+            return null;
+        }
         
-        // clear out pending updates
+        // clear out pending pushes (which can be updates or discards)
         mailbox.revoke_matching((op) => {
+            // count and notify of dropped drafts
+            if (op.op_type == OperationType.PUSH && op.draft != null) {
+                versions_dropped++;
+                dropped(op.draft);
+            }
+            
             return op.op_type == OperationType.PUSH;
         });
         
+        Nonblocking.Semaphore semaphore = new Nonblocking.Semaphore();
+        
         // schedule this draft for update (if null, it's a discard)
-        mailbox.send(new Operation(OperationType.PUSH, draft, flags, date_received, null));
+        mailbox.send(new Operation(OperationType.PUSH, draft, flags, date_received, semaphore));
+        
+        return semaphore;
     }
     
     private async void operation_loop_async() {
@@ -297,24 +420,32 @@ public class Geary.App.DraftManager : BaseObject {
         
         // delete old draft for all PUSHes: best effort ... since create_email_async() will handle
         // replacement in a transactional-kinda-way, only outright delete if not using create
-        if (current_draft != null && op.draft == null) {
+        if (current_draft_id != null && op.draft == null) {
+            bool success = false;
             try {
-                yield remove_support.remove_single_email_async(current_draft);
+                yield remove_support.remove_single_email_async(current_draft_id);
+                success = true;
             } catch (Error err) {
-                debug("%s: Unable to remove existing draft %s: %s", to_string(), current_draft.to_string(),
+                debug("%s: Unable to remove existing draft %s: %s", to_string(), 
current_draft_id.to_string(),
                     err.message);
             }
             
-            current_draft = null;
+            // always clear draft id (assuming that retrying a failed remove is unnecessary), but
+            // only signal the discard if it actually was removed
+            current_draft_id = null;
+            if (success)
+                notify_discarded();
         }
         
         // if draft supplied, save it
         if (op.draft != null) {
             RFC822.Message rfc822 = new RFC822.Message.from_composed_email(op.draft, null);
             try {
-                current_draft = yield create_support.create_email_async(rfc822, op.flags,
-                    op.date_received, current_draft, null);
+                current_draft_id = yield create_support.create_email_async(rfc822, op.flags,
+                    op.date_received, current_draft_id, null);
+                
                 draft_state = DraftState.STORED;
+                notify_stored(op.draft);
             } catch (Error err) {
                 draft_state = DraftState.ERROR;
                 


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