avatar: Use HdyAvatar for displaying avatars
This commit is contained in:
parent
baf29a9214
commit
6a052031df
11 changed files with 66 additions and 273 deletions
|
|
@ -1,166 +0,0 @@
|
|||
/*
|
||||
* Copyright 2016-2019 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.
|
||||
*
|
||||
* 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<Gdk.Pixbuf> pixbufs = new Gee.LinkedList<Gdk.Pixbuf>();
|
||||
|
||||
|
||||
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<CacheEntry> folks_cache =
|
||||
new Util.Cache.Lru<CacheEntry>(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<CacheEntry> name_cache =
|
||||
new Util.Cache.Lru<CacheEntry>(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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Geary.AccountInformation,AccountContext> accounts =
|
||||
new Gee.HashMap<Geary.AccountInformation,AccountContext>();
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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) &&
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,9 +60,11 @@
|
|||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImage" id="avatar">
|
||||
<object class="HdyAvatar" id="avatar">
|
||||
<property name="visible">True</property>
|
||||
<property name="pixel_size">48</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="show-initials">True</property>
|
||||
<property name="size">48</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
<!-- Generated with glade 3.22.2 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.14"/>
|
||||
<requires lib="libhandy" version="1.0"/>
|
||||
<template class="ConversationMessage" parent="GtkGrid">
|
||||
<property name="visible">True</property>
|
||||
<property name="orientation">vertical</property>
|
||||
|
|
@ -11,12 +12,11 @@
|
|||
<property name="hexpand">True</property>
|
||||
<property name="column_spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="avatar">
|
||||
<property name="width_request">18</property>
|
||||
<property name="height_request">18</property>
|
||||
<object class="HdyAvatar" id="avatar">
|
||||
<property name="visible">True</property>
|
||||
<property name="valign">start</property>
|
||||
<property name="pixel_size">48</property>
|
||||
<property name="show-initials">True</property>
|
||||
<property name="size">48</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue