Handle long-loading indication for conversations better
Moving the loading placeholder from ConversationListBox to ConversationEmail allows a more fine-grained indication of what is happening - only show the loading indicator when the remote actually needs to get hit, display the email's details and load the rest of the conversation while waiting for the remote body load. Also lets us pass errors loading the initial email locally all the way up to the controller.
This commit is contained in:
parent
bd960dcaf5
commit
2f35f58610
6 changed files with 141 additions and 110 deletions
|
|
@ -1274,6 +1274,8 @@ public class GearyController : Geary.BaseObject {
|
|||
get_window_action(
|
||||
ACTION_FIND_IN_CONVERSATION
|
||||
).set_enabled(true);
|
||||
} catch (GLib.IOError.CANCELLED err) {
|
||||
// All good
|
||||
} catch (Error err) {
|
||||
debug("Unable to load conversation: %s",
|
||||
err.message);
|
||||
|
|
|
|||
|
|
@ -274,10 +274,6 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface {
|
|||
/** Determines if all message have had loaded their bodies. */
|
||||
public bool message_bodies_loaded { get; private set; default = false; }
|
||||
|
||||
/** Determines if all message's web views have finished loading. */
|
||||
private Geary.Nonblocking.Spinlock message_bodies_loaded_lock =
|
||||
new Geary.Nonblocking.Spinlock();
|
||||
|
||||
// Cancellable to use when loading message content
|
||||
private GLib.Cancellable load_cancellable;
|
||||
|
||||
|
|
@ -286,6 +282,10 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface {
|
|||
|
||||
private Configuration config;
|
||||
|
||||
/** Determines if all message's web views have finished loading. */
|
||||
private Geary.Nonblocking.Spinlock message_bodies_loaded_lock =
|
||||
new Geary.Nonblocking.Spinlock();
|
||||
|
||||
// Message view with selected text, if any
|
||||
private ConversationMessage? body_selection_message = null;
|
||||
|
||||
|
|
@ -338,6 +338,7 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface {
|
|||
private Menu email_menu_delete;
|
||||
private bool shift_key_down;
|
||||
|
||||
|
||||
/** Fired when the user clicks "reply" in the message menu. */
|
||||
public signal void reply_to_message();
|
||||
|
||||
|
|
@ -542,70 +543,30 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface {
|
|||
|
||||
// Ensure we have required data to load the message
|
||||
|
||||
Geary.Email email = this.email;
|
||||
if (!this.email.fields.fulfills(REQUIRED_FOR_LOAD)) {
|
||||
email = yield email_store.fetch_email_async(
|
||||
email.id,
|
||||
Geary.Email.REQUIRED_FOR_MESSAGE,
|
||||
Geary.Folder.ListFlags.NONE,
|
||||
Geary.Email? email = null;
|
||||
if (this.email.fields.fulfills(REQUIRED_FOR_LOAD)) {
|
||||
email = this.email;
|
||||
} else {
|
||||
try {
|
||||
email = yield email_store.fetch_email_async(
|
||||
this.email.id,
|
||||
Geary.Email.REQUIRED_FOR_MESSAGE,
|
||||
LOCAL_ONLY, // Throw an error if not downloaded
|
||||
this.load_cancellable
|
||||
);
|
||||
} catch (Geary.EngineError.INCOMPLETE_MESSAGE err) {
|
||||
// Don't have the complete message at the moment, so
|
||||
// download it in the background.
|
||||
this.fetch_body_remote.begin(email_store, contact_store);
|
||||
}
|
||||
}
|
||||
|
||||
if (email != null) {
|
||||
yield update_body(email, contact_store);
|
||||
yield this.message_bodies_loaded_lock.wait_async(
|
||||
this.load_cancellable
|
||||
);
|
||||
}
|
||||
Geary.RFC822.Message message = email.get_message();
|
||||
|
||||
// Load all mime parts and construct CID resources from them
|
||||
|
||||
Gee.Map<string,Geary.Memory.Buffer> cid_resources =
|
||||
new Gee.HashMap<string,Geary.Memory.Buffer>();
|
||||
foreach (Geary.Attachment attachment in email.attachments) {
|
||||
// Assume all parts are attachments. As the primary and
|
||||
// secondary message bodies are loaded, any displayed
|
||||
// inline will be removed from the list.
|
||||
this.displayed_attachments.add(attachment);
|
||||
|
||||
if (attachment.content_id != null) {
|
||||
try {
|
||||
cid_resources[attachment.content_id] =
|
||||
new Geary.Memory.FileBuffer(attachment.file, true);
|
||||
} catch (Error err) {
|
||||
debug("Could not open attachment: %s", err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.attachments_button.set_visible(!this.displayed_attachments.is_empty);
|
||||
|
||||
// Load all messages
|
||||
|
||||
this.primary_message.web_view.add_internal_resources(cid_resources);
|
||||
yield this.primary_message.load_message_body(
|
||||
message, this.load_cancellable
|
||||
);
|
||||
|
||||
Gee.List<Geary.RFC822.Message> sub_messages = message.get_sub_messages();
|
||||
if (sub_messages.size > 0) {
|
||||
this.primary_message.body_container.add(this.sub_messages);
|
||||
}
|
||||
foreach (Geary.RFC822.Message sub_message in sub_messages) {
|
||||
ConversationMessage attached_message =
|
||||
new ConversationMessage.from_message(
|
||||
sub_message, false, this.config
|
||||
);
|
||||
connect_message_view_signals(attached_message);
|
||||
attached_message.web_view.add_internal_resources(cid_resources);
|
||||
this.sub_messages.add(attached_message);
|
||||
this._attached_messages.add(attached_message);
|
||||
attached_message.load_avatar.begin(
|
||||
contact_store, this.load_cancellable
|
||||
);
|
||||
yield attached_message.load_message_body(
|
||||
sub_message, this.load_cancellable
|
||||
);
|
||||
if (!this.is_collapsed) {
|
||||
attached_message.show_message_body(false);
|
||||
}
|
||||
}
|
||||
|
||||
yield this.message_bodies_loaded_lock.wait_async(this.load_cancellable);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -735,6 +696,92 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface {
|
|||
});
|
||||
}
|
||||
|
||||
private async void fetch_body_remote(Geary.App.EmailStore email_store,
|
||||
Application.AvatarStore contact_store)
|
||||
throws GLib.Error {
|
||||
// XXX Need proper progress reporting here, rather than just
|
||||
// doing a pulse
|
||||
this.primary_message.start_progress_pulse();
|
||||
Geary.Email? email = null;
|
||||
try {
|
||||
email = yield email_store.fetch_email_async(
|
||||
this.email.id,
|
||||
Geary.Email.REQUIRED_FOR_MESSAGE,
|
||||
FORCE_UPDATE,
|
||||
this.load_cancellable
|
||||
);
|
||||
} catch (GLib.Error err) {
|
||||
debug("Remote message download failed: %s", err.message);
|
||||
}
|
||||
|
||||
if (email != null) {
|
||||
this.primary_message.stop_progress_pulse();
|
||||
try {
|
||||
yield update_body(email, contact_store);
|
||||
} catch (GLib.Error err) {
|
||||
debug("Remote message update failed: %s", err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void update_body(Geary.Email email,
|
||||
Application.AvatarStore contact_store)
|
||||
throws GLib.Error {
|
||||
Geary.RFC822.Message message = email.get_message();
|
||||
|
||||
// Load all mime parts and construct CID resources from them
|
||||
|
||||
Gee.Map<string,Geary.Memory.Buffer> cid_resources =
|
||||
new Gee.HashMap<string,Geary.Memory.Buffer>();
|
||||
foreach (Geary.Attachment attachment in email.attachments) {
|
||||
// Assume all parts are attachments. As the primary and
|
||||
// secondary message bodies are loaded, any displayed
|
||||
// inline will be removed from the list.
|
||||
this.displayed_attachments.add(attachment);
|
||||
|
||||
if (attachment.content_id != null) {
|
||||
try {
|
||||
cid_resources[attachment.content_id] =
|
||||
new Geary.Memory.FileBuffer(attachment.file, true);
|
||||
} catch (Error err) {
|
||||
debug("Could not open attachment: %s", err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.attachments_button.set_visible(!this.displayed_attachments.is_empty);
|
||||
|
||||
// Load all messages
|
||||
|
||||
this.primary_message.web_view.add_internal_resources(cid_resources);
|
||||
yield this.primary_message.load_message_body(
|
||||
message, this.load_cancellable
|
||||
);
|
||||
|
||||
Gee.List<Geary.RFC822.Message> sub_messages = message.get_sub_messages();
|
||||
if (sub_messages.size > 0) {
|
||||
this.primary_message.body_container.add(this.sub_messages);
|
||||
}
|
||||
foreach (Geary.RFC822.Message sub_message in sub_messages) {
|
||||
ConversationMessage attached_message =
|
||||
new ConversationMessage.from_message(
|
||||
sub_message, false, this.config
|
||||
);
|
||||
connect_message_view_signals(attached_message);
|
||||
attached_message.web_view.add_internal_resources(cid_resources);
|
||||
this.sub_messages.add(attached_message);
|
||||
this._attached_messages.add(attached_message);
|
||||
attached_message.load_avatar.begin(
|
||||
contact_store, this.load_cancellable
|
||||
);
|
||||
yield attached_message.load_message_body(
|
||||
sub_message, this.load_cancellable
|
||||
);
|
||||
if (!this.is_collapsed) {
|
||||
attached_message.show_message_body(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void update_email_state() {
|
||||
Geary.EmailFlags? flags = this.email.email_flags;
|
||||
Gtk.StyleContext style = get_style_context();
|
||||
|
|
|
|||
|
|
@ -38,9 +38,6 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface {
|
|||
// account.
|
||||
private const int EMAIL_TOP_OFFSET = 32;
|
||||
|
||||
// Loading spinner timeout
|
||||
private const int LOADING_TIMEOUT_MSEC = 150;
|
||||
|
||||
|
||||
// Base class for list rows it the list box
|
||||
private abstract class ConversationRow : Gtk.ListBoxRow, Geary.BaseInterface {
|
||||
|
|
@ -312,8 +309,6 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface {
|
|||
// Total number of search matches found
|
||||
private uint search_matches_found = 0;
|
||||
|
||||
private Geary.TimeoutManager loading_timeout;
|
||||
|
||||
|
||||
/** Keyboard action to scroll the conversation. */
|
||||
[Signal (action=true)]
|
||||
|
|
@ -399,11 +394,6 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface {
|
|||
this.conversation.appended.connect(on_conversation_appended);
|
||||
this.conversation.trimmed.connect(on_conversation_trimmed);
|
||||
this.conversation.email_flags_changed.connect(on_update_flags);
|
||||
|
||||
// If the load is taking too long, display a spinner
|
||||
this.loading_timeout = new Geary.TimeoutManager.milliseconds(
|
||||
LOADING_TIMEOUT_MSEC, show_loading
|
||||
);
|
||||
}
|
||||
|
||||
~ConversationListBox() {
|
||||
|
|
@ -411,7 +401,6 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface {
|
|||
}
|
||||
|
||||
public override void destroy() {
|
||||
this.loading_timeout.reset();
|
||||
this.cancellable.cancel();
|
||||
this.email_rows.clear();
|
||||
base.destroy();
|
||||
|
|
@ -425,12 +414,6 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface {
|
|||
Geary.App.Conversation.Ordering.SENT_DATE_ASCENDING
|
||||
);
|
||||
|
||||
// Now have the full set of email and a UI update is
|
||||
// imminent. So cancel the spinner timeout if still running,
|
||||
// and remove the spinner it may have set in any case.
|
||||
this.loading_timeout.reset();
|
||||
set_placeholder(null);
|
||||
|
||||
// Work out what the first interesting email is, and load it
|
||||
// before all of the email before and after that so we can
|
||||
// load them in an optimal order.
|
||||
|
|
@ -477,7 +460,6 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface {
|
|||
|
||||
/** Cancels loading the current conversation, if still in progress */
|
||||
public void cancel_conversation_load() {
|
||||
this.loading_timeout.reset();
|
||||
this.cancellable.cancel();
|
||||
}
|
||||
|
||||
|
|
@ -830,15 +812,6 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface {
|
|||
}
|
||||
}
|
||||
|
||||
private void show_loading() {
|
||||
Gtk.Spinner spinner = new Gtk.Spinner();
|
||||
spinner.set_size_request(32, 32);
|
||||
spinner.halign = spinner.valign = Gtk.Align.CENTER;
|
||||
spinner.start();
|
||||
spinner.show();
|
||||
set_placeholder(spinner);
|
||||
}
|
||||
|
||||
private void scroll_to(ConversationRow row) {
|
||||
Gtk.Allocation? alloc = null;
|
||||
row.get_allocation(out alloc);
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface {
|
|||
|
||||
private const int MAX_PREVIEW_BYTES = Geary.Email.MAX_PREVIEW_BYTES;
|
||||
|
||||
private const int PULSE_TIMEOUT_MSEC = 250;
|
||||
|
||||
|
||||
// Widget used to display sender/recipient email addresses in
|
||||
// message header Gtk.FlowBox instances.
|
||||
|
|
@ -248,6 +250,9 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface {
|
|||
private Geary.TimeoutManager show_progress_timeout = null;
|
||||
private Geary.TimeoutManager hide_progress_timeout = null;
|
||||
|
||||
// Timer for pulsing progress bar
|
||||
private Geary.TimeoutManager progress_pulse;
|
||||
|
||||
|
||||
/** Fired when the user clicks a link in the email. */
|
||||
public signal void link_activated(string link);
|
||||
|
|
@ -460,6 +465,11 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface {
|
|||
this.hide_progress_timeout = new Geary.TimeoutManager.seconds(
|
||||
1, () => { this.body_progress.hide(); }
|
||||
);
|
||||
|
||||
this.progress_pulse = new Geary.TimeoutManager.milliseconds(
|
||||
PULSE_TIMEOUT_MSEC, this.body_progress.pulse
|
||||
);
|
||||
this.progress_pulse.repetition = FOREVER;
|
||||
}
|
||||
|
||||
~ConversationMessage() {
|
||||
|
|
@ -469,6 +479,7 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface {
|
|||
public override void destroy() {
|
||||
this.show_progress_timeout.reset();
|
||||
this.hide_progress_timeout.reset();
|
||||
this.progress_pulse.reset();
|
||||
this.resources.clear();
|
||||
this.searchable_addresses.clear();
|
||||
base.destroy();
|
||||
|
|
@ -492,6 +503,18 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface {
|
|||
body_revealer.set_reveal_child(false);
|
||||
}
|
||||
|
||||
/** Shows and starts pulsing the progress meter. */
|
||||
public void start_progress_pulse() {
|
||||
this.body_progress.show();
|
||||
this.progress_pulse.start();
|
||||
}
|
||||
|
||||
/** Hides and stops pulsing the progress meter. */
|
||||
public void stop_progress_pulse() {
|
||||
this.body_progress.hide();
|
||||
this.progress_pulse.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts loading the avatar for the message's sender.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -221,7 +221,8 @@ public class ConversationViewer : Gtk.Stack, Geary.BaseInterface {
|
|||
*/
|
||||
public async void load_conversation(Geary.App.Conversation conversation,
|
||||
Geary.App.EmailStore email_store,
|
||||
Application.AvatarStore avatar_store) {
|
||||
Application.AvatarStore avatar_store)
|
||||
throws GLib.Error {
|
||||
remove_current_list();
|
||||
|
||||
ConversationListBox new_list = new ConversationListBox(
|
||||
|
|
@ -267,23 +268,7 @@ public class ConversationViewer : Gtk.Stack, Geary.BaseInterface {
|
|||
}
|
||||
}
|
||||
|
||||
// Launch this as a background task so that additional
|
||||
// conversation selection events can get processed before
|
||||
// loading this one has completed.
|
||||
//
|
||||
// XXX we really should be yielding until the first
|
||||
// interesting email has been loaded, and the rest should be
|
||||
// loaded in he background.
|
||||
new_list.load_conversation.begin(
|
||||
query,
|
||||
(obj, res) => {
|
||||
try {
|
||||
new_list.load_conversation.end(res);
|
||||
} catch (GLib.Error err) {
|
||||
debug("Error loading conversation: %s", err.message);
|
||||
}
|
||||
}
|
||||
);
|
||||
yield new_list.load_conversation(query);
|
||||
}
|
||||
|
||||
// Add a new conversation list to the UI
|
||||
|
|
|
|||
|
|
@ -595,6 +595,7 @@
|
|||
</child>
|
||||
<child>
|
||||
<object class="GtkOverlay">
|
||||
<property name="height_request">6</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue