[geary/wip/721828-undo] Round-trip of archive/trash and undo works
- From: Jim Nelson <jnelson src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [geary/wip/721828-undo] Round-trip of archive/trash and undo works
- Date: Tue, 23 Dec 2014 22:58:27 +0000 (UTC)
commit 81ed97044a5cd8d191130fcb645fa3b2b9b0bb79
Author: Jim Nelson <jim yorba org>
Date: Tue Dec 23 14:58:00 2014 -0800
Round-trip of archive/trash and undo works
Slow right now, but basics and plumbing are in place.
src/CMakeLists.txt | 2 +
src/client/application/geary-controller.vala | 63 ++++++++++++++-
src/client/components/main-toolbar.vala | 8 ++-
src/engine/api/geary-folder-supports-archive.vala | 10 ++-
src/engine/api/geary-folder-supports-copy.vala | 4 +-
src/engine/api/geary-folder-supports-move.vala | 4 +-
src/engine/api/geary-revokable.vala | 49 ++++++++++++
.../gmail/imap-engine-gmail-folder.vala | 15 +++-
.../imap-engine/imap-engine-minimal-folder.vala | 55 +++++++++++++-
.../imap-engine/imap-engine-revokable-move.vala | 80 ++++++++++++++++++++
10 files changed, 273 insertions(+), 17 deletions(-)
---
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 251b3b0..3ce824a 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -38,6 +38,7 @@ engine/api/geary-logging.vala
engine/api/geary-named-flag.vala
engine/api/geary-named-flags.vala
engine/api/geary-progress-monitor.vala
+engine/api/geary-revokable.vala
engine/api/geary-search-folder.vala
engine/api/geary-search-query.vala
engine/api/geary-service.vala
@@ -191,6 +192,7 @@ engine/imap-engine/imap-engine-generic-folder.vala
engine/imap-engine/imap-engine-minimal-folder.vala
engine/imap-engine/imap-engine-replay-operation.vala
engine/imap-engine/imap-engine-replay-queue.vala
+engine/imap-engine/imap-engine-revokable-move.vala
engine/imap-engine/imap-engine-send-replay-operation.vala
engine/imap-engine/gmail/imap-engine-gmail-account.vala
engine/imap-engine/gmail/imap-engine-gmail-folder.vala
diff --git a/src/client/application/geary-controller.vala b/src/client/application/geary-controller.vala
index 0116df7..87e6124 100644
--- a/src/client/application/geary-controller.vala
+++ b/src/client/application/geary-controller.vala
@@ -48,6 +48,7 @@ public class GearyController : Geary.BaseObject {
public const string ACTION_MOVE_MENU = "GearyMoveMenuButton";
public const string ACTION_GEAR_MENU = "GearyGearMenuButton";
public const string ACTION_SEARCH = "GearySearch";
+ public const string ACTION_UNDO = "GearyUndo";
public const string PROP_CURRENT_CONVERSATION ="current-conversations";
@@ -124,6 +125,7 @@ public class GearyController : Geary.BaseObject {
private Gee.List<string> pending_mailtos = new Gee.ArrayList<string>();
private Geary.Nonblocking.Mutex untrusted_host_prompt_mutex = new Geary.Nonblocking.Mutex();
private Gee.HashSet<Geary.Endpoint> validating_endpoints = new Gee.HashSet<Geary.Endpoint>();
+ private Geary.Revokable? revokable = null;
// List of windows we're waiting to close before Geary closes.
private Gee.List<ComposerWidget> waiting_to_close = new Gee.ArrayList<ComposerWidget>();
@@ -237,6 +239,9 @@ public class GearyController : Geary.BaseObject {
// instantiate here to ensure that Config is initialized and ready
autostart_manager = new AutostartManager();
+ // initialize revokable
+ save_revokable(null, null);
+
// Start Geary.
try {
yield Geary.Engine.instance.open_async(GearyApplication.instance.get_user_data_directory(),
@@ -410,6 +415,9 @@ public class GearyController : Geary.BaseObject {
entries += search;
add_accelerator("<Ctrl>S", ACTION_SEARCH);
+ Gtk.ActionEntry undo = { ACTION_UNDO, null, null, "<Ctrl>Z", null, on_revoke };
+ entries += undo;
+
return entries;
}
@@ -2297,10 +2305,13 @@ public class GearyController : Geary.BaseObject {
debug("Archiving selected messages");
Geary.FolderSupport.Archive? supports_archive = current_folder as Geary.FolderSupport.Archive;
- if (supports_archive == null)
+ if (supports_archive == null) {
debug("Folder %s doesn't support archive", current_folder.to_string());
- else
- yield supports_archive.archive_email_async(ids, cancellable);
+ } else {
+ save_revokable(yield supports_archive.archive_email_async(ids, cancellable),
+ _("Undo archive"));
+ }
+
return;
}
@@ -2312,7 +2323,9 @@ public class GearyController : Geary.BaseObject {
Geary.SpecialFolderType.TRASH, cancellable)).path;
Geary.FolderSupport.Move? supports_move = current_folder as Geary.FolderSupport.Move;
if (supports_move != null) {
- yield supports_move.move_email_async(ids, trash_path, cancellable);
+ save_revokable(yield supports_move.move_email_async(ids, trash_path, cancellable),
+ _("Undo trash"));
+
return;
}
}
@@ -2343,6 +2356,48 @@ 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);
+
+ // 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);
+
+ Gtk.Action undo_action = GearyApplication.instance.get_action(ACTION_UNDO);
+ undo_action.sensitive = revokable != null && revokable.can_revoke;
+ undo_action.tooltip = (revokable != null && description != null) ? description : "";
+ }
+
+ private void on_can_revoke_changed() {
+ // remove revokable if it goes invalid
+ if (revokable != null && !revokable.can_revoke)
+ save_revokable(null, null);
+ }
+
+ private void on_revoke() {
+ if (revokable != null && revokable.can_revoke)
+ revokable.revoke_async.begin(null, on_revoke_completed);
+ }
+
+ private void on_revoke_completed(Object? object, AsyncResult result) {
+ // Don't use the "revokable" instance because it might have gone null before this callback
+ // was reached
+ Geary.Revokable? origin = object as Geary.Revokable;
+ if (origin == null)
+ return;
+
+ try {
+ origin.revoke_async.end(result);
+ } catch (Error err) {
+ debug("Unable to revoke operation: %s", err.message);
+ }
+ }
+
private void on_zoom_in() {
main_window.conversation_viewer.web_view.zoom_in();
}
diff --git a/src/client/components/main-toolbar.vala b/src/client/components/main-toolbar.vala
index 4f0a960..ecf7999 100644
--- a/src/client/components/main-toolbar.vala
+++ b/src/client/components/main-toolbar.vala
@@ -71,7 +71,11 @@ public class MainToolbar : PillHeaderbar {
Gtk.Box trash_archive = create_pill_buttons(insert);
insert.clear();
insert.add(trash_buttons[1] = create_toolbar_button(null, GearyController.ACTION_TRASH_MESSAGE,
true));
- Gtk.Box trash = create_pill_buttons(insert, false);
+ Gtk.Box trash = create_pill_buttons(insert, false, false);
+
+ insert.clear();
+ insert.add(create_toolbar_button("edit-undo-symbolic", GearyController.ACTION_UNDO));
+ Gtk.Box undo = create_pill_buttons(insert, false, false);
// Search bar.
search_entry.width_chars = 28;
@@ -90,6 +94,7 @@ public class MainToolbar : PillHeaderbar {
#if !GTK_3_12
add_end(trash_archive);
add_end(trash);
+ add_end(undo);
add_end(search_upgrade_progress_bar);
add_end(search_entry);
#endif
@@ -105,6 +110,7 @@ public class MainToolbar : PillHeaderbar {
#if GTK_3_12
add_end(search_entry);
add_end(search_upgrade_progress_bar);
+ add_end(undo);
add_end(trash);
add_end(trash_archive);
#endif
diff --git a/src/engine/api/geary-folder-supports-archive.vala
b/src/engine/api/geary-folder-supports-archive.vala
index 9796a76..73ce06d 100644
--- a/src/engine/api/geary-folder-supports-archive.vala
+++ b/src/engine/api/geary-folder-supports-archive.vala
@@ -18,21 +18,25 @@ public interface Geary.FolderSupport.Archive : Geary.Folder {
* Archives the specified emails from the folder.
*
* The { link Geary.Folder} must be opened prior to attempting this operation.
+ *
+ * @returns A { link Geary.Revokable} that may be used to revoke (undo) this operation later.
*/
- public abstract async void archive_email_async(Gee.List<Geary.EmailIdentifier> email_ids,
+ public abstract async Geary.Revokable? archive_email_async(Gee.List<Geary.EmailIdentifier> email_ids,
Cancellable? cancellable = null) throws Error;
/**
* Archive one email from the folder.
*
* The { link Geary.Folder} must be opened prior to attempting this operation.
+ *
+ * @returns A { link Geary.Revokable} that may be used to revoke (undo) this operation later.
*/
- public virtual async void archive_single_email_async(Geary.EmailIdentifier email_id,
+ public virtual async Geary.Revokable? archive_single_email_async(Geary.EmailIdentifier email_id,
Cancellable? cancellable = null) throws Error {
Gee.ArrayList<Geary.EmailIdentifier> ids = new Gee.ArrayList<Geary.EmailIdentifier>();
ids.add(email_id);
- yield archive_email_async(ids, cancellable);
+ return yield archive_email_async(ids, cancellable);
}
}
diff --git a/src/engine/api/geary-folder-supports-copy.vala b/src/engine/api/geary-folder-supports-copy.vala
index 351a16a..ca2f3b7 100644
--- a/src/engine/api/geary-folder-supports-copy.vala
+++ b/src/engine/api/geary-folder-supports-copy.vala
@@ -20,8 +20,10 @@ public interface Geary.FolderSupport.Copy : Geary.Folder {
* but will return success.
*
* The Folder must be opened prior to attempting this operation.
+ *
+ * @returns A { link Geary.Revokable} that may be used to revoke (undo) this operation later.
*/
- public abstract async void copy_email_async(Gee.List<Geary.EmailIdentifier> to_copy,
+ public abstract async Geary.Revokable? copy_email_async(Gee.List<Geary.EmailIdentifier> to_copy,
Geary.FolderPath destination, Cancellable? cancellable = null) throws Error;
}
diff --git a/src/engine/api/geary-folder-supports-move.vala b/src/engine/api/geary-folder-supports-move.vala
index 58ededf..e76062f 100644
--- a/src/engine/api/geary-folder-supports-move.vala
+++ b/src/engine/api/geary-folder-supports-move.vala
@@ -19,8 +19,10 @@ public interface Geary.FolderSupport.Move : Geary.Folder {
* way but will return success.
*
* The { link Geary.Folder} must be opened prior to attempting this operation.
+ *
+ * @returns A { link Geary.Revokable} that may be used to revoke (undo) this operation later.
*/
- public abstract async void move_email_async(Gee.List<Geary.EmailIdentifier> to_move,
+ public abstract async Geary.Revokable? move_email_async(Gee.List<Geary.EmailIdentifier> to_move,
Geary.FolderPath destination, Cancellable? cancellable = null) throws Error;
}
diff --git a/src/engine/api/geary-revokable.vala b/src/engine/api/geary-revokable.vala
new file mode 100644
index 0000000..51b6f95
--- /dev/null
+++ b/src/engine/api/geary-revokable.vala
@@ -0,0 +1,49 @@
+/* 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.
+ */
+
+/**
+ * A representation of an operation with the Geary Engine that make be revoked (undone) at a later
+ * time.
+ */
+
+public abstract class Geary.Revokable : BaseObject {
+ public const string PROP_CAN_REVOKE = "can-revoke";
+ public const string PROP_IS_REVOKING = "is-revoking";
+
+ /**
+ * Indicates if { link revoke_async} is a valid operation 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.
+ */
+ public bool can_revoke { get; protected set; default = true; }
+
+ /**
+ * Indicates a { link revoke_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.
+ */
+ public bool is_revoking { get; protected set; default = false; }
+
+ protected Revokable() {
+ }
+
+ /**
+ * 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}.
+ *
+ * @throws EngineError.ALREADY_OPEN if { link is_revoking} is true when called.
+ */
+ public abstract async bool revoke_async(Cancellable? cancellable = null) throws Error;
+}
+
diff --git a/src/engine/imap-engine/gmail/imap-engine-gmail-folder.vala
b/src/engine/imap-engine/gmail/imap-engine-gmail-folder.vala
index 0d233ec..8cb3a25 100644
--- a/src/engine/imap-engine/gmail/imap-engine-gmail-folder.vala
+++ b/src/engine/imap-engine/gmail/imap-engine-gmail-folder.vala
@@ -17,12 +17,21 @@ private class Geary.ImapEngine.GmailFolder : MinimalFolder, FolderSupport.Archiv
return yield base.create_email_async(rfc822, flags, date_received, id, cancellable);
}
- public async void archive_email_async(Gee.List<Geary.EmailIdentifier> email_ids,
+ public async Geary.Revokable? archive_email_async(Gee.List<Geary.EmailIdentifier> email_ids,
Cancellable? cancellable = null) throws Error {
- // TODO: Use move_email_async("All Mail") here; Gmail will do the right thing and report
+ // Use move_email_async("All Mail") here; Gmail will do the right thing and report
// it was copied with the All Mail UID (in other words, no actual copy is performed).
- // Stash returned All Mail identifier as the Undo identifier
+ // This allows for undoing an archive with the same code path as a move.
+ Geary.Folder? all_mail = account.get_special_folder(Geary.SpecialFolderType.ALL_MAIL);
+ if (all_mail != null)
+ return yield move_email_async(email_ids, all_mail.path, cancellable);
+
+ // although this shouldn't happen, fall back on our traditional archive, which is simply
+ // to remove the message from this label
+ message("Unable to perform revokable archive on %s: All Mail not found", to_string());
yield expunge_email_async(email_ids, cancellable);
+
+ return null;
}
}
diff --git a/src/engine/imap-engine/imap-engine-minimal-folder.vala
b/src/engine/imap-engine/imap-engine-minimal-folder.vala
index 45e49f0..2fa79b1 100644
--- a/src/engine/imap-engine/imap-engine-minimal-folder.vala
+++ b/src/engine/imap-engine/imap-engine-minimal-folder.vala
@@ -46,6 +46,9 @@ private class Geary.ImapEngine.MinimalFolder : Geary.AbstractFolder, Geary.Folde
private uint open_remote_timer_id = 0;
private int reestablish_delay_msec = DEFAULT_REESTABLISH_DELAY_MSEC;
+ // Used by ImapEngine Revokables.
+ public signal void uids_removed(Gee.Collection<Imap.UID> uids);
+
public MinimalFolder(GenericAccount account, Imap.Account remote, ImapDB.Account local,
ImapDB.Folder local_folder, SpecialFolderType special_folder_type) {
_account = account;
@@ -72,6 +75,24 @@ private class Geary.ImapEngine.MinimalFolder : Geary.AbstractFolder, Geary.Folde
local_folder.email_complete.disconnect(on_email_complete);
}
+ public override void notify_email_removed(Gee.Collection<Geary.EmailIdentifier> ids) {
+ // fire base signal first
+ base.notify_email_removed(ids);
+
+ // parse out UIDs and fire signal for Revokables
+ Gee.HashSet<Imap.UID> uids = new Gee.HashSet<Imap.UID>();
+ foreach (Geary.EmailIdentifier id in ids) {
+ ImapDB.EmailIdentifier? imapdb_id = id as ImapDB.EmailIdentifier;
+ if (imapdb_id != null && imapdb_id.uid != null)
+ uids.add(imapdb_id.uid);
+ }
+
+ debug("Notifying of %d UIDs removed from %s", uids.size, to_string());
+
+ if (uids.size > 0)
+ uids_removed(uids);
+ }
+
public void set_special_folder_type(SpecialFolderType new_type) {
SpecialFolderType old_type = _special_folder_type;
_special_folder_type = new_type;
@@ -1237,32 +1258,36 @@ private class Geary.ImapEngine.MinimalFolder : Geary.AbstractFolder, Geary.Folde
yield mark.wait_for_ready_async(cancellable);
}
- public virtual async void copy_email_async(Gee.List<Geary.EmailIdentifier> to_copy,
+ public virtual async Geary.Revokable? copy_email_async(Gee.List<Geary.EmailIdentifier> to_copy,
Geary.FolderPath destination, Cancellable? cancellable = null) throws Error {
check_open("copy_email_async");
check_ids("copy_email_async", to_copy);
// watch for copying to this folder, which is treated as a no-op
if (destination.equal_to(path))
- return;
+ return null;
CopyEmail copy = new CopyEmail(this, (Gee.List<ImapDB.EmailIdentifier>) to_copy, destination);
replay_queue.schedule(copy);
yield copy.wait_for_ready_async(cancellable);
+
+ return null;
}
- public virtual async void move_email_async(Gee.List<Geary.EmailIdentifier> to_move,
+ public virtual async Geary.Revokable? move_email_async(Gee.List<Geary.EmailIdentifier> to_move,
Geary.FolderPath destination, Cancellable? cancellable = null) throws Error {
check_open("move_email_async");
check_ids("move_email_async", to_move);
// watch for moving to this folder, which is treated as a no-op
if (destination.equal_to(path))
- return;
+ return null;
MoveEmail move = new MoveEmail(this, (Gee.List<ImapDB.EmailIdentifier>) to_move, destination);
replay_queue.schedule(move);
yield move.wait_for_ready_async(cancellable);
+
+ return new RevokableMove(account, path, destination, move.destination_uids);
}
private void on_email_flags_changed(Gee.Map<Geary.EmailIdentifier, Geary.EmailFlags> changed) {
@@ -1359,5 +1384,27 @@ private class Geary.ImapEngine.MinimalFolder : Geary.AbstractFolder, Geary.Folde
return ret;
}
+
+ // To be used only by RevokableMove. Return false if UIDs are unknown to local store.
+ internal async bool revoke_move_async(Gee.Collection<Imap.UID> uids, FolderPath source,
+ Cancellable? cancellable) throws Error {
+ check_open("revoke_move_async");
+
+ // need to wait for fully open to ensure UIDs are normalized between local and remove
+ yield wait_for_open_async(cancellable);
+
+ Gee.Set<ImapDB.EmailIdentifier>? ids = yield local_folder.get_ids_async(uids,
+ ImapDB.Folder.ListFlags.NONE, cancellable);
+ if (ids == null || ids.size == 0)
+ return false;
+
+ MoveEmail move = new MoveEmail(this, traverse<ImapDB.EmailIdentifier>(ids).to_array_list(),
+ source, cancellable);
+ replay_queue.schedule(move);
+
+ yield move.wait_for_ready_async(cancellable);
+
+ return true;
+ }
}
diff --git a/src/engine/imap-engine/imap-engine-revokable-move.vala
b/src/engine/imap-engine/imap-engine-revokable-move.vala
new file mode 100644
index 0000000..80c047e
--- /dev/null
+++ b/src/engine/imap-engine/imap-engine-revokable-move.vala
@@ -0,0 +1,80 @@
+/* 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.ImapEngine.RevokableMove : Revokable {
+ private Account account;
+ private FolderPath original_source;
+ private FolderPath original_dest;
+ private Gee.Set<Imap.UID> uids;
+ private MinimalFolder? dest_folder = null;
+
+ public RevokableMove(Account account, FolderPath original_source, FolderPath original_dest,
+ Gee.Set<Imap.UID> uids) {
+ this.account = account;
+ this.original_source = original_source;
+ this.original_dest = original_dest;
+ this.uids = uids;
+ }
+
+ public override async bool revoke_async(Cancellable? cancellable) throws Error {
+ if (is_revoking)
+ throw new EngineError.ALREADY_OPEN("Already revoking operation");
+
+ is_revoking = true;
+ try {
+ return yield internal_revoke_async(cancellable);
+ } finally {
+ is_revoking = false;
+ }
+ }
+
+ private async bool internal_revoke_async(Cancellable? cancellable) throws Error {
+ // moving from original destination to original source
+ try {
+ Geary.Folder folder = yield account.fetch_folder_async(original_dest, cancellable);
+ dest_folder = folder as ImapEngine.MinimalFolder;
+ } catch (Error err) {
+ debug("Unable to revoke move to %s: %s", original_dest.to_string(), err.message);
+ }
+
+ if (dest_folder == null)
+ return can_revoke = false;
+
+ // trap removal of the UIDs we're moving
+ dest_folder.uids_removed.connect(on_folder_uids_removed);
+
+ // open, revoke, close, ensuring the close and signal disconnect are performed in all cases
+ try {
+ yield dest_folder.open_async(Geary.Folder.OpenFlags.NO_DELAY, cancellable);
+
+ if (!yield dest_folder.revoke_move_async(uids, original_source, cancellable))
+ can_revoke = false;
+ } finally {
+ // note that the Cancellable is not used
+ try {
+ yield dest_folder.close_async();
+ } catch (Error err) {
+ // ignored
+ }
+
+ dest_folder.uids_removed.disconnect(on_folder_uids_removed);
+ dest_folder = null;
+ }
+
+ return can_revoke;
+ }
+
+ private void on_folder_uids_removed(Gee.Collection<Imap.UID> removed_uids) {
+ // one-way switch
+ if (!can_revoke)
+ return;
+
+ // otherwise, ability to revoke is best-effort
+ uids.remove_all(removed_uids);
+ can_revoke = uids.size > 0;
+ }
+}
+
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]