From aabfb1d6de286d1346ce19a3165a47ec504ade5f Mon Sep 17 00:00:00 2001 From: Michael James Gratton Date: Fri, 22 Apr 2016 14:55:40 +1000 Subject: [PATCH] Reenable displaying sender avatars using Gravatar. Since we're no longer using the web view to display the user avatar, use libsoup and add some additional infrastructure to support caching the avatars. Also switches to HTTPS for accessing the Gravatar service. * src/client/application/geary-application.vala (GearyApplication::get_user_cache_directory): New method to return the XDG cache directory for Geary. * src/client/application/geary-controller.vala: Add both a Soup session and cache for fetching avatars. Write the cache to disk on controller close. * src/client/conversation-viewer/conversation-email.vala (ConversationEmail::start_loading): Trigger avatar loads when loading the email. * src/client/conversation-viewer/conversation-message.vala: Replace single avatar image widget with two, so the image does not need to be rescaled when expanded/collapsed. * src/client/conversation-viewer/conversation-message.vala (ConversationMessage::load_avatar): Queue a request for a Gravatar avatar. (ConversationMessage::set_avatar): Load pixbuf returned by Gravatar, scale and set it for the preview and expanded avatar images. * src/client/conversation-viewer/conversation-viewer.vala (ConversationViewer::clear): Cancel any outsanding avatar loads. * src/client/util/util-gravatar.vala (Gravatar): Construct a HTTPS URL to avoid advertising to the NSA who we are receiving email from. * ui/conversation-message.ui: Add the second avatar image. --- src/client/application/geary-application.vala | 6 +- src/client/application/geary-controller.vala | 28 +- .../conversation-email.vala | 8 + .../conversation-message.vala | 68 +- .../conversation-viewer.vala | 7 +- src/client/util/util-gravatar.vala | 2 +- ui/conversation-message.ui | 586 ++++++++++-------- 7 files changed, 420 insertions(+), 285 deletions(-) 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