Commit 049a718c authored by Eric Gregory's avatar Eric Gregory

Archive/delete and mark unread now use ReplayQueue. Closes #4526

parent eea2b31a
......@@ -54,7 +54,8 @@ CREATE TABLE MessageLocationTable (
id INTEGER PRIMARY KEY,
message_id INTEGER REFERENCES MessageTable ON DELETE CASCADE,
folder_id INTEGER REFERENCES FolderTable ON DELETE CASCADE,
ordering INTEGER
ordering INTEGER,
remove_marker INTEGER DEFAULT 0
);
CREATE INDEX MessageLocationTableMessageIDIndex ON MessageLocationTable(message_id);
......
......@@ -81,6 +81,7 @@ public class GearyController {
private bool second_list_pass_required = false;
private int busy_count = 0;
private Geary.Conversation? current_conversation = null;
private Geary.Conversation? last_deleted_conversation = null;
public GearyController() {
// Setup actions.
......@@ -573,6 +574,12 @@ public class GearyController {
}
private void on_delete_message() {
// Prevent deletes of the same conversation from repeating.
if (current_conversation == last_deleted_conversation)
return;
last_deleted_conversation = current_conversation;
Gee.Set<Geary.Email>? pool = current_conversation.get_pool();
if (pool == null)
return;
......
......@@ -27,11 +27,11 @@ public class Geary.EmailFlags : Geary.Equalable {
return list.read_only_view;
}
public void add(EmailFlag flag) {
public virtual void add(EmailFlag flag) {
list.add(flag);
}
public bool remove(EmailFlag flag) {
public virtual bool remove(EmailFlag flag) {
return list.remove(flag);
}
......
......@@ -14,6 +14,6 @@ public errordomain Geary.EngineError {
BAD_RESPONSE,
INCOMPLETE_MESSAGE,
SERVER_UNAVAILABLE,
CLOSED
ALREADY_CLOSED
}
......@@ -384,9 +384,9 @@ public interface Geary.Folder : Object {
*
* The Folder must be opened prior to attempting this operation.
*/
public abstract async Gee.Map<Geary.EmailIdentifier, Geary.EmailFlags> mark_email_async(
Gee.List<Geary.EmailIdentifier> to_mark, Geary.EmailFlags? flags_to_add,
Geary.EmailFlags? flags_to_remove, Cancellable? cancellable = null) throws Error;
public abstract async void mark_email_async(Gee.List<Geary.EmailIdentifier> to_mark,
Geary.EmailFlags? flags_to_add, Geary.EmailFlags? flags_to_remove,
Cancellable? cancellable = null) throws Error;
/**
* check_span_specifiers() verifies that the span specifiers match the requirements set by
......
......@@ -13,5 +13,19 @@ public class Geary.Imap.EmailFlags : Geary.EmailFlags {
if (!flags.contains(MessageFlag.SEEN))
add(UNREAD);
}
public override void add(EmailFlag flag) {
if (flag.equals(UNREAD))
message_flags.remove(MessageFlag.SEEN);
base.add(flag);
}
public override bool remove(EmailFlag flag) {
if (flag.equals(UNREAD))
message_flags.add(MessageFlag.SEEN);
return base.remove(flag);
}
}
......@@ -196,7 +196,7 @@ private class Geary.Imap.Folder : Geary.AbstractFolder, Geary.RemoteFolder {
yield mailbox.expunge_email_async(cancellable);
}
public override async Gee.Map<Geary.EmailIdentifier, Geary.EmailFlags> mark_email_async(
public override async void mark_email_async(
Gee.List<Geary.EmailIdentifier> to_mark, Geary.EmailFlags? flags_to_add,
Geary.EmailFlags? flags_to_remove, Cancellable? cancellable = null) throws Error {
if (mailbox == null)
......@@ -207,7 +207,7 @@ private class Geary.Imap.Folder : Geary.AbstractFolder, Geary.RemoteFolder {
MessageFlag.from_email_flags(flags_to_add, flags_to_remove, out msg_flags_add,
out msg_flags_remove);
return yield mailbox.mark_email_async(message_set_from_id_list(to_mark), msg_flags_add,
yield mailbox.mark_email_async(message_set_from_id_list(to_mark), msg_flags_add,
msg_flags_remove, cancellable);
}
......
......@@ -44,7 +44,7 @@ public class Geary.Imap.MessageNumber : Geary.Common.IntMessageData, Geary.Imap.
public abstract class Geary.Imap.Flags : Geary.Common.MessageData, Geary.Imap.MessageData, Equalable {
public int size { get { return list.size; } }
private Gee.Set<Flag> list;
protected Gee.Set<Flag> list;
public Flags(Gee.Collection<Flag> flags) {
list = new Gee.HashSet<Flag>(Hashable.hash_func, Equalable.equal_func);
......@@ -121,6 +121,14 @@ public class Geary.Imap.MessageFlags : Geary.Imap.Flags {
return new MessageFlags(flags);
}
internal void add(MessageFlag flag) {
list.add(flag);
}
internal void remove(MessageFlag flag) {
list.remove(flag);
}
}
public class Geary.Imap.MailboxAttributes : Geary.Imap.Flags {
......
......@@ -13,19 +13,19 @@ public abstract class Geary.AbstractFolder : Object, Geary.Folder {
closed(reason);
}
protected virtual void notify_messages_appended(int total) {
internal virtual void notify_messages_appended(int total) {
messages_appended(total);
}
protected virtual void notify_message_removed(Geary.EmailIdentifier id) {
internal virtual void notify_message_removed(Geary.EmailIdentifier id) {
message_removed(id);
}
protected virtual void notify_email_count_changed(int new_count, Folder.CountChangeReason reason) {
internal virtual void notify_email_count_changed(int new_count, Folder.CountChangeReason reason) {
email_count_changed(new_count, reason);
}
protected virtual void notify_email_flags_changed(Gee.Map<Geary.EmailIdentifier,
internal virtual void notify_email_flags_changed(Gee.Map<Geary.EmailIdentifier,
Geary.EmailFlags> flag_map) {
email_flags_changed(flag_map);
}
......@@ -135,7 +135,7 @@ public abstract class Geary.AbstractFolder : Object, Geary.Folder {
yield remove_email_async(list, cancellable);
}
public abstract async Gee.Map<Geary.EmailIdentifier, Geary.EmailFlags> mark_email_async(
public abstract async void mark_email_async(
Gee.List<Geary.EmailIdentifier> to_mark, Geary.EmailFlags? flags_to_add,
Geary.EmailFlags? flags_to_remove, Cancellable? cancellable = null) throws Error;
......
......@@ -7,51 +7,6 @@
private class Geary.EngineFolder : Geary.AbstractFolder {
private const int REMOTE_FETCH_CHUNK_COUNT = 5;
private class ReplayAppend : ReplayOperation {
public EngineFolder owner;
public int new_remote_count;
public ReplayAppend(EngineFolder owner, int new_remote_count) {
base ("Append");
this.owner = owner;
this.new_remote_count = new_remote_count;
}
public override async void replay() {
yield owner.do_replay_appended_messages(new_remote_count);
}
}
private class ReplayRemoval : ReplayOperation {
public EngineFolder owner;
public int position;
public int new_remote_count;
public EmailIdentifier? id;
public ReplayRemoval(EngineFolder owner, int position, int new_remote_count) {
base ("Removal");
this.owner = owner;
this.position = position;
this.new_remote_count = new_remote_count;
this.id = null;
}
public ReplayRemoval.with_id(EngineFolder owner, EmailIdentifier id) {
base ("Removal.with_id");
this.owner = owner;
position = -1;
new_remote_count = -1;
this.id = id;
}
public override async void replay() {
yield owner.do_replay_remove_message(position, new_remote_count, id);
}
}
private class CommitOperation : NonblockingBatchOperation {
public Folder folder;
public Geary.Email email;
......@@ -68,15 +23,16 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
}
}
protected LocalFolder local_folder;
protected RemoteFolder? remote_folder = null;
internal LocalFolder local_folder { get; protected set; }
internal RemoteFolder? remote_folder { get; protected set; default = null; }
internal int remote_count { get; private set; default = -1; }
private RemoteAccount remote;
private LocalAccount local;
private int remote_count = -1;
private bool opened = false;
private NonblockingSemaphore remote_semaphore = new NonblockingSemaphore();
private ReplayQueue? replay_queue = null;
private ReceiveReplayQueue? recv_replay_queue = null;
private SendReplayQueue? send_replay_queue = null;
public EngineFolder(RemoteAccount remote, LocalAccount local, LocalFolder local_folder) {
this.remote = remote;
......@@ -165,8 +121,9 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
// all set; bless the remote folder as opened
remote_folder = folder;
// start the replay queue
replay_queue = new ReplayQueue();
// start the replay queues
recv_replay_queue = new ReceiveReplayQueue();
send_replay_queue = new SendReplayQueue();
} else {
debug("Unable to prepare remote folder %s: prepare_opened_file() failed", to_string());
}
......@@ -225,10 +182,12 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
folder.close_async.begin(cancellable);
// close the replay queue *after* the folder has been closed (in case any final upcalls
// close the replay queues *after* the folder has been closed (in case any final upcalls
// come and can be handled)
yield replay_queue.close_async();
replay_queue = null;
yield recv_replay_queue.close_async();
yield send_replay_queue.close_async();
recv_replay_queue = null;
send_replay_queue = null;
notify_closed(CloseReason.FOLDER_CLOSED);
}
......@@ -238,7 +197,7 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
private void on_remote_messages_appended(int total) {
debug("on_remote_messages_appended: total=%d", total);
replay_queue.schedule(new ReplayAppend(this, total));
recv_replay_queue.schedule(new ReplayAppend(this, total));
}
// Need to prefetch at least an EmailIdentifier (and duplicate detection fields) to create a
......@@ -248,7 +207,7 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
// which is exactly what we want.
//
// This MUST only be called from ReplayAppend.
private async void do_replay_appended_messages(int new_remote_count) {
internal async void do_replay_appended_messages(int new_remote_count) {
// this only works when the list is grown
if (remote_count >= new_remote_count) {
debug("Message reported appended by server but remote count %d already known",
......@@ -286,16 +245,16 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
private void on_remote_message_removed(Geary.EmailIdentifier id) {
debug("on_remote_message_removed: %s", id.to_string());
replay_queue.schedule(new ReplayRemoval.with_id(this, id));
recv_replay_queue.schedule(new ReplayRemoval.with_id(this, id));
}
private void on_remote_message_at_removed(int position, int total) {
debug("on_remote_message_at_removed: position=%d total=%d", position, total);
replay_queue.schedule(new ReplayRemoval(this, position, total));
recv_replay_queue.schedule(new ReplayRemoval(this, position, total));
}
// This MUST only be called from ReplayRemoval.
private async void do_replay_remove_message(int remote_position, int new_remote_count,
internal async void do_replay_remove_message(int remote_position, int new_remote_count,
Geary.EmailIdentifier? id) {
if (remote_position < 1)
assert(id != null);
......@@ -305,38 +264,22 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
Geary.EmailIdentifier? owned_id = id;
if (owned_id == null) {
try {
// convert remote positional addressing to local positional addressing
int local_count = yield local_folder.get_email_count_async();
int local_position = remote_position_to_local_position(remote_position, local_count);
// possible we don't have the remote email locally
if (local_position >= 1) {
// get EmailIdentifier for removed email
Gee.List<Geary.Email>? local = yield local_folder.list_email_async(local_position, 1,
Geary.Email.Field.NONE, Geary.Folder.ListFlags.NONE, null);
if (local != null && local.size == 1) {
owned_id = local[0].id;
} else {
debug("list_email_async unable to convert position %d into id (count=%d)",
local_position, yield local_folder.get_email_count_async());
}
} else {
debug("Unable to get local position for remote position %d (local_count=%d remote_count=%d)",
remote_position, local_count, remote_count);
}
owned_id = yield local_folder.id_from_remote_position(remote_position, remote_count);
} catch (Error err) {
debug("Unable to determine ID of removed message #%d from %s: %s", remote_position,
to_string(), err.message);
}
}
bool marked = false;
if (owned_id != null) {
debug("Removing from local store Email ID %s", owned_id.to_string());
try {
// Reflect change in the local store and notify subscribers
yield local_folder.remove_single_email_async(owned_id, null);
yield local_folder.remove_marked_email_async(owned_id, out marked, null);
notify_message_removed(owned_id);
if (!marked)
notify_message_removed(owned_id);
} catch (Error err2) {
debug("Unable to remove message #%d from %s: %s", remote_position, to_string(),
err2.message);
......@@ -346,7 +289,8 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
// save new remote count and notify of change
remote_count = new_remote_count;
notify_email_count_changed(remote_count, CountChangeReason.REMOVED);
if (!marked)
notify_email_count_changed(remote_count, CountChangeReason.REMOVED);
}
public override async int get_email_count_async(Cancellable? cancellable = null) throws Error {
......@@ -875,9 +819,7 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
if (!opened)
throw new EngineError.OPEN_REQUIRED("Folder %s not opened", to_string());
// Only need to remove from remote folder, since it will be signaled and automatically
// removed from the local folder.
yield remote_folder.remove_email_async(email_ids, cancellable);
send_replay_queue.schedule(new RemoveEmail(this, email_ids, cancellable));
}
// Converts a remote position to a local position, assuming that the remote has been completely
......@@ -948,19 +890,14 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
debug("prefetched %d for %s", prefetch_count, to_string());
}
public override async Gee.Map<Geary.EmailIdentifier, Geary.EmailFlags> mark_email_async(
Gee.List<Geary.EmailIdentifier> to_mark, Geary.EmailFlags? flags_to_add,
Geary.EmailFlags? flags_to_remove, Cancellable? cancellable = null) throws Error {
public override async void mark_email_async(Gee.List<Geary.EmailIdentifier> to_mark,
Geary.EmailFlags? flags_to_add, Geary.EmailFlags? flags_to_remove,
Cancellable? cancellable = null) throws Error {
if (!yield wait_for_remote_to_open())
throw new EngineError.SERVER_UNAVAILABLE("No connection to %s", remote.to_string());
Gee.Map<Geary.EmailIdentifier, Geary.EmailFlags> map =
yield remote_folder.mark_email_async(to_mark, flags_to_add, flags_to_remove, cancellable);
yield local_folder.set_email_flags_async(map, cancellable);
notify_email_flags_changed(map);
return map;
send_replay_queue.schedule(new MarkEmail(this, to_mark, flags_to_add, flags_to_remove,
cancellable));
}
}
......@@ -38,11 +38,36 @@ private interface Geary.LocalFolder : Object, Geary.Folder {
public async abstract int get_id_position_async(Geary.EmailIdentifier id, Cancellable? cancellable)
throws Error;
/**
* Removes an email while returning the "marked" status flag. This flag is used internally
* by the SendReplayQueue to record whether we've already notified for the removal.
*/
public async abstract void remove_marked_email_async(Geary.EmailIdentifier id, out bool marked,
Cancellable? cancellable) throws Error;
/**
* Marks or unmarks an e-mail for removal.
*/
public async abstract void mark_removed_async(Geary.EmailIdentifier id, bool remove,
Cancellable? cancellable) throws Error;
/**
* Retrieves email flags for the given list of email identifiers.
*/
public async abstract Gee.Map<Geary.EmailIdentifier, Geary.EmailFlags> get_email_flags_async(
Gee.List<Geary.EmailIdentifier> to_get, Cancellable? cancellable) throws Error;
/**
* Sets an e-mails flags based on the MessageFlags. Note that the EmailFlags MUST be of
* type Geary.Imap.EmailFlags and contain a valid MessageFlags object.
*/
public async abstract void set_email_flags_async(Gee.Map<Geary.EmailIdentifier,
Geary.EmailFlags> map, Cancellable? cancellable) throws Error;
/**
* Converts a remote position and count into an email ID.
*/
public async abstract Geary.EmailIdentifier? id_from_remote_position(int remote_position,
int new_remote_count) throws Error;
}
/* Copyright 2011 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.ReplayAppend : Geary.ReceiveReplayOperation {
public EngineFolder owner;
public int new_remote_count;
public ReplayAppend(EngineFolder owner, int new_remote_count) {
base ("Append");
this.owner = owner;
this.new_remote_count = new_remote_count;
}
public override async void replay() {
yield owner.do_replay_appended_messages(new_remote_count);
}
}
private class Geary.ReplayRemoval : Geary.ReceiveReplayOperation {
public EngineFolder owner;
public int position;
public int new_remote_count;
public EmailIdentifier? id;
public ReplayRemoval(EngineFolder owner, int position, int new_remote_count) {
base ("Removal");
this.owner = owner;
this.position = position;
this.new_remote_count = new_remote_count;
this.id = null;
}
public ReplayRemoval.with_id(EngineFolder owner, EmailIdentifier id) {
base ("Removal.with_id");
this.owner = owner;
position = -1;
new_remote_count = -1;
this.id = id;
}
public override async void replay() {
yield owner.do_replay_remove_message(position, new_remote_count, id);
}
}
......@@ -4,18 +4,18 @@
* (version 2.1 or later). See the COPYING file in this distribution.
*/
private abstract class Geary.ReplayOperation {
private abstract class Geary.ReceiveReplayOperation {
private string name;
public ReplayOperation(string name) {
public ReceiveReplayOperation(string name) {
this.name = name;
}
public abstract async void replay();
}
private class Geary.ReplayQueue {
private class ReplayClose : ReplayOperation {
private class Geary.ReceiveReplayQueue {
private class ReplayClose : ReceiveReplayOperation {
public NonblockingSemaphore semaphore = new NonblockingSemaphore();
public ReplayClose() {
......@@ -31,14 +31,15 @@ private class Geary.ReplayQueue {
}
}
private NonblockingMailbox<ReplayOperation> queue = new NonblockingMailbox<ReplayOperation>();
private NonblockingMailbox<ReceiveReplayOperation> queue = new
NonblockingMailbox<ReceiveReplayOperation>();
private bool closed = false;
public ReplayQueue() {
public ReceiveReplayQueue() {
do_process_queue.begin();
}
public void schedule(ReplayOperation op) {
public void schedule(ReceiveReplayOperation op) {
try {
queue.send(op);
} catch (Error err) {
......@@ -48,7 +49,7 @@ private class Geary.ReplayQueue {
public async void close_async() throws EngineError {
if (closed)
throw new EngineError.CLOSED("Closed");
throw new EngineError.ALREADY_CLOSED("Closed");
closed = true;
......@@ -58,7 +59,7 @@ private class Geary.ReplayQueue {
try {
yield replay_close.semaphore.wait_async();
} catch (Error err) {
error("Error waiting for replay queue to close: %s", err.message);
error("Error waiting for receive replay queue to close: %s", err.message);
}
}
......@@ -69,7 +70,7 @@ private class Geary.ReplayQueue {
if (queue.size == 0 && closed)
break;
ReplayOperation op;
ReceiveReplayOperation op;
try {
op = yield queue.recv_async();
} catch (Error err) {
......
/* Copyright 2011 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.MarkEmail : Geary.SendReplayOperation {
private EngineFolder engine;
private Gee.List<Geary.EmailIdentifier> to_mark;
private Geary.EmailFlags? flags_to_add;
private Geary.EmailFlags? flags_to_remove;
private Gee.Map<Geary.EmailIdentifier, Geary.EmailFlags>? original_flags = null;
private Cancellable? cancellable;
public MarkEmail(EngineFolder engine, Gee.List<Geary.EmailIdentifier> to_mark,
Geary.EmailFlags? flags_to_add, Geary.EmailFlags? flags_to_remove,
Cancellable? cancellable = null) {
base("MarkEmail");
this.engine = engine;
this.to_mark = to_mark;
this.flags_to_add = flags_to_add;
this.flags_to_remove = flags_to_remove;
this.cancellable = cancellable;
}
public override async bool replay_local() throws Error {
// Save original flags, then set new ones.
original_flags = yield engine.local_folder.get_email_flags_async(to_mark, cancellable);
yield engine.local_folder.mark_email_async(to_mark, flags_to_add, flags_to_remove,
cancellable);
// Notify using flags from DB.
engine.notify_email_flags_changed(yield engine.local_folder.get_email_flags_async(to_mark,
cancellable));
return false;
}
public override async bool replay_remote() throws Error {
yield engine.remote_folder.mark_email_async(to_mark, flags_to_add, flags_to_remove,
cancellable);
return true;
}
public override async void backout_local() throws Error {
// Restore original flags.
yield engine.local_folder.set_email_flags_async(original_flags, cancellable);
}
}
private class Geary.RemoveEmail : Geary.SendReplayOperation {
private EngineFolder engine;
private Gee.List<Geary.EmailIdentifier> to_remove;
private Cancellable? cancellable;
private int original_count = 0;
public RemoveEmail(EngineFolder engine, Gee.List<Geary.EmailIdentifier> to_remove,
Cancellable? cancellable = null) {
base("RemoveEmail");
this.engine = engine;
this.to_remove = to_remove;
this.cancellable = cancellable;
}
public override async bool replay_local() throws Error {
foreach (Geary.EmailIdentifier id in to_remove) {
yield engine.local_folder.mark_removed_async(id, true, cancellable);
engine.notify_message_removed(id);
}
original_count = engine.remote_count;
engine.notify_email_count_changed(original_count - to_remove.size,
Geary.Folder.CountChangeReason.REMOVED);
return false;
}
public override async bool replay_remote() throws Error {
// Remove from server. Note that this causes the receive replay queue to kick into
// action, removing the e-mail but *NOT* firing a signal; the "remove marker" indicates
// that the signal has already been fired.
yield engine.remote_folder.remove_email_async(to_remove, cancellable);
return true;
}
public override async void backout_local() throws Error {
foreach (Geary.EmailIdentifier id in to_remove)
yield engine.local_folder.mark_removed_async(id, false, cancellable);
engine.notify_messages_appended(to_remove.size);
engine.notify_email_count_changed(original_count, Geary.Folder.CountChangeReason.REMOVED);
}
}
/* Copyright 2011 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 abstract class Geary.SendReplayOperation {
public Error? error { get; private set; default = null; }
private NonblockingSemaphore semaphore = new NonblockingSemaphore();
private string name;
public SendReplayOperation(string name) {
this.name = name;
}
/**
* Runs the local operation.
* Returns true if the operation is complete, or false if a call to
* replay_remote() is also needed.
*/
public abstract async bool replay_local() throws Error;
/**
* Runs the remote operation.
* Returns true if the operation is complete, or false if the remote operation
* didn't complete successfully; in that case, backout_local() will be called.
*/
public abstract async bool replay_remote() throws Error;
/**
* Backs out the local operation.
* This is effectively an "undo" for when the remote operation failed.
*/
public virtual async void backout_local() throws Error {}
/**
* Waits until the operation is ready.
* On a read, this should wait until the entire operation completes.
* On a write, this should wait until the local operation completes.
*
* To trigger this, call set_ready() with either a null or an Error. Either will
* trigger completion, if an error is passed in this function will throw that error.
*/
public async void wait_for_ready() throws Error {
yield semaphore.wait_async();
if (error != null)
throw error;
}
// See the comments on wait_for_ready() on how to use this function.
internal void set_ready(Error? error) {
this.error = error;
try {
semaphore.notify();
} catch (Error e) {
debug("Unable to compelte send replay queue operation [%s] error: %s", name, e.message);