[geary/wip/774603-configurable-gravatar-uri: 1/2] Break out AvatarStore to a top-level class so it can be re-used



commit 3eb40c5e7dd3a454b98b81550a8abc242c1df8d2
Author: Michael Gratton <mike vee net>
Date:   Sun Oct 7 13:18:23 2018 +1100

    Break out AvatarStore to a top-level class so it can be re-used
    
    Make the controller manage the store's lifecycle, and pass that around
    to the conversation viewer and notifications to use, instead of
    a soup cache and/or doing HTTP calls themselves.

 po/POTFILES.in                                     |   1 +
 .../application/application-avatar-store.vala      | 130 +++++++++++++++++++++
 src/client/application/geary-controller.vala       |  42 +++----
 .../conversation-viewer/conversation-email.vala    |   2 +-
 .../conversation-viewer/conversation-list-box.vala | 105 +----------------
 .../conversation-viewer/conversation-message.vala  |   2 +-
 .../conversation-viewer/conversation-viewer.vala   |   4 +-
 src/client/meson.build                             |   1 +
 src/client/notification/libnotify.vala             |  38 +++---
 9 files changed, 169 insertions(+), 156 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index e04b93c6..e23b079a 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -20,6 +20,7 @@ src/client/accounts/add-edit-page.vala
 src/client/accounts/goa-service-information.vala
 src/client/accounts/local-service-information.vala
 src/client/accounts/login-dialog.vala
+src/client/application/application-avatar-store.vala
 src/client/application/autostart-manager.vala
 src/client/application/geary-application.vala
 src/client/application/geary-args.vala
diff --git a/src/client/application/application-avatar-store.vala 
b/src/client/application/application-avatar-store.vala
new file mode 100644
index 00000000..a5a2495b
--- /dev/null
+++ b/src/client/application/application-avatar-store.vala
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2016-2018 Michael Gratton <mike vee net>
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+
+/**
+ * Email address avatar loader and cache.
+ */
+public class Application.AvatarStore : Geary.BaseObject {
+
+
+    // Initiates and manages an avatar load using Gravatar
+    private class AvatarLoader : Geary.BaseObject {
+
+        internal Gdk.Pixbuf? avatar = null;
+        internal Geary.Nonblocking.Semaphore lock =
+            new Geary.Nonblocking.Semaphore();
+
+        private string base_url;
+        private Geary.RFC822.MailboxAddress address;
+        private int pixel_size;
+
+
+        internal AvatarLoader(Geary.RFC822.MailboxAddress address,
+                              string base_url,
+                              int pixel_size) {
+            this.address = address;
+            this.base_url = base_url;
+            this.pixel_size = pixel_size;
+        }
+
+        internal async void load(Soup.Session session,
+                                 Cancellable load_cancelled)
+            throws GLib.Error {
+            Error? workaround_err = null;
+            if (!Geary.String.is_empty_or_whitespace(this.base_url)) {
+                string md5 = GLib.Checksum.compute_for_string(
+                    GLib.ChecksumType.MD5, this.address.address.strip().down()
+                );
+                Soup.Message message = new Soup.Message(
+                    "GET",
+                    "%s/%s?d=%s&s=%d".printf(
+                        this.base_url, md5, "404", this.pixel_size
+                    )
+                );
+
+                try {
+                    // We want to just pass load_cancelled to send_async
+                    // here, but per Bug 778720 this is causing some
+                    // crashy race in libsoup's cache implementation, so
+                    // for now just let the load go through and manually
+                    // check to see if the load has been cancelled before
+                    // setting the avatar
+                    InputStream data = yield session.send_async(
+                        message,
+                        null // should be 'load_cancelled'
+                    );
+                    if (message.status_code == 200 &&
+                        data != null &&
+                        !load_cancelled.is_cancelled()) {
+                        this.avatar = yield new Gdk.Pixbuf.from_stream_at_scale_async(
+                            data, pixel_size, pixel_size, true, load_cancelled
+                        );
+                    }
+                } catch (Error err) {
+                    workaround_err = err;
+                }
+            }
+
+            this.lock.blind_notify();
+
+            if (workaround_err != null) {
+                throw workaround_err;
+            }
+        }
+
+    }
+
+
+    private Configuration config;
+
+    private Soup.Session session;
+    private Soup.Cache cache;
+    private Gee.Map<string,AvatarLoader> loaders =
+        new Gee.HashMap<string,AvatarLoader>();
+
+
+    public AvatarStore(Configuration config, GLib.File cache_root) {
+        this.config = config;
+
+        File avatar_cache_dir = cache_root.get_child("avatars");
+        this.cache = new Soup.Cache(
+            avatar_cache_dir.get_path(),
+            Soup.CacheType.SINGLE_USER
+        );
+        this.cache.load();
+        this.cache.set_max_size(16 * 1024 * 1024); // 16MB
+        this.session = new Soup.Session();
+        this.session.add_feature(this.cache);
+    }
+
+    public void close() {
+        this.cache.flush();
+        this.cache.dump();
+    }
+
+    public async Gdk.Pixbuf? load(Geary.RFC822.MailboxAddress address,
+                                    int pixel_size,
+                                    Cancellable load_cancelled)
+        throws Error {
+        string key = address.to_string();
+        AvatarLoader loader = this.loaders.get(key);
+        if (loader == null) {
+            // Haven't started loading the avatar, so do it now
+            loader = new AvatarLoader(
+                address, this.config.avatar_url, pixel_size
+            );
+            this.loaders.set(key, loader);
+            yield loader.load(this.session, load_cancelled);
+        } else {
+            // Load has already started, so wait for it to finish
+            yield loader.lock.wait_async();
+        }
+        return loader.avatar;
+    }
+
+}
diff --git a/src/client/application/geary-controller.vala b/src/client/application/geary-controller.vala
index 91462e91..2f496491 100644
--- a/src/client/application/geary-controller.vala
+++ b/src/client/application/geary-controller.vala
@@ -86,8 +86,9 @@ public class GearyController : Geary.BaseObject {
     
     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;
+    public Application.AvatarStore? avatar_store {
+        get; private set; default = null;
+    }
 
     private Geary.Account? current_account = null;
     private Gee.Map<Geary.AccountInformation,AccountContext> accounts =
@@ -219,21 +220,10 @@ public class GearyController : Geary.BaseObject {
             error("Error loading web resources: %s", err.message);
         }
 
-        // 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 = this.application.get_user_cache_directory()
-            .get_child("avatars");
-        this.avatar_cache = new Soup.Cache(
-            avatar_cache_dir.get_path(),
-            Soup.CacheType.SINGLE_USER
-        );
-        this.avatar_cache.load();
-        this.avatar_cache.set_max_size(10 * 1024 * 1024); // 4MB
-        this.avatar_session = new Soup.Session.with_options(
-            Soup.SESSION_USER_AGENT, "Geary/" + GearyApplication.VERSION
+        this.avatar_store = new Application.AvatarStore(
+            this.application.config,
+            this.application.get_user_cache_directory()
         );
-        this.avatar_session.add_feature(avatar_cache);
 
         // Create the main window (must be done after creating actions.)
         main_window = new MainWindow(this.application);
@@ -271,12 +261,13 @@ public class GearyController : Geary.BaseObject {
         new_messages_indicator.application_activated.connect(on_indicator_activated_application);
         new_messages_indicator.composer_activated.connect(on_indicator_activated_composer);
         new_messages_indicator.inbox_activated.connect(on_indicator_activated_inbox);
-        
+
         unity_launcher = new UnityLauncher(new_messages_monitor);
-        
-        // libnotify
-        libnotify = new Libnotify(new_messages_monitor);
-        libnotify.invoked.connect(on_libnotify_invoked);
+
+        this.libnotify = new Libnotify(
+            this.new_messages_monitor, this.avatar_store
+        );
+        this.libnotify.invoked.connect(on_libnotify_invoked);
 
         this.main_window.conversation_list_view.grab_focus();
 
@@ -402,7 +393,7 @@ public class GearyController : Geary.BaseObject {
         foreach (AccountContext context in accounts) {
             Geary.Folder? inbox = context.inbox;
             if (inbox != null) {
-                debug("Closing %s...", inbox.to_string());
+                debug("Closing inbox: %s...", inbox.to_string());
                 inbox.close_async.begin(null, (obj, ret) => {
                         try {
                             inbox.close_async.end(ret);
@@ -481,8 +472,9 @@ public class GearyController : Geary.BaseObject {
 
         this.autostart_manager = null;
 
-        this.avatar_cache.flush();
-        this.avatar_cache.dump();
+        this.avatar_store.close();
+        this.avatar_store = null;
+
 
         debug("Closed GearyController");
     }
@@ -1536,7 +1528,7 @@ public class GearyController : Geary.BaseObject {
                     Geary.Collection.get_first(selected),
                     this.current_folder,
                     this.application.config,
-                    this.avatar_session,
+                    this.avatar_store,
                     (obj, ret) => {
                         try {
                             viewer.load_conversation.end(ret);
diff --git a/src/client/conversation-viewer/conversation-email.vala 
b/src/client/conversation-viewer/conversation-email.vala
index fa940841..d19af9e4 100644
--- a/src/client/conversation-viewer/conversation-email.vala
+++ b/src/client/conversation-viewer/conversation-email.vala
@@ -526,7 +526,7 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface {
      * primary message and any attached messages, as well as
      * attachment names, types and icons.
      */
-    public async void start_loading(ConversationListBox.AvatarStore avatars,
+    public async void start_loading(Application.AvatarStore avatars,
                                     Cancellable load_cancelled) {
         foreach (ConversationMessage view in this)  {
             if (load_cancelled.is_cancelled()) {
diff --git a/src/client/conversation-viewer/conversation-list-box.vala 
b/src/client/conversation-viewer/conversation-list-box.vala
index 928952f5..6ee9059f 100644
--- a/src/client/conversation-viewer/conversation-list-box.vala
+++ b/src/client/conversation-viewer/conversation-list-box.vala
@@ -226,105 +226,6 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface {
     }
 
 
-    /**
-     * Email address avatar loader and cache.
-     */
-    public class AvatarStore {
-
-
-        private Soup.Session session;
-        private Gee.Map<string,AvatarLoader> loaders =
-            new Gee.HashMap<string,AvatarLoader>();
-
-
-        internal AvatarStore(Soup.Session session) {
-            this.session = session;
-        }
-
-        internal async Gdk.Pixbuf? load(Geary.RFC822.MailboxAddress address,
-                                        int pixel_size,
-                                        Cancellable load_cancelled)
-            throws Error {
-            string key = address.to_string();
-            AvatarLoader loader = this.loaders.get(key);
-            if (loader == null) {
-                // Haven't started loading the avatar, so do it now
-                loader = new AvatarLoader(address, pixel_size);
-                this.loaders.set(key, loader);
-                yield loader.load(this.session, load_cancelled);
-            } else {
-                // Load has already started, so wait for it to finish
-                yield loader.lock.wait_async();
-            }
-            return loader.avatar;
-        }
-
-    }
-
-
-    // Initiates and manages an avatar load
-    private class AvatarLoader : Geary.BaseObject {
-
-
-        internal Gdk.Pixbuf? avatar = null;
-        internal Geary.Nonblocking.Semaphore lock =
-            new Geary.Nonblocking.Semaphore();
-
-        private Geary.RFC822.MailboxAddress address;
-        private int pixel_size;
-
-
-        internal AvatarLoader(Geary.RFC822.MailboxAddress address,
-                              int pixel_size) {
-            this.address = address;
-            this.pixel_size = pixel_size;
-        }
-
-        internal async void load(Soup.Session session,
-                                 Cancellable load_cancelled)
-            throws Error {
-            Soup.Message message = new Soup.Message(
-                "GET",
-                Gravatar.get_image_uri(
-                    this.address,
-                    Gravatar.Default.NOT_FOUND,
-                    this.pixel_size
-                )
-            );
-
-            Error? workaround_err = null;
-            try {
-                // We want to just pass load_cancelled to send_async
-                // here, but per Bug 778720 this is causing some
-                // crashy race in libsoup's cache implementation, so
-                // for now just let the load go through and manually
-                // check to see if the load has been cancelled before
-                // setting the avatar
-                InputStream data = yield session.send_async(
-                    message,
-                    null // should be 'load_cancelled'
-                );
-                if (message.status_code == 200 &&
-                    data != null &&
-                    !load_cancelled.is_cancelled()) {
-                    this.avatar = yield new Gdk.Pixbuf.from_stream_at_scale_async(
-                        data, pixel_size, pixel_size, true, load_cancelled
-                    );
-                }
-            } catch (Error err) {
-                workaround_err = err;
-            }
-
-            this.lock.blind_notify();
-
-            if (workaround_err != null) {
-                throw workaround_err;
-            }
-        }
-
-    }
-
-
     static construct {
         // Set up custom keybindings
         unowned Gtk.BindingSet bindings = Gtk.BindingSet.by_class(
@@ -396,7 +297,7 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface {
     private Geary.ContactStore contact_store;
 
     // Avatars for this conversation
-    private AvatarStore avatar_store;
+    private Application.AvatarStore avatar_store;
 
     // Account this conversation belongs to
     private Geary.AccountInformation account_info;
@@ -497,14 +398,14 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface {
                                Geary.AccountInformation account_info,
                                bool is_draft_folder,
                                Configuration config,
-                               Soup.Session avatar_session,
+                               Application.AvatarStore avatar_store,
                                Gtk.Adjustment adjustment) {
         base_ref();
         this.conversation = conversation;
         this.location = location;
         this.email_store = email_store;
         this.contact_store = contact_store;
-        this.avatar_store = new AvatarStore(avatar_session);
+        this.avatar_store = avatar_store;
         this.account_info = account_info;
         this.is_draft_folder = is_draft_folder;
         this.config = config;
diff --git a/src/client/conversation-viewer/conversation-message.vala 
b/src/client/conversation-viewer/conversation-message.vala
index dfbce2ce..60faa8ac 100644
--- a/src/client/conversation-viewer/conversation-message.vala
+++ b/src/client/conversation-viewer/conversation-message.vala
@@ -436,7 +436,7 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface {
     /**
      * Starts loading the avatar for the message's sender.
      */
-    public async void load_avatar(ConversationListBox.AvatarStore loader,
+    public async void load_avatar(Application.AvatarStore loader,
                                   Cancellable load_cancelled) {
         const int PIXEL_SIZE = 32;
         Geary.RFC822.MailboxAddress? primary = message.get_primary_originator();
diff --git a/src/client/conversation-viewer/conversation-viewer.vala 
b/src/client/conversation-viewer/conversation-viewer.vala
index 8e92a513..f53b719a 100644
--- a/src/client/conversation-viewer/conversation-viewer.vala
+++ b/src/client/conversation-viewer/conversation-viewer.vala
@@ -199,7 +199,7 @@ public class ConversationViewer : Gtk.Stack, Geary.BaseInterface {
     public async void load_conversation(Geary.App.Conversation conversation,
                                         Geary.Folder location,
                                         Configuration config,
-                                        Soup.Session avatar_session)
+                                        Application.AvatarStore avatars)
         throws Error {
         remove_current_list();
 
@@ -212,7 +212,7 @@ public class ConversationViewer : Gtk.Stack, Geary.BaseInterface {
             account.information,
             location.special_folder_type == Geary.SpecialFolderType.DRAFTS,
             config,
-            avatar_session,
+            avatars,
             this.conversation_scroller.get_vadjustment()
         );
 
diff --git a/src/client/meson.build b/src/client/meson.build
index 7fba7c62..ba97b8e3 100644
--- a/src/client/meson.build
+++ b/src/client/meson.build
@@ -1,5 +1,6 @@
 # Geary client
 geary_client_vala_sources = files(
+  'application/application-avatar-store.vala',
   'application/autostart-manager.vala',
   'application/geary-application.vala',
   'application/geary-args.vala',
diff --git a/src/client/notification/libnotify.vala b/src/client/notification/libnotify.vala
index 4207bc8c..059e1978 100644
--- a/src/client/notification/libnotify.vala
+++ b/src/client/notification/libnotify.vala
@@ -8,10 +8,13 @@
 public class Libnotify : Geary.BaseObject {
     public const Geary.Email.Field REQUIRED_FIELDS =
         Geary.Email.Field.ORIGINATORS | Geary.Email.Field.SUBJECT;
-    
+
+    private const int AVATAR_SIZE = 32;
+
     private static Canberra.Context? sound_context = null;
-    
+
     private weak NewMessagesMonitor monitor;
+    private weak Application.AvatarStore avatars;
     private Notify.Notification? current_notification = null;
     private Notify.Notification? error_notification = null;
     private Geary.Folder? folder = null;
@@ -19,10 +22,12 @@ public class Libnotify : Geary.BaseObject {
     private List<string>? caps = null;
 
     public signal void invoked(Geary.Folder? folder, Geary.Email? email);
-    
-    public Libnotify(NewMessagesMonitor monitor) {
+
+    public Libnotify(NewMessagesMonitor monitor,
+                     Application.AvatarStore avatars) {
         this.monitor = monitor;
-        
+        this.avatars = avatars;
+
         monitor.add_required_fields(REQUIRED_FIELDS);
         
         if (!Notify.is_initted()) {
@@ -106,26 +111,9 @@ public class Libnotify : Geary.BaseObject {
                 EmailUtil.strip_subject_prefixes(email), count - 1, folder.account.information.display_name);
         }
 
-        // get the avatar
-        Gdk.Pixbuf? avatar = null;
-        InputStream? ins = null;
-        File file = File.new_for_uri(Gravatar.get_image_uri(primary, Gravatar.Default.MYSTERY_MAN));
-        try {
-            ins = yield file.read_async(GLib.Priority.DEFAULT, cancellable);
-            avatar = yield new Gdk.Pixbuf.from_stream_async(ins, cancellable);
-        } catch (Error err) {
-            debug("Failed to get avatar for notification: %s", err.message);
-        }
-        
-        if (ins != null) {
-            try {
-                yield ins.close_async(Priority.DEFAULT, cancellable);
-            } catch (Error close_err) {
-                // ignored
-            }
-            
-            ins = null;
-        }
+        Gdk.Pixbuf? avatar = yield this.avatars.load(
+            primary, AVATAR_SIZE, cancellable
+        );
 
         issue_current_notification(primary.to_short_display(), body, avatar);
     }


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]