diff --git a/src/client/application/geary-application.vala b/src/client/application/geary-application.vala index 6d7e0b69..e5978994 100644 --- a/src/client/application/geary-application.vala +++ b/src/client/application/geary-application.vala @@ -229,7 +229,11 @@ public class GearyApplication : Gtk.Application { public File get_user_data_directory() { return File.new_for_path(Environment.get_user_data_dir()).get_child("geary"); } - + + public File get_user_cache_directory() { + return File.new_for_path(Environment.get_user_cache_dir()).get_child("geary"); + } + public File get_user_config_directory() { return File.new_for_path(Environment.get_user_config_dir()).get_child("geary"); } diff --git a/src/client/application/geary-controller.vala b/src/client/application/geary-controller.vala index 81660daa..7f251a42 100644 --- a/src/client/application/geary-controller.vala +++ b/src/client/application/geary-controller.vala @@ -91,7 +91,10 @@ public class GearyController : Geary.BaseObject { public AutostartManager? autostart_manager { get; private set; default = null; } public LoginDialog? login_dialog { get; private set; default = null; } - + + public Soup.Session? avatar_session { get; private set; default = null; } + private Soup.Cache? avatar_cache = null; + private Geary.Account? current_account = null; private Gee.HashMap email_stores = new Gee.HashMap(); @@ -183,7 +186,22 @@ public class GearyController : Geary.BaseObject { // Create DB upgrade dialog. upgrade_dialog = new UpgradeDialog(); upgrade_dialog.notify[UpgradeDialog.PROP_VISIBLE_NAME].connect(display_main_window_if_ready); - + + // Use a global avatar session because a cache must be used + // per-session, and we don't want to have to load the cache + // for each conversation load. + File avatar_cache_dir = GearyApplication.instance.get_user_cache_directory() + .get_child("avatar_cache"); + avatar_cache = new Soup.Cache( + avatar_cache_dir.get_path(), + Soup.CacheType.SINGLE_USER + ); + avatar_cache.set_max_size(4 * 1024 * 1024); // 4MB + avatar_session = new Soup.Session.with_options( + Soup.SESSION_USER_AGENT, "Geary/" + GearyApplication.VERSION + ); + avatar_session.add_feature(avatar_cache); + // Create the main window (must be done after creating actions.) main_window = new MainWindow(GearyApplication.instance); main_window.on_shift_key.connect(on_shift_key); @@ -352,7 +370,11 @@ public class GearyController : Geary.BaseObject { } main_window.destroy(); - + + debug("Flushing avatar cache..."); + avatar_cache.flush(); + avatar_cache.dump(); + // Turn off the lights and lock the door behind you try { debug("Closing Engine..."); diff --git a/src/client/conversation-viewer/conversation-email.vala b/src/client/conversation-viewer/conversation-email.vala index e93111b0..af40dc41 100644 --- a/src/client/conversation-viewer/conversation-email.vala +++ b/src/client/conversation-viewer/conversation-email.vala @@ -145,8 +145,16 @@ public class ConversationEmail : Gtk.Box { } public async void start_loading(Cancellable load_cancelled) { + yield primary_message.load_avatar( + GearyApplication.instance.controller.avatar_session, + load_cancelled + ); yield primary_message.load_message_body(load_cancelled); foreach (ConversationMessage message in conversation_messages) { + yield message.load_avatar( + GearyApplication.instance.controller.avatar_session, + load_cancelled + ); yield message.load_message_body(load_cancelled); } yield load_attachments(load_cancelled); diff --git a/src/client/conversation-viewer/conversation-message.vala b/src/client/conversation-viewer/conversation-message.vala index 9ab6bcef..b17ba875 100644 --- a/src/client/conversation-viewer/conversation-message.vala +++ b/src/client/conversation-viewer/conversation-message.vala @@ -66,12 +66,11 @@ public class ConversationMessage : Gtk.Box { [GtkChild] public Gtk.Box infobar_box; // not yet supported: { get; private set; } - [GtkChild] - private Gtk.Image avatar_image; - [GtkChild] private Gtk.Revealer preview_revealer; [GtkChild] + private Gtk.Image preview_avatar; + [GtkChild] private Gtk.Label from_preview; [GtkChild] private Gtk.Label body_preview; @@ -79,6 +78,8 @@ public class ConversationMessage : Gtk.Box { [GtkChild] private Gtk.Revealer header_revealer; [GtkChild] + private Gtk.Image header_avatar; + [GtkChild] private Gtk.Box from_header; [GtkChild] private Gtk.Box to_header; @@ -183,8 +184,6 @@ public class ConversationMessage : Gtk.Box { } public void show_message_body(bool include_transitions=true) { - avatar_image.set_pixel_size(32); // XXX constant - Gtk.RevealerTransitionType revealer = preview_revealer.get_transition_type(); if (!include_transitions) { preview_revealer.set_transition_type(Gtk.RevealerTransitionType.NONE); @@ -208,12 +207,34 @@ public class ConversationMessage : Gtk.Box { } public void hide_message_body() { - avatar_image.set_pixel_size(24); // XXX constant preview_revealer.set_reveal_child(true); header_revealer.set_reveal_child(false); body_revealer.set_reveal_child(false); } + public async void load_avatar(Soup.Session session, Cancellable load_cancellable) { + // Queued messages are cancelled in ConversationViewer.clear() + // rather than here using a callback on load_cancellable since + // we don't have per-message control using + // Soup.Session.queue_message. + Geary.RFC822.MailboxAddress? primary = message.get_primary_originator(); + if (primary != null) { + int window_scale = get_window().get_scale_factor(); + int pixel_size = header_avatar.get_pixel_size(); + Soup.Message message = new Soup.Message( + "GET", + Gravatar.get_image_uri( + primary, Gravatar.Default.NOT_FOUND, pixel_size * window_scale + ) + ); + session.queue_message(message, (session, message) => { + if (message.status_code == 200) { + set_avatar(message.response_body.data); + } + }); + } + } + public async void load_message_body(Cancellable load_cancelled) { bool remote_images = false; bool load_images = false; @@ -295,6 +316,41 @@ public class ConversationMessage : Gtk.Box { header.set_visible(true); } + private void set_avatar(uint8[] image_data) { + Gdk.Pixbuf avatar = null; + Gdk.PixbufLoader loader = new Gdk.PixbufLoader(); + try { + loader.write(image_data); + loader.close(); + avatar = loader.get_pixbuf(); + } catch (Error err) { + debug("Error loading Gravatar response: %s", err.message); + } + + if (avatar != null) { + Gdk.Window window = get_window(); + int window_scale = window.get_scale_factor(); + int preview_size = preview_avatar.pixel_size * window_scale; + preview_avatar.set_from_surface( + Gdk.cairo_surface_create_from_pixbuf( + avatar.scale_simple( + preview_size, preview_size, Gdk.InterpType.BILINEAR + ), + window_scale, + window) + ); + int header_size = header_avatar.pixel_size * window_scale; + if (avatar.width != header_size) { + avatar = avatar.scale_simple( + header_size, header_size, Gdk.InterpType.BILINEAR + ); + } + header_avatar.set_from_surface( + Gdk.cairo_surface_create_from_pixbuf(avatar, window_scale, window) + ); + } + } + // This delegate is called from within Geary.RFC822.Message.get_body while assembling the plain // or HTML document when a non-text MIME part is encountered within a multipart/mixed container. // If this returns null, the MIME part is dropped from the final returned document; otherwise, diff --git a/src/client/conversation-viewer/conversation-viewer.vala b/src/client/conversation-viewer/conversation-viewer.vala index b7d36aec..95d787e8 100644 --- a/src/client/conversation-viewer/conversation-viewer.vala +++ b/src/client/conversation-viewer/conversation-viewer.vala @@ -348,9 +348,14 @@ public class ConversationViewer : Gtk.Stack { debug("Showing child: %s\n", widget.get_name()); base.set_visible_child(widget); } - + // Removes all displayed e-mails from the view. private void clear() { + // Cancel any pending avatar loads here, rather than in + // ConversationMessage using a Cancellable callback since we + // don't have per-message control of it when using + // Soup.Session.queue_message. + GearyApplication.instance.controller.avatar_session.flush_queue(); foreach (Gtk.Widget child in conversation_listbox.get_children()) { conversation_listbox.remove(child); } diff --git a/src/client/util/util-gravatar.vala b/src/client/util/util-gravatar.vala index c221ced8..a5f099ee 100644 --- a/src/client/util/util-gravatar.vala +++ b/src/client/util/util-gravatar.vala @@ -56,7 +56,7 @@ public string get_image_uri(Geary.RFC822.MailboxAddress addr, Default def, int s // Gravatar spec for preparing address and hashing: // http://en.gravatar.com/site/implement/hash/ string md5 = Checksum.compute_for_string(ChecksumType.MD5, addr.address.strip().down()); - + return "https://secure.gravatar.com/avatar/%s?d=%s&s=%d".printf(md5, def.to_param(), size); } diff --git a/ui/conversation-message.ui b/ui/conversation-message.ui index e1596e31..1028d0cc 100644 --- a/ui/conversation-message.ui +++ b/ui/conversation-message.ui @@ -11,23 +11,7 @@ True False - 8 - - - 18 - 18 - True - False - start - 24 - avatar-default-symbolic - - - False - False - 0 - - + 6 True @@ -43,37 +27,65 @@ True False - vertical + 6 - + + 18 + 18 True False - start - From <email> - end - + start + 32 + avatar-default-symbolic - True - True + False + False 0 - + True False - start - Preview body text. - end - + vertical + + + True + False + start + From <email> + end + + + + True + True + 0 + + + + + True + False + start + Preview body text. + end + + + + True + True + 1 + + - True + False True 1 @@ -93,69 +105,70 @@ False none - + True False - vertical + 6 - + + 18 + 18 True False - - - True - False - From: - 1 - - - - False - True - 0 - - - - - True - False - start - From <email> - True - end - - - - True - True - 1 - - + start + 48 + avatar-default-symbolic False - True + False 0 - + + True False + vertical - + True False - To: - 1 - + + + True + False + From: + 1 + + + + False + True + 0 + + + + + True + False + start + From <email> + True + end + + + + True + True + 1 + + False @@ -164,23 +177,230 @@ - - True + False - start - To <email> - True - end + + + True + False + To: + 1 + + + + False + True + 0 + + + + + True + False + start + To <email> + True + end + + + True + True + 1 + + + - True + False True 1 - + + + False + + + True + False + Cc: + 1 + + + + False + True + 0 + + + + + True + False + start + CC <email> + True + end + + + True + True + 1 + + + + + + False + True + 2 + + + + + False + + + True + False + Bcc: + 1 + + + + False + True + 0 + + + + + True + False + start + BCC <email> + True + end + + + True + True + 1 + + + + + + False + True + 3 + + + + + False + + + True + False + Subject: + 1 + + + + False + True + 0 + + + + + True + False + start + Subject + True + end + + + True + True + 1 + + + + + + False + True + 4 + + + + + False + + + True + False + Date: + 1 + + + + False + True + 0 + + + + + True + False + start + 1/1/1970 + True + end + + + True + True + 1 + + + + + + False + True + 5 + + False @@ -188,186 +408,6 @@ 1 - - - False - - - True - False - Cc: - 1 - - - - False - True - 0 - - - - - True - False - start - CC <email> - True - end - - - True - True - 1 - - - - - - False - True - 2 - - - - - False - - - True - False - Bcc: - 1 - - - - False - True - 0 - - - - - True - False - start - BCC <email> - True - end - - - True - True - 1 - - - - - - False - True - 3 - - - - - False - - - True - False - Subject: - 1 - - - - False - True - 0 - - - - - True - False - start - Subject - True - end - - - True - True - 1 - - - - - - False - True - 4 - - - - - False - - - True - False - Date: - 1 - - - - False - True - 0 - - - - - True - False - start - 1/1/1970 - True - end - - - True - True - 1 - - - - - - False - True - 5 - - @@ -381,7 +421,7 @@ True True - 1 + 0