From ac41e9269a9513f191d0797eb6b10a0a5c923130 Mon Sep 17 00:00:00 2001 From: Jim Nelson Date: Mon, 13 Jun 2011 15:16:57 -0700 Subject: [PATCH] Fixes message list not updated when folder is selected: #3741 Engine now uses a master Folder object that synchronizes between the network and the local store. Also, the Geary.Folder interface was getting ugly to code when the implementation was more than trivial, so moved to standard getters for it. --- Makefile | 1 + src/client/ui/FolderListStore.vala | 2 +- src/client/ui/MainWindow.vala | 2 +- src/engine/Engine.vala | 49 ++++++++++--- src/engine/EngineFolder.vala | 89 +++++++++++++++++++++++ src/engine/api/Account.vala | 3 + src/engine/api/EngineError.vala | 3 +- src/engine/api/Folder.vala | 17 +++-- src/engine/imap/ClientSessionManager.vala | 14 ++++ src/engine/imap/api/Account.vala | 10 +++ src/engine/imap/api/Folder.vala | 42 ++++++++--- src/engine/imap/decoders/ListResults.vala | 27 +++++-- src/engine/sqlite/FolderTable.vala | 11 +++ src/engine/sqlite/api/Account.vala | 17 ++++- src/engine/sqlite/api/Folder.vala | 41 ++++++++--- 15 files changed, 273 insertions(+), 55 deletions(-) create mode 100644 src/engine/EngineFolder.vala diff --git a/Makefile b/Makefile index 211ad516..c6cdb1f8 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,7 @@ APPS := geary console watchmbox ENGINE_SRC := \ src/engine/Engine.vala \ + src/engine/EngineFolder.vala \ src/engine/api/Account.vala \ src/engine/api/Email.vala \ src/engine/api/Folder.vala \ diff --git a/src/client/ui/FolderListStore.vala b/src/client/ui/FolderListStore.vala index 070b5f53..8febb083 100644 --- a/src/client/ui/FolderListStore.vala +++ b/src/client/ui/FolderListStore.vala @@ -47,7 +47,7 @@ public class FolderListStore : Gtk.TreeStore { append(out iter, null); set(iter, - Column.NAME, folder.name, + Column.NAME, folder.get_name(), Column.FOLDER_OBJECT, folder ); } diff --git a/src/client/ui/MainWindow.vala b/src/client/ui/MainWindow.vala index 5ea68388..43b51ee8 100644 --- a/src/client/ui/MainWindow.vala +++ b/src/client/ui/MainWindow.vala @@ -188,7 +188,7 @@ public class MainWindow : Gtk.Window { return; } - debug("Folder %s selected", folder.name); + debug("Folder %s selected", folder.get_name()); do_select_folder.begin(folder, on_select_folder_completed); } diff --git a/src/engine/Engine.vala b/src/engine/Engine.vala index 0c01376b..372def40 100644 --- a/src/engine/Engine.vala +++ b/src/engine/Engine.vala @@ -21,19 +21,31 @@ public class Geary.Engine : Object, Geary.Account { public async Gee.Collection list_async(string? parent_folder, Cancellable? cancellable = null) throws Error { - Gee.Collection list = yield local.list_async(parent_folder, cancellable); + Gee.Collection local_list = yield local.list_async(parent_folder, cancellable); - background_update_folders.begin(parent_folder, list); + Gee.Collection engine_list = new Gee.ArrayList(); + foreach (Geary.Folder local_folder in local_list) + engine_list.add(new EngineFolder(net, local, local_folder)); - debug("Reporting %d folders", list.size); + background_update_folders.begin(parent_folder, engine_list); - return list; + debug("Reporting %d folders", engine_list.size); + + return engine_list; + } + + public async Geary.Folder fetch_async(string? parent_folder, string folder_name, + Cancellable? cancellable = null) throws Error { + Geary.Folder local_folder = yield local.fetch_async(parent_folder, folder_name, cancellable); + Geary.Folder engine_folder = new EngineFolder(net, local, local_folder); + + return engine_folder; } private Gee.Set get_folder_names(Gee.Collection folders) { Gee.Set names = new Gee.HashSet(); foreach (Geary.Folder folder in folders) - names.add(folder.name); + names.add(folder.get_name()); return names; } @@ -42,7 +54,7 @@ public class Geary.Engine : Object, Geary.Account { Gee.Set names) { Gee.List excluded = new Gee.ArrayList(); foreach (Geary.Folder folder in folders) { - if (!names.contains(folder.name)) + if (!names.contains(folder.get_name())) excluded.add(folder); } @@ -50,7 +62,7 @@ public class Geary.Engine : Object, Geary.Account { } private async void background_update_folders(string? parent_folder, - Gee.Collection local_folders) { + Gee.Collection engine_folders) { Gee.Collection net_folders; try { net_folders = yield net.list_async(parent_folder); @@ -58,13 +70,13 @@ public class Geary.Engine : Object, Geary.Account { error("Unable to retrieve folder list from server: %s", neterror.message); } - Gee.Set local_names = get_folder_names(local_folders); + Gee.Set local_names = get_folder_names(engine_folders); Gee.Set net_names = get_folder_names(net_folders); debug("%d local names, %d net names", local_names.size, net_names.size); Gee.List? to_add = get_excluded_folders(net_folders, local_names); - Gee.List? to_remove = get_excluded_folders(local_folders, net_names); + Gee.List? to_remove = get_excluded_folders(engine_folders, net_names); debug("Adding %d, removing %d to/from local store", to_add.size, to_remove.size); @@ -74,15 +86,28 @@ public class Geary.Engine : Object, Geary.Account { if (to_remove.size == 0) to_remove = null; - if (to_add != null || to_remove != null) - notify_folders_added_removed(to_add, null); - try { if (to_add != null) yield local.create_many_async(to_add); } catch (Error err) { error("Unable to add/remove folders: %s", err.message); } + + Gee.Collection engine_added = null; + if (to_add != null) { + engine_added = new Gee.ArrayList(); + foreach (Geary.Folder net_folder in to_add) { + try { + engine_added.add(new EngineFolder(net, local, + yield local.fetch_async(parent_folder, net_folder.get_name()))); + } catch (Error convert_err) { + error("Unable to fetch local folder: %s", convert_err.message); + } + } + } + + if (engine_added != null) + notify_folders_added_removed(engine_added, null); } public async void create_async(Geary.Folder folder, Cancellable? cancellable = null) throws Error { diff --git a/src/engine/EngineFolder.vala b/src/engine/EngineFolder.vala new file mode 100644 index 00000000..f8da8b3f --- /dev/null +++ b/src/engine/EngineFolder.vala @@ -0,0 +1,89 @@ +/* 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.EngineFolder : Object, Geary.Folder { + private NetworkAccount net; + private LocalAccount local; + private Geary.Folder local_folder; + private Geary.Folder net_folder; + + public EngineFolder(NetworkAccount net, LocalAccount local, Geary.Folder local_folder) { + this.net = net; + this.local = local; + this.local_folder = local_folder; + + local_folder.updated.connect(on_local_updated); + } + + ~EngineFolder() { + local_folder.updated.disconnect(on_local_updated); + } + + public string get_name() { + return local_folder.get_name(); + } + + public Trillian is_readonly() { + return local_folder.is_readonly(); + } + + public Trillian does_support_children() { + return local_folder.does_support_children(); + } + + public Trillian has_children() { + return local_folder.has_children(); + } + + public Trillian is_openable() { + return local_folder.is_openable(); + } + + public async void open_async(bool readonly, Cancellable? cancellable = null) throws Error { + if (net_folder == null) { + net_folder = yield net.fetch_async(null, local_folder.get_name(), cancellable); + net_folder.updated.connect(on_net_updated); + } + + yield net_folder.open_async(readonly, cancellable); + } + + public async void close_async(Cancellable? cancellable = null) throws Error { + if (net_folder != null) { + net_folder.updated.disconnect(on_net_updated); + yield net_folder.close_async(cancellable); + } + + net_folder = null; + } + + public int get_message_count() throws Error { + return 0; + } + + public async Gee.List? read_async(int low, int count, + Cancellable? cancellable = null) throws Error { + if (net_folder == null) + throw new EngineError.OPEN_REQUIRED("Folder %s not opened", get_name()); + + return yield net_folder.read_async(low, count, cancellable); + } + + public async Geary.Email fetch_async(Geary.EmailHeader header, + Cancellable? cancellable = null) throws Error { + if (net_folder == null) + throw new EngineError.OPEN_REQUIRED("Folder %s not opened", get_name()); + + return yield net_folder.fetch_async(header, cancellable); + } + + private void on_local_updated() { + } + + private void on_net_updated() { + } +} + diff --git a/src/engine/api/Account.vala b/src/engine/api/Account.vala index f6f9a22d..f720817b 100644 --- a/src/engine/api/Account.vala +++ b/src/engine/api/Account.vala @@ -16,6 +16,9 @@ public interface Geary.Account : Object { public abstract async Gee.Collection list_async(string? parent_folder, Cancellable? cancellable = null) throws Error; + public abstract async Geary.Folder fetch_async(string? parent_folder, string folder_name, + Cancellable? cancellable = null) throws Error; + public abstract async void create_async(Geary.Folder folder, Cancellable? cancellable = null) throws Error; diff --git a/src/engine/api/EngineError.vala b/src/engine/api/EngineError.vala index 573d725f..ec6c83db 100644 --- a/src/engine/api/EngineError.vala +++ b/src/engine/api/EngineError.vala @@ -6,6 +6,7 @@ public errordomain Geary.EngineError { OPEN_REQUIRED, - ALREADY_OPEN + ALREADY_OPEN, + NOT_FOUND } diff --git a/src/engine/api/Folder.vala b/src/engine/api/Folder.vala index 1255146e..6ba16887 100644 --- a/src/engine/api/Folder.vala +++ b/src/engine/api/Folder.vala @@ -11,18 +11,23 @@ public interface Geary.Folder : Object { FOLDER_CLOSED } - public abstract string name { get; protected set; } - public abstract Trillian is_readonly { get; protected set; } - public abstract Trillian supports_children { get; protected set; } - public abstract Trillian has_children { get; protected set; } - public abstract Trillian is_openable { get; protected set; } - public signal void opened(); public signal void closed(CloseReason reason); public signal void updated(); + public abstract string get_name(); + + // This is only for when a context has been selected + public abstract Trillian is_readonly(); + + public abstract Trillian does_support_children(); + + public abstract Trillian has_children(); + + public abstract Trillian is_openable(); + public abstract async void open_async(bool readonly, Cancellable? cancellable = null) throws Error; public abstract async void close_async(Cancellable? cancellable = null) throws Error; diff --git a/src/engine/imap/ClientSessionManager.vala b/src/engine/imap/ClientSessionManager.vala index 90e38670..89017529 100644 --- a/src/engine/imap/ClientSessionManager.vala +++ b/src/engine/imap/ClientSessionManager.vala @@ -43,6 +43,20 @@ public class Geary.Imap.ClientSessionManager { return results.get_all(); } + public async Geary.Imap.MailboxInformation? fetch_async(string? parent_name, string folder_name, + Cancellable? cancellable = null) throws Error { + // build a proper IMAP specifier + string specifier = parent_name ?? "/"; + specifier += (specifier.has_suffix("/")) ? folder_name : "/%s".printf(folder_name); + + ClientSession session = yield get_authorized_session(cancellable); + + ListResults results = ListResults.decode(yield session.send_command_async( + new ListCommand(session.generate_tag(), specifier), cancellable)); + + return (results.get_count() > 0) ? results.get_all()[0] : null; + } + public async Mailbox select_mailbox(string path, Cancellable? cancellable = null) throws Error { return yield select_examine_mailbox(path, true, cancellable); } diff --git a/src/engine/imap/api/Account.vala b/src/engine/imap/api/Account.vala index 9f99fb3e..8e0ea796 100644 --- a/src/engine/imap/api/Account.vala +++ b/src/engine/imap/api/Account.vala @@ -26,6 +26,16 @@ public class Geary.Imap.Account : Object, Geary.Account, Geary.NetworkAccount { return folders; } + public async Geary.Folder fetch_async(string? parent_folder, string folder_name, + Cancellable? cancellable = null) throws Error { + MailboxInformation? mbox = yield session_mgr.fetch_async(parent_folder, folder_name, + cancellable); + if (mbox == null) + throw new EngineError.NOT_FOUND("Folder %s not found on server", folder_name); + + return new Geary.Imap.Folder(session_mgr, mbox); + } + public async void create_async(Geary.Folder folder, Cancellable? cancellable = null) throws Error { // TODO } diff --git a/src/engine/imap/api/Folder.vala b/src/engine/imap/api/Folder.vala index d12e70d8..27290a6b 100644 --- a/src/engine/imap/api/Folder.vala +++ b/src/engine/imap/api/Folder.vala @@ -5,15 +5,13 @@ */ public class Geary.Imap.Folder : Object, Geary.Folder { - public string name { get; protected set; } - // This is only for when a context has been selected - public Trillian is_readonly { get; protected set; } - public Trillian supports_children { get; protected set; } - public Trillian has_children { get; protected set; } - public Trillian is_openable { get; protected set; } - private ClientSessionManager session_mgr; private MailboxInformation info; + private string name; + private Trillian readonly; + private Trillian supports_children; + private Trillian children; + private Trillian openable; private Mailbox? mailbox = null; internal Folder(ClientSessionManager session_mgr, MailboxInformation info) { @@ -21,12 +19,32 @@ public class Geary.Imap.Folder : Object, Geary.Folder { this.info = info; name = info.name; - is_readonly = Trillian.UNKNOWN; + readonly = Trillian.UNKNOWN; supports_children = Trillian.from_boolean(!info.attrs.contains(MailboxAttribute.NO_INFERIORS)); // \HasNoChildren is an optional attribute and lack of presence doesn't indiciate anything - has_children = info.attrs.contains(MailboxAttribute.HAS_NO_CHILDREN) ? Trillian.TRUE + children = info.attrs.contains(MailboxAttribute.HAS_NO_CHILDREN) ? Trillian.TRUE : Trillian.UNKNOWN; - is_openable = Trillian.from_boolean(!info.attrs.contains(MailboxAttribute.NO_SELECT)); + openable = Trillian.from_boolean(!info.attrs.contains(MailboxAttribute.NO_SELECT)); + } + + public string get_name() { + return name; + } + + public Trillian is_readonly() { + return readonly; + } + + public Trillian does_support_children() { + return supports_children; + } + + public Trillian has_children() { + return children; + } + + public Trillian is_openable() { + return openable; } public async void open_async(bool readonly, Cancellable? cancellable = null) throws Error { @@ -36,12 +54,12 @@ public class Geary.Imap.Folder : Object, Geary.Folder { mailbox = yield session_mgr.select_examine_mailbox(name, !readonly, cancellable); // hook up signals - this.is_readonly = Trillian.from_boolean(readonly); + this.readonly = Trillian.from_boolean(readonly); } public async void close_async(Cancellable? cancellable = null) throws Error { mailbox = null; - is_readonly = Trillian.UNKNOWN; + readonly = Trillian.UNKNOWN; } public int get_message_count() throws Error { diff --git a/src/engine/imap/decoders/ListResults.vala b/src/engine/imap/decoders/ListResults.vala index c2d20b7c..5f8aa93c 100644 --- a/src/engine/imap/decoders/ListResults.vala +++ b/src/engine/imap/decoders/ListResults.vala @@ -17,17 +17,21 @@ public class Geary.Imap.MailboxInformation { } public class Geary.Imap.ListResults : Geary.Imap.CommandResults { + private Gee.List list; private Gee.Map map; - public ListResults(StatusResponse status_response, Gee.Map map) { + public ListResults(StatusResponse status_response, Gee.Map map, + Gee.List list) { base (status_response); this.map = map; + this.list = list; } public static ListResults decode(CommandResponse response) { assert(response.is_sealed()); + Gee.List list = new Gee.ArrayList(); Gee.Map map = new Gee.HashMap(); foreach (ServerData data in response.server_data) { try { @@ -43,7 +47,7 @@ public class Geary.Imap.ListResults : Geary.Imap.CommandResults { continue; } - Gee.Collection list = new Gee.ArrayList(); + Gee.Collection attrlist = new Gee.ArrayList(); foreach (Parameter attr in attrs.get_all()) { StringParameter? stringp = attr as StringParameter; if (stringp == null) { @@ -53,25 +57,32 @@ public class Geary.Imap.ListResults : Geary.Imap.CommandResults { continue; } - list.add(new MailboxAttribute(stringp.value)); + attrlist.add(new MailboxAttribute(stringp.value)); } - map.set(mailbox.value, - new MailboxInformation(mailbox.value, delim.value, new MailboxAttributes(list))); + MailboxInformation info = new MailboxInformation(mailbox.value, delim.value, + new MailboxAttributes(attrlist)); + + map.set(mailbox.value, info); + list.add(info); } catch (ImapError ierr) { debug("Unable to decode \"%s\": %s", data.to_string(), ierr.message); } } - return new ListResults(response.status_response, map); + return new ListResults(response.status_response, map, list); + } + + public int get_count() { + return list.size; } public Gee.Collection get_names() { return map.keys; } - public Gee.Collection get_all() { - return map.values; + public Gee.List get_all() { + return list; } public MailboxInformation? get_info(string name) { diff --git a/src/engine/sqlite/FolderTable.vala b/src/engine/sqlite/FolderTable.vala index b8320d0e..03722a3e 100644 --- a/src/engine/sqlite/FolderTable.vala +++ b/src/engine/sqlite/FolderTable.vala @@ -57,6 +57,17 @@ public class Geary.Sqlite.FolderTable : Geary.Sqlite.Table { return rows; } + public async FolderRow? fetch_async(int64 parent_id, string name, Cancellable? cancellable = null) + throws Error { + SQLHeavy.Query query = db.prepare("SELECT * FROM FolderTable WHERE parent_id=? AND name=?"); + query.bind_int64(0, parent_id); + query.bind_string(1, name); + + SQLHeavy.QueryResult result = yield query.execute_async(cancellable); + + return (!result.finished) ? new FolderRow.from_query_result(result) : null; + } + private SQLHeavy.Query create_query(SQLHeavy.Queryable? queryable = null) throws SQLHeavy.Error { SQLHeavy.Queryable q = queryable ?? db; SQLHeavy.Query query = q.prepare( diff --git a/src/engine/sqlite/api/Account.vala b/src/engine/sqlite/api/Account.vala index d8dfdb95..dc707813 100644 --- a/src/engine/sqlite/api/Account.vala +++ b/src/engine/sqlite/api/Account.vala @@ -29,17 +29,28 @@ public class Geary.Sqlite.Account : Object, Geary.Account, Geary.LocalAccount { return folders; } + public async Geary.Folder fetch_async(string? parent_folder, string folder_name, + Cancellable? cancellable = null) throws Error { + FolderRow? row = yield folder_table.fetch_async(Row.INVALID_ID, folder_name, cancellable); + if (row == null) + throw new EngineError.NOT_FOUND("%s not found in local database", folder_name); + + return new Geary.Sqlite.Folder(row); + } + public async void create_async(Geary.Folder folder, Cancellable? cancellable = null) throws Error { yield folder_table.create_async( - new FolderRow(folder.name, folder.supports_children, folder.is_openable), + new FolderRow(folder.get_name(), folder.does_support_children(), folder.is_openable()), cancellable); } public async void create_many_async(Gee.Collection folders, Cancellable? cancellable = null) throws Error { Gee.List rows = new Gee.ArrayList(); - foreach (Geary.Folder folder in folders) - rows.add(new FolderRow(folder.name, folder.supports_children, folder.is_openable)); + foreach (Geary.Folder folder in folders) { + rows.add(new FolderRow(folder.get_name(), folder.does_support_children(), + folder.is_openable())); + } yield folder_table.create_many_async(rows, cancellable); } diff --git a/src/engine/sqlite/api/Folder.vala b/src/engine/sqlite/api/Folder.vala index 38bd4413..acce3b11 100644 --- a/src/engine/sqlite/api/Folder.vala +++ b/src/engine/sqlite/api/Folder.vala @@ -6,29 +6,48 @@ public class Geary.Sqlite.Folder : Object, Geary.Folder { private FolderRow row; - - public string name { get; protected set; } - public Trillian is_readonly { get; protected set; } - public Trillian supports_children { get; protected set; } - public Trillian has_children { get; protected set; } - public Trillian is_openable { get; protected set; } + private string name; + private Trillian readonly; + private Trillian supports_children; + private Trillian children; + private Trillian openable; internal Folder(FolderRow row) throws Error { this.row = row; name = row.name; - is_readonly = Trillian.UNKNOWN; + readonly = Trillian.UNKNOWN; supports_children = row.supports_children; - has_children = Trillian.UNKNOWN; - is_openable = row.is_openable; + children = Trillian.UNKNOWN; + openable = row.is_openable; + } + + public string get_name() { + return name; + } + + public Trillian is_readonly() { + return readonly; + } + + public Trillian does_support_children() { + return supports_children; + } + + public Trillian has_children() { + return children; + } + + public Trillian is_openable() { + return openable; } public async void open_async(bool readonly, Cancellable? cancellable = null) throws Error { - is_readonly = Trillian.TRUE; + this.readonly = Trillian.TRUE; } public async void close_async(Cancellable? cancellable = null) throws Error { - is_readonly = Trillian.UNKNOWN; + this.readonly = Trillian.UNKNOWN; } public int get_message_count() throws Error {