Fix delay showing composer for accounts with large numbers of messages.
* src/client/application/geary-controller.vala (GearyController::create_compose_widget_async): Load draft manager and email entry completion model asynchronously after the UI has been made visible. * src/client/composer/composer-widget.vala (ComposerWidget): Rename set_entry_completions to load_entry_completions, make loading async. Make open_draft_manager_async and load_entry_completions public so they can be invoked by the controller. * src/client/composer/contact-list-store.vala (ContactListStore): Load contacts asynchronously in smaller batches, so the UI remains responsive.
This commit is contained in:
parent
ad1fc5bafc
commit
6b58da6b99
3 changed files with 123 additions and 95 deletions
|
|
@ -2258,6 +2258,19 @@ public class GearyController : Geary.BaseObject {
|
|||
new ComposerWindow(widget);
|
||||
widget.state = ComposerWidget.ComposerState.DETACHED;
|
||||
}
|
||||
|
||||
if (is_draft) {
|
||||
try {
|
||||
yield widget.open_draft_manager_async(referred.id);
|
||||
} catch (Error e) {
|
||||
message("Could not open draft manager: %s", e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// For accounts with large numbers of contacts, loading the
|
||||
// entry completions can some time, so do it after the UI has
|
||||
// been shown
|
||||
yield widget.load_entry_completions();
|
||||
}
|
||||
|
||||
private bool should_create_new_composer(ComposerWidget.ComposeType? compose_type,
|
||||
|
|
|
|||
|
|
@ -429,9 +429,6 @@ public class ComposerWidget : Gtk.EventBox {
|
|||
this.plain_menu = (Menu) builder.get_object("plain_menu_model");
|
||||
this.context_menu_model = (Menu) builder.get_object("context_menu_model");
|
||||
|
||||
// TODO: It would be nicer to set the completions inside the EmailEntry constructor. But in
|
||||
// testing, this can cause non-deterministic segfaults. Investigate why, and fix if possible.
|
||||
set_entry_completions();
|
||||
this.subject_entry.bind_property("text", this, "window-title", BindingFlags.SYNC_CREATE,
|
||||
(binding, source_value, ref target_value) => {
|
||||
target_value = Geary.String.is_empty_or_whitespace(this.subject_entry.text)
|
||||
|
|
@ -457,9 +454,8 @@ public class ComposerWidget : Gtk.EventBox {
|
|||
|
||||
this.from = new Geary.RFC822.MailboxAddresses.single(account.information.primary_mailbox);
|
||||
|
||||
Geary.EmailIdentifier? editing_draft_id = null;
|
||||
if (referred != null)
|
||||
editing_draft_id = fill_in_from_referred(referred, quote, is_referred_draft);
|
||||
fill_in_from_referred(referred, quote);
|
||||
|
||||
update_from_field();
|
||||
|
||||
|
|
@ -500,8 +496,6 @@ public class ComposerWidget : Gtk.EventBox {
|
|||
this.editor.navigation_policy_decision_requested.connect(on_navigation_policy_decision_requested);
|
||||
this.editor.new_window_policy_decision_requested.connect(on_navigation_policy_decision_requested);
|
||||
|
||||
open_draft_manager_async.begin(editing_draft_id);
|
||||
|
||||
GearyApplication.instance.config.settings.changed[Configuration.SPELL_CHECK_KEY].connect(
|
||||
on_spell_check_changed);
|
||||
|
||||
|
|
@ -599,10 +593,37 @@ public class ComposerWidget : Gtk.EventBox {
|
|||
update_actions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads and sets contact auto-complete data for the current account.
|
||||
*/
|
||||
public async void load_entry_completions() {
|
||||
// XXX Since ContactListStore hooks into ContactStore to
|
||||
// listen for contacts being added and removed,
|
||||
// GearyController or some composer-related controller should
|
||||
// construct an instance per account and keep it around for
|
||||
// the lifetime of the app, since there can be tens of
|
||||
// thousands of contacts for large accounts.
|
||||
Geary.ContactStore contacts = this.account.get_contact_store();
|
||||
if (this.contact_list_store == null ||
|
||||
this.contact_list_store.contact_store != contacts) {
|
||||
ContactListStore store = new ContactListStore(contacts);
|
||||
this.contact_list_store = store;
|
||||
yield store.load();
|
||||
|
||||
this.to_entry.completion = new ContactEntryCompletion(store);
|
||||
this.cc_entry.completion = new ContactEntryCompletion(store);
|
||||
this.bcc_entry.completion = new ContactEntryCompletion(store);
|
||||
this.reply_to_entry.completion = new ContactEntryCompletion(store);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the composer's widget state from its draft.
|
||||
*/
|
||||
public async void restore_draft_state_async(Geary.Account account) {
|
||||
bool first_email = true;
|
||||
|
||||
foreach (Geary.RFC822.MessageID mid in in_reply_to) {
|
||||
foreach (Geary.RFC822.MessageID mid in this.in_reply_to) {
|
||||
Gee.MultiMap<Geary.Email, Geary.FolderPath?>? email_map;
|
||||
try {
|
||||
email_map =
|
||||
|
|
@ -664,12 +685,46 @@ public class ComposerWidget : Gtk.EventBox {
|
|||
}
|
||||
}
|
||||
|
||||
// Copies the addresses (e.g. From/To/CC) and content from referred into this one
|
||||
private Geary.EmailIdentifier? fill_in_from_referred(Geary.Email referred, string? quote, bool is_referred_draft) {
|
||||
if (referred == null)
|
||||
return null;
|
||||
/**
|
||||
* Creates and opens the composer's draft manager.
|
||||
*/
|
||||
public async void open_draft_manager_async(
|
||||
Geary.EmailIdentifier? editing_draft_id = null,
|
||||
Cancellable? cancellable = null)
|
||||
throws Error {
|
||||
this.draft_save_text = "";
|
||||
yield close_draft_manager_async(cancellable);
|
||||
|
||||
Geary.EmailIdentifier? editing_draft_id = null;
|
||||
SimpleAction close_and_save = get_action(ACTION_CLOSE_AND_SAVE);
|
||||
if (!this.account.information.save_drafts) {
|
||||
this.header.save_and_close_button.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
this.draft_manager = new Geary.App.DraftManager(account);
|
||||
try {
|
||||
yield this.draft_manager.open_async(editing_draft_id, cancellable);
|
||||
} catch (Error err) {
|
||||
debug("Unable to open draft manager %s: %s",
|
||||
this.draft_manager.to_string(), err.message);
|
||||
|
||||
this.draft_manager = null;
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
close_and_save.set_enabled(true);
|
||||
this.header.save_and_close_button.show();
|
||||
|
||||
this.draft_manager.notify[Geary.App.DraftManager.PROP_DRAFT_STATE]
|
||||
.connect(on_draft_state_changed);
|
||||
this.draft_manager.notify[Geary.App.DraftManager.PROP_CURRENT_DRAFT_ID]
|
||||
.connect(on_draft_id_changed);
|
||||
this.draft_manager.fatal.connect(on_draft_manager_fatal);
|
||||
}
|
||||
|
||||
// Copies the addresses (e.g. From/To/CC) and content from referred into this one
|
||||
private void fill_in_from_referred(Geary.Email referred, string? quote) {
|
||||
if (this.compose_type != ComposeType.NEW_MESSAGE) {
|
||||
add_recipients_and_ids(this.compose_type, referred);
|
||||
this.reply_subject = Geary.RFC822.Utils.create_subject_for_reply(referred);
|
||||
|
|
@ -703,9 +758,6 @@ public class ComposerWidget : Gtk.EventBox {
|
|||
debug("Error getting message body: %s", error.message);
|
||||
}
|
||||
|
||||
if (is_referred_draft)
|
||||
editing_draft_id = referred.id;
|
||||
|
||||
add_attachments(referred.attachments);
|
||||
break;
|
||||
|
||||
|
|
@ -730,7 +782,6 @@ public class ComposerWidget : Gtk.EventBox {
|
|||
this.pending_attachments = referred.attachments;
|
||||
break;
|
||||
}
|
||||
return editing_draft_id;
|
||||
}
|
||||
|
||||
public void set_focus() {
|
||||
|
|
@ -1332,42 +1383,6 @@ public class ComposerWidget : Gtk.EventBox {
|
|||
this.draft_save_text = DRAFT_ERROR_TEXT;
|
||||
}
|
||||
|
||||
// Returns the drafts folder for the current From account.
|
||||
private async void open_draft_manager_async(
|
||||
Geary.EmailIdentifier? editing_draft_id = null,
|
||||
Cancellable? cancellable = null)
|
||||
throws Error {
|
||||
this.draft_save_text = "";
|
||||
yield close_draft_manager_async(cancellable);
|
||||
|
||||
SimpleAction close_and_save = get_action(ACTION_CLOSE_AND_SAVE);
|
||||
if (!this.account.information.save_drafts) {
|
||||
this.header.save_and_close_button.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
this.draft_manager = new Geary.App.DraftManager(account);
|
||||
try {
|
||||
yield this.draft_manager.open_async(editing_draft_id, cancellable);
|
||||
} catch (Error err) {
|
||||
debug("Unable to open draft manager %s: %s",
|
||||
this.draft_manager.to_string(), err.message);
|
||||
|
||||
this.draft_manager = null;
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
close_and_save.set_enabled(true);
|
||||
this.header.save_and_close_button.show();
|
||||
|
||||
this.draft_manager.notify[Geary.App.DraftManager.PROP_DRAFT_STATE]
|
||||
.connect(on_draft_state_changed);
|
||||
this.draft_manager.notify[Geary.App.DraftManager.PROP_CURRENT_DRAFT_ID]
|
||||
.connect(on_draft_id_changed);
|
||||
this.draft_manager.fatal.connect(on_draft_manager_fatal);
|
||||
}
|
||||
|
||||
private async void close_draft_manager_async(Cancellable? cancellable) throws Error {
|
||||
get_action(ACTION_CLOSE_AND_SAVE).set_enabled(false);
|
||||
if (this.draft_manager == null)
|
||||
|
|
@ -2335,23 +2350,10 @@ public class ComposerWidget : Gtk.EventBox {
|
|||
return false;
|
||||
|
||||
this.account = new_account;
|
||||
set_entry_completions();
|
||||
|
||||
load_entry_completions.begin();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void set_entry_completions() {
|
||||
if (this.contact_list_store != null
|
||||
&& this.contact_list_store.contact_store == account.get_contact_store())
|
||||
return;
|
||||
|
||||
this.contact_list_store = new ContactListStore(account.get_contact_store());
|
||||
|
||||
this.to_entry.completion = new ContactEntryCompletion(this.contact_list_store);
|
||||
this.cc_entry.completion = new ContactEntryCompletion(this.contact_list_store);
|
||||
this.bcc_entry.completion = new ContactEntryCompletion(this.contact_list_store);
|
||||
this.reply_to_entry.completion = new ContactEntryCompletion(this.contact_list_store);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,9 +5,13 @@
|
|||
*/
|
||||
|
||||
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;
|
||||
|
||||
|
||||
// Batch size for loading contacts asynchronously
|
||||
private uint LOAD_BATCH_SIZE = 4096;
|
||||
|
||||
public enum Column {
|
||||
CONTACT_OBJECT,
|
||||
CONTACT_MARKUP_NAME,
|
||||
|
|
@ -26,25 +30,35 @@ public class ContactListStore : Gtk.ListStore {
|
|||
|
||||
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);
|
||||
this.contact_store.contact_added.disconnect(on_contact_added);
|
||||
this.contact_store.contact_updated.disconnect(on_contact_updated);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Loads contacts from the model's contact store.
|
||||
*/
|
||||
public async void load() {
|
||||
uint count = 0;
|
||||
foreach (Geary.Contact contact in this.contact_store.contacts) {
|
||||
add_contact(contact);
|
||||
count++;
|
||||
if (count % LOAD_BATCH_SIZE == 0) {
|
||||
Idle.add(load.callback);
|
||||
yield;
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
public Geary.Contact get_contact(Gtk.TreeIter iter) {
|
||||
GLib.Value contact_value;
|
||||
get_value(iter, Column.CONTACT_OBJECT, out contact_value);
|
||||
|
|
@ -73,20 +87,19 @@ public class ContactListStore : Gtk.ListStore {
|
|||
set(iter, Column.CONTACT_MARKUP_NAME, highlighted_result, -1);
|
||||
}
|
||||
}
|
||||
|
||||
private void add_contact(Geary.Contact contact) {
|
||||
if (contact.highest_importance < CONTACT_VISIBILITY_THRESHOLD)
|
||||
return;
|
||||
|
||||
string full_address = contact.get_rfc822_address().to_rfc822_string();
|
||||
Gtk.TreeIter iter;
|
||||
append(out iter);
|
||||
set(iter,
|
||||
Column.CONTACT_OBJECT, contact,
|
||||
Column.CONTACT_MARKUP_NAME, Markup.escape_text(full_address),
|
||||
Column.PRIOR_KEYS, new Gee.HashSet<string>());
|
||||
|
||||
private inline void add_contact(Geary.Contact contact) {
|
||||
if (contact.highest_importance >= CONTACT_VISIBILITY_THRESHOLD) {
|
||||
string full_address = contact.get_rfc822_address().to_rfc822_string();
|
||||
Gtk.TreeIter iter;
|
||||
append(out iter);
|
||||
set(iter,
|
||||
Column.CONTACT_OBJECT, contact,
|
||||
Column.CONTACT_MARKUP_NAME, Markup.escape_text(full_address),
|
||||
Column.PRIOR_KEYS, new Gee.HashSet<string>());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void update_contact(Geary.Contact updated_contact) {
|
||||
Gtk.TreeIter iter;
|
||||
if (!get_iter_first(out iter))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue