From 6a052031df579556ada034d0a9bb00797d368d63 Mon Sep 17 00:00:00 2001 From: Julian Sparber Date: Thu, 14 Jan 2021 17:33:58 +0100 Subject: [PATCH] avatar: Use HdyAvatar for displaying avatars --- .../application/application-avatar-store.vala | 166 ------------------ .../application-contact-store.vala | 5 +- .../application/application-contact.vala | 29 ++- .../application/application-controller.vala | 6 +- .../conversation-contact-popover.vala | 39 ++-- .../conversation-message.vala | 34 ++-- src/client/meson.build | 1 - .../desktop-notifications.vala | 42 ++--- .../client/composer/composer-widget-test.vala | 3 +- ui/conversation-contact-popover.ui | 6 +- ui/conversation-message.ui | 8 +- 11 files changed, 66 insertions(+), 273 deletions(-) delete mode 100644 src/client/application/application-avatar-store.vala diff --git a/src/client/application/application-avatar-store.vala b/src/client/application/application-avatar-store.vala deleted file mode 100644 index 0add17d6..00000000 --- a/src/client/application/application-avatar-store.vala +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright 2016-2019 Michael Gratton - * - * 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. - * - * Avatars are loaded from a {@link Contact} object's Folks individual - * if present, else one will be generated using initials and - * background colour based on the of the source mailbox's name if - * present, or address. Avatars are cached at each requested logical - * pixel size, per Folks individual and then per source mailbox - * name. This strategy allows avatar bitmaps to reflect the desktop - * address-book's user picture if present, else provide individualised - * avatars, even for mail sent by software like Mailman and Discourse. - * - * Unlike {@link ContactStore}, once store instance is useful for - * loading and caching avatars across accounts. - */ -internal class Application.AvatarStore : Geary.BaseObject { - - - // Max size is low since most conversations don't get above the - // low hundreds of messages, and those that do will likely get - // many repeated participants - private const uint MAX_CACHE_SIZE = 128; - - - private class CacheEntry { - - - public static string to_name_key(Geary.RFC822.MailboxAddress source) { - // Use short name as the key, since it will use the name - // first, then the email address, which is especially - // important for things like GitLab email where the - // address is always the same, but the name changes. This - // ensures that each such user gets different initials. - return source.to_short_display().normalize().casefold(); - } - - public Contact contact; - public Geary.RFC822.MailboxAddress source; - - private Gee.List pixbufs = new Gee.LinkedList(); - - - public CacheEntry(Contact contact, - Geary.RFC822.MailboxAddress source) { - this.contact = contact; - this.source = source; - - contact.changed.connect(on_contact_changed); - } - - ~CacheEntry() { - this.contact.changed.disconnect(on_contact_changed); - } - - public async Gdk.Pixbuf? load(int pixel_size, - GLib.Cancellable cancellable) - throws GLib.Error { - Gdk.Pixbuf? pixbuf = null; - foreach (Gdk.Pixbuf cached in this.pixbufs) { - if ((cached.height == pixel_size && cached.width >= pixel_size) || - (cached.width == pixel_size && cached.height >= pixel_size)) { - pixbuf = cached; - break; - } - } - - if (pixbuf == null) { - Folks.Individual? individual = contact.individual; - if (individual != null && - individual.avatar != null) { - GLib.InputStream data = - yield individual.avatar.load_async( - pixel_size, cancellable - ); - pixbuf = yield new Gdk.Pixbuf.from_stream_at_scale_async( - data, pixel_size, pixel_size, true, cancellable - ); - pixbuf = Util.Avatar.round_image(pixbuf); - this.pixbufs.add(pixbuf); - } - } - - if (pixbuf == null) { - string? name = null; - if (this.contact.is_trusted) { - name = this.contact.display_name; - } else { - // Use short display because it will clean up the - // string, use the name if present and fall back - // on the address if not. - name = this.source.to_short_display(); - } - pixbuf = Util.Avatar.generate_user_picture(name, pixel_size); - pixbuf = Util.Avatar.round_image(pixbuf); - this.pixbufs.add(pixbuf); - } - - return pixbuf; - } - - private void on_contact_changed() { - this.pixbufs.clear(); - } - - } - - - // Folks cache used for storing contacts backed by a Folk - // individual. This is the primary cache since we want to use the - // details for avatar and display name lookup from the desktop - // address-book if available. - private Util.Cache.Lru folks_cache = - new Util.Cache.Lru(MAX_CACHE_SIZE); - - // Name cache uses the source mailbox's short name as the key, - // since this will make avatar initials match well. It is used to - // cache avatars for contacts not saved in the desktop - // address-book. - private Util.Cache.Lru name_cache = - new Util.Cache.Lru(MAX_CACHE_SIZE); - - - /** Closes the store, flushing all caches. */ - public void close() { - this.folks_cache.clear(); - this.name_cache.clear(); - } - - public async Gdk.Pixbuf? load(Contact contact, - Geary.RFC822.MailboxAddress source, - int pixel_size, - GLib.Cancellable cancellable) - throws GLib.Error { - CacheEntry hit = null; - if (contact.is_desktop_contact && contact.is_trusted) { - string key = contact.individual.id; - hit = this.folks_cache.get_entry(key); - - if (hit == null) { - hit = new CacheEntry(contact, source); - this.folks_cache.set_entry(key, hit); - } - } - - if (hit == null) { - string key = CacheEntry.to_name_key(source); - hit = this.name_cache.get_entry(key); - - if (hit == null) { - hit = new CacheEntry(contact, source); - this.name_cache.set_entry(key, hit); - } - } - - return yield hit.load(pixel_size, cancellable); - } - -} diff --git a/src/client/application/application-contact-store.vala b/src/client/application/application-contact-store.vala index 981483fd..bf991649 100644 --- a/src/client/application/application-contact-store.vala +++ b/src/client/application/application-contact-store.vala @@ -44,7 +44,6 @@ public class Application.ContactStore : Geary.BaseObject { public Geary.Account account { get; private set; } internal Folks.IndividualAggregator individuals; - internal AvatarStore avatars; // Cache for storing Folks individuals by email address. Store // nulls so that negative lookups are cached as well. @@ -62,14 +61,12 @@ public class Application.ContactStore : Geary.BaseObject { /** Constructs a new contact store for an account. */ internal ContactStore(Geary.Account account, - Folks.IndividualAggregator individuals, - AvatarStore avatars) { + Folks.IndividualAggregator individuals) { this.account = account; this.individuals = individuals; this.individuals.individuals_changed_detailed.connect( on_individuals_changed ); - this.avatars = avatars; } ~ContactStore() { diff --git a/src/client/application/application-contact.vala b/src/client/application/application-contact.vala index 7f9a7cc5..71397cf0 100644 --- a/src/client/application/application-contact.vala +++ b/src/client/application/application-contact.vala @@ -19,6 +19,14 @@ public class Application.Contact : Geary.BaseObject { /** The human-readable name of the contact. */ public string display_name { get; private set; } + /** The avatar of the contact. */ + public GLib.LoadableIcon? avatar { get { + if (this.individual != null) + return this.individual.avatar; + else + return null; + }} + /** Determines if {@link display_name} the same as its email address. */ public bool display_name_is_email { get; private set; default = false; } @@ -265,21 +273,6 @@ public class Application.Contact : Geary.BaseObject { ); } - /** Returns the avatar for this contact. */ - public async Gdk.Pixbuf? load_avatar(Geary.RFC822.MailboxAddress source, - int pixel_size, - GLib.Cancellable cancellable) - throws GLib.Error { - Gdk.Pixbuf? avatar = null; - ContactStore? store = this.store; - if (store != null) { - avatar = yield store.avatars.load( - this, source, pixel_size, cancellable - ); - } - return avatar; - } - /** Sets remote resource loading for this contact. */ public async void set_remote_resource_loading(bool enabled, GLib.Cancellable? cancellable) @@ -332,8 +325,13 @@ public class Application.Contact : Geary.BaseObject { Geary.RFC822.MailboxAddress.is_valid_address(name); } + private void on_individual_avatar_notify() { + notify_property("avatar"); + } + private void update_from_individual(Folks.Individual? replacement) { if (this.individual != null) { + this.individual.notify["avatar"].disconnect(this.on_individual_avatar_notify); this.individual.notify.disconnect(this.on_individual_notify); this.individual.removed.disconnect(this.on_individual_removed); } @@ -341,6 +339,7 @@ public class Application.Contact : Geary.BaseObject { this.individual = replacement; if (this.individual != null) { + this.individual.notify["avatar"].connect(this.on_individual_avatar_notify); this.individual.notify.connect(this.on_individual_notify); this.individual.removed.connect(this.on_individual_removed); } diff --git a/src/client/application/application-controller.vala b/src/client/application/application-controller.vala index 4bb2f631..e6c5dfe4 100644 --- a/src/client/application/application-controller.vala +++ b/src/client/application/application-controller.vala @@ -73,9 +73,6 @@ internal class Application.Controller : get; private set; } - // Avatar store for the application. - private Application.AvatarStore avatars = new Application.AvatarStore(); - // Primary collection of the application's open accounts private Gee.Map accounts = new Gee.HashMap(); @@ -317,7 +314,6 @@ internal class Application.Controller : } catch (GLib.Error err) { warning("Error closing plugin manager: %s", err.message); } - this.avatars.close(); this.pending_mailtos.clear(); this.composer_widgets.clear(); @@ -983,7 +979,7 @@ internal class Application.Controller : account, new Geary.App.SearchFolder(account, account.local_folder_root), new Geary.App.EmailStore(account), - new Application.ContactStore(account, this.folks, this.avatars) + new Application.ContactStore(account, this.folks) ); this.accounts.set(account.information, context); diff --git a/src/client/conversation-viewer/conversation-contact-popover.vala b/src/client/conversation-viewer/conversation-contact-popover.vala index 97d27bb1..1ec220f1 100644 --- a/src/client/conversation-viewer/conversation-contact-popover.vala +++ b/src/client/conversation-viewer/conversation-contact-popover.vala @@ -43,7 +43,8 @@ public class Conversation.ContactPopover : Gtk.Popover { [GtkChild] private unowned Gtk.Grid contact_pane; - [GtkChild] private unowned Gtk.Image avatar; + [GtkChild] + private Hdy.Avatar avatar; [GtkChild] private unowned Gtk.Label contact_name; @@ -82,6 +83,16 @@ public class Conversation.ContactPopover : Gtk.Popover { this.load_remote_button.role = CHECK; + this.contact.bind_property("display-name", + this.avatar, + "text", + BindingFlags.SYNC_CREATE); + + this.contact.bind_property("avatar", + this.avatar, + "loadable-icon", + BindingFlags.SYNC_CREATE); + this.actions.add_action_entries(ACTION_ENTRIES, this); insert_action_group(ACTION_GROUP, this.actions); @@ -92,32 +103,6 @@ public class Conversation.ContactPopover : Gtk.Popover { /** * Starts loading the avatar for the message's sender. */ - public async void load_avatar() { - var main = this.get_toplevel() as Application.MainWindow; - if (main != null) { - int window_scale = get_scale_factor(); - int pixel_size = ( - Application.Client.AVATAR_SIZE_PIXELS * window_scale - ); - try { - Gdk.Pixbuf? avatar_buf = yield contact.load_avatar( - this.mailbox, - pixel_size, - this.load_cancellable - ); - if (avatar_buf != null) { - this.avatar.set_from_surface( - Gdk.cairo_surface_create_from_pixbuf( - avatar_buf, window_scale, get_window() - ) - ); - } - } catch (GLib.Error err) { - debug("Conversation load failed: %s", err.message); - } - } - } - public override void destroy() { this.contact.changed.disconnect(this.on_contact_changed); this.load_cancellable.cancel(); diff --git a/src/client/conversation-viewer/conversation-message.vala b/src/client/conversation-viewer/conversation-message.vala index b987dbdd..becf3bf7 100644 --- a/src/client/conversation-viewer/conversation-message.vala +++ b/src/client/conversation-viewer/conversation-message.vala @@ -335,7 +335,7 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { private GLib.DateTime? local_date = null; - [GtkChild] private unowned Gtk.Image avatar; + [GtkChild] private unowned Hdy.Avatar avatar; [GtkChild] private unowned Gtk.Revealer compact_revealer; [GtkChild] private unowned Gtk.Label compact_from; @@ -822,31 +822,18 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { this.primary_originator, cancellable ); - int window_scale = get_scale_factor(); - int pixel_size = - Application.Client.AVATAR_SIZE_PIXELS * window_scale; - Gdk.Pixbuf? avatar_buf = yield this.primary_contact.load_avatar( - this.primary_originator, - pixel_size, - cancellable - ); - if (avatar_buf != null) { - this.avatar.set_from_surface( - Gdk.cairo_surface_create_from_pixbuf( - avatar_buf, window_scale, get_window() - ) - ); + if (this.primary_contact != null) { + this.primary_contact.bind_property("display-name", + this.avatar, + "text", + BindingFlags.SYNC_CREATE); + this.primary_contact.bind_property("avatar", + this.avatar, + "loadable-icon", + BindingFlags.SYNC_CREATE); } - } else { - this.avatar.set_from_icon_name( - "avatar-default-symbolic", Gtk.IconSize.DIALOG - ); - this.avatar.set_pixel_size( - Application.Client.AVATAR_SIZE_PIXELS - ); } - // Preview headers this.compact_from.set_text( yield format_originator_compact(cancellable) @@ -1268,7 +1255,6 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { address_child.contact, address ); - popover.load_avatar.begin(); popover.set_position(Gtk.PositionType.BOTTOM); popover.load_remote_resources_changed.connect((enabled) => { if (this.primary_contact.equal_to(address_child.contact) && diff --git a/src/client/meson.build b/src/client/meson.build index f20fe5c8..11cf375f 100644 --- a/src/client/meson.build +++ b/src/client/meson.build @@ -14,7 +14,6 @@ client_vala_sources = files( 'application/application-account-context.vala', 'application/application-account-interface.vala', 'application/application-attachment-manager.vala', - 'application/application-avatar-store.vala', 'application/application-certificate-manager.vala', 'application/application-client.vala', 'application/application-command.vala', diff --git a/src/client/plugin/desktop-notifications/desktop-notifications.vala b/src/client/plugin/desktop-notifications/desktop-notifications.vala index 68dfa618..255e4ec7 100644 --- a/src/client/plugin/desktop-notifications/desktop-notifications.vala +++ b/src/client/plugin/desktop-notifications/desktop-notifications.vala @@ -97,7 +97,7 @@ public class Plugin.DesktopNotifications : Email email ) throws GLib.Error { string title = to_notitication_title(folder.account, total); - Gdk.Pixbuf? icon = null; + GLib.Icon icon = null; Geary.RFC822.MailboxAddress? originator = email.get_primary_originator(); if (originator != null) { ContactStore contacts = @@ -112,19 +112,7 @@ public class Plugin.DesktopNotifications : : originator.to_short_display() ); - int window_scale = 1; - Gdk.Display? display = Gdk.Display.get_default(); - if (display != null) { - Gdk.Monitor? monitor = display.get_primary_monitor(); - if (monitor != null) { - window_scale = monitor.scale_factor; - } - } - icon = yield contact.load_avatar( - originator, - global::Application.Client.AVATAR_SIZE_PIXELS * window_scale, - this.cancellable - ); + icon = contact.avatar; } string body = Util.Email.strip_subject_prefixes(email); @@ -144,10 +132,24 @@ public class Plugin.DesktopNotifications : ); } + int window_scale = 1; + Gdk.Display? display = Gdk.Display.get_default(); + if (display != null) { + Gdk.Monitor? monitor = display.get_primary_monitor(); + if (monitor != null) { + window_scale = monitor.scale_factor; + } + } + + var avatar = new Hdy.Avatar(32, title, true); + avatar.loadable_icon = icon as GLib.LoadableIcon; + icon = yield avatar.draw_to_pixbuf_async(32, window_scale, null); + issue_arrived_notification(title, body, icon, folder, email.identifier); } private void notify_general(Folder folder, int total, int added) { + GLib.Icon icon = new GLib.ThemedIcon("%s-symbolic".printf(global::Application.Client.APP_ID)); string title = to_notitication_title(folder.account, total); string body = ngettext( /// Notification body when multiple messages have been @@ -170,12 +172,12 @@ public class Plugin.DesktopNotifications : ).printf(body, total); } - issue_arrived_notification(title, body, null, folder, null); + issue_arrived_notification(title, body, icon, folder, null); } private void issue_arrived_notification(string summary, string body, - Gdk.Pixbuf? icon, + GLib.Icon icon, Folder folder, EmailIdentifier? id) { // only one outstanding notification at a time @@ -204,15 +206,9 @@ public class Plugin.DesktopNotifications : private GLib.Notification issue_notification(string id, string summary, string body, - Gdk.Pixbuf? avatar, + GLib.Icon icon, string? action, GLib.Variant? action_target) { - GLib.Icon icon = avatar; - if (avatar == null) { - icon = new GLib.ThemedIcon( - "%s-symbolic".printf(global::Application.Client.APP_ID) - ); - } GLib.Notification notification = new GLib.Notification(summary); notification.set_body(body); notification.set_icon(icon); diff --git a/test/client/composer/composer-widget-test.vala b/test/client/composer/composer-widget-test.vala index 6b31c943..cdbcd3d6 100644 --- a/test/client/composer/composer-widget-test.vala +++ b/test/client/composer/composer-widget-test.vala @@ -125,8 +125,7 @@ public class Composer.WidgetTest : TestCase { new Geary.App.EmailStore(mock_account), new Application.ContactStore( mock_account, - Folks.IndividualAggregator.dup(), - new Application.AvatarStore() + Folks.IndividualAggregator.dup() ) ); } diff --git a/ui/conversation-contact-popover.ui b/ui/conversation-contact-popover.ui index 113f718c..6030fee1 100644 --- a/ui/conversation-contact-popover.ui +++ b/ui/conversation-contact-popover.ui @@ -60,9 +60,11 @@ - + True - 48 + False + True + 48 0 diff --git a/ui/conversation-message.ui b/ui/conversation-message.ui index 769a3e5f..c6afd79c 100644 --- a/ui/conversation-message.ui +++ b/ui/conversation-message.ui @@ -2,6 +2,7 @@ +