[geary/wip/713592-drafts] First go, basic implementation and interface
- From: Jim Nelson <jnelson src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [geary/wip/713592-drafts] First go, basic implementation and interface
- Date: Wed, 7 Jan 2015 23:46:51 +0000 (UTC)
commit 16ca99ccdb82253476b6367099b315ce9bbc5615
Author: Jim Nelson <jim yorba org>
Date: Wed Jan 7 15:46:17 2015 -0800
First go, basic implementation and interface
DraftManager fleshed out but not hooked up to the composer
src/CMakeLists.txt | 1 +
src/engine/app/app-draft-manager.vala | 335 ++++++++++++++++++++
.../app-conversation-operation-queue.vala | 2 +-
src/engine/nonblocking/nonblocking-mailbox.vala | 2 +-
4 files changed, 338 insertions(+), 2 deletions(-)
---
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 251b3b0..4f3a552 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -46,6 +46,7 @@ engine/api/geary-special-folder-type.vala
engine/app/app-conversation.vala
engine/app/app-conversation-monitor.vala
+engine/app/app-draft-manager.vala
engine/app/app-email-store.vala
engine/app/conversation-monitor/app-append-operation.vala
diff --git a/src/engine/app/app-draft-manager.vala b/src/engine/app/app-draft-manager.vala
new file mode 100644
index 0000000..2724686
--- /dev/null
+++ b/src/engine/app/app-draft-manager.vala
@@ -0,0 +1,335 @@
+/* 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.
+ */
+
+/**
+ * Manage saving, replacing, and deleting the various versions of a draft message while the user is
+ * editing it.
+ *
+ * Each composer should create a single DraftManager object for the lifetime of the compose
+ * session. The DraftManager interface offers "fire-and-forget" nonblocking (but not
+ * asynchronous) methods for the composer to schedule remote operations without worrying about
+ * synchronization, operation ordering, error-handling, and so forth.
+ *
+ * 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.
+ *
+ * The only async calls for DraftManager is { link open_async} and { link close_async}, which give
+ * it a chance to initialize and tear-down in an orderly manner.
+ */
+
+public class Geary.App.DraftManager : BaseObject {
+ public const string PROP_IS_OPEN = "is-open";
+ public const string PROP_DRAFT_STATE = "draft-state";
+
+ /**
+ * Current saved state of the draft.
+ */
+ public enum DraftState {
+ /**
+ * Indicates not stored on the remote server (either not uploaded or discarded).
+ */
+ NOT_STORED,
+ /**
+ * A save (upload) is in process.
+ */
+ STORING,
+ /**
+ * Draft is stored on the remote.
+ */
+ STORED,
+ /**
+ * A non-fatal error occurred attempting to store the draft.
+ *
+ * @see draft_failed
+ */
+ ERROR
+ }
+
+ private enum OperationType {
+ PUSH,
+ CLOSE
+ }
+
+ private class Operation : BaseObject {
+ public OperationType op_type;
+ public ComposedEmail? draft;
+ public EmailFlags? flags;
+ public DateTime? date_received;
+ public Nonblocking.Semaphore? semaphore;
+
+ public Operation(OperationType op_type, ComposedEmail? draft, EmailFlags? flags,
+ DateTime? date_received, Nonblocking.Semaphore? semaphore) {
+ this.op_type = op_type;
+ this.draft = draft;
+ this.flags = flags;
+ this.date_received = date_received;
+ this.semaphore = semaphore;
+ }
+ }
+
+ /**
+ * Indicates the { link DraftsManager} is open and ready for service.
+ *
+ * Although this property can be monitored, the object is considered "open" when
+ * { link open_async} completes, not when this property changes to true.
+ */
+ public bool is_open { get; private set; default = false; }
+
+ /**
+ * The current saved state of the draft.
+ */
+ public DraftState draft_state { get; private set; default = DraftState.NOT_STORED; }
+
+ 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.
+ *
+ * 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);
+ }
+
+ /**
+ * Fired when unable to save a draft but the { link DraftManager} remains open.
+ *
+ * Due to unpredictability of errors being reported, it's possible this signal will fire after
+ * { link fatal}. It should not be assumed this signal firing means DraftManager is still
+ * operational, but if fatal fires, it definitely is not operational.
+ */
+ public virtual signal void draft_failed(Geary.ComposedEmail draft, Error err) {
+ debug("%s: Unable to create draft: %s", to_string(), err.message);
+ }
+
+ public DraftManager(Geary.Account account) {
+ this.account = account;
+ }
+
+ /**
+ * Open the { link DraftManager} and prepare it for handling composed messages.
+ *
+ * An initial draft EmailIdentifier may be supplied indicating the starting draft (when editing,
+ * not creating a new draft). No checking is performed to ensure this EmailIdentifier is valid
+ * for the drafts folder, nor is it downloaded by DraftManager. In essence, this email is
+ * deleted by the manager when the first { link update} occurs or the draft is
+ * { link discard}ed.
+ *
+ * If an initial_draft is supplied, { link draft_state} will be set to { link DraftState.STORED}.
+ *
+ * Other method calls should not be invoked until this completes successfully.
+ *
+ * @see is_open
+ */
+ public async void open_async(Geary.EmailIdentifier? initial_draft, Cancellable? cancellable = null)
+ throws Error {
+ if (is_open)
+ throw new EngineError.ALREADY_OPEN("%s is already open", to_string());
+ else if (was_opened)
+ throw new EngineError.UNSUPPORTED("%s cannot be re-opened", to_string());
+
+ was_opened = true;
+
+ current_draft = initial_draft;
+ if (current_draft != 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());
+
+ create_support = drafts_folder as Geary.FolderSupport.Create;
+ remove_support = drafts_folder as Geary.FolderSupport.Remove;
+ if (create_support == null || remove_support == null) {
+ throw new EngineError.UNSUPPORTED("%s: Drafts folder %s does not support create and remove",
+ to_string(), drafts_folder.to_string());
+ }
+
+ drafts_folder.closed.connect(on_folder_closed);
+
+ yield drafts_folder.open_async(Folder.OpenFlags.NONE, cancellable);
+
+ // start the operation message loop, which ensures commands are handled in orderly fashion
+ operation_loop_async.begin();
+
+ // done
+ is_open = true;
+ }
+
+ private void on_folder_closed(Folder.CloseReason reason) {
+ fatal(new EngineError.SERVER_UNAVAILABLE("%s: Unexpected drafts folder closed (%s)",
+ to_string(), reason.to_string()));
+ }
+
+ /**
+ * Flush pending operations and close the { link DraftsManager}.
+ *
+ * Once closed, the object cannot be opened again. Create a new object in that case.
+ *
+ * @see open_async
+ * @see is_open
+ */
+ public async void close_async(Cancellable? cancellable = null) throws Error {
+ if (!is_open || drafts_folder == null)
+ return;
+
+ // prevent further operations
+ is_open = false;
+
+ // don't flush a CLOSE down the pipe if failed, the operation loop is closed for business
+ if (fatal_err == 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);
+ }
+
+ // 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;
+ }
+
+ private void check_open() throws EngineError {
+ if (!is_open)
+ throw new EngineError.OPEN_REQUIRED("%s is not open", to_string());
+ }
+
+ /**
+ * Save draft on the server, potentially replacing (deleting) an already-existing version.
+ *
+ * See { link FolderSupport.Create.create_email_async} for more information on the flags and
+ * date_received arguments.
+ */
+ public void update(Geary.ComposedEmail draft, Geary.EmailFlags? flags, DateTime? date_received)
+ throws Error {
+ submit_push(draft, flags, date_received);
+ }
+
+ /**
+ * Delete all versions of the composed email from the server.
+ *
+ * This is appropriate both for the user cancelling (discarding) a composed message or if the
+ * user sends it.
+ *
+ * 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.
+ */
+ public void discard() throws Error {
+ submit_push(null, null, null);
+ }
+
+ private void submit_push(ComposedEmail? draft, EmailFlags? flags, DateTime? date_received)
+ throws Error {
+ check_open();
+
+ // clear out pending updates
+ mailbox.revoke_matching((op) => {
+ return op.op_type == OperationType.PUSH;
+ });
+
+ // schedule this draft for update (if null, it's a discard)
+ mailbox.send(new Operation(OperationType.PUSH, draft, flags, date_received, null));
+ }
+
+ private async void operation_loop_async() {
+ for (;;) {
+ // if a fatal error occurred (it can happen outside the loop), shutdown without
+ // reporting it again
+ if (fatal_err != null)
+ break;
+
+ Operation op;
+ try {
+ op = yield mailbox.recv_async(null);
+ } catch (Error err) {
+ fatal(err);
+
+ break;
+ }
+
+ bool continue_loop = yield operation_loop_iteration_async(op);
+
+ // fire semaphore, if present
+ if (op.semaphore != null)
+ op.semaphore.blind_notify();
+
+ if (!continue_loop)
+ break;
+ }
+ }
+
+ // Returns false if time to exit.
+ private async bool operation_loop_iteration_async(Operation op) {
+ // watch for CLOSE
+ if (op.op_type == OperationType.CLOSE)
+ return false;
+
+ // make sure there's a folder to work with
+ if (drafts_folder == null || drafts_folder.get_open_state() == Folder.OpenState.CLOSED) {
+ fatal(new EngineError.SERVER_UNAVAILABLE("%s: premature drafts folder close", to_string()));
+
+ return false;
+ }
+
+ // at this point, only operation left is PUSH
+ assert(op.op_type == OperationType.PUSH);
+
+ draft_state = DraftState.STORING;
+
+ // 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) {
+ try {
+ yield remove_support.remove_single_email_async(current_draft);
+ } catch (Error err) {
+ debug("%s: Unable to remove existing draft %s: %s", to_string(), current_draft.to_string(),
+ err.message);
+ }
+
+ current_draft = null;
+ }
+
+ // 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);
+ draft_state = DraftState.STORED;
+ } catch (Error err) {
+ draft_state = DraftState.ERROR;
+
+ // notify subscribers
+ draft_failed(op.draft, err);
+ }
+ } else {
+ draft_state = DraftState.NOT_STORED;
+ }
+
+ return true;
+ }
+
+ public string to_string() {
+ return "%s DraftManager".printf(account.to_string());
+ }
+}
+
diff --git a/src/engine/app/conversation-monitor/app-conversation-operation-queue.vala
b/src/engine/app/conversation-monitor/app-conversation-operation-queue.vala
index 5d951ae..7577e4f 100644
--- a/src/engine/app/conversation-monitor/app-conversation-operation-queue.vala
+++ b/src/engine/app/conversation-monitor/app-conversation-operation-queue.vala
@@ -23,7 +23,7 @@ private class Geary.App.ConversationOperationQueue : BaseObject {
FillWindowOperation? fill_op = op as FillWindowOperation;
if (fill_op != null) {
Gee.Collection<ConversationOperation> removed
- = mailbox.remove_matching(o => o is FillWindowOperation);
+ = mailbox.revoke_matching(o => o is FillWindowOperation);
// If there were any "insert" fill window ops, preserve that flag,
// as otherwise we might miss some data.
diff --git a/src/engine/nonblocking/nonblocking-mailbox.vala b/src/engine/nonblocking/nonblocking-mailbox.vala
index 52d45db..3337b44 100644
--- a/src/engine/nonblocking/nonblocking-mailbox.vala
+++ b/src/engine/nonblocking/nonblocking-mailbox.vala
@@ -73,7 +73,7 @@ public class Geary.Nonblocking.Mailbox<G> : BaseObject {
/**
* Remove messages matching the given predicate. Return the removed messages.
*/
- public Gee.Collection<G> remove_matching(owned Gee.Predicate<G> predicate) {
+ public Gee.Collection<G> revoke_matching(owned Gee.Predicate<G> predicate) {
Gee.ArrayList<G> removed = new Gee.ArrayList<G>();
// Iterate over a copy so we can modify the original.
foreach (G msg in queue.to_array()) {
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]