diff --git a/debian/control b/debian/control index 0f92eaa6..5f1060ac 100644 --- a/debian/control +++ b/debian/control @@ -13,7 +13,7 @@ Build-Depends: debhelper (>= 8), libxml2-dev (>= 2.7.8), libsecret-1-dev (>= 0.11), libgmime-2.6-dev (>= 2.6.0), - valac-0.20 (>= 0.20.1), + valac-0.22 (>= 0.21.1), cmake (>= 2.8.0), libsqlite3-dev (>= 3.7.4), libmessaging-menu-dev (>= 12.10.2), diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9511b024..7f825f62 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -295,6 +295,7 @@ client/accounts/login-dialog.vala client/composer/composer-window.vala client/composer/contact-entry-completion.vala +client/composer/contact-list-store.vala client/composer/email-entry.vala client/composer/webview-edit-fixer.vala @@ -324,6 +325,7 @@ client/notification/unity-launcher.vala client/sidebar/sidebar-branch.vala client/sidebar/sidebar-common.vala +client/sidebar/sidebar-count-cell-renderer.vala client/sidebar/sidebar-entry.vala client/sidebar/sidebar-tree.vala @@ -333,6 +335,7 @@ client/ui/main-toolbar.vala client/ui/main-window.vala client/ui/monitored-progress-bar.vala client/ui/monitored-spinner.vala +client/ui/stock.vala client/util/util-date.vala client/util/util-email.vala diff --git a/src/client/accounts/account-dialog-add-edit-pane.vala b/src/client/accounts/account-dialog-add-edit-pane.vala index 38d96e82..a5563e19 100644 --- a/src/client/accounts/account-dialog-add-edit-pane.vala +++ b/src/client/accounts/account-dialog-add-edit-pane.vala @@ -8,8 +8,8 @@ public class AccountDialogAddEditPane : AccountDialogPane { public AddEditPage add_edit_page { get; private set; default = new AddEditPage(); } private Gtk.ButtonBox button_box = new Gtk.ButtonBox(Gtk.Orientation.HORIZONTAL); - private Gtk.Button ok_button = new Gtk.Button.from_stock(Gtk.Stock.OK); - private Gtk.Button cancel_button = new Gtk.Button.from_stock(Gtk.Stock.CANCEL); + private Gtk.Button ok_button = new Gtk.Button.with_mnemonic(Stock._OK); + private Gtk.Button cancel_button = new Gtk.Button.with_mnemonic(Stock._CANCEL); public signal void ok(Geary.AccountInformation info); diff --git a/src/client/accounts/login-dialog.vala b/src/client/accounts/login-dialog.vala index a25e3816..b483a514 100644 --- a/src/client/accounts/login-dialog.vala +++ b/src/client/accounts/login-dialog.vala @@ -24,9 +24,9 @@ public class LoginDialog : Gtk.Dialog { page.size_changed.connect(() => { resize(1, 1); }); page.info_changed.connect(on_info_changed); - cancel_button = new Gtk.Button.from_stock(Gtk.Stock.CANCEL); + cancel_button = new Gtk.Button.from_stock(Stock._CANCEL); add_action_widget(cancel_button, Gtk.ResponseType.CANCEL); - ok_button = new Gtk.Button.from_stock(Gtk.Stock.ADD); + ok_button = new Gtk.Button.from_stock(Stock._ADD); ok_button.can_default = true; add_action_widget(ok_button, Gtk.ResponseType.OK); set_default_response(Gtk.ResponseType.OK); diff --git a/src/client/composer/composer-window.vala b/src/client/composer/composer-window.vala index 5bb83e09..cbe1b1b8 100644 --- a/src/client/composer/composer-window.vala +++ b/src/client/composer/composer-window.vala @@ -127,6 +127,8 @@ public class ComposerWindow : Gtk.Window { public ComposeType compose_type { get; private set; default = ComposeType.NEW_MESSAGE; } + private ContactListStore? contact_list_store = null; + private string? body_html = null; private Gee.Set attachment_files = new Gee.HashSet(Geary.Files.nullable_hash, Geary.Files.nullable_equal); @@ -650,7 +652,7 @@ public class ComposerWindow : Gtk.Window { if (editor.can_undo()) { present(); ConfirmationDialog dialog = new ConfirmationDialog(this, - _("Do you want to discard the unsaved message?"), null, Gtk.Stock.DISCARD); + _("Do you want to discard the unsaved message?"), null, Stock._DISCARD); if (dialog.run() != Gtk.ResponseType.OK) return false; } @@ -734,7 +736,7 @@ public class ComposerWindow : Gtk.Window { } if (confirmation != null) { ConfirmationDialog dialog = new ConfirmationDialog(this, - confirmation, null, Gtk.Stock.OK); + confirmation, null, Stock._OK); if (dialog.run() != Gtk.ResponseType.OK) return false; } @@ -896,7 +898,7 @@ public class ComposerWindow : Gtk.Window { label.halign = Gtk.Align.START; label.xpad = 4; - Gtk.Button remove_button = new Gtk.Button.from_stock(Gtk.Stock.REMOVE); + Gtk.Button remove_button = new Gtk.Button.with_mnemonic(Stock._REMOVE); box.pack_start(remove_button, false, false); remove_button.clicked.connect(() => remove_attachment(attachment_file, box)); @@ -1209,10 +1211,10 @@ public class ComposerWindow : Gtk.Window { if (selected != null && (selected is WebKit.DOM.HTMLAnchorElement || selected.get_parent_element() is WebKit.DOM.HTMLAnchorElement)) { existing_link = true; - dialog.add_buttons(Gtk.Stock. REMOVE, Gtk.ResponseType.REJECT); + dialog.add_buttons(Stock._REMOVE, Gtk.ResponseType.REJECT); } - dialog.add_buttons(Gtk.Stock.CANCEL, Gtk.ResponseType.CANCEL, Gtk.Stock.OK, + dialog.add_buttons(Stock._CANCEL, Gtk.ResponseType.CANCEL, Stock._OK, Gtk.ResponseType.OK); Gtk.Entry entry = new Gtk.Entry(); @@ -1377,7 +1379,7 @@ public class ComposerWindow : Gtk.Window { context_menu.append(new Gtk.SeparatorMenuItem()); // Select all. - Gtk.MenuItem select_all_item = new Gtk.ImageMenuItem.from_stock(Gtk.Stock.SELECT_ALL, null); + Gtk.MenuItem select_all_item = new Gtk.MenuItem.with_mnemonic(Stock.SELECT__ALL); select_all_item.activate.connect(on_select_all); context_menu.append(select_all_item); @@ -1560,10 +1562,14 @@ public class ComposerWindow : Gtk.Window { } private void set_entry_completions() { - Geary.ContactStore contact_store = account.get_contact_store(); - to_entry.completion = new ContactEntryCompletion(contact_store); - cc_entry.completion = new ContactEntryCompletion(contact_store); - bcc_entry.completion = new ContactEntryCompletion(contact_store); + if (contact_list_store != null && contact_list_store.contact_store == account.get_contact_store()) + return; + + contact_list_store = new ContactListStore(account.get_contact_store()); + + to_entry.completion = new ContactEntryCompletion(contact_list_store); + cc_entry.completion = new ContactEntryCompletion(contact_list_store); + bcc_entry.completion = new ContactEntryCompletion(contact_list_store); } } diff --git a/src/client/composer/contact-entry-completion.vala b/src/client/composer/contact-entry-completion.vala index c9c7a7fc..4db3d9ab 100644 --- a/src/client/composer/contact-entry-completion.vala +++ b/src/client/composer/contact-entry-completion.vala @@ -5,98 +5,26 @@ */ public class ContactEntryCompletion : Gtk.EntryCompletion { - // Sort column indices. - private const int SORT_COLUMN = 0; - - // Minimum visibility for the contact to appear in autocompletion. - private const Geary.ContactImportance CONTACT_VISIBILITY_THRESHOLD = Geary.ContactImportance.TO_TO; - - private Gtk.ListStore list_store; - + private ContactListStore list_store; private Gtk.TreeIter? last_iter = null; - private enum Column { - CONTACT_OBJECT, - CONTACT_MARKUP_NAME, - LAST_KEY; - - public static Type[] get_types() { - return { - typeof (Geary.Contact), // CONTACT_OBJECT - typeof (string), // CONTACT_MARKUP_NAME - typeof (string) // LAST_KEY - }; - } - } - - public ContactEntryCompletion(Geary.ContactStore? contact_store) { - list_store = new Gtk.ListStore.newv(Column.get_types()); - list_store.set_sort_func(SORT_COLUMN, sort_func); - list_store.set_sort_column_id(SORT_COLUMN, Gtk.SortType.ASCENDING); - - if (contact_store == null) - return; - - foreach (Geary.Contact contact in contact_store.contacts) - add_contact(contact); - - contact_store.contact_added.connect(on_contact_added); - contact_store.contact_updated.connect(on_contact_updated); + public ContactEntryCompletion(ContactListStore list_store) { + this.list_store = list_store; model = list_store; set_match_func(completion_match_func); Gtk.CellRendererText text_renderer = new Gtk.CellRendererText(); pack_start(text_renderer, true); - add_attribute(text_renderer, "markup", Column.CONTACT_MARKUP_NAME); + add_attribute(text_renderer, "markup", ContactListStore.Column.CONTACT_MARKUP_NAME); set_inline_selection(true); match_selected.connect(on_match_selected); cursor_on_match.connect(on_cursor_on_match); } - private void add_contact(Geary.Contact contact) { - if (contact.highest_importance < CONTACT_VISIBILITY_THRESHOLD) - return; - - string full_address = contact.get_rfc822_address().get_full_address(); - Gtk.TreeIter iter; - list_store.append(out iter); - list_store.set(iter, - Column.CONTACT_OBJECT, contact, - Column.CONTACT_MARKUP_NAME, Markup.escape_text(full_address), - Column.LAST_KEY, ""); - } - - private void update_contact(Geary.Contact updated_contact) { - Gtk.TreeIter iter; - if (!list_store.get_iter_first(out iter)) - return; - - do { - if (get_contact(iter) != updated_contact) - continue; - - Gtk.TreePath? path = list_store.get_path(iter); - if (path != null) - list_store.row_changed(path, iter); - - return; - } while (list_store.iter_next(ref iter)); - } - - private void on_contact_added(Geary.Contact contact) { - add_contact(contact); - } - - private void on_contact_updated(Geary.Contact contact) { - update_contact(contact); - } - private bool on_match_selected(Gtk.EntryCompletion sender, Gtk.TreeModel model, Gtk.TreeIter iter) { - string? full_address = get_full_address(iter); - if (full_address == null) - return false; + string full_address = list_store.get_full_address(iter); Gtk.Entry? entry = sender.get_entry() as Gtk.Entry; if (entry == null) @@ -137,43 +65,21 @@ public class ContactEntryCompletion : Gtk.EntryCompletion { last_iter = null; } - private Geary.Contact? get_contact(Gtk.TreeIter iter) { - GLib.Value contact_value; - list_store.get_value(iter, Column.CONTACT_OBJECT, out contact_value); - return contact_value.get_object() as Geary.Contact; - } - - private string? get_full_address(Gtk.TreeIter iter) { - Geary.Contact? contact = get_contact(iter); - return contact == null ? null : contact.get_rfc822_address().to_rfc822_string(); - } - private bool completion_match_func(Gtk.EntryCompletion completion, string key, Gtk.TreeIter iter) { // We don't use the provided key, because the user can enter multiple addresses. int current_address_index; string current_address_key; get_addresses(completion, out current_address_index, out current_address_key); - Geary.Contact? contact = get_contact(iter); + Geary.Contact? contact = list_store.get_contact(iter); if (contact == null) return false; string highlighted_result; if (!match_prefix_contact(current_address_key, contact, out highlighted_result)) return false; - - // Changing a row in the list store causes Gtk.EntryCompletion to re-evaluate - // completion_match_func for that row. Thus we need to make sure the key has - // actually changed before settings the highlighting--otherwise we will cause - // an infinite loop. - GLib.Value last_key_value; - list_store.get_value(iter, Column.LAST_KEY, out last_key_value); - string? last_key = last_key_value.get_string(); - if (current_address_key != last_key) { - list_store.set(iter, - Column.CONTACT_MARKUP_NAME, highlighted_result, - Column.LAST_KEY, current_address_key, -1); - } + + list_store.set_highlighted_result(iter, highlighted_result, current_address_key); return true; } @@ -258,14 +164,15 @@ public class ContactEntryCompletion : Gtk.EntryCompletion { private bool match_prefix_string(string needle, string? haystack = null, out string highlighted_result = null) { - bool matched = false; highlighted_result = ""; - if (haystack == null) + + if (Geary.String.is_empty(haystack) || Geary.String.is_empty(needle)) return false; // Default result if there is no match or we encounter an error. highlighted_result = haystack; + bool matched = false; try { string escaped_needle = Regex.escape_string(needle.normalize()); Regex regex = new Regex("\\b" + escaped_needle, RegexCompileFlags.CASELESS); @@ -279,6 +186,7 @@ public class ContactEntryCompletion : Gtk.EntryCompletion { highlighted_result = Markup.escape_text(highlighted_result) .replace("‘", "").replace("’", ""); + return matched; } @@ -291,43 +199,5 @@ public class ContactEntryCompletion : Gtk.EntryCompletion { return false; } - - private int sort_func(Gtk.TreeModel model, Gtk.TreeIter aiter, Gtk.TreeIter biter) { - // Order by importance, then by real name, then by email. - GLib.Value avalue, bvalue; - model.get_value(aiter, Column.CONTACT_OBJECT, out avalue); - model.get_value(biter, Column.CONTACT_OBJECT, out bvalue); - Geary.Contact? acontact = avalue.get_object() as Geary.Contact; - Geary.Contact? bcontact = bvalue.get_object() as Geary.Contact; - - // Contacts can be null if the sort func is called between TreeModel.append and - // TreeModel.set. - if (acontact == bcontact) - return 0; - if (acontact == null && bcontact != null) - return -1; - if (acontact != null && bcontact == null) - return 1; - - // First order by importance. - if (acontact.highest_importance > bcontact.highest_importance) - return -1; - if (acontact.highest_importance < bcontact.highest_importance) - return 1; - - // Then order by real name. - string? anormalized_real_name = acontact.real_name == null ? null : - acontact.real_name.normalize().casefold(); - string? bnormalized_real_name = bcontact.real_name == null ? null : - bcontact.real_name.normalize().casefold(); - // strcmp correctly marks 'null' as first in lexigraphic order, so we don't need to - // special-case it. - int result = strcmp(anormalized_real_name, bnormalized_real_name); - if (result != 0) - return result; - - // Finally, order by email. - return strcmp(acontact.normalized_email, bcontact.normalized_email); - } } diff --git a/src/client/composer/contact-list-store.vala b/src/client/composer/contact-list-store.vala new file mode 100644 index 00000000..9b4dade6 --- /dev/null +++ b/src/client/composer/contact-list-store.vala @@ -0,0 +1,154 @@ +/* Copyright 2013 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 ContactListStore : Gtk.ListStore { + // Minimum visibility for the contact to appear in autocompletion. + private const Geary.ContactImportance CONTACT_VISIBILITY_THRESHOLD = Geary.ContactImportance.TO_TO; + + public enum Column { + CONTACT_OBJECT, + CONTACT_MARKUP_NAME, + LAST_KEY; + + public static Type[] get_types() { + return { + typeof (Geary.Contact), // CONTACT_OBJECT + typeof (string), // CONTACT_MARKUP_NAME + typeof (string) // LAST_KEY + }; + } + } + + public Geary.ContactStore contact_store { get; private set; } + + public ContactListStore(Geary.ContactStore contact_store) { + set_column_types(Column.get_types()); + + this.contact_store = contact_store; + + foreach (Geary.Contact contact in contact_store.contacts) + add_contact(contact); + + // set sort function *after* adding all the contacts + set_sort_func(Column.CONTACT_OBJECT, sort_func); + set_sort_column_id(Column.CONTACT_OBJECT, Gtk.SortType.ASCENDING); + + contact_store.contact_added.connect(on_contact_added); + contact_store.contact_updated.connect(on_contact_updated); + } + + ~ContactListStore() { + contact_store.contact_added.disconnect(on_contact_added); + contact_store.contact_updated.disconnect(on_contact_updated); + } + + public Geary.Contact get_contact(Gtk.TreeIter iter) { + GLib.Value contact_value; + get_value(iter, Column.CONTACT_OBJECT, out contact_value); + + return (Geary.Contact) contact_value.get_object(); + } + + public string get_full_address(Gtk.TreeIter iter) { + return get_contact(iter).get_rfc822_address().get_full_address(); + } + + // Highlighted result should be Markup.escaped for presentation to the user + public void set_highlighted_result(Gtk.TreeIter iter, string highlighted_result, + string current_address_key) { + // get the last key for this row for comparison + GLib.Value last_key_value; + get_value(iter, Column.LAST_KEY, out last_key_value); + string? last_key = last_key_value.get_string(); + + // Changing a row in the list store causes Gtk.EntryCompletion to re-evaluate + // completion_match_func for that row. Thus we need to make sure the key has + // actually changed before settings the highlighting--otherwise we will cause + // an infinite loop. + if (current_address_key != last_key) { + set(iter, + Column.CONTACT_MARKUP_NAME, highlighted_result, + Column.LAST_KEY, current_address_key, -1); + } + } + + private void add_contact(Geary.Contact contact) { + if (contact.highest_importance < CONTACT_VISIBILITY_THRESHOLD) + return; + + string full_address = contact.get_rfc822_address().get_full_address(); + Gtk.TreeIter iter; + append(out iter); + set(iter, + Column.CONTACT_OBJECT, contact, + Column.CONTACT_MARKUP_NAME, Markup.escape_text(full_address), + Column.LAST_KEY, ""); + } + + private void update_contact(Geary.Contact updated_contact) { + Gtk.TreeIter iter; + if (!get_iter_first(out iter)) + return; + + do { + if (get_contact(iter) != updated_contact) + continue; + + Gtk.TreePath? path = get_path(iter); + if (path != null) + row_changed(path, iter); + + return; + } while (iter_next(ref iter)); + } + + private void on_contact_added(Geary.Contact contact) { + add_contact(contact); + } + + private void on_contact_updated(Geary.Contact contact) { + update_contact(contact); + } + + private int sort_func(Gtk.TreeModel model, Gtk.TreeIter aiter, Gtk.TreeIter biter) { + // Order by importance, then by real name, then by email. + GLib.Value avalue, bvalue; + model.get_value(aiter, Column.CONTACT_OBJECT, out avalue); + model.get_value(biter, Column.CONTACT_OBJECT, out bvalue); + Geary.Contact? acontact = avalue.get_object() as Geary.Contact; + Geary.Contact? bcontact = bvalue.get_object() as Geary.Contact; + + // Contacts can be null if the sort func is called between TreeModel.append and + // TreeModel.set. + if (acontact == bcontact) + return 0; + if (acontact == null && bcontact != null) + return -1; + if (acontact != null && bcontact == null) + return 1; + + // First order by importance. + if (acontact.highest_importance > bcontact.highest_importance) + return -1; + if (acontact.highest_importance < bcontact.highest_importance) + return 1; + + // Then order by real name. + string? anormalized_real_name = acontact.real_name == null ? null : + acontact.real_name.normalize().casefold(); + string? bnormalized_real_name = bcontact.real_name == null ? null : + bcontact.real_name.normalize().casefold(); + // strcmp correctly marks 'null' as first in lexigraphic order, so we don't need to + // special-case it. + int result = strcmp(anormalized_real_name, bnormalized_real_name); + if (result != 0) + return result; + + // Finally, order by email. + return strcmp(acontact.normalized_email, bcontact.normalized_email); + } +} + diff --git a/src/client/dialogs/alert-dialog.vala b/src/client/dialogs/alert-dialog.vala index 8f02f527..6789b817 100644 --- a/src/client/dialogs/alert-dialog.vala +++ b/src/client/dialogs/alert-dialog.vala @@ -48,14 +48,14 @@ abstract class AlertDialog : Object { class ConfirmationDialog : AlertDialog { public ConfirmationDialog(Gtk.Window? parent, string primary, string? secondary, string? ok_button) { - base (parent, Gtk.MessageType.WARNING, primary, secondary, ok_button, Gtk.Stock.CANCEL, + base (parent, Gtk.MessageType.WARNING, primary, secondary, ok_button, Stock._CANCEL, null, Gtk.ResponseType.NONE); } } class ErrorDialog : AlertDialog { public ErrorDialog(Gtk.Window? parent, string primary, string? secondary) { - base (parent, Gtk.MessageType.ERROR, primary, secondary, Gtk.Stock.OK, null, null, + base (parent, Gtk.MessageType.ERROR, primary, secondary, Stock._OK, null, null, Gtk.ResponseType.NONE); } } diff --git a/src/client/dialogs/attachment-dialog.vala b/src/client/dialogs/attachment-dialog.vala index 27d44dbc..cce71ce5 100644 --- a/src/client/dialogs/attachment-dialog.vala +++ b/src/client/dialogs/attachment-dialog.vala @@ -19,7 +19,7 @@ public class AttachmentDialog : Gtk.FileChooserDialog { } construct { - add_button(Gtk.Stock.CANCEL, Gtk.ResponseType.CANCEL); + add_button(Stock._CANCEL, Gtk.ResponseType.CANCEL); add_button(_("_Attach"), Gtk.ResponseType.ACCEPT); if (!Geary.String.is_empty(current_folder)) { diff --git a/src/client/dialogs/password-dialog.vala b/src/client/dialogs/password-dialog.vala index 4f6a5b97..caf13d1b 100644 --- a/src/client/dialogs/password-dialog.vala +++ b/src/client/dialogs/password-dialog.vala @@ -97,8 +97,8 @@ public class PasswordDialog { check_remember_password.active = account_information.imap_remember_password; // Add action buttons - Gtk.Button cancel_button = new Gtk.Button.from_stock(Gtk.Stock.CANCEL); - ok_button = new Gtk.Button.from_stock(Gtk.Stock.OK); + Gtk.Button cancel_button = new Gtk.Button.from_stock(Stock._CANCEL); + ok_button = new Gtk.Button.from_stock(Stock._OK); ok_button.can_default = true; dialog.add_action_widget(cancel_button, Gtk.ResponseType.CANCEL); dialog.add_action_widget(ok_button, Gtk.ResponseType.OK); diff --git a/src/client/folder-list/folder-list-abstract-folder-entry.vala b/src/client/folder-list/folder-list-abstract-folder-entry.vala index 307fc581..94c15d23 100644 --- a/src/client/folder-list/folder-list-abstract-folder-entry.vala +++ b/src/client/folder-list/folder-list-abstract-folder-entry.vala @@ -22,6 +22,8 @@ public abstract class FolderList.AbstractFolderEntry : Geary.BaseObject, Sidebar public abstract Icon? get_sidebar_icon(); + public abstract int get_count(); + public virtual string to_string() { return "AbstractFolderEntry: " + get_sidebar_name(); } diff --git a/src/client/folder-list/folder-list-folder-entry.vala b/src/client/folder-list/folder-list-folder-entry.vala index ed8afdd1..551eccdf 100644 --- a/src/client/folder-list/folder-list-folder-entry.vala +++ b/src/client/folder-list/folder-list-folder-entry.vala @@ -22,10 +22,7 @@ public class FolderList.FolderEntry : FolderList.AbstractFolderEntry, Sidebar.In } public override string get_sidebar_name() { - return (folder.properties.email_unread == 0 ? folder.get_display_name() : - /// This string gets the folder name and the unread messages count, - /// e.g. All Mail (5). - _("%s (%d)").printf(folder.get_display_name(), folder.properties.email_unread)); + return folder.get_display_name(); } public override string? get_sidebar_tooltip() { @@ -103,7 +100,11 @@ public class FolderList.FolderEntry : FolderList.AbstractFolderEntry, Sidebar.In } private void on_email_unread_count_changed() { - sidebar_name_changed(get_sidebar_name()); + sidebar_count_changed(get_count()); sidebar_tooltip_changed(get_sidebar_tooltip()); } + + public override int get_count() { + return folder.properties.email_unread; + } } diff --git a/src/client/folder-list/folder-list-inbox-folder-entry.vala b/src/client/folder-list/folder-list-inbox-folder-entry.vala index 96f86504..04d656c4 100644 --- a/src/client/folder-list/folder-list-inbox-folder-entry.vala +++ b/src/client/folder-list/folder-list-inbox-folder-entry.vala @@ -16,10 +16,7 @@ public class FolderList.InboxFolderEntry : FolderList.FolderEntry { } public override string get_sidebar_name() { - return (folder.properties.email_unread == 0 ? folder.account.information.nickname : - /// This string gets the account nickname and the unread messages count, - /// e.g. Work (5). - _("%s (%d)").printf(folder.account.information.nickname, folder.properties.email_unread)); + return folder.account.information.nickname; } public Geary.AccountInformation get_account_information() { diff --git a/src/client/folder-list/folder-list-search-branch.vala b/src/client/folder-list/folder-list-search-branch.vala index 51e27da9..2508959a 100644 --- a/src/client/folder-list/folder-list-search-branch.vala +++ b/src/client/folder-list/folder-list-search-branch.vala @@ -59,5 +59,9 @@ public class FolderList.SearchEntry : FolderList.AbstractFolderEntry { private void on_email_total_changed() { sidebar_tooltip_changed(get_sidebar_tooltip()); } + + public override int get_count() { + return 0; + } } diff --git a/src/client/geary-controller.vala b/src/client/geary-controller.vala index c3fceb1a..f8ebdc25 100644 --- a/src/client/geary-controller.vala +++ b/src/client/geary-controller.vala @@ -5,7 +5,7 @@ */ // Primary controller object for Geary. -public class GearyController { +public class GearyController : Geary.BaseObject { // Named actions. public const string ACTION_HELP = "GearyHelp"; public const string ACTION_ABOUT = "GearyAbout"; @@ -32,7 +32,9 @@ public class GearyController { public const string ACTION_COPY_MENU = "GearyCopyMenuButton"; public const string ACTION_MOVE_MENU = "GearyMoveMenuButton"; public const string ACTION_GEAR_MENU = "GearyGearMenuButton"; - + + public const string PROP_CURRENT_CONVERSATION ="current-conversations"; + public const int MIN_CONVERSATION_COUNT = 50; private const string DELETE_MESSAGE_LABEL = _("_Delete"); @@ -62,11 +64,12 @@ public class GearyController { public MainWindow main_window { get; private set; } + public Geary.App.ConversationMonitor? current_conversations { get; private set; default = null; } + private Geary.Account? current_account = null; private Gee.HashMap inboxes = new Gee.HashMap(); private Geary.Folder? current_folder = null; - private Geary.App.ConversationMonitor? current_conversations = null; private Cancellable cancellable_folder = new Cancellable(); private Cancellable cancellable_message = new Cancellable(); private Cancellable cancellable_search = new Cancellable(); @@ -229,20 +232,20 @@ public class GearyController { accounts.label = _("A_ccounts"); entries += accounts; - Gtk.ActionEntry prefs = { ACTION_PREFERENCES, Gtk.Stock.PREFERENCES, TRANSLATABLE, "E", + Gtk.ActionEntry prefs = { ACTION_PREFERENCES, Stock._PREFERENCES, TRANSLATABLE, "E", null, on_preferences }; prefs.label = _("_Preferences"); entries += prefs; - Gtk.ActionEntry help = { ACTION_HELP, Gtk.Stock.HELP, TRANSLATABLE, "F1", null, on_help }; + Gtk.ActionEntry help = { ACTION_HELP, Stock._HELP, TRANSLATABLE, "F1", null, on_help }; help.label = _("_Help"); entries += help; - Gtk.ActionEntry about = { ACTION_ABOUT, Gtk.Stock.ABOUT, TRANSLATABLE, null, null, on_about }; + Gtk.ActionEntry about = { ACTION_ABOUT, Stock._ABOUT, TRANSLATABLE, null, null, on_about }; about.label = _("_About"); entries += about; - Gtk.ActionEntry quit = { ACTION_QUIT, Gtk.Stock.QUIT, TRANSLATABLE, "Q", null, on_quit }; + Gtk.ActionEntry quit = { ACTION_QUIT, Stock._QUIT, TRANSLATABLE, "Q", null, on_quit }; quit.label = _("_Quit"); entries += quit; @@ -760,7 +763,6 @@ public class GearyController { if (current_conversations != null) { yield current_conversations.stop_monitoring_async(!current_is_inbox, null); current_conversations = null; - main_window.set_progress_monitor(null); } else if (current_folder != null && !current_is_inbox) { yield current_folder.close_async(); } @@ -802,10 +804,6 @@ public class GearyController { current_conversations.scan_error.connect(on_scan_error); current_conversations.seed_completed.connect(on_seed_completed); - main_window.conversation_list_store.set_conversation_monitor(current_conversations); - main_window.conversation_list_view.set_conversation_monitor(current_conversations); - main_window.set_progress_monitor(current_conversations.progress_monitor); - if (!current_conversations.is_monitoring) yield current_conversations.start_monitoring_async(conversation_cancellable); @@ -1034,7 +1032,7 @@ public class GearyController { } catch (Error error) { debug("Error showing help: %s", error.message); Gtk.Dialog dialog = new Gtk.Dialog.with_buttons("Error", null, - Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.Stock.CLOSE, Gtk.ResponseType.CLOSE, null); + Gtk.DialogFlags.DESTROY_WITH_PARENT, Stock._CLOSE, Gtk.ResponseType.CLOSE, null); dialog.response.connect(() => { dialog.destroy(); }); dialog.get_content_area().add(new Gtk.Label("Error showing help: %s".printf(error.message))); dialog.show_all(); @@ -1301,7 +1299,7 @@ public class GearyController { QuestionDialog ask_to_open = new QuestionDialog.with_checkbox(main_window, _("Are you sure you want to open \"%s\"?").printf(attachment.filename), _("Attachments may cause damage to your system if opened. Only open files from trusted sources."), - Gtk.Stock.OPEN, Gtk.Stock.CANCEL, _("Don't _ask me again"), false); + Stock._OPEN, Stock._CANCEL, _("Don't _ask me again"), false); if (ask_to_open.run() != Gtk.ResponseType.OK) return; @@ -1337,7 +1335,7 @@ public class GearyController { ? Gtk.FileChooserAction.SAVE : Gtk.FileChooserAction.SELECT_FOLDER; Gtk.FileChooserDialog dialog = new Gtk.FileChooserDialog(null, main_window, action, - Gtk.Stock.CANCEL, Gtk.ResponseType.CANCEL, Gtk.Stock.SAVE, Gtk.ResponseType.ACCEPT, null); + Stock._CANCEL, Gtk.ResponseType.CANCEL, Stock._SAVE, Gtk.ResponseType.ACCEPT, null); if (last_save_directory != null) dialog.set_current_folder(last_save_directory.get_path()); if (attachments.size == 1) { diff --git a/src/client/models/conversation-list-store.vala b/src/client/models/conversation-list-store.vala index d16d9dcb..49f22142 100644 --- a/src/client/models/conversation-list-store.vala +++ b/src/client/models/conversation-list-store.vala @@ -36,6 +36,8 @@ public class ConversationListStore : Gtk.ListStore { } public string? account_owner_email { get; set; default = null; } + public Geary.ProgressMonitor preview_monitor { get; private set; default = + new Geary.SimpleProgressMonitor(Geary.ProgressType.ACTIVITY); } private Geary.App.ConversationMonitor conversation_monitor; private Geary.Folder? current_folder = null; @@ -56,16 +58,17 @@ public class ConversationListStore : Gtk.ListStore { GearyApplication.instance.config.display_preview_changed.connect(on_display_preview_changed); update_id = Timeout.add_seconds_full(Priority.LOW, 60, update_date_strings); + + GearyApplication.instance.controller.notify[GearyController.PROP_CURRENT_CONVERSATION]. + connect(on_conversation_monitor_changed); } ~ConversationListStore() { - set_conversation_monitor(null); - if (update_id != 0) Source.remove(update_id); } - public void set_conversation_monitor(Geary.App.ConversationMonitor? new_conversation_monitor) { + private void on_conversation_monitor_changed() { if (conversation_monitor != null) { conversation_monitor.scan_completed.disconnect(on_scan_completed); conversation_monitor.conversations_added.disconnect(on_conversations_added); @@ -76,7 +79,7 @@ public class ConversationListStore : Gtk.ListStore { } clear(); - conversation_monitor = new_conversation_monitor; + conversation_monitor = GearyApplication.instance.controller.current_conversations; if (conversation_monitor != null) { // add all existing conversations @@ -148,8 +151,12 @@ public class ConversationListStore : Gtk.ListStore { return; } + preview_monitor.notify_start(); + yield do_refresh_previews_async(conversation_monitor); + preview_monitor.notify_finish(); + try { refresh_mutex.release(ref token); } catch (Error err) { diff --git a/src/client/sidebar/sidebar-common.vala b/src/client/sidebar/sidebar-common.vala index eff02ad7..253106e9 100644 --- a/src/client/sidebar/sidebar-common.vala +++ b/src/client/sidebar/sidebar-common.vala @@ -49,6 +49,10 @@ public class Sidebar.Grouping : Object, Sidebar.Entry, Sidebar.ExpandableEntry, return closed_icon; } + public int get_count() { + return -1; + } + public string to_string() { return name; } diff --git a/src/client/sidebar/sidebar-count-cell-renderer.vala b/src/client/sidebar/sidebar-count-cell-renderer.vala new file mode 100644 index 00000000..d405548e --- /dev/null +++ b/src/client/sidebar/sidebar-count-cell-renderer.vala @@ -0,0 +1,69 @@ +/* Copyright 2013 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. + */ + +/** + * Cell renderer for counter in sidebar. + */ +public class SidebarCountCellRenderer : Gtk.CellRenderer { + private const int HORIZONTAL_MARGIN = 4; + + public int counter { get; set; } + + public SidebarCountCellRenderer() { + } + + public override Gtk.SizeRequestMode get_request_mode() { + return Gtk.SizeRequestMode.WIDTH_FOR_HEIGHT; + } + + public override void get_preferred_width(Gtk.Widget widget, out int minimum_size, out int natural_size) { + minimum_size = render_counter(widget, null, null, false); // Calculate width. + natural_size = minimum_size; + } + + public override void render(Cairo.Context ctx, Gtk.Widget widget, Gdk.Rectangle background_area, + Gdk.Rectangle cell_area, Gtk.CellRendererState flags) { + render_counter(widget, cell_area, ctx, false); + } + + // Renders the counter. Returns its own width. + private int render_counter(Gtk.Widget widget, Gdk.Rectangle? cell_area, Cairo.Context? ctx, + bool selected) { + if (counter < 1) + return 0; + + string unread_string = + " %d " + .printf(8, counter); + + Pango.Layout layout_num = widget.create_pango_layout(null); + layout_num.set_markup(unread_string, -1); + + Pango.Rectangle? ink_rect; + Pango.Rectangle? logical_rect; + layout_num.get_pixel_extents(out ink_rect, out logical_rect); + if (ctx != null && cell_area != null) { + // Compute x and y locations to right-align and vertically center the count. + int x = cell_area.x + (cell_area.width - logical_rect.width) - HORIZONTAL_MARGIN; + int y = cell_area.y + ((cell_area.height - logical_rect.height) / 2); + ctx.move_to(x, y); + Pango.cairo_show_layout(ctx, layout_num); + } + + return ink_rect.width + (HORIZONTAL_MARGIN * 2); + } + + // This is implemented because it's required; ignore it and look at get_preferred_width() instead. + public override void get_size(Gtk.Widget widget, Gdk.Rectangle? cell_area, out int x_offset, + out int y_offset, out int width, out int height) { + // Set values to avoid compiler warning. + x_offset = 0; + y_offset = 0; + width = 0; + height = 0; + } +} + diff --git a/src/client/sidebar/sidebar-entry.vala b/src/client/sidebar/sidebar-entry.vala index a49b20cd..398ef781 100644 --- a/src/client/sidebar/sidebar-entry.vala +++ b/src/client/sidebar/sidebar-entry.vala @@ -11,12 +11,16 @@ public interface Sidebar.Entry : Object { public signal void sidebar_icon_changed(Icon? icon); + public signal void sidebar_count_changed(int count); + public abstract string get_sidebar_name(); public abstract string? get_sidebar_tooltip(); public abstract Icon? get_sidebar_icon(); + public abstract int get_count(); + public abstract string to_string(); internal virtual void grafted(Sidebar.Tree tree) { diff --git a/src/client/sidebar/sidebar-tree.vala b/src/client/sidebar/sidebar-tree.vala index 38a3dc7c..71726d6c 100644 --- a/src/client/sidebar/sidebar-tree.vala +++ b/src/client/sidebar/sidebar-tree.vala @@ -51,6 +51,7 @@ public class Sidebar.Tree : Gtk.TreeView { PIXBUF, CLOSED_PIXBUF, OPEN_PIXBUF, + COUNTER, N_COLUMNS } @@ -60,7 +61,8 @@ public class Sidebar.Tree : Gtk.TreeView { typeof (EntryWrapper), // WRAPPER typeof (Gdk.Pixbuf?), // PIXBUF typeof (Gdk.Pixbuf?), // CLOSED_PIXBUF - typeof (Gdk.Pixbuf?) // OPEN_PIXBUF + typeof (Gdk.Pixbuf?), // OPEN_PIXBUF + typeof (int) // COUNTER ); private Gtk.IconTheme? icon_theme; @@ -98,7 +100,7 @@ public class Sidebar.Tree : Gtk.TreeView { get_style_context().add_class("sidebar"); Gtk.TreeViewColumn text_column = new Gtk.TreeViewColumn(); - text_column.set_sizing(Gtk.TreeViewColumnSizing.FIXED); + text_column.set_expand(true); Gtk.CellRendererPixbuf icon_renderer = new Gtk.CellRendererPixbuf(); text_column.pack_start(icon_renderer, false); text_column.add_attribute(icon_renderer, "pixbuf", Columns.PIXBUF); @@ -112,6 +114,13 @@ public class Sidebar.Tree : Gtk.TreeView { text_column.add_attribute(text_renderer, "markup", Columns.NAME); append_column(text_column); + // Count column. + Gtk.TreeViewColumn count_column = new Gtk.TreeViewColumn(); + SidebarCountCellRenderer unread_renderer = new SidebarCountCellRenderer(); + count_column.pack_start(unread_renderer, false); + count_column.add_attribute(unread_renderer, "counter", Columns.COUNTER); + append_column(count_column); + set_headers_visible(false); set_enable_search(false); set_search_column(-1); @@ -170,6 +179,14 @@ public class Sidebar.Tree : Gtk.TreeView { renderer.visible = !(wrapper.entry is Sidebar.Header); } + public void counter_renderer_function(Gtk.CellLayout layout, Gtk.CellRenderer renderer, Gtk.TreeModel model, Gtk.TreeIter iter) { + EntryWrapper? wrapper = get_wrapper_at_iter(iter); + if (wrapper == null) { + return; + } + renderer.visible = !(wrapper.entry is Sidebar.Header); + } + private void on_drag_begin(Gdk.DragContext ctx) { is_internal_drag_in_progress = true; } @@ -471,11 +488,13 @@ public class Sidebar.Tree : Gtk.TreeView { store.set(assoc_iter, Columns.TOOLTIP, entry.get_sidebar_tooltip() != null ? Geary.HTML.escape_markup(entry.get_sidebar_tooltip()) : null); store.set(assoc_iter, Columns.WRAPPER, wrapper); + store.set(assoc_iter, Columns.COUNTER, entry.get_count()); load_entry_icons(assoc_iter); entry.sidebar_tooltip_changed.connect(on_sidebar_tooltip_changed); entry.sidebar_icon_changed.connect(on_sidebar_icon_changed); entry.sidebar_name_changed.connect(on_sidebar_name_changed); + entry.sidebar_count_changed.connect(on_sidebar_count_changed); Sidebar.EmphasizableEntry? emphasizable = entry as Sidebar.EmphasizableEntry; if (emphasizable != null) @@ -499,6 +518,7 @@ public class Sidebar.Tree : Gtk.TreeView { store.set(new_iter, Columns.NAME, get_name_for_entry(entry)); store.set(new_iter, Columns.TOOLTIP, Geary.HTML.escape_markup(entry.get_sidebar_tooltip())); + store.set(new_iter, Columns.COUNTER, entry.get_count()); store.set(new_iter, Columns.WRAPPER, new_wrapper); load_entry_icons(new_iter); @@ -589,6 +609,7 @@ public class Sidebar.Tree : Gtk.TreeView { entry.sidebar_tooltip_changed.disconnect(on_sidebar_tooltip_changed); entry.sidebar_icon_changed.disconnect(on_sidebar_icon_changed); entry.sidebar_name_changed.disconnect(on_sidebar_name_changed); + entry.sidebar_count_changed.disconnect(on_sidebar_count_changed); Sidebar.EmphasizableEntry? emphasizable = entry as Sidebar.EmphasizableEntry; if (emphasizable != null) @@ -758,6 +779,13 @@ public class Sidebar.Tree : Gtk.TreeView { rename_entry(entry); } + private void on_sidebar_count_changed(Sidebar.Entry entry, int coun) { + EntryWrapper? wrapper = get_wrapper(entry); + assert(wrapper != null); + + store.set(wrapper.get_iter(), Columns.COUNTER, entry.get_count()); + } + private Gdk.Pixbuf? fetch_icon_pixbuf(GLib.Icon? gicon) { if (gicon == null) return null; diff --git a/src/client/ui/main-window.vala b/src/client/ui/main-window.vala index de3e8ea5..ae44fa1b 100644 --- a/src/client/ui/main-window.vala +++ b/src/client/ui/main-window.vala @@ -23,6 +23,8 @@ public class MainWindow : Gtk.Window { private Gtk.ScrolledWindow conversation_list_scrolled; private MonitoredSpinner spinner = new MonitoredSpinner(); + private Geary.AggregateProgressMonitor progress_monitor = new Geary.AggregateProgressMonitor(); + private Geary.ProgressMonitor? conversation_monitor_progress = null; public MainWindow() { title = GearyApplication.NAME; @@ -42,12 +44,19 @@ public class MainWindow : Gtk.Window { add_accel_group(GearyApplication.instance.ui_manager.get_accel_group()); + spinner.set_progress_monitor(progress_monitor); + progress_monitor.add(conversation_list_store.preview_monitor); + GLib.List pixbuf_list = new GLib.List(); pixbuf_list.append(IconFactory.instance.application_icon); set_default_icon_list(pixbuf_list); delete_event.connect(on_delete_event); key_press_event.connect(on_key_press_event); + GearyApplication.instance.controller.notify[GearyController.PROP_CURRENT_CONVERSATION]. + connect(on_conversation_monitor_changed); + Geary.Engine.instance.account_available.connect(on_account_available); + Geary.Engine.instance.account_unavailable.connect(on_account_unavailable); create_layout(); } @@ -82,13 +91,6 @@ public class MainWindow : Gtk.Window { return base.configure_event(event); } - /** - * Sets the progress monitor to display in the status bar. - */ - public void set_progress_monitor(Geary.ProgressMonitor? monitor) { - spinner.set_progress_monitor(monitor); - } - private void create_layout() { Gtk.Box main_layout = new Gtk.Box(Gtk.Orientation.VERTICAL, 0); @@ -146,5 +148,38 @@ public class MainWindow : Gtk.Window { // via the default handling return propagate_key_event(event); } + + private void on_conversation_monitor_changed() { + Geary.App.ConversationMonitor? conversation_monitor = + GearyApplication.instance.controller.current_conversations; + + // Remove existing progress monitor. + if (conversation_monitor_progress != null) { + progress_monitor.remove(conversation_monitor_progress); + conversation_monitor_progress = null; + } + + // Add new one. + if (conversation_monitor != null) { + conversation_monitor_progress = conversation_monitor.progress_monitor; + progress_monitor.add(conversation_monitor_progress); + } + } + + private void on_account_available(Geary.AccountInformation account) { + try { + progress_monitor.add(Geary.Engine.instance.get_account_instance(account).opening_monitor); + } catch (Error e) { + debug("Could not access account opening progress monitor: %s", e.message); + } + } + + private void on_account_unavailable(Geary.AccountInformation account) { + try { + progress_monitor.remove(Geary.Engine.instance.get_account_instance(account).opening_monitor); + } catch (Error e) { + debug("Could not access account opening progress monitor: %s", e.message); + } + } } diff --git a/src/client/ui/stock.vala b/src/client/ui/stock.vala new file mode 100644 index 00000000..a17bf5e6 --- /dev/null +++ b/src/client/ui/stock.vala @@ -0,0 +1,35 @@ +/* Copyright 2013 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. + */ + +/** + * With GtkStock deprecated in GTK+ 3.10, these strings offer replacements for commonly-needed + * text labels. + * + * Plain text use all-caps constant names and an underscore indicating where in the English text the + * mnemonic lies. This can be used to ensure that the mnemonic doesn't interfere with other custom + * strings in the grouping. + */ + +namespace Stock { + +public const string _OK = _("_OK"); +public const string _CANCEL = _("_Cancel"); + +public const string _ABOUT = _("_About"); +public const string _ADD = _("_Add"); +public const string _CLOSE = _("_Close"); +public const string _DISCARD = _("_Discard"); +public const string _HELP = _("_Help"); +public const string _OPEN = _("_Open"); +public const string _PREFERENCES = _("_Preferences"); +public const string _PRINT = _("_Print"); +public const string _QUIT = _("_Quit"); +public const string _REMOVE = _("_Remove"); +public const string _SAVE = _("_Save"); +public const string SELECT__ALL = _("Select _All"); + +} + diff --git a/src/client/util/util-date.vala b/src/client/util/util-date.vala index 442c79c3..f7a690f1 100644 --- a/src/client/util/util-date.vala +++ b/src/client/util/util-date.vala @@ -144,7 +144,7 @@ public CoarseDate as_coarse_date(DateTime datetime, DateTime now, TimeSpan diff) if (same_day(temp, now)) { return CoarseDate.YESTERDAY; } - temp = datetime.add_weeks(1); + temp = datetime.add_days(6); if (same_day(temp, now) || temp.compare(now) >= 0) { return CoarseDate.THIS_WEEK; } diff --git a/src/client/views/conversation-list-view.vala b/src/client/views/conversation-list-view.vala index edf581ed..6df9d766 100644 --- a/src/client/views/conversation-list-view.vala +++ b/src/client/views/conversation-list-view.vala @@ -69,16 +69,18 @@ public class ConversationListView : Gtk.TreeView { Gdk.DragAction.COPY | Gdk.DragAction.MOVE); GearyApplication.instance.config.display_preview_changed.connect(on_display_preview_changed); + GearyApplication.instance.controller.notify[GearyController.PROP_CURRENT_CONVERSATION]. + connect(on_conversation_monitor_changed); } - public void set_conversation_monitor(Geary.App.ConversationMonitor? new_conversation_monitor) { + private void on_conversation_monitor_changed() { if (conversation_monitor != null) { conversation_monitor.scan_started.disconnect(on_scan_started); conversation_monitor.scan_completed.disconnect(on_scan_completed); conversation_monitor.conversation_removed.disconnect(on_conversation_removed); } - conversation_monitor = new_conversation_monitor; + conversation_monitor = GearyApplication.instance.controller.current_conversations; if (conversation_monitor != null) { conversation_monitor.scan_started.connect(on_scan_started); diff --git a/src/client/views/conversation-viewer.vala b/src/client/views/conversation-viewer.vala index 76f76836..062150c6 100644 --- a/src/client/views/conversation-viewer.vala +++ b/src/client/views/conversation-viewer.vala @@ -1362,7 +1362,7 @@ public class ConversationViewer : Gtk.Box { } // Print a message. - Gtk.MenuItem print_item = new Gtk.ImageMenuItem.from_stock(Gtk.Stock.PRINT, null); + Gtk.MenuItem print_item = new Gtk.MenuItem.with_mnemonic(Stock._PRINT); print_item.activate.connect(() => on_print_message(email)); menu.append(print_item); diff --git a/src/engine/abstract/geary-abstract-account.vala b/src/engine/abstract/geary-abstract-account.vala index 3e0f5b0e..0da874cf 100644 --- a/src/engine/abstract/geary-abstract-account.vala +++ b/src/engine/abstract/geary-abstract-account.vala @@ -8,6 +8,7 @@ public abstract class Geary.AbstractAccount : BaseObject, Geary.Account { public Geary.AccountInformation information { get; protected set; } public Geary.ProgressMonitor search_upgrade_monitor { get; protected set; } public Geary.ProgressMonitor db_upgrade_monitor { get; protected set; } + public Geary.ProgressMonitor opening_monitor { get; protected set; } private string name; diff --git a/src/engine/api/geary-account.vala b/src/engine/api/geary-account.vala index 60c3531c..a07ee788 100644 --- a/src/engine/api/geary-account.vala +++ b/src/engine/api/geary-account.vala @@ -17,6 +17,7 @@ public interface Geary.Account : BaseObject { public abstract Geary.ProgressMonitor search_upgrade_monitor { get; protected set; } public abstract Geary.ProgressMonitor db_upgrade_monitor { get; protected set; } + public abstract Geary.ProgressMonitor opening_monitor { get; protected set; } public signal void opened(); diff --git a/src/engine/api/geary-progress-monitor.vala b/src/engine/api/geary-progress-monitor.vala index 548a8ccc..4e7427dc 100644 --- a/src/engine/api/geary-progress-monitor.vala +++ b/src/engine/api/geary-progress-monitor.vala @@ -172,6 +172,28 @@ public class Geary.AggregateProgressMonitor : Geary.ProgressMonitor { pm.finish.connect(on_finish); } + public void remove(Geary.ProgressMonitor pm) { + // TODO: Handle the case where we remove a new monitor during progress. + monitors.remove(pm); + pm.start.disconnect(on_start); + pm.update.disconnect(on_update); + pm.finish.disconnect(on_finish); + + if (pm.is_in_progress) { + // If no other PMs are in progress, we must issue a finish signal. + bool issue_signal = true; + foreach(ProgressMonitor p in monitors) { + if (p.is_in_progress) { + issue_signal = false; + break; + } + } + + if (issue_signal) + notify_finish(); + } + } + private void on_start() { if (!is_in_progress) notify_start(); diff --git a/src/engine/api/geary-search-folder.vala b/src/engine/api/geary-search-folder.vala index f193801e..5f191dd9 100644 --- a/src/engine/api/geary-search-folder.vala +++ b/src/engine/api/geary-search-folder.vala @@ -276,7 +276,7 @@ public class Geary.SearchFolder : Geary.AbstractLocalFolder { int result_mutex_token = yield result_mutex.claim_async(); Geary.EmailIdentifier[] ids = new Geary.EmailIdentifier[search_results.size]; - int initial_index = -1; + int initial_index = 0; int i = 0; foreach (Geary.Email email in search_results) { if (initial_id != null && email.id.equal_to(initial_id)) @@ -284,12 +284,12 @@ public class Geary.SearchFolder : Geary.AbstractLocalFolder { ids[i++] = email.id; } - if (initial_id == null) + if (initial_id == null && flags.is_all_set(Folder.ListFlags.OLDEST_TO_NEWEST)) initial_index = ids.length - 1; Gee.List results = new Gee.ArrayList(); if (initial_index >= 0) { - int increment = flags.is_oldest_to_newest() ? 1 : -1; + int increment = flags.is_oldest_to_newest() ? -1 : 1; i = initial_index; if (!flags.is_including_id() && initial_id != null) i += increment; diff --git a/src/engine/app/app-conversation-monitor.vala b/src/engine/app/app-conversation-monitor.vala index 6d73113a..d018f334 100644 --- a/src/engine/app/app-conversation-monitor.vala +++ b/src/engine/app/app-conversation-monitor.vala @@ -673,6 +673,9 @@ public class Geary.App.ConversationMonitor : BaseObject { if (get_search_blacklist().contains(folder.path)) return; + if (conversations.is_empty) + return; + debug("%d out of folder message(s) appended to %s, fetching to add to conversations...", appended_ids.size, folder.to_string()); @@ -718,7 +721,8 @@ public class Geary.App.ConversationMonitor : BaseObject { if (earliest_id != null) { debug("ConversationMonitor (%s) reseeding starting from Email ID %s on opened %s", why, earliest_id.to_string(), folder.to_string()); - yield load_by_id_async(earliest_id, int.MAX, Geary.Folder.ListFlags.OLDEST_TO_NEWEST, + yield load_by_id_async(earliest_id, int.MAX, + Geary.Folder.ListFlags.OLDEST_TO_NEWEST | Geary.Folder.ListFlags.INCLUDING_ID, cancellable_monitor); } else { debug("ConversationMonitor (%s) reseeding latest %d emails on opened %s", why, diff --git a/src/engine/app/conversation-monitor/app-conversation-set.vala b/src/engine/app/conversation-monitor/app-conversation-set.vala index 2994ed98..1c074d26 100644 --- a/src/engine/app/conversation-monitor/app-conversation-set.vala +++ b/src/engine/app/conversation-monitor/app-conversation-set.vala @@ -107,7 +107,7 @@ private class Geary.App.ConversationSet : BaseObject { Email? existing = null; foreach (Geary.Email other in conversation.get_emails(Geary.Conversation.Ordering.NONE)) { if (other.message_id != null && email.message_id.equal_to(other.message_id)) { - existing = email; + existing = other; break; } } diff --git a/src/engine/imap-db/imap-db-database.vala b/src/engine/imap-db/imap-db-database.vala index c68ac85c..cb9a538b 100644 --- a/src/engine/imap-db/imap-db-database.vala +++ b/src/engine/imap-db/imap-db-database.vala @@ -195,7 +195,7 @@ private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase { cx.set_busy_timeout_msec(Db.Connection.RECOMMENDED_BUSY_TIMEOUT_MSEC); cx.set_foreign_keys(true); cx.set_recursive_triggers(true); - cx.set_synchronous(Db.SynchronousMode.NORMAL); + cx.set_synchronous(Db.SynchronousMode.OFF); sqlite3_unicodesn_register_tokenizer(cx.db); } } diff --git a/src/engine/imap-db/imap-db-folder.vala b/src/engine/imap-db/imap-db-folder.vala index bb863d57..e53bf596 100644 --- a/src/engine/imap-db/imap-db-folder.vala +++ b/src/engine/imap-db/imap-db-folder.vala @@ -232,47 +232,40 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics { Gee.HashMap results = new Gee.HashMap(); Gee.ArrayList complete_ids = new Gee.ArrayList(); Gee.Collection updated_contacts = new Gee.ArrayList(); - Error? error = null; - int unread_change = 0; - try { + int total_unread_change = 0; + yield db.exec_transaction_async(Db.TransactionType.RW, (cx) => { foreach (Geary.Email email in emails) { - Db.TransactionOutcome outcome = yield db.exec_transaction_async(Db.TransactionType.RW, - (cx) => { - Gee.Collection? contacts_this_email = null; - Geary.Email.Field combined_fields; - bool created = do_create_or_merge_email(cx, email, out combined_fields, - out contacts_this_email, ref unread_change, cancellable); - - if (contacts_this_email != null) - updated_contacts.add_all(contacts_this_email); - - results.set(email, created); - - if (combined_fields.is_all_set(Geary.Email.Field.ALL)) - complete_ids.add(email.id); - - // Update unread count in DB. - do_add_to_unread_count(cx, unread_change, cancellable); - - return Db.TransactionOutcome.COMMIT; - }, cancellable); + Gee.Collection? contacts_this_email = null; + Geary.Email.Field pre_fields; + Geary.Email.Field post_fields; + int unread_change = 0; + bool created = do_create_or_merge_email(cx, email, out pre_fields, + out post_fields, out contacts_this_email, ref unread_change, cancellable); - if (outcome == Db.TransactionOutcome.COMMIT && updated_contacts.size > 0) - contact_store.update_contacts(updated_contacts); + if (contacts_this_email != null) + updated_contacts.add_all(contacts_this_email); - // clear each iteration - updated_contacts.clear(); + results.set(email, created); + + // in essence, only fire the "email-completed" signal if the local version didn't + // have all the fields but after the create/merge now does + if (post_fields.is_all_set(Geary.Email.Field.ALL) && !pre_fields.is_all_set(Geary.Email.Field.ALL)) + complete_ids.add(email.id); + + // Update unread count in DB. + do_add_to_unread_count(cx, unread_change, cancellable); + + total_unread_change += unread_change; } - } catch (Error e) { - error = e; - } + + return Db.TransactionOutcome.COMMIT; + }, cancellable); + + if (updated_contacts.size > 0) + contact_store.update_contacts(updated_contacts); // Update the email_unread properties. - if (error == null) { - properties.set_status_unseen((properties.email_unread + unread_change).clamp(0, int.MAX)); - } else { - throw error; - } + properties.set_status_unseen((properties.email_unread + total_unread_change).clamp(0, int.MAX)); if (complete_ids.size > 0) email_complete(complete_ids); @@ -934,8 +927,9 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics { } private bool do_create_or_merge_email(Db.Connection cx, Geary.Email email, - out Geary.Email.Field combined_fields, out Gee.Collection updated_contacts, - ref int unread_count_change, Cancellable? cancellable) throws Error { + out Geary.Email.Field pre_fields, out Geary.Email.Field post_fields, + out Gee.Collection updated_contacts, ref int unread_count_change, + Cancellable? cancellable) throws Error { // see if message already present in current folder, if not, search for duplicate throughout // mailbox bool associated; @@ -943,8 +937,8 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics { // if found, merge, and associate if necessary if (message_id != Db.INVALID_ROWID) { - do_merge_email(cx, message_id, email, out combined_fields, out updated_contacts, - ref unread_count_change, !associated, cancellable); + do_merge_email(cx, message_id, email, out pre_fields, out post_fields, + out updated_contacts, ref unread_count_change, !associated, cancellable); // return false to indicate a merge return false; @@ -953,7 +947,8 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics { // not found, so create and associate with this folder MessageRow row = new MessageRow.from_email(email); - combined_fields = email.fields; + pre_fields = Geary.Email.Field.NONE; + post_fields = email.fields; Db.Statement stmt = cx.prepare( "INSERT INTO MessageTable " @@ -1465,23 +1460,29 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics { } private void do_merge_email(Db.Connection cx, int64 message_id, Geary.Email email, - out Geary.Email.Field combined_fields, out Gee.Collection updated_contacts, - ref int unread_count_change, bool associate_with_folder, - Cancellable? cancellable) throws Error { + out Geary.Email.Field pre_fields, out Geary.Email.Field post_fields, + out Gee.Collection updated_contacts, ref int unread_count_change, + bool associate_with_folder, Cancellable? cancellable) throws Error { assert(message_id != Db.INVALID_ROWID); int new_unread_count = 0; + if (associate_with_folder) { + // Note: no check is performed here to prevent double-adds. The caller of this method + // is responsible for only setting associate_with_folder if required. + do_associate_with_folder(cx, message_id, email, cancellable); + unread_count_change++; + } + // Default to an empty list, in case we never call do_merge_message_row. updated_contacts = new Gee.LinkedList(); // fetch message from database and merge in this email - Geary.Email.Field db_fields; MessageRow row = do_fetch_message_row(cx, message_id, email.fields | Email.REQUIRED_FOR_MESSAGE | Attachment.REQUIRED_FIELDS, - out db_fields, cancellable); + out pre_fields, cancellable); Geary.Email.Field fetched_fields = row.fields; - combined_fields = db_fields | email.fields; + post_fields = pre_fields | email.fields; row.merge_from_remote(email); if (email.fields == Geary.Email.Field.NONE) @@ -1511,13 +1512,6 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics { do_add_email_to_search_table(cx, message_id, combined_email, cancellable); } - if (associate_with_folder) { - // Note: no check is performed here to prevent double-adds. The caller of this method - // is responsible for only setting associate_with_folder if required. - do_associate_with_folder(cx, message_id, email, cancellable); - unread_count_change++; - } - unread_count_change += new_unread_count; } diff --git a/src/engine/imap-engine/imap-engine-account-synchronizer.vala b/src/engine/imap-engine/imap-engine-account-synchronizer.vala index 4080f647..710fbfeb 100644 --- a/src/engine/imap-engine/imap-engine-account-synchronizer.vala +++ b/src/engine/imap-engine/imap-engine-account-synchronizer.vala @@ -6,6 +6,7 @@ private class Geary.ImapEngine.AccountSynchronizer : Geary.BaseObject { private const int FETCH_DATE_RECEIVED_CHUNK_COUNT = 25; + private const int SYNC_DELAY_SEC = 15; public GenericAccount account { get; private set; } @@ -14,6 +15,7 @@ private class Geary.ImapEngine.AccountSynchronizer : Geary.BaseObject { private GenericFolder? current_folder = null; private Cancellable? bg_cancellable = null; private Nonblocking.Semaphore stopped = new Nonblocking.Semaphore(); + private Gee.HashSet unavailable_paths = new Gee.HashSet(); public AccountSynchronizer(GenericAccount account) { this.account = account; @@ -51,6 +53,7 @@ private class Geary.ImapEngine.AccountSynchronizer : Geary.BaseObject { bg_queue.allow_duplicates = false; bg_queue.requeue_duplicate = false; bg_cancellable = new Cancellable(); + unavailable_paths.clear(); // immediately start processing folders as they are announced as available process_queue_async.begin(); @@ -61,6 +64,7 @@ private class Geary.ImapEngine.AccountSynchronizer : Geary.BaseObject { bg_cancellable.cancel(); bg_queue.clear(); + unavailable_paths.clear(); } private void on_account_prefetch_changed() { @@ -68,7 +72,7 @@ private class Geary.ImapEngine.AccountSynchronizer : Geary.BaseObject { // treat as an availability check (i.e. as if the account had just opened) because // just because this value has changed doesn't mean the contents in the folders // have changed - send_all(account.list_folders(), true); + delayed_send_all(account.list_folders(), true); } catch (Error err) { debug("Unable to schedule re-sync for %s due to prefetch time changing: %s", account.to_string(), err.message); @@ -80,15 +84,38 @@ private class Geary.ImapEngine.AccountSynchronizer : Geary.BaseObject { if (stopped.is_passed()) return; - if (available != null) - send_all(available, true); + if (available != null) { + foreach (Folder folder in available) + unavailable_paths.remove(folder.path); + + delayed_send_all(available, true); + } - if (unavailable != null) + if (unavailable != null) { + foreach (Folder folder in unavailable) + unavailable_paths.add(folder.path); + revoke_all(unavailable); + } } private void on_folders_contents_altered(Gee.Collection altered) { - send_all(altered, false); + delayed_send_all(altered, false); + } + + private void delayed_send_all(Gee.Collection folders, bool reason_available) { + Timeout.add_seconds(SYNC_DELAY_SEC, () => { + // remove any unavailable folders + Gee.ArrayList trimmed_folders = new Gee.ArrayList(); + foreach (Folder folder in folders) { + if (!unavailable_paths.contains(folder.path)) + trimmed_folders.add(folder); + } + + send_all(trimmed_folders, reason_available); + + return false; + }); } private void send_all(Gee.Collection folders, bool reason_available) { @@ -244,11 +271,12 @@ private class Geary.ImapEngine.AccountSynchronizer : Geary.BaseObject { // Oldest local email before epoch, don't sync from network return true; } else if (folder.properties.email_total == local_count) { - // Local email is after epoch, but there's nothing before it + // Local earliest email is after epoch, but there's nothing before it return true; } else { - debug("Oldest local email in %s not old enough (%s vs. %s), synchronizing...", - folder.to_string(), oldest_local.to_string(), epoch.to_string()); + debug("Oldest local email in %s not old enough (%s vs. %s), email_total=%d vs. local_count=%d, synchronizing...", + folder.to_string(), oldest_local.to_string(), epoch.to_string(), + folder.properties.email_total, local_count); } } else if (folder.properties.email_total == 0) { // no local messages, no remote messages -- this is as good as having everything up @@ -305,12 +333,36 @@ private class Geary.ImapEngine.AccountSynchronizer : Geary.BaseObject { // only perform vector expansion if oldest isn't old enough if (oldest_local == null || oldest_local.compare(epoch) > 0) { - Geary.EmailIdentifier? epoch_id = yield folder.find_earliest_email_async(epoch, - oldest_local_id, bg_cancellable); - if (epoch_id == null) { - debug("Unable to locate epoch messages on remote folder %s%s", folder.to_string(), - (oldest_local_id != null) ? " earlier than oldest local" : ""); - } + // go back one month at a time to the epoch, performing a little vector expansion at a + // time rather than all at once (which will stall the replay queue) + DateTime current_epoch = (oldest_local != null) ? oldest_local : new DateTime.now_local(); + do { + current_epoch = current_epoch.add_months(-1); + + // don't go past epoch + if (current_epoch.compare(epoch) < 0) + current_epoch = epoch; + + debug("Background sync'ing %s to %s", folder.to_string(), current_epoch.to_string()); + Geary.EmailIdentifier? epoch_id = yield folder.find_earliest_email_async(current_epoch, + oldest_local_id, bg_cancellable); + if (epoch_id == null && current_epoch.compare(epoch) <= 0) { + debug("Unable to locate epoch messages on remote folder %s%s, fetching one past oldest...", + folder.to_string(), + (oldest_local_id != null) ? " earlier than oldest local" : ""); + + // if there's nothing between the oldest local and the epoch, that means the + // mail just prior to our local oldest is oldest than the epoch; rather than + // continually thrashing looking for something that's just out of reach, add it + // to the folder and be done with it ... note that this even works if oldest_local_id + // is null, as that means the local folder is empty and so we should at least + // pull the first one to get a marker of age + yield folder.list_email_by_id_async(oldest_local_id, 1, Geary.Email.Field.NONE, + Geary.Folder.ListFlags.NONE, bg_cancellable); + } else if (epoch_id != null) { + oldest_local_id = epoch_id; + } + } while (current_epoch.compare(epoch) > 0); } else { debug("No expansion necessary for %s, oldest local (%s) is before epoch (%s)", folder.to_string(), oldest_local.to_string(), epoch.to_string()); diff --git a/src/engine/imap-engine/imap-engine-email-prefetcher.vala b/src/engine/imap-engine/imap-engine-email-prefetcher.vala index d73a4bbc..de4e2d15 100644 --- a/src/engine/imap-engine/imap-engine-email-prefetcher.vala +++ b/src/engine/imap-engine/imap-engine-email-prefetcher.vala @@ -14,9 +14,11 @@ private class Geary.ImapEngine.EmailPrefetcher : Object { public const int PREFETCH_DELAY_SEC = 1; - private const Geary.Email.Field PREFETCH_FIELDS = Geary.Email.Field.ALL; + // Don't fetch FLAGS; those are fetched by the FlagWatcher and during normalization when a + // standard open_async() is invoked on the Folder + private const Geary.Email.Field PREFETCH_FIELDS = Geary.Email.Field.ALL & ~(Geary.Email.MUTABLE_FIELDS); private const int PREFETCH_IDS_CHUNKS = 500; - private const int PREFETCH_CHUNK_BYTES = 128 * 1024; + private const int PREFETCH_CHUNK_BYTES = 64 * 1024; public Nonblocking.CountingSemaphore active_sem { get; private set; default = new Nonblocking.CountingSemaphore(null); } @@ -169,8 +171,6 @@ private class Geary.ImapEngine.EmailPrefetcher : Object { if (emails.size == 0) return; - debug("do_prefetch_batch_async %s start_total=%d", folder.to_string(), emails.size); - // Remove anything that is fully prefetched Gee.Map? fields = null; try { @@ -192,6 +192,11 @@ private class Geary.ImapEngine.EmailPrefetcher : Object { return !fields.get(email.id).fulfills(PREFETCH_FIELDS); }); + if (emails.size == 0) + return; + + debug("do_prefetch_batch_async %s start_total=%d", folder.to_string(), emails.size); + // Big TODO: The engine needs to be able to synthesize ENVELOPE (and any of the fields // constituting it) and PREVIEW from HEADER and BODY if available. When it can do that // won't need to prefetch ENVELOPE or PREVIEW; prefetching HEADER and BODY will be enough. diff --git a/src/engine/imap-engine/imap-engine-generic-account.vala b/src/engine/imap-engine/imap-engine-generic-account.vala index 98625d7c..5069894e 100644 --- a/src/engine/imap-engine/imap-engine-generic-account.vala +++ b/src/engine/imap-engine/imap-engine-generic-account.vala @@ -33,6 +33,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount { search_upgrade_monitor = local.search_index_monitor; db_upgrade_monitor = local.upgrade_monitor; + opening_monitor = new Geary.SimpleProgressMonitor(Geary.ProgressType.ACTIVITY); if (outbox_path == null) { outbox_path = new SmtpOutboxFolderRoot(); @@ -245,6 +246,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount { private bool on_refresh_folders() { in_refresh_enumerate = true; + opening_monitor.notify_start(); enumerate_folders_async.begin(refresh_cancellable, on_refresh_completed); refresh_folder_timeout_id = 0; @@ -253,6 +255,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount { } private void on_refresh_completed(Object? source, AsyncResult result) { + opening_monitor.notify_finish(); try { enumerate_folders_async.end(result); } catch (Error err) { diff --git a/src/engine/imap-engine/imap-engine-generic-folder.vala b/src/engine/imap-engine/imap-engine-generic-folder.vala index 9e8128a4..9503954b 100644 --- a/src/engine/imap-engine/imap-engine-generic-folder.vala +++ b/src/engine/imap-engine/imap-engine-generic-folder.vala @@ -86,7 +86,10 @@ private class Geary.ImapEngine.GenericFolder : Geary.AbstractFolder, Geary.Folde } public void set_special_folder_type(SpecialFolderType new_type) { + SpecialFolderType old_type = _special_folder_type; _special_folder_type = new_type; + if(old_type != new_type) + notify_special_folder_type_changed(old_type, new_type); } public override Geary.Folder.OpenState get_open_state() { @@ -261,6 +264,10 @@ private class Geary.ImapEngine.GenericFolder : Geary.AbstractFolder, Geary.Folde Gee.ArrayList appended_ids = new Gee.ArrayList(); Gee.ArrayList removed_ids = new Gee.ArrayList(); for (;;) { + // this loop can be long, so manually check for cancellation + if (cancellable != null && cancellable.is_cancelled()) + throw new IOError.CANCELLED("Folder %s normalization cancelled", to_string()); + Geary.Email? remote_email = null; Geary.Imap.UID? remote_uid = null; if (old_remote != null && remote_ctr < remote_length) { @@ -791,11 +798,18 @@ private class Geary.ImapEngine.GenericFolder : Geary.AbstractFolder, Geary.Folde // marked for removal, which that helper function doesn't like local_position = remote_position - (remote_count - local_count); - debug("do_replay_remove_message: local_count=%d local_position=%d", local_count, local_position); - - Imap.UID? uid = yield local_folder.get_uid_at_async(local_position, null); - if (uid != null) - owned_id = new Imap.EmailIdentifier(uid, path); + // zero or negative means the message exists beyond the local vector's range, so + // nothing to do there + if (local_position > 0) { + debug("do_replay_remove_message: local_count=%d local_position=%d", local_count, local_position); + + Imap.UID? uid = yield local_folder.get_uid_at_async(local_position, null); + if (uid != null) + owned_id = new Imap.EmailIdentifier(uid, path); + } else { + debug("do_replay_remove_message: message not stored locally (local_count=%d local_position=%d)", + local_count, local_position); + } } catch (Error err) { debug("Unable to determine ID of removed message #%d from %s: %s", remote_position, to_string(), err.message); diff --git a/src/engine/imap-engine/replay-ops/imap-engine-fetch-email.vala b/src/engine/imap-engine/replay-ops/imap-engine-fetch-email.vala index c60b418a..ca505b94 100644 --- a/src/engine/imap-engine/replay-ops/imap-engine-fetch-email.vala +++ b/src/engine/imap-engine/replay-ops/imap-engine-fetch-email.vala @@ -51,8 +51,7 @@ private class Geary.ImapEngine.FetchEmail : Geary.ImapEngine.SendReplayOperation if (email != null && email.fields.fulfills(required_fields)) return ReplayOperation.Status.COMPLETED; - // If local only (or not connected) and not found fully in local store, throw NOT_FOUND; - // there is no fallback + // If local only and not found fully in local store, throw NOT_FOUND if (flags.is_all_set(Folder.ListFlags.LOCAL_ONLY)) { throw new EngineError.NOT_FOUND("Email %s with fields %Xh not found in %s", id.to_string(), required_fields, to_string()); diff --git a/src/engine/imap-engine/replay-ops/imap-engine-list-email-by-id.vala b/src/engine/imap-engine/replay-ops/imap-engine-list-email-by-id.vala index d862fd0a..bd9b82c9 100644 --- a/src/engine/imap-engine/replay-ops/imap-engine-list-email-by-id.vala +++ b/src/engine/imap-engine/replay-ops/imap-engine-list-email-by-id.vala @@ -7,7 +7,8 @@ private class Geary.ImapEngine.ListEmailByID : Geary.ImapEngine.AbstractListEmail { private Imap.EmailIdentifier? initial_id; private int count; - private int local_list_count = 0; + private int fulfilled_count = 0; + private bool initial_id_found = false; public ListEmailByID(GenericFolder owner, Geary.EmailIdentifier? initial_id, int count, Geary.Email.Field required_fields, Folder.ListFlags flags, Gee.List? accumulator, @@ -32,6 +33,9 @@ private class Geary.ImapEngine.ListEmailByID : Geary.ImapEngine.AbstractListEmai Gee.ArrayList fulfilled = new Gee.ArrayList(); if (list != null) { foreach (Geary.Email email in list) { + if (initial_id != null && email.id.equal_to(initial_id)) + initial_id_found = true; + if (email.fields.fulfills(required_fields)) fulfilled.add(email); else @@ -39,8 +43,16 @@ private class Geary.ImapEngine.ListEmailByID : Geary.ImapEngine.AbstractListEmai } } + // verify that the initial_id was found; if not, then want to get it from the remote + // (this will force a vector expansion, if required) + if (initial_id != null && !initial_id_found) { + unfulfilled.set(required_fields | ImapDB.Folder.REQUIRED_FOR_DUPLICATE_DETECTION, + initial_id); + } + // report fulfilled items - if (fulfilled.size > 0) { + fulfilled_count = fulfilled.size; + if (fulfilled_count > 0) { if (accumulator != null) accumulator.add_all(fulfilled); @@ -48,10 +60,33 @@ private class Geary.ImapEngine.ListEmailByID : Geary.ImapEngine.AbstractListEmai cb(fulfilled, null); } + // determine if everything was listed + bool finished = false; + if (flags.is_local_only()) { + // local-only operations stop here + finished = true; + } else if (count != int.MAX) { + // fetching 'count' fulfilled items and no unfulfilled items means listing is done + // this is true for both oldest-to-newest, newest-to-oldest, whether or not they have + // an initial_id + finished = (unfulfilled.size == 0 && fulfilled_count >= count); + } else { + // count == int.MAX + // This sentinel means "get everything from this point", so this has different meanings + // depending on direction + if (flags.is_newest_to_oldest()) { + // only finished if the folder is entirely normalized + Trillian is_fully_expanded = yield is_fully_expanded_async(); + finished = (is_fully_expanded == Trillian.TRUE); + } else { + // for oldest-to-newest, finished if no unfulfilled items + finished = (unfulfilled.size == 0); + } + } + // local-only operations stop here; also, since the local store is normalized from the top // of the vector on down, if enough items came back fulfilled, then done - local_list_count = (list != null) ? list.size : 0; - if (flags.is_local_only() || (unfulfilled.size == 0 && local_list_count >= count)) { + if (finished) { if (cb != null) cb(null, null); @@ -62,10 +97,36 @@ private class Geary.ImapEngine.ListEmailByID : Geary.ImapEngine.AbstractListEmai } public override async ReplayOperation.Status replay_remote_async() throws Error { - // To get this far, either the local store doesn't have all the contents of the items in - // the request range, or it doesn't have any row for items in the range (i.e. the vector - // is too short). - if (local_list_count + unfulfilled.size < count) { + bool expansion_required = false; + Trillian is_fully_expanded = yield is_fully_expanded_async(); + if (is_fully_expanded == Trillian.FALSE) { + if (flags.is_oldest_to_newest()) { + if (initial_id != null) { + // expand vector if not initial_id not discovered + expansion_required = !initial_id_found; + } else { + // initial_id == null, expansion required if not fully already + expansion_required = true; + } + } else { + // newest-to-oldest + if (count == int.MAX) { + // if infinite count, expansion required if not already + expansion_required = true; + } else if (initial_id != null) { + // finite count, expansion required if initial not found *or* not enough + // items were pulled in + expansion_required = !initial_id_found || (fulfilled_count + unfulfilled.size < count); + } else { + // initial_id == null + // finite count, expansion required if not enough found + expansion_required = (fulfilled_count + unfulfilled.size < count); + } + } + } + + // If the vector is too short, expand it now + if (expansion_required) { Gee.List? expanded = yield expand_vector_async(); if (expanded != null) { // take all the IDs from the expanded vector and call them unfulfilled; base class @@ -85,6 +146,23 @@ private class Geary.ImapEngine.ListEmailByID : Geary.ImapEngine.AbstractListEmai return yield base.replay_remote_async(); } + private async Trillian is_fully_expanded_async() throws Error { + int remote_count; + owner.get_remote_counts(out remote_count, null); + + // if unknown (unconnected), say so + if (remote_count < 0) + return Trillian.UNKNOWN; + + // include marked for removed in the count in case this is being called while a removal + // is in process, in which case don't want to expand vector this moment because the + // vector is in flux + int local_count_with_marked = yield owner.local_folder.get_email_count_async( + ImapDB.Folder.ListFlags.INCLUDE_MARKED_FOR_REMOVE, cancellable); + + return Trillian.from_boolean(local_count_with_marked >= remote_count); + } + private async Gee.List? expand_vector_async() throws Error { // watch out for situations where the entire folder is represented locally (i.e. no // expansion necessary) @@ -97,18 +175,6 @@ private class Geary.ImapEngine.ListEmailByID : Geary.ImapEngine.AbstractListEmai // vector is in flux int local_count = yield owner.local_folder.get_email_count_async( ImapDB.Folder.ListFlags.NONE, cancellable); - int local_count_with_marked = yield owner.local_folder.get_email_count_async( - ImapDB.Folder.ListFlags.INCLUDE_MARKED_FOR_REMOVE, cancellable); - - if (local_count_with_marked >= remote_count) { - // watch for sync discrepencies ... this is not something that can be fixed up here - if (local_count_with_marked > remote_count) { - message("%s: not expanding vector remote_count=%d local_count=%d", to_string(), - remote_count, local_count); - } - - return null; - } // determine low and high position for expansion ... default in most code paths for high // is the SequenceNumber just below the lowest known message, unless no local messages @@ -169,6 +235,13 @@ private class Geary.ImapEngine.ListEmailByID : Geary.ImapEngine.AbstractListEmai // low_pos must be defined by this point assert(low_pos != null); + if (high_pos != null && low_pos.value > high_pos.value) { + debug("%s: Aborting vector expansion, low_pos=%s > high_pos=%s", owner.to_string(), + low_pos.to_string(), high_pos.to_string()); + + return null; + } + Imap.MessageSet msg_set; if (high_pos != null) msg_set = new Imap.MessageSet.range_by_first_last(low_pos, high_pos); diff --git a/src/engine/imap/transport/imap-client-session-manager.vala b/src/engine/imap/transport/imap-client-session-manager.vala index 7e7addd2..94ceaaea 100644 --- a/src/engine/imap/transport/imap-client-session-manager.vala +++ b/src/engine/imap/transport/imap-client-session-manager.vala @@ -45,6 +45,7 @@ public class Geary.Imap.ClientSessionManager : BaseObject { private AccountInformation account_information; private Gee.HashSet sessions = new Gee.HashSet(); + private int pending_sessions = 0; private Nonblocking.Mutex sessions_mutex = new Nonblocking.Mutex(); private Gee.HashSet reserved_sessions = new Gee.HashSet(); private bool authentication_failed = false; @@ -133,16 +134,8 @@ public class Geary.Imap.ClientSessionManager : BaseObject { return; } - while (sessions.size < min_pool_size && !authentication_failed && is_open) { - try { - yield create_new_authorized_session(null); - } catch (Error err) { - debug("Unable to create authorized session to %s: %s", - account_information.get_imap_endpoint().to_string(), err.message); - - break; - } - } + while ((sessions.size + pending_sessions) < min_pool_size && !authentication_failed && is_open) + schedule_new_authorized_session(); try { sessions_mutex.release(ref token); @@ -151,6 +144,26 @@ public class Geary.Imap.ClientSessionManager : BaseObject { } } + private void schedule_new_authorized_session() { + pending_sessions++; + + create_new_authorized_session.begin(null, on_created_new_authorized_session); + } + + private void on_created_new_authorized_session(Object? source, AsyncResult result) { + pending_sessions--; + + try { + create_new_authorized_session.end(result); + } catch (Error err) { + debug("Unable to create authorized session to %s: %s", + account_information.get_imap_endpoint().to_string(), err.message); + + // try again + adjust_session_pool.begin(); + } + } + // This should only be called when sessions_mutex is locked. private async ClientSession create_new_authorized_session(Cancellable? cancellable) throws Error { if (authentication_failed) diff --git a/theming/message-viewer.css b/theming/message-viewer.css index d19b14d5..8307b2de 100644 --- a/theming/message-viewer.css +++ b/theming/message-viewer.css @@ -5,9 +5,9 @@ * recv-collapsed: #f5f5f5 * * Background colors associated with sent emails: - * sent-normal: #ffd - * sent-quoted: #eeb - * sent-collapsed: #f7f7c7 + * sent-normal: white + * sent-quoted: #e8e8e8 + * sent-collapsed: #f5f5f5 */ @media print { @@ -102,7 +102,7 @@ hr { } .email.sent { - background-color: #ffd;/* sent-normal */ + background-color: white;/* sent-normal */ } .email .starred { @@ -241,7 +241,7 @@ body:not(.nohide) .email.hide .header_container .avatar { } body:not(.nohide) .email.sent.hide, body:not(.nohide) .email.sent .email.hide { - background-color: #f7f7c7;/* sent-collapsed */ + background-color: #f5f5f5;/* sent-collapsed */ } body:not(.nohide) .email.hide .body, body:not(.nohide) .email.hide > .attachment_container, @@ -317,7 +317,7 @@ body:not(.nohide) .email.hide .header_container .avatar { cursor: hand; } .email.sent .compressed_note > span { - background-color: #f7f7c7;/* sent-collapsed */ + background-color: #f5f5f5;/* sent-collapsed */ } body.nohide .email .compressed_note > span { display: none !important; @@ -331,7 +331,7 @@ body.nohide .email .compressed_note > span { background-color: white;/* recv-normal */ } .email.sent .email { - background-color: #ffd;/* sent-normal */ + background-color: white;/* sent-normal */ } .email .email .email_container .menu, .email .email .email_container .starred, @@ -475,7 +475,7 @@ body.nohide .email .compressed_note > span { } .email.sent .quote_container { - background-color: #eeb;/* sent-quoted */ + background-color: #e8e8e8;/* sent-quoted */ } .quote_container > .shower, diff --git a/ui/account_cannot_remove.glade b/ui/account_cannot_remove.glade index 75c04c01..83a75614 100644 --- a/ui/account_cannot_remove.glade +++ b/ui/account_cannot_remove.glade @@ -3,9 +3,7 @@ - - gtk-ok - + @@ -17,7 +15,8 @@ True False 0 - gtk-dialog-error + 60 + dialog-error 6 @@ -72,7 +71,8 @@ end - _Remove + _OK + False ok_action True True diff --git a/ui/account_list.glade b/ui/account_list.glade index 9a6ac2d0..5023ca55 100644 --- a/ui/account_list.glade +++ b/ui/account_list.glade @@ -8,9 +8,7 @@ - - gtk-close - + @@ -122,11 +120,13 @@ end + _Close + False close True False False - True + True 0.54000002145767212 diff --git a/ui/composer.glade b/ui/composer.glade index 5dec6edb..31d9bd25 100644 --- a/ui/composer.glade +++ b/ui/composer.glade @@ -3,57 +3,43 @@ - - gtk-undo - + - - gtk-redo - + - - gtk-cut - + - - gtk-copy - + - - gtk-paste - + _Left - gtk-justify-left _Right - gtk-justify-right _Center - gtk-justify-center _Justify - gtk-justify-fill @@ -66,27 +52,26 @@ C_olor - gtk-select-color Menu - gtk-go-down + go-down Quote text - gtk-indent + format-indent-more Unquote text - gtk-unindent + format-indent-less @@ -110,25 +95,25 @@ - gtk-bold + format-text-bold - gtk-italic + format-text-italic - gtk-underline + format-text-underline - gtk-strikethrough + format-text-strikethrough @@ -697,11 +682,11 @@ - gtk-discard + _Discard True True True - True + True False diff --git a/ui/find_bar.glade b/ui/find_bar.glade index 2b3c947b..2c329a53 100644 --- a/ui/find_bar.glade +++ b/ui/find_bar.glade @@ -9,7 +9,7 @@ True False - gtk-close + window-close 1 diff --git a/ui/password-dialog.glade b/ui/password-dialog.glade index e5a12e16..8c8d5c82 100644 --- a/ui/password-dialog.glade +++ b/ui/password-dialog.glade @@ -24,7 +24,8 @@ True False 0 - gtk-dialog-authentication + 60 + security-high 6 diff --git a/ui/preferences.glade b/ui/preferences.glade index 71b0c9e6..c1efd992 100644 --- a/ui/preferences.glade +++ b/ui/preferences.glade @@ -18,15 +18,13 @@ end - gtk-close - False + _Close True True True True True - False - True + True False @@ -69,7 +67,6 @@ _Automatically select next message - False True True False @@ -77,7 +74,6 @@ 5 5 5 - False True 0 True @@ -92,7 +88,6 @@ _Display conversation preview - False True True False @@ -100,7 +95,6 @@ 5 5 5 - False True 0 True @@ -135,7 +129,6 @@ Enable _spell checking - False True True False @@ -143,7 +136,6 @@ 5 5 5 - False True 0 True @@ -178,7 +170,6 @@ _Play notification sounds - False True True False @@ -186,7 +177,6 @@ 5 5 5 - False True 0 True @@ -201,7 +191,6 @@ Show _notifications for new mail - False True True False @@ -209,7 +198,6 @@ 5 5 5 - False True 0 True @@ -221,6 +209,9 @@ 1 + + + False diff --git a/ui/remove_confirm.glade b/ui/remove_confirm.glade index 898f7b69..6d670a66 100644 --- a/ui/remove_confirm.glade +++ b/ui/remove_confirm.glade @@ -3,14 +3,10 @@ - - gtk-cancel - + - - gtk-remove - + @@ -22,7 +18,8 @@ True False 0 - gtk-dialog-warning + 60 + dialog-warning 6