From e784d1b9b3b6cda0507545f0cef01668bd528d32 Mon Sep 17 00:00:00 2001 From: Eric Gregory Date: Tue, 13 Dec 2011 16:13:20 -0800 Subject: [PATCH] Signal for updating EmailFlags. Closes #4478 --- src/client/geary-controller.vala | 22 +++- src/client/ui/message-list-cell-renderer.vala | 2 +- src/client/ui/message-list-store.vala | 38 +++++- src/engine/api/geary-email-flag.vala | 32 +++++ src/engine/api/geary-email-flags.vala | 71 +++++++++++ src/engine/api/geary-email-properties.vala | 28 +---- src/engine/api/geary-folder.vala | 30 ++++- src/engine/imap/api/imap-email-flags.vala | 17 +++ .../imap/api/imap-email-properties.vala | 15 +-- src/engine/imap/api/imap-folder.vala | 8 +- src/engine/imap/message/imap-flag.vala | 16 ++- src/engine/imap/transport/imap-mailbox.vala | 66 ++++++---- src/engine/impl/geary-abstract-folder.vala | 11 +- src/engine/impl/geary-engine-folder.vala | 48 +++++--- .../impl/geary-generic-imap-folder.vala | 115 ++++++++++++++++++ src/engine/impl/geary-local-interfaces.vala | 7 ++ src/engine/sqlite/api/sqlite-folder.vala | 44 +++---- .../sqlite-imap-message-properties-row.vala | 2 +- src/wscript | 3 + 19 files changed, 443 insertions(+), 132 deletions(-) create mode 100644 src/engine/api/geary-email-flag.vala create mode 100644 src/engine/api/geary-email-flags.vala create mode 100644 src/engine/imap/api/imap-email-flags.vala diff --git a/src/client/geary-controller.vala b/src/client/geary-controller.vala index 6409e867..0ccff911 100644 --- a/src/client/geary-controller.vala +++ b/src/client/geary-controller.vala @@ -219,6 +219,8 @@ public class GearyController { current_conversations.conversation_removed.connect(on_conversation_removed); current_conversations.updated_placeholders.connect(on_updated_placeholders); + current_folder.email_flags_changed.connect(on_email_flags_changed); + // Do a quick-list of the messages (which should return what's in the local store) if // supported by the Folder, followed by a complete list if needed second_list_pass_required = @@ -298,6 +300,11 @@ public class GearyController { set_busy(false); } + private void on_email_flags_changed(Gee.Map map) { + foreach (Geary.EmailIdentifier id in map.keys) + main_window.message_list_store.update_flags(id, map.get(id)); + } + private async void do_fetch_previews(Cancellable? cancellable) throws Error { set_busy(true); Geary.NonblockingBatch batch = new Geary.NonblockingBatch(); @@ -370,12 +377,15 @@ public class GearyController { if (full_email.properties.email_flags.is_unread()) messages.add(full_email.id); - } - - // Mark as read. - if (messages.size > 0) - yield current_folder.mark_email_async(messages, Geary.EmailProperties.EmailFlags.NONE, - Geary.EmailProperties.EmailFlags.UNREAD, cancellable); + } + + // Mark as read. + if (messages.size > 0) { + Geary.EmailFlags flags = new Geary.EmailFlags(); + flags.add(Geary.EmailFlags.UNREAD); + + yield current_folder.mark_email_async(messages, null, flags, cancellable); + } } private void on_select_message_completed(Object? source, AsyncResult result) { diff --git a/src/client/ui/message-list-cell-renderer.vala b/src/client/ui/message-list-cell-renderer.vala index e139feb2..9f472318 100644 --- a/src/client/ui/message-list-cell-renderer.vala +++ b/src/client/ui/message-list-cell-renderer.vala @@ -19,7 +19,7 @@ public class FormattedMessageData : Object { private static int preview_height = -1; public Geary.Email email { get; private set; default = null; } - public bool is_unread { get; private set; default = false; } + public bool is_unread { get; set; default = false; } public string date { get; private set; default = ""; } public string from { get; private set; default = ""; } public string subject { get; private set; default = ""; } diff --git a/src/client/ui/message-list-store.vala b/src/client/ui/message-list-store.vala index 50869131..4474d5fe 100644 --- a/src/client/ui/message-list-store.vala +++ b/src/client/ui/message-list-store.vala @@ -64,7 +64,10 @@ public class MessageListStore : Gtk.TreeStore { ); } - public void update_conversation(Geary.Conversation conversation) { + // Updates a converstaion. + // only_update_flags: if true, we'll only update the read/unread status + public void update_conversation(Geary.Conversation conversation, bool only_update_flags = false) { + debug("update conversation. is unread: %s", conversation.is_unread() ? "yes" : "no"); Gtk.TreeIter iter; if (!find_conversation(conversation, out iter)) { // Unknown conversation, attempt to append it. @@ -86,10 +89,16 @@ public class MessageListStore : Gtk.TreeStore { FormattedMessageData? existing = null; get(iter, Column.MESSAGE_DATA, out existing); - // Update the preview if needed. - if (existing == null || !existing.email.id.equals(preview.id)) - set(iter, Column.MESSAGE_DATA, new FormattedMessageData.from_email(preview, pool.size, - conversation.is_unread())); + // Update preview if text or unread status changed. + if (existing != null && existing.is_unread != conversation.is_unread()) { + existing.is_unread = conversation.is_unread(); + set(iter, Column.MESSAGE_DATA, existing); + } + + if (!only_update_flags && (existing == null || !existing.email.id.equals(preview.id))) { + set(iter, Column.MESSAGE_DATA, new FormattedMessageData.from_email(preview, pool.size, + conversation.is_unread())); + } } public void remove_conversation(Geary.Conversation conversation) { @@ -179,6 +188,25 @@ public class MessageListStore : Gtk.TreeStore { return low; } + public void update_flags(Geary.EmailIdentifier id, Geary.EmailFlags flags) { + int count = get_count(); + for (int ctr = 0; ctr < count; ctr++) { + Geary.Conversation c = get_conversation_at_index(ctr); + Gee.SortedSet? mail = c.get_pool_sorted(compare_email_id_desc); + if (mail == null) + continue; + + foreach (Geary.Email e in mail) { + if (e.id.equals(id)) { + e.properties.email_flags = flags; + update_conversation(c, true); + + return; + } + } + } + } + private bool find_conversation(Geary.Conversation conversation, out Gtk.TreeIter iter) { iter = Gtk.TreeIter(); int count = get_count(); diff --git a/src/engine/api/geary-email-flag.vala b/src/engine/api/geary-email-flag.vala new file mode 100644 index 00000000..e7424a5b --- /dev/null +++ b/src/engine/api/geary-email-flag.vala @@ -0,0 +1,32 @@ +/* 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. + */ + +public class Geary.EmailFlag : Geary.Equalable, Geary.Hashable { + private string name; + public EmailFlag(string name) { + this.name = name; + } + + public bool equals(Equalable o) { + EmailFlag? other = o as EmailFlag; + if (other == null) + return false; + + if (this == other) + return true; + + return name.down() == other.name.down(); + } + + public uint to_hash() { + return name.down().hash(); + } + + public string to_string() { + return name; + } +} + diff --git a/src/engine/api/geary-email-flags.vala b/src/engine/api/geary-email-flags.vala new file mode 100644 index 00000000..b47c927b --- /dev/null +++ b/src/engine/api/geary-email-flags.vala @@ -0,0 +1,71 @@ +/* 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. + */ + +public class Geary.EmailFlags : Geary.Equalable { + + private static EmailFlag? _unread = null; + public static EmailFlag UNREAD { get { + if (_unread == null) + _unread = new EmailFlag("UNREAD"); + + return _unread; + } } + + private Gee.Set list = new Gee.HashSet(Hashable.hash_func, Equalable.equal_func); + + public EmailFlags() { + } + + public bool contains(EmailFlag flag) { + return list.contains(flag); + } + + public Gee.Set get_all() { + return list.read_only_view; + } + + public void add(EmailFlag flag) { + list.add(flag); + } + + public bool remove(EmailFlag flag) { + return list.remove(flag); + } + + // Convenience method to check if the unread flag is set. + public inline bool is_unread() { + return contains(UNREAD); + } + + public bool equals(Equalable o) { + Geary.EmailFlags? other = o as Geary.EmailFlags; + if (other == null) + return false; + + if (this == other) + return true; + + if (list.size != other.list.size) + return false; + + foreach (EmailFlag flag in list) { + if (!other.contains(flag)) + return false; + } + + return true; + } + + public string to_string() { + string ret = ""; + foreach (EmailFlag flag in list) { + ret += flag.to_string() + " "; + } + + return "[" + ret + "]"; + } +} + diff --git a/src/engine/api/geary-email-properties.vala b/src/engine/api/geary-email-properties.vala index 3e80c67d..59abb3ce 100644 --- a/src/engine/api/geary-email-properties.vala +++ b/src/engine/api/geary-email-properties.vala @@ -5,32 +5,10 @@ */ public abstract class Geary.EmailProperties : Object { - // Flags that can be set or cleared on a given e-mail. - public enum EmailFlags { - NONE = 0, - UNREAD = 1 << 0; - - public inline bool is_all_set(EmailFlags required_flags) { - return (this & required_flags) == required_flags; - } - - public inline EmailFlags set(EmailFlags flags) { - return (this | flags); - } - - public inline EmailFlags clear(EmailFlags flags) { - return (this & ~(flags)); - } - - // Convenience method to check if the unread flag is set. - public inline bool is_unread() { - return is_all_set(UNREAD); - } - } - - // Flags se on the email object. - public EmailFlags email_flags { get; protected set; default = EmailFlags.NONE; } + // Flags set on the email object. + public EmailFlags email_flags { get; set; } + public EmailProperties() { } } diff --git a/src/engine/api/geary-folder.vala b/src/engine/api/geary-folder.vala index 6af46a0a..25af1280 100644 --- a/src/engine/api/geary-folder.vala +++ b/src/engine/api/geary-folder.vala @@ -24,10 +24,17 @@ public interface Geary.Folder : Object { REMOVED } + /** + * Flags used for retrieving email. + * FAST: fetch from the DB only + * FORCE_UPDATE: fetch from remote only + * EXCLUDING_ID: exclude the provided ID + */ [Flags] public enum ListFlags { NONE = 0, FAST, + FORCE_UPDATE, EXCLUDING_ID; public bool is_any_set(ListFlags flags) { @@ -84,6 +91,15 @@ public interface Geary.Folder : Object { */ public signal void email_count_changed(int new_count, CountChangeReason reason); + /** + * "email-flags-changed" is fired when an email's flag changed. + * + * This signal will be fired both when changes occur on the client side via the + * mark_email_async() method as well as changes occur remotely. + */ + public signal void email_flags_changed(Gee.Map flag_map); + /** * This helper method should be called by implementors of Folder rather than firing the signal * directly. This allows subclasses and superclasses the opportunity to inspect the email @@ -119,6 +135,14 @@ public interface Geary.Folder : Object { */ protected abstract void notify_email_count_changed(int new_count, CountChangeReason reason); + /** + * This helper method should be called by implementors of Folder rather than firing the signal + * directly. This allows subclasses and superclasses the opportunity to inspect the email + * and update state before and/or after the signal has been fired. + */ + protected abstract void notify_email_flags_changed(Gee.Map flag_map); + public abstract Geary.FolderPath get_path(); public abstract Geary.FolderProperties? get_properties(); @@ -353,9 +377,9 @@ public interface Geary.Folder : Object { * * The Folder must be opened prior to attempting this operation. */ - public abstract async void mark_email_async(Gee.List to_mark, - Geary.EmailProperties.EmailFlags flags_to_add, Geary.EmailProperties.EmailFlags - flags_to_remove, Cancellable? cancellable = null) throws Error; + public abstract async Gee.Map mark_email_async( + Gee.List 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 diff --git a/src/engine/imap/api/imap-email-flags.vala b/src/engine/imap/api/imap-email-flags.vala new file mode 100644 index 00000000..c37855c8 --- /dev/null +++ b/src/engine/imap/api/imap-email-flags.vala @@ -0,0 +1,17 @@ +/* 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. + */ + +public class Geary.Imap.EmailFlags : Geary.EmailFlags { + public MessageFlags message_flags { get; private set; } + + public EmailFlags(MessageFlags flags) { + message_flags = flags; + + if (!flags.contains(MessageFlag.SEEN)) + add(UNREAD); + } +} + diff --git a/src/engine/imap/api/imap-email-properties.vala b/src/engine/imap/api/imap-email-properties.vala index a49526fb..5a28f6b1 100644 --- a/src/engine/imap/api/imap-email-properties.vala +++ b/src/engine/imap/api/imap-email-properties.vala @@ -11,12 +11,11 @@ public class Geary.Imap.EmailProperties : Geary.EmailProperties, Equalable { public bool flagged { get; private set; } public bool recent { get; private set; } public bool seen { get; private set; } - public MessageFlags flags { get; private set; } public InternalDate? internaldate { get; private set; } public RFC822.Size? rfc822_size { get; private set; } public EmailProperties(MessageFlags flags, InternalDate? internaldate, RFC822.Size? rfc822_size) { - this.flags = flags; + email_flags = new Geary.Imap.EmailFlags(flags); this.internaldate = internaldate; this.rfc822_size = rfc822_size; @@ -26,9 +25,6 @@ public class Geary.Imap.EmailProperties : Geary.EmailProperties, Equalable { flagged = flags.contains(MessageFlag.FLAGGED); recent = flags.contains(MessageFlag.RECENT); seen = flags.contains(MessageFlag.SEEN); - - if (!seen) - email_flags = email_flags.set(Geary.EmailProperties.EmailFlags.UNREAD); } public bool equals(Equalable e) { @@ -47,8 +43,13 @@ public class Geary.Imap.EmailProperties : Geary.EmailProperties, Equalable { if (rfc822_size == null || other.rfc822_size == null) return false; - return flags.equals(other.flags) && internaldate.equals(other.internaldate) - && rfc822_size.equals(other.rfc822_size); + return get_message_flags().equals(get_message_flags()) && + internaldate.equals(other.internaldate) && + rfc822_size.equals(other.rfc822_size); + } + + public Geary.Imap.MessageFlags get_message_flags() { + return ((Geary.Imap.EmailFlags) this.email_flags).message_flags; } } diff --git a/src/engine/imap/api/imap-folder.vala b/src/engine/imap/api/imap-folder.vala index 59994a97..db5f8303 100644 --- a/src/engine/imap/api/imap-folder.vala +++ b/src/engine/imap/api/imap-folder.vala @@ -192,9 +192,9 @@ private class Geary.Imap.Folder : Geary.AbstractFolder, Geary.RemoteFolder { throw new EngineError.READONLY("IMAP currently read-only"); } - public override async void mark_email_async(Gee.List to_mark, - Geary.EmailProperties.EmailFlags flags_to_add, Geary.EmailProperties.EmailFlags - flags_to_remove, Cancellable? cancellable = null) throws Error { + public override async Gee.Map mark_email_async( + Gee.List to_mark, Geary.EmailFlags? flags_to_add, + Geary.EmailFlags? flags_to_remove, Cancellable? cancellable = null) throws Error { if (mailbox == null) throw new EngineError.OPEN_REQUIRED("%s not opened", to_string()); @@ -207,7 +207,7 @@ private class Geary.Imap.Folder : Geary.AbstractFolder, Geary.RemoteFolder { } MessageSet message_set = new MessageSet.uid_sparse(sparse_set); - mailbox.mark_email_async(message_set, flags_to_add, flags_to_remove, cancellable); + return yield mailbox.mark_email_async(message_set, flags_to_add, flags_to_remove, cancellable); } } diff --git a/src/engine/imap/message/imap-flag.vala b/src/engine/imap/message/imap-flag.vala index 33c03c19..578bf178 100644 --- a/src/engine/imap/message/imap-flag.vala +++ b/src/engine/imap/message/imap-flag.vala @@ -99,17 +99,21 @@ public class Geary.Imap.MessageFlag : Geary.Imap.Flag { // Converts a list of email flags to add and remove to a list of message // flags to add and remove. - public static void from_email_flags(Geary.EmailProperties.EmailFlags email_flags_add, - Geary.EmailProperties.EmailFlags email_flags_remove, out Gee.List msg_flags_add, + public static void from_email_flags(Geary.EmailFlags? email_flags_add, + Geary.EmailFlags? email_flags_remove, out Gee.List msg_flags_add, out Gee.List msg_flags_remove) { msg_flags_add = new Gee.ArrayList(); msg_flags_remove = new Gee.ArrayList(); - if (email_flags_add.is_all_set(Geary.EmailProperties.EmailFlags.UNREAD)) - msg_flags_remove.add(MessageFlag.SEEN); + if (email_flags_add != null) { + if (email_flags_add.contains(Geary.EmailFlags.UNREAD)) + msg_flags_remove.add(MessageFlag.SEEN); + } - if (email_flags_remove.is_all_set(Geary.EmailProperties.EmailFlags.UNREAD)) - msg_flags_add.add(MessageFlag.SEEN); + if (email_flags_remove != null) { + if (email_flags_remove.contains(Geary.EmailFlags.UNREAD)) + msg_flags_add.add(MessageFlag.SEEN); + } } } diff --git a/src/engine/imap/transport/imap-mailbox.vala b/src/engine/imap/transport/imap-mailbox.vala index 6f11f39d..ad3b6002 100644 --- a/src/engine/imap/transport/imap-mailbox.vala +++ b/src/engine/imap/transport/imap-mailbox.vala @@ -496,10 +496,13 @@ public class Geary.Imap.Mailbox : Geary.SmartReference { email.set_full_references(message_id, in_reply_to, references); } - public async void mark_email_async(MessageSet to_mark, Geary.EmailProperties.EmailFlags - flags_to_add, Geary.EmailProperties.EmailFlags flags_to_remove, + public async Gee.Map mark_email_async( + MessageSet to_mark, Geary.EmailFlags? flags_to_add, Geary.EmailFlags? flags_to_remove, Cancellable? cancellable = null) throws Error { + Gee.Map ret = + new Gee.HashMap(); + if (context.is_closed()) throw new ImapError.NOT_SELECTED("Mailbox %s closed", name); @@ -514,40 +517,53 @@ public class Geary.Imap.Mailbox : Geary.SmartReference { if (msg_flags_add.size > 0) add_flags_id = batch.add(new MailboxOperation(context, new StoreCommand( - to_mark, msg_flags_add, true, true))); + to_mark, msg_flags_add, true, false))); if (msg_flags_remove.size > 0) remove_flags_id = batch.add(new MailboxOperation(context, new StoreCommand( - to_mark, msg_flags_remove, false, true))); + to_mark, msg_flags_remove, false, false))); yield batch.execute_all_async(cancellable); if (add_flags_id != NonblockingBatch.INVALID_ID) { - MailboxOperation add_op = (MailboxOperation) batch.get_operation(add_flags_id); - CommandResponse add_resp = (CommandResponse) batch.get_result(add_flags_id); - - if (add_resp.status_response == null) - throw new ImapError.SERVER_ERROR("Server error. Command: %s No status response. %s", - add_op.cmd.to_string(), add_resp.to_string()); - - if (add_resp.status_response.status != Status.OK) - throw new ImapError.SERVER_ERROR("Server error. Command: %s Response: %s Error: %s", - add_op.cmd.to_string(), add_resp.to_string(), - add_resp.status_response.status.to_string()); + gather_flag_results((MailboxOperation) batch.get_operation(add_flags_id), + (CommandResponse) batch.get_result(add_flags_id), ref ret); } if (remove_flags_id != NonblockingBatch.INVALID_ID) { - MailboxOperation remove_op = (MailboxOperation) batch.get_operation(remove_flags_id); - CommandResponse remove_resp = (CommandResponse) batch.get_result(remove_flags_id); + gather_flag_results((MailboxOperation) batch.get_operation(remove_flags_id), + (CommandResponse) batch.get_result(remove_flags_id), ref ret); + } + + return ret; + } + + // Helper function for building results for mark_email_async + private void gather_flag_results(MailboxOperation operation, CommandResponse response, + ref Gee.Map map) throws Error { + + if (response.status_response == null) + throw new ImapError.SERVER_ERROR("Server error. Command: %s No status response. %s", + operation.cmd.to_string(), response.to_string()); + + if (response.status_response.status != Status.OK) + throw new ImapError.SERVER_ERROR("Server error. Command: %s Response: %s Error: %s", + operation.cmd.to_string(), response.to_string(), + response.status_response.status.to_string()); + + FetchResults[] results = FetchResults.decode(response); + foreach (FetchResults res in results) { + UID? uid = res.get_data(FetchDataType.UID) as UID; + assert(uid != null); - if (remove_resp.status_response == null) - throw new ImapError.SERVER_ERROR("Server error. Command: %s No status response. %s", - remove_op.cmd.to_string(), remove_resp.to_string()); - - if (remove_resp.status_response.status != Status.OK) - throw new ImapError.SERVER_ERROR("Server error. Command: %s Response: %s Error: %s", - remove_op.cmd.to_string(), remove_resp.to_string(), - remove_resp.status_response.status.to_string()); + Geary.Imap.MessageFlags? msg_flags = res.get_data(FetchDataType.FLAGS) as MessageFlags; + if (msg_flags != null) { + Geary.Imap.EmailFlags email_flags = new Geary.Imap.EmailFlags(msg_flags); + + map.set(new Geary.Imap.EmailIdentifier(uid) , email_flags); + } else { + debug("No flags returned"); + } } } } diff --git a/src/engine/impl/geary-abstract-folder.vala b/src/engine/impl/geary-abstract-folder.vala index e1fb7b90..e48817b3 100644 --- a/src/engine/impl/geary-abstract-folder.vala +++ b/src/engine/impl/geary-abstract-folder.vala @@ -25,6 +25,11 @@ public abstract class Geary.AbstractFolder : Object, Geary.Folder { email_count_changed(new_count, reason); } + protected virtual void notify_email_flags_changed(Gee.Map flag_map) { + email_flags_changed(flag_map); + } + public abstract Geary.FolderPath get_path(); public abstract Geary.FolderProperties? get_properties(); @@ -122,9 +127,9 @@ public abstract class Geary.AbstractFolder : Object, Geary.Folder { public abstract async void remove_email_async(Geary.EmailIdentifier email_id, Cancellable? cancellable = null) throws Error; - public abstract async void mark_email_async(Gee.List to_mark, - Geary.EmailProperties.EmailFlags flags_to_add, Geary.EmailProperties.EmailFlags - flags_to_remove, Cancellable? cancellable = null) throws Error; + public abstract async Gee.Map mark_email_async( + Gee.List to_mark, Geary.EmailFlags? flags_to_add, + Geary.EmailFlags? flags_to_remove, Cancellable? cancellable = null) throws Error; public virtual string to_string() { return get_path().to_string(); diff --git a/src/engine/impl/geary-engine-folder.vala b/src/engine/impl/geary-engine-folder.vala index 2105d505..dd1b54d8 100644 --- a/src/engine/impl/geary-engine-folder.vala +++ b/src/engine/impl/geary-engine-folder.vala @@ -68,10 +68,11 @@ private class Geary.EngineFolder : Geary.AbstractFolder { } } + protected LocalFolder local_folder; + protected RemoteFolder? remote_folder = null; + private RemoteAccount remote; private LocalAccount local; - private LocalFolder local_folder; - private RemoteFolder? remote_folder = null; private int remote_count = -1; private bool opened = false; private NonblockingSemaphore remote_semaphore = new NonblockingSemaphore(); @@ -97,7 +98,8 @@ private class Geary.EngineFolder : Geary.AbstractFolder { } public override Geary.Folder.ListFlags get_supported_list_flags() { - return Geary.Folder.ListFlags.FAST; + return Geary.Folder.ListFlags.FAST | Geary.Folder.ListFlags.FORCE_UPDATE | + Geary.Folder.ListFlags.EXCLUDING_ID; } public override async void create_email_async(Geary.Email email, Cancellable? cancellable) throws Error { @@ -370,7 +372,7 @@ private class Geary.EngineFolder : Geary.AbstractFolder { // them all at once to the caller Gee.List accumulator = new Gee.ArrayList(); yield do_list_email_async(low, count, required_fields, accumulator, null, cancellable, - flags.is_any_set(Folder.ListFlags.FAST)); + flags.is_any_set(Folder.ListFlags.FAST), flags.is_any_set(Folder.ListFlags.FORCE_UPDATE)); return accumulator; } @@ -380,7 +382,7 @@ private class Geary.EngineFolder : Geary.AbstractFolder { Geary.Folder.ListFlags flags, EmailCallback cb, Cancellable? cancellable = null) { // schedule do_list_email_async(), using the callback to drive availability of email do_list_email_async.begin(low, count, required_fields, null, cb, cancellable, - flags.is_any_set(Folder.ListFlags.FAST)); + flags.is_any_set(Folder.ListFlags.FAST), flags.is_any_set(Folder.ListFlags.FORCE_UPDATE)); } // TODO: A great optimization would be to fetch message "fragments" from the local database @@ -389,12 +391,15 @@ private class Geary.EngineFolder : Geary.AbstractFolder { // would have to be parallelized. private async void do_list_email_async(int low, int count, Geary.Email.Field required_fields, Gee.List? accumulator, EmailCallback? cb, Cancellable? cancellable, - bool local_only) throws Error { + bool local_only, bool remote_only) throws Error { check_span_specifiers(low, count); if (!opened) throw new EngineError.OPEN_REQUIRED("%s is not open", to_string()); + if (local_only && remote_only) + throw new EngineError.BAD_PARAMETERS("local_only and remote_only are mutually exlusive"); + if (count == 0) { // signal finished if (cb != null) @@ -433,7 +438,7 @@ private class Geary.EngineFolder : Geary.AbstractFolder { low, count, local_count, remote_count, local_low); Gee.List? local_list = null; - if (local_low > 0) { + if (!remote_only && local_low > 0) { try { local_list = yield local_folder.list_email_async(local_low, count, required_fields, Geary.Folder.ListFlags.NONE, cancellable); @@ -676,7 +681,8 @@ private class Geary.EngineFolder : Geary.AbstractFolder { Cancellable? cancellable = null) throws Error { Gee.List list = new Gee.ArrayList(); yield do_list_email_by_id_async(initial_id, count, required_fields, list, null, cancellable, - flags.is_all_set(Folder.ListFlags.FAST), flags.is_all_set(Folder.ListFlags.EXCLUDING_ID)); + flags.is_all_set(Folder.ListFlags.FAST), flags.is_all_set(Folder.ListFlags.FORCE_UPDATE), + flags.is_all_set(Folder.ListFlags.EXCLUDING_ID)); return (list.size > 0) ? list : null; } @@ -685,15 +691,16 @@ private class Geary.EngineFolder : Geary.AbstractFolder { Geary.Email.Field required_fields, Folder.ListFlags flags, EmailCallback cb, Cancellable? cancellable = null) { do_lazy_list_email_by_id_async.begin(initial_id, count, required_fields, cb, cancellable, - flags.is_all_set(Folder.ListFlags.FAST), flags.is_all_set(Folder.ListFlags.EXCLUDING_ID)); + flags.is_all_set(Folder.ListFlags.FAST), flags.is_all_set(Folder.ListFlags.FORCE_UPDATE), + flags.is_all_set(Folder.ListFlags.EXCLUDING_ID)); } private async void do_lazy_list_email_by_id_async(Geary.EmailIdentifier initial_id, int count, Geary.Email.Field required_fields, EmailCallback cb, Cancellable? cancellable, bool local_only, - bool excluding_id) { + bool remote_only, bool excluding_id) { try { yield do_list_email_by_id_async(initial_id, count, required_fields, null, cb, cancellable, - local_only, excluding_id); + local_only, remote_only, excluding_id); } catch (Error err) { cb(null, err); } @@ -701,7 +708,7 @@ private class Geary.EngineFolder : Geary.AbstractFolder { private async void do_list_email_by_id_async(Geary.EmailIdentifier initial_id, int count, Geary.Email.Field required_fields, Gee.List? accumulator, EmailCallback? cb, - Cancellable? cancellable, bool local_only, bool excluding_id) throws Error { + Cancellable? cancellable, bool local_only, bool remote_only, bool excluding_id) throws Error { if (!opened) throw new EngineError.OPEN_REQUIRED("%s is not open", to_string()); @@ -766,7 +773,7 @@ private class Geary.EngineFolder : Geary.AbstractFolder { remote_count, excluding_id.to_string()); yield do_list_email_async(low, actual_count, required_fields, accumulator, cb, cancellable, - local_only); + local_only, remote_only); } private async Gee.List? remote_list_email(int[] needed_by_position, @@ -940,14 +947,19 @@ private class Geary.EngineFolder : Geary.AbstractFolder { debug("prefetched %d for %s", prefetch_count, to_string()); } - public override async void mark_email_async(Gee.List to_mark, - Geary.EmailProperties.EmailFlags flags_to_add, Geary.EmailProperties.EmailFlags - flags_to_remove, Cancellable? cancellable = null) throws Error { + public override async Gee.Map mark_email_async( + Gee.List 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()); - yield remote_folder.mark_email_async(to_mark, flags_to_add, flags_to_remove, cancellable); - yield local_folder.mark_email_async(to_mark, flags_to_add, flags_to_remove, cancellable); + Gee.Map 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; } } diff --git a/src/engine/impl/geary-generic-imap-folder.vala b/src/engine/impl/geary-generic-imap-folder.vala index e033c815..2299ecb2 100644 --- a/src/engine/impl/geary-generic-imap-folder.vala +++ b/src/engine/impl/geary-generic-imap-folder.vala @@ -5,10 +5,21 @@ */ private class Geary.GenericImapFolder : Geary.EngineFolder { + public const int DEFAULT_FLAG_WATCH_SEC = 3 * 60; + + private uint flag_watch_id = 0; + private Cancellable flag_watch_cancellable = new Cancellable(); + private bool in_flag_watch = false; + public GenericImapFolder(RemoteAccount remote, LocalAccount local, LocalFolder local_folder) { base (remote, local, local_folder); } + ~GenericImapFolder() { + disable_flag_watch(); + flag_watch_cancellable.cancel(); + } + // Check if the remote folder's ordering has changed since last opened protected override async bool prepare_opened_folder(Geary.Folder local_folder, Geary.Folder remote_folder, Cancellable? cancellable) throws Error { @@ -223,4 +234,108 @@ private class Geary.GenericImapFolder : Geary.EngineFolder { return true; } + + protected override void notify_opened(Geary.Folder.OpenState state, int count) { + base.notify_opened(state, count); + + if (state == Geary.Folder.OpenState.BOTH) { + flag_watch_cancellable = new Cancellable(); + enable_flag_watch(); + } + } + + protected override void notify_closed(Geary.Folder.CloseReason reason) { + disable_flag_watch(); + flag_watch_cancellable.cancel(); + + base.notify_closed(reason); + } + + /** + * Turns on the "flag watch." This periodtically checks if the flags on any messages have changed. + * + * If seconds is negative or zero, keepalives will be disabled. (This is not recommended.) + */ + private void enable_flag_watch(int seconds = DEFAULT_FLAG_WATCH_SEC) { + if (seconds <= 0) { + disable_flag_watch(); + + return; + } + + if (flag_watch_id != 0) + Source.remove(flag_watch_id); + + flag_watch_id = Timeout.add_seconds(seconds, on_flag_watch); + } + + private bool disable_flag_watch() { + if (flag_watch_id == 0) + return false; + + Source.remove(flag_watch_id); + flag_watch_id = 0; + + return true; + } + + private bool on_flag_watch() { + flag_watch_async.begin(); + return true; + } + + private async void flag_watch_async() { + if (in_flag_watch) + return; + + in_flag_watch = true; + try { + yield do_flag_watch_async(); + } catch (Error err) { + message("Flag watch error: %s", err.message); + } + + in_flag_watch = false; + } + + private async void do_flag_watch_async() throws Error { + Gee.HashMap local_map = + new Gee.HashMap(Geary.Hashable.hash_func, + Geary.Equalable.equal_func); + Gee.HashMap changed_map = + new Gee.HashMap(Geary.Hashable.hash_func, + Geary.Equalable.equal_func); + + // Fetch all email properties in local folder. + Gee.List? list_local = yield local_folder.list_email_async(-1, int.MAX, + Email.Field.PROPERTIES, ListFlags.FAST, flag_watch_cancellable); + + if (list_local == null) + return; + + // Build local map and find lowest ID. + Geary.EmailIdentifier? low = null; + foreach (Geary.Email e in list_local) { + local_map.set(e.id, e.properties.email_flags); + + if (low == null || e.id.compare(low) < 0) + low = e.id; + } + + // Fetch corresponding e-mail from folder. + Gee.List? list_remote = yield list_email_by_id_async(low, int.MAX, + Email.Field.PROPERTIES, ListFlags.FORCE_UPDATE, flag_watch_cancellable); + + // Build map of emails that have changed. + foreach (Geary.Email e in list_remote) { + if (!local_map.has_key(e.id)) + continue; + + if (!local_map.get(e.id).equals(e.properties.email_flags)) + changed_map.set(e.id, e.properties.email_flags); + } + + if (!flag_watch_cancellable.is_cancelled() && changed_map.size > 0) + notify_email_flags_changed(changed_map); + } } diff --git a/src/engine/impl/geary-local-interfaces.vala b/src/engine/impl/geary-local-interfaces.vala index dfeb720a..c486b75a 100644 --- a/src/engine/impl/geary-local-interfaces.vala +++ b/src/engine/impl/geary-local-interfaces.vala @@ -37,5 +37,12 @@ private interface Geary.LocalFolder : Object, Geary.Folder { */ public async abstract int get_id_position_async(Geary.EmailIdentifier id, 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 map, Cancellable? cancellable) throws Error; } diff --git a/src/engine/sqlite/api/sqlite-folder.vala b/src/engine/sqlite/api/sqlite-folder.vala index bf710aef..1d0185a2 100644 --- a/src/engine/sqlite/api/sqlite-folder.vala +++ b/src/engine/sqlite/api/sqlite-folder.vala @@ -471,21 +471,24 @@ private class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder, Gea notify_message_removed(id); } - public override async void mark_email_async(Gee.List to_mark, - Geary.EmailProperties.EmailFlags flags_to_add, Geary.EmailProperties.EmailFlags - flags_to_remove, Cancellable? cancellable = null) throws Error { + // This isn't implemented yet since it was simpler to replace the flags for a message wholesale + // rather than adding and removing flags. + // Use set_email_flags_async() instead. + public override async Gee.Map mark_email_async( + Gee.List to_mark, Geary.EmailFlags? flags_to_add, + Geary.EmailFlags? flags_to_remove, Cancellable? cancellable = null) throws Error { + + assert_not_reached(); + } + + public async void set_email_flags_async(Gee.Map map, Cancellable? cancellable) throws Error { check_open(); Transaction transaction = yield db.begin_transaction_async("Folder.mark_email_async", cancellable); - Gee.List msg_flags_add = new Gee.ArrayList(); - Gee.List msg_flags_remove = - new Gee.ArrayList(); - Geary.Imap.MessageFlag.from_email_flags(flags_to_add, flags_to_remove, out msg_flags_add, - out msg_flags_remove); - - foreach (Geary.EmailIdentifier id in to_mark) { + foreach (Geary.EmailIdentifier id in map.keys) { MessageLocationRow? location_row = yield location_table.fetch_by_ordering_async( transaction, folder_row.id, ((Geary.Imap.EmailIdentifier) id).uid.value, cancellable); if (location_row == null) { @@ -493,25 +496,10 @@ private class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder, Gea to_string()); } - ImapMessagePropertiesRow? row = yield imap_message_properties_table.fetch_async( - transaction, location_row.id, cancellable); - - if (row == null) { - warning("Message not found in database: %lld", location_row.id); - continue; - } - - // Create new set of message flags with the appropriate flags added and/or removed. - Gee.HashSet mutable_copy = - new Gee.HashSet(); - mutable_copy.add_all(Geary.Imap.MessageFlags.deserialize(row.flags).get_all()); - mutable_copy.remove_all(msg_flags_remove); - mutable_copy.add_all(msg_flags_add); - - Geary.Imap.MessageFlags new_flags = new Geary.Imap.MessageFlags(mutable_copy); + Geary.Imap.MessageFlags flags = ((Geary.Imap.EmailFlags) map.get(id)).message_flags; yield imap_message_properties_table.update_flags_async(transaction, location_row.id, - new_flags.serialize(), cancellable); + flags.serialize(), cancellable); } yield transaction.commit_async(cancellable); @@ -566,7 +554,7 @@ private class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder, Gea (properties.rfc822_size != null) ? properties.rfc822_size.value : -1; yield imap_message_properties_table.update_async(transaction, message_id, - properties.flags.serialize(), internaldate, rfc822_size, cancellable); + properties.get_message_flags().serialize(), internaldate, rfc822_size, cancellable); } } } diff --git a/src/engine/sqlite/imap/sqlite-imap-message-properties-row.vala b/src/engine/sqlite/imap/sqlite-imap-message-properties-row.vala index 8b01f60e..1e1f7325 100644 --- a/src/engine/sqlite/imap/sqlite-imap-message-properties-row.vala +++ b/src/engine/sqlite/imap/sqlite-imap-message-properties-row.vala @@ -28,7 +28,7 @@ public class Geary.Sqlite.ImapMessagePropertiesRow : Geary.Sqlite.Row { id = Row.INVALID_ID; this.message_id = message_id; - flags = properties.flags.serialize(); + flags = properties.get_message_flags().serialize(); internaldate = properties.internaldate.original; rfc822_size = properties.rfc822_size.value; } diff --git a/src/wscript b/src/wscript index 68fa2db3..96d5bca1 100644 --- a/src/wscript +++ b/src/wscript @@ -22,6 +22,8 @@ def build(bld): '../engine/api/geary-conversation.vala', '../engine/api/geary-conversations.vala', '../engine/api/geary-credentials.vala', + '../engine/api/geary-email-flag.vala', + '../engine/api/geary-email-flags.vala', '../engine/api/geary-email-identifier.vala', '../engine/api/geary-email-properties.vala', '../engine/api/geary-email.vala', @@ -37,6 +39,7 @@ def build(bld): '../engine/common/common-message-data.vala', '../engine/imap/api/imap-account.vala', + '../engine/imap/api/imap-email-flags.vala', '../engine/imap/api/imap-email-identifier.vala', '../engine/imap/api/imap-email-properties.vala', '../engine/imap/api/imap-folder-properties.vala',