diff --git a/src/client/geary-controller.vala b/src/client/geary-controller.vala index 8a8d4934..4e7eeaf3 100644 --- a/src/client/geary-controller.vala +++ b/src/client/geary-controller.vala @@ -6,34 +6,6 @@ // Primary controller object for Geary. public class GearyController { - private class ListFoldersOperation : Geary.NonblockingBatchOperation { - public Geary.Account account; - public Geary.FolderPath path; - - public ListFoldersOperation(Geary.Account account, Geary.FolderPath path) { - this.account = account; - this.path = path; - } - - public override async Object? execute_async(Cancellable? cancellable) throws Error { - return yield account.list_folders_async(path, cancellable); - } - } - - private class FetchFolderOperation : Geary.NonblockingBatchOperation { - public Geary.Account account; - public Geary.FolderPath folder_path; - - public FetchFolderOperation(Geary.Account account, Geary.FolderPath folder_path) { - this.account = account; - this.folder_path = folder_path; - } - - public override async Object? execute_async(Cancellable? cancellable) throws Error { - return yield account.fetch_folder_async(folder_path); - } - } - // Named actions. public const string ACTION_HELP = "GearyHelp"; public const string ACTION_ABOUT = "GearyAbout"; @@ -274,7 +246,7 @@ public class GearyController { cancel_inbox(); cancel_message(); - account.folders_added_removed.disconnect(on_folders_added_removed); + account.folders_available_unavailable.disconnect(on_folders_available_unavailable); main_window.title = GearyApplication.NAME; main_window.conversation_list_store.account_owner_email = null; @@ -312,6 +284,8 @@ public class GearyController { // Connect the new account, if any. if (account != null) { + account.folders_available_unavailable.connect(on_folders_available_unavailable); + try { yield account.open_async(cancellable); } catch (Error open_err) { @@ -323,7 +297,6 @@ public class GearyController { GearyApplication.instance.panic(); } - account.folders_added_removed.connect(on_folders_added_removed); account.email_sent.connect(on_sent); if (account.settings.service_provider == Geary.ServiceProvider.YAHOO) @@ -331,7 +304,6 @@ public class GearyController { main_window.conversation_list_store.account_owner_email = account.settings.email.address; main_window.folder_list.set_user_folders_root_name(_("Labels")); - load_folders.begin(cancellable_folder); } } @@ -361,19 +333,6 @@ public class GearyController { } } - private async void load_folders(Cancellable? cancellable) { - try { - // pull down the root-level user folders and recursively add to sidebar - Gee.Collection folders = yield account.list_folders_async(null); - if (folders != null) - on_folders_added_removed(folders, null); - else - debug("no folders"); - } catch (Error err) { - message("%s", err.message); - } - } - private void on_folder_selected(Geary.Folder? folder) { if (folder == null) { debug("no folder selected"); @@ -624,11 +583,11 @@ public class GearyController { main_window.folder_list.add_folder(folder); } - private void on_folders_added_removed(Gee.Collection? added, - Gee.Collection? removed) { + private void on_folders_available_unavailable(Gee.Collection? available, + Gee.Collection? unavailable) { - if (added != null && added.size > 0) { - foreach (Geary.Folder folder in added) { + if (available != null && available.size > 0) { + foreach (Geary.Folder folder in available) { main_window.folder_list.add_folder(folder); main_window.main_toolbar.copy_folder_menu.add_folder(folder); main_window.main_toolbar.move_folder_menu.add_folder(folder); @@ -660,49 +619,9 @@ public class GearyController { folder.special_folder_type_changed.connect(on_special_folder_type_changed); } - - search_folders_for_children.begin(added); } } - private async void search_folders_for_children(Gee.Collection folders) { - set_busy(true); - Geary.NonblockingBatch batch = new Geary.NonblockingBatch(); - foreach (Geary.Folder folder in folders) { - // Search for children unless Folder is absolutely certain it doesn't have any - if (folder.has_children().is_possible()) - batch.add(new ListFoldersOperation(account, folder.get_path())); - } - - debug("Listing %d folder children", batch.size); - try { - yield batch.execute_all_async(); - } catch (Error err) { - debug("Unable to execute batch: %s", err.message); - set_busy(false); - - return; - } - debug("Completed listing folder children"); - - Gee.ArrayList accumulator = new Gee.ArrayList(); - foreach (int id in batch.get_ids()) { - ListFoldersOperation op = (ListFoldersOperation) batch.get_operation(id); - try { - Gee.Collection children = (Gee.Collection) - batch.get_result(id); - accumulator.add_all(children); - } catch (Error err2) { - debug("Unable to list children of %s: %s", op.path.to_string(), err2.message); - } - } - - if (accumulator.size > 0) - on_folders_added_removed(accumulator, null); - - set_busy(false); - } - private void cancel_folder() { Cancellable old_cancellable = cancellable_folder; cancellable_folder = new Cancellable(); diff --git a/src/engine/abstract/geary-abstract-account.vala b/src/engine/abstract/geary-abstract-account.vala index b3fad193..e1967e74 100644 --- a/src/engine/abstract/geary-abstract-account.vala +++ b/src/engine/abstract/geary-abstract-account.vala @@ -14,6 +14,11 @@ public abstract class Geary.AbstractAccount : Object, Geary.Account { this.settings = settings; } + protected virtual void notify_folders_available_unavailable(Gee.Collection? available, + Gee.Collection? unavailable) { + folders_available_unavailable(available, unavailable); + } + protected virtual void notify_folders_added_removed(Gee.Collection? added, Gee.Collection? removed) { folders_added_removed(added, removed); diff --git a/src/engine/api/geary-account.vala b/src/engine/api/geary-account.vala index 9f059cea..67cad88f 100644 --- a/src/engine/api/geary-account.vala +++ b/src/engine/api/geary-account.vala @@ -24,6 +24,18 @@ public interface Geary.Account : Object { public signal void report_problem(Geary.Account.Problem problem, Geary.AccountSettings settings, Error? err); + /** + * Fired when folders become available or unavailable in the account. + * Folders become available when the account is first opened or when + * they're created later; they become unavailable when the account is + * closed or they're deleted later. + */ + public signal void folders_available_unavailable(Gee.Collection? available, + Gee.Collection? unavailable); + + /** + * Fired when folders are created or deleted. + */ public signal void folders_added_removed(Gee.Collection? added, Gee.Collection? removed); @@ -48,6 +60,12 @@ public interface Geary.Account : Object { protected abstract void notify_report_problem(Geary.Account.Problem problem, Geary.AccountSettings? settings, Error? err); + /** + * Signal notification method for subclasses to use. + */ + public abstract void notify_folders_available_unavailable(Gee.Collection? available, + Gee.Collection? unavailable); + /** * Signal notification method for subclasses to use. */ diff --git a/src/engine/imap-engine/imap-engine-generic-account.vala b/src/engine/imap-engine/imap-engine-generic-account.vala index 79ec1ce4..7fa081d6 100644 --- a/src/engine/imap-engine/imap-engine-generic-account.vala +++ b/src/engine/imap-engine/imap-engine-generic-account.vala @@ -15,7 +15,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount { FolderPath, Imap.FolderProperties>(Hashable.hash_func, Equalable.equal_func); private Gee.HashMap existing_folders = new Gee.HashMap< FolderPath, GenericFolder>(Hashable.hash_func, Equalable.equal_func); - private Gee.HashSet local_only = new Gee.HashSet( + private Gee.HashMap local_only = new Gee.HashMap( Hashable.hash_func, Equalable.equal_func); public GenericAccount(string name, Geary.AccountSettings settings, Imap.Account remote, @@ -35,7 +35,6 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount { if (outbox_path == null) { outbox_path = new SmtpOutboxFolderRoot(); - local_only.add(outbox_path); } } @@ -48,6 +47,26 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount { throw new EngineError.OPEN_REQUIRED("Account %s not opened", to_string()); } + protected override void notify_folders_available_unavailable(Gee.Collection? available, + Gee.Collection? unavailable) { + base.notify_folders_available_unavailable(available, unavailable); + + if (available != null) { + foreach(Folder f in available) { + if (f.has_children().is_possible()) + enumerate_folders_async.begin(f.get_path(), null, on_enumerate_folders_async_complete); + } + } + } + + private void on_enumerate_folders_async_complete(Object? object, AsyncResult result) { + try { + enumerate_folders_async.end(result); + } catch (Error e) { + debug("Error enumerating subfolders: %s", e.message); + } + } + public override async void open_async(Cancellable? cancellable = null) throws Error { if (open) throw new EngineError.ALREADY_OPEN("Account %s already opened", to_string()); @@ -56,6 +75,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount { // outbox is now available local.outbox.report_problem.connect(notify_report_problem); + local_only.set(outbox_path, local.outbox); // need to back out local.open_async() if remote fails try { @@ -74,11 +94,17 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount { open = true; notify_opened(); + + notify_folders_available_unavailable(local_only.values, null); + yield enumerate_folders_async(null, cancellable); } public override async void close_async(Cancellable? cancellable = null) throws Error { if (!open) return; + + notify_folders_available_unavailable(null, local_only.values); + notify_folders_available_unavailable(null, existing_folders.values); local.outbox.report_problem.disconnect(notify_report_problem); @@ -96,6 +122,10 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount { } catch (Error rclose_err) { remote_err = rclose_err; } + + properties_map.clear(); + existing_folders.clear(); + local_only.clear(); if (local_err != null) throw local_err; @@ -113,17 +143,48 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount { ImapDB.Account local_account, ImapDB.Folder local_folder); private GenericFolder build_folder(ImapDB.Folder local_folder) { - GenericFolder? folder = existing_folders.get(local_folder.get_path()); - if (folder != null) - return folder; - - folder = new_folder(local_folder.get_path(), remote, local, local_folder); - existing_folders.set(folder.get_path(), folder); - - return folder; + return Geary.Collection.get_first(build_folders(new Geary.Singleton(local_folder))); + } + + private Gee.Collection build_folders(Gee.Collection local_folders) { + Gee.ArrayList folders_to_build = new Gee.ArrayList(); + Gee.ArrayList built_folders = new Gee.ArrayList(); + Gee.ArrayList return_folders = new Gee.ArrayList(); + + foreach(ImapDB.Folder local_folder in local_folders) { + if (existing_folders.has_key(local_folder.get_path())) + return_folders.add(existing_folders.get(local_folder.get_path())); + else + folders_to_build.add(local_folder); + } + + foreach(ImapDB.Folder folder_to_build in folders_to_build) { + GenericFolder folder = new_folder(folder_to_build.get_path(), remote, local, folder_to_build); + existing_folders.set(folder.get_path(), folder); + built_folders.add(folder); + return_folders.add(folder); + } + + if (built_folders.size > 0) + notify_folders_available_unavailable(built_folders, null); + return return_folders; } public override async Gee.Collection list_folders_async(Geary.FolderPath? parent, + Cancellable? cancellable = null) throws Error { + Gee.ArrayList matches = new Gee.ArrayList(); + + foreach(FolderPath path in existing_folders.keys) { + FolderPath? path_parent = path.get_parent(); + if ((parent == null && path_parent == null) || + (parent != null && path_parent != null && path_parent.equals(parent))) { + matches.add(existing_folders.get(path)); + } + } + return matches; + } + + private async Gee.Collection enumerate_folders_async(Geary.FolderPath? parent, Cancellable? cancellable = null) throws Error { check_open(); @@ -138,13 +199,12 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount { Gee.Collection engine_list = new Gee.ArrayList(); if (local_list != null && local_list.size > 0) { - foreach (ImapDB.Folder local_folder in local_list) - engine_list.add(build_folder(local_folder)); + engine_list.add_all(build_folders(local_list)); } - // Add Outbox to root + // Add local folders (assume that local-only folders always go in root) if (parent == null) - engine_list.add(local.outbox); + engine_list.add_all(local_only.values); background_update_folders.begin(parent, engine_list, cancellable); @@ -170,8 +230,8 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount { Cancellable? cancellable = null) throws Error { check_open(); - if (path.equals(local.outbox.get_path())) - return local.outbox; + if (local_only.has_key(path)) + return local_only.get(path); try { return build_folder((ImapDB.Folder) yield local.fetch_folder_async(path, cancellable)); @@ -255,7 +315,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount { // it Gee.List? to_remove = new Gee.ArrayList(); foreach (Geary.Folder folder in engine_folders) { - if (!remote_paths.contains(folder.get_path()) && !local_only.contains(folder.get_path())) + if (!remote_paths.contains(folder.get_path()) && !local_only.keys.contains(folder.get_path())) to_remove.add(folder); } @@ -281,10 +341,12 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount { Gee.Collection engine_added = null; if (to_add != null) { engine_added = new Gee.ArrayList(); + + Gee.ArrayList folders_to_build = new Gee.ArrayList(); foreach (Geary.Imap.Folder remote_folder in to_add) { try { - engine_added.add(build_folder((ImapDB.Folder) yield local.fetch_folder_async( - remote_folder.get_path(), cancellable))); + folders_to_build.add((ImapDB.Folder) yield local.fetch_folder_async( + remote_folder.get_path(), cancellable)); } catch (Error convert_err) { // This isn't fatal, but irksome ... in the future, when local folders are // removed, it's possible for one to disappear between cloning it and fetching @@ -292,6 +354,8 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount { debug("Unable to fetch local folder after cloning: %s", convert_err.message); } } + + engine_added.add_all(build_folders(folders_to_build)); } // TODO: Remove local folders no longer available remotely.