gnome-contacts/src/contacts-contact.vala
David King c1ea64d128 Remove Contact.serializable_avatar_icon
There is no need to provide two ways to serialize the icon, when
avatar_icon_data (which uses g_icon_serialize) is sufficient.

https://bugzilla.gnome.org/show_bug.cgi?id=756098
2015-10-07 11:09:44 -04:00

1554 lines
43 KiB
Vala

/* -*- Mode: vala; indent-tabs-mode: t; c-basic-offset: 2; tab-width: 8 -*- */
/*
* Copyright (C) 2011 Alexander Larsson <alexl@redhat.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using Gtk;
using Folks;
using Gee;
using TelepathyGLib;
using Geocode;
public errordomain ContactError {
NOT_IMPLEMENTED,
NO_PRIMARY
}
public class Contacts.ContactPresence : Grid {
Contact contact;
Image image;
Image phone_image;
Label label;
string last_class;
private void update_presence_widgets () {
PresenceType type;
string message;
bool is_phone;
type = contact.presence_type;
message = contact.presence_message;
is_phone = contact.is_phone;
if (type == PresenceType.UNSET ||
type == PresenceType.ERROR ||
type == PresenceType.OFFLINE ||
type == PresenceType.UNKNOWN) {
image.clear ();
image.hide ();
label.hide ();
label.set_text ("");
phone_image.hide ();
return;
}
image.set_from_icon_name (Contact.presence_to_icon_full (type), IconSize.MENU);
if (last_class != null)
image.get_style_context ().remove_class (last_class);
last_class = Contact.presence_to_class (type);
image.get_style_context ().add_class (last_class);
image.show ();
label.show ();
phone_image.show ();
if (message.length == 0)
message = PresenceDetails.get_default_message_from_type (type);
label.set_markup (Markup.printf_escaped ("<span font='11px'>%s</span>", message));
label.set_margin_bottom (3);
if (is_phone)
phone_image.show ();
else
phone_image.hide ();
}
public ContactPresence (Contact contact) {
this.contact = contact;
this.set_column_spacing (4);
image = new Image ();
image.set_no_show_all (true);
this.add (image);
label = new Label ("");
label.set_no_show_all (true);
label.set_ellipsize (Pango.EllipsizeMode.END);
label.xalign = 0.0f;
this.add (label);
phone_image = new Image ();
phone_image.set_no_show_all (true);
phone_image.set_from_icon_name ("phone-symbolic", IconSize.MENU);
this.add (phone_image);
update_presence_widgets ();
var id = contact.presence_changed.connect ( () => {
update_presence_widgets ();
});
var id2 = contact.personas_changed.connect ( () => {
update_presence_widgets ();
});
this.destroy.connect (() => {
contact.disconnect (id);
contact.disconnect (id2);
});
}
}
public class Contacts.Contact : GLib.Object {
public const int LIST_AVATAR_SIZE = 48;
public const int SMALL_AVATAR_SIZE = 54;
public Store store;
public bool is_main;
public PresenceType presence_type;
public string presence_message;
public bool is_phone;
struct ContactDataRef {
void *key;
void *data;
}
private ContactDataRef[] refs;
public Individual individual;
uint changed_id;
bool changed_personas;
public Persona? fake_persona = null;
private Gdk.Pixbuf? _small_avatar;
public Gdk.Pixbuf small_avatar {
get {
if (_small_avatar == null) {
var pixbuf = load_icon (individual.avatar, SMALL_AVATAR_SIZE);
if (pixbuf == null)
pixbuf = draw_fallback_avatar (SMALL_AVATAR_SIZE, this);
_small_avatar = frame_icon (pixbuf);
}
return _small_avatar;
}
}
private Variant? _avatar_icon_data;
public Variant? avatar_icon_data {
get {
if (individual.avatar == null)
return null;
if (_avatar_icon_data == null) {
if (small_avatar == null)
return null;
_avatar_icon_data = small_avatar.serialize ();
}
return _avatar_icon_data;
}
}
public string display_name {
get {
unowned string? name = individual.full_name;
if (is_set (name))
return name;
unowned string? alias = individual.alias;
if (is_set (alias))
return alias;
unowned string? nickname = individual.nickname;
if (is_set (nickname))
return nickname;
foreach (var email in individual.email_addresses) {
string? e = email.value;
if (is_set (e))
return email.value;
}
return "";
}
}
// Synchronize with get_secondary_string_source ()
public string? get_secondary_string (out string [] sources = null) {
var nick = individual.nickname;
if (is_set (nick)) {
sources = new string[1];
sources[0] = "nickname";
return "\xE2\x80\x9C" + nick + "\xE2\x80\x9D";
}
foreach (var role_detail in individual.roles) {
var role = role_detail.value;
if (is_set (role.organisation_name)) {
if (is_set (role.title)) {
sources = new string[2];
sources[0] = "title";
sources[1] = "organisation-name";
return "%s, %s".printf (role.title, role.organisation_name);
} else if (is_set (role.role)) {
sources = new string[2];
sources[0] = "role";
sources[1] = "organisation-name";
return "%s, %s".printf (role.role, role.organisation_name);
} else {
sources = new string[0];
sources[0] = "organisation-name";
return role.organisation_name;
}
} else if (is_set (role.title)) {
sources = new string[0];
sources[0] = "title";
return role.title;
} else if (is_set (role.role)) {
sources = new string[0];
sources[0] = "role";
return role.role;
}
}
sources = null;
return null;
}
public static bool persona_has_writable_property (Persona persona, string property) {
// TODO: This should check the writibility on the FakePersona store,
// but that is not availible in folks yet
if (persona is FakePersona)
return true;
foreach (unowned string p in persona.writeable_properties) {
if (p == property)
return true;
}
return false;
}
public static string get_display_name_for_persona (Persona persona) {
var name_details = persona as NameDetails;
var alias_details = persona as AliasDetails;
var email_details = persona as EmailDetails;
if (name_details != null) {
unowned string? name = name_details.full_name;
if (is_set (name))
return name;
}
if (alias_details != null) {
unowned string? alias = alias_details.alias;
if (is_set (alias))
return alias;
}
if (name_details != null) {
unowned string? nickname = name_details.nickname;
if (is_set (nickname))
return nickname;
}
if (email_details != null) {
foreach (var email in email_details.email_addresses) {
string e = email.value;
if (is_set (e))
return e;
}
}
return "";
}
public unichar initial_letter {
get {
string name = display_name;
if (name.length == 0)
return 0;
return name.get_char ().totitle ();
}
}
private string filter_data;
public signal void presence_changed ();
public signal void changed ();
public signal void personas_changed ();
private bool _is_hidden;
private bool _is_hidden_uptodate;
private bool _is_hidden_to_delete;
private bool _get_is_hidden () {
// Don't show the user itself
if (individual.is_user)
return true;
// Contact has been deleted (but this is not actually posted, for undo support)
if (_is_hidden_to_delete)
return true;
var personas = individual.personas;
var i = personas.iterator();
// Look for single-persona individuals
if (i.next() && !i.has_next ()) {
var persona = i.get();
var store = persona.store;
// Filter out pure key-file persona individuals as these are
// not very interesting
if (store.type_id == "key-file")
return true;
// Filter out uncertain things like link-local xmpp
if (store.type_id == "telepathy" &&
store.trust_level == PersonaStoreTrust.NONE)
return true;
}
return false;
}
public bool is_hidden {
get {
if (!_is_hidden_uptodate) {
_is_hidden = _get_is_hidden ();
_is_hidden_uptodate = true;
}
return _is_hidden;
}
}
public void hide () {
_is_hidden_to_delete = true;
queue_changed (false);
}
public void show () {
_is_hidden_to_delete = false;
queue_changed (false);
}
public static Contact from_individual (Individual i) {
return i.get_data ("contact");
}
private void persona_notify_cb (ParamSpec pspec) {
this.presence_changed ();
queue_changed (false);
}
private void connect_persona (Persona p) {
p.notify["presence-type"].connect (persona_notify_cb);
p.notify["presence-message"].connect (persona_notify_cb);
var tp = p as Tpf.Persona;
if (tp != null && tp.contact != null)
tp.contact.notify["client-types"].connect (persona_notify_cb);
}
private void disconnect_persona (Persona p) {
SignalHandler.disconnect_by_func (individual, (void *)persona_notify_cb, this);
var tp = p as Tpf.Persona;
if (tp != null && tp.contact != null)
SignalHandler.disconnect_by_func (tp.contact, (void *)persona_notify_cb, this);
}
public unowned T lookup<T> (void *key) {
foreach (unowned ContactDataRef? data_ref in refs) {
if (data_ref.key == key)
return (T*)data_ref.data;
}
return null;
}
public void set_lookup<T> (void *key, owned T data) {
int i = refs.length;
refs.resize(i+1);
refs[i].key = key;
// Transfer ownership to the array
refs[i].data = (void *)(owned)data;
}
public void remove_lookup<T> (void *key) {
int i;
for (i = 0; i < refs.length; i++) {
if (refs[i].key == key) {
// We need to unref the data so we take a local
// owned copy and let it go out of scope
T old_val = (owned)refs[i].data;
// Reference the variable to avoid warning
(void)old_val;
for (int j = i + 1; j < refs.length; j++) {
refs[j-1] = refs[j];
}
refs.resize(refs.length-1);
return;
}
}
}
public static bool persona_is_main (Persona persona) {
var store = persona.store;
if (!store.is_primary_store)
return false;
// Mark google contacts not in "My Contacts" as non-main
if (persona_is_google_other (persona)) {
return false;
}
return true;
}
private bool calc_is_main () {
var res = false;
foreach (var p in individual.personas) {
if (persona_is_main (p))
res = true;
}
return res;
}
public Contact (Store store, Individual i) {
this.store = store;
individual = i;
individual.set_data ("contact", this);
this.refs = new ContactDataRef[0];
is_main = calc_is_main ();
foreach (var p in individual.personas) {
connect_persona (p);
}
individual.personas_changed.connect ( (added, removed) => {
foreach (var p in added)
connect_persona (p);
foreach (var p in removed)
disconnect_persona (p);
queue_changed (true);
});
update ();
individual.notify.connect(notify_cb);
}
public void replace_individual (Individual new_individual) {
foreach (var p in individual.personas) {
disconnect_persona (p);
}
individual.notify.disconnect(notify_cb);
individual = new_individual;
individual.set_data ("contact", this);
foreach (var p in individual.personas) {
connect_persona (p);
}
_small_avatar = null;
_avatar_icon_data = null;
individual.notify.connect(notify_cb);
queue_changed (true);
}
public void remove () {
unqueue_changed ();
foreach (var p in individual.personas) {
disconnect_persona (p);
}
individual.notify.disconnect(notify_cb);
}
public bool has_email (string email_address) {
var addrs = individual.email_addresses;
foreach (var detail in addrs) {
if (detail.value == email_address)
return true;
}
return false;
}
public bool has_birthday () {
return individual.birthday != null;
}
public bool has_nickname () {
return individual.nickname != null &&
individual.nickname != "";
}
public bool has_notes () {
foreach (var p in get_personas_for_display ()) {
var note_details = p as NoteDetails;
if (note_details != null) {
foreach (var note in note_details.notes) {
if (note.value != "")
return true;
}
}
}
return false;
}
public bool contains_strings (string [] strings) {
foreach (string i in strings) {
if (! (i in filter_data))
return false;
}
return true;
}
public static string presence_to_icon_symbolic (PresenceType presence) {
string? iconname = null;
switch (presence) {
default:
case PresenceType.OFFLINE:
case PresenceType.UNSET:
case PresenceType.ERROR:
case PresenceType.UNKNOWN:
break;
case PresenceType.AVAILABLE:
iconname = "user-available-symbolic";
break;
case PresenceType.AWAY:
case PresenceType.EXTENDED_AWAY:
iconname = "user-away-symbolic";
break;
case PresenceType.BUSY:
iconname = "user-busy-symbolic";
break;
case PresenceType.HIDDEN:
iconname = "user-invisible-symbolic";
break;
}
return iconname;
}
public static string presence_to_icon_symbolic_full (PresenceType presence) {
string? iconname = presence_to_icon_symbolic (presence);
if (iconname != null)
return iconname;
return "user-offline-symbolic";
}
public static string presence_to_icon (PresenceType presence) {
string? iconname = null;
switch (presence) {
default:
case PresenceType.OFFLINE:
case PresenceType.UNSET:
case PresenceType.ERROR:
case PresenceType.UNKNOWN:
break;
case PresenceType.AVAILABLE:
iconname = "user-available";
break;
case PresenceType.AWAY:
case PresenceType.EXTENDED_AWAY:
iconname = "user-away";
break;
case PresenceType.BUSY:
iconname = "user-busy";
break;
case PresenceType.HIDDEN:
iconname = "user-invisible";
break;
}
return iconname;
}
public static string presence_to_icon_full (PresenceType presence) {
string? iconname = presence_to_icon (presence);
if (iconname != null)
return iconname;
return "user-offline";
}
public static string presence_to_class (PresenceType presence) {
string? classname = null;
switch (presence) {
default:
case PresenceType.HIDDEN:
case PresenceType.OFFLINE:
case PresenceType.UNSET:
case PresenceType.ERROR:
classname = "presence-icon-offline";
break;
case PresenceType.AVAILABLE:
case PresenceType.UNKNOWN:
classname = "presence-icon-available";
break;
case PresenceType.AWAY:
case PresenceType.EXTENDED_AWAY:
classname = "presence-icon-away";
break;
case PresenceType.BUSY:
classname = "presence-icon-busy";
break;
}
return classname;
}
static string? get_first_string (Collection<string>? collection) {
if (collection != null) {
var i = collection.iterator();
if (i.next())
return i.get();
}
return null;
}
private static bool has_pref (AbstractFieldDetails details) {
if (get_first_string (details.get_parameter_values ("x-evolution-ui-slot")) == "1")
return true;
foreach (var param in details.parameters.get ("type")) {
if (param.ascii_casecmp ("PREF") == 0)
return true;
}
return false;
}
public static int compare_fields (void *_a, void *_b) {
AbstractFieldDetails *a = (AbstractFieldDetails *)_a;
AbstractFieldDetails *b = (AbstractFieldDetails *)_b;
/* Compare by pref */
bool first_a = has_pref (a);
bool first_b = has_pref (b);
if (first_a != first_b) {
if (first_a)
return -1;
else
return 1;
}
if (a is EmailFieldDetails || a is PhoneFieldDetails) {
var aa = a as AbstractFieldDetails<string>;
var bb = b as AbstractFieldDetails<string>;
return strcmp (aa.value, bb.value);
}
warning ("Unsupported AbstractFieldDetails value type");
return 0;
}
public static ArrayList<T> sort_fields<T> (Collection<T> fields) {
var res = new ArrayList<T>();
res.add_all (fields);
res.sort (Contact.compare_fields);
return res;
}
public static async Place geocode_address (PostalAddress addr) {
SourceFunc callback = geocode_address.callback;
var params = new HashTable<string, GLib.Value?>(str_hash, str_equal);
if (is_set (addr.street))
params.insert("street", addr.street);
if (is_set (addr.locality))
params.insert("locality", addr.locality);
if (is_set (addr.region))
params.insert("region", addr.region);
if (is_set (addr.country))
params.insert("country", addr.country);
Place? place = null;
var forward = new Forward.for_params (params);
forward.search_async.begin (null, (object, res) => {
try {
var places = forward.search_async.end (res);
place = places.nth_data (0);
callback ();
} catch (GLib.Error e) {
debug ("No geocode result found for contact");
callback ();
}
});
yield;
return place;
}
public static string[] format_address (PostalAddress addr) {
string[] lines = {};
if (is_set (addr.street))
lines += addr.street;
if (is_set (addr.extension))
lines += addr.extension;
if (is_set (addr.locality))
lines += addr.locality;
if (is_set (addr.region))
lines += addr.region;
if (is_set (addr.postal_code))
lines += addr.postal_code;
if (is_set (addr.po_box))
lines += addr.po_box;
if (is_set (addr.country))
lines += addr.country;
if (is_set (addr.address_format))
lines += addr.address_format;
return lines;
}
public Tpf.Persona? find_im_persona (string protocol, string im_address) {
var iid = protocol + ":" + im_address;
foreach (var p in individual.personas) {
var tp = p as Tpf.Persona;
if (tp != null && tp.iid == iid) {
return tp;
}
}
return null;
}
public enum ImDisplay {
DEFAULT, /* $id ($service) */
ALIAS_SERVICE /* $alias ($service) */
}
private struct ImData {
unowned string service;
unowned string display_name;
ImDisplay display;
}
public static string format_im_service (string service, out ImDisplay display) {
const ImData[] data = {
{ "google-talk", N_("Google Talk") },
{ "ovi-chat", N_("Ovi Chat") },
{ "facebook", N_("Facebook"), ImDisplay.ALIAS_SERVICE },
{ "lj-talk", N_("Livejournal") },
{ "aim", N_("AOL Instant Messenger") },
{ "gadugadu", N_("Gadu-Gadu") },
{ "groupwise", N_("Novell Groupwise") },
{ "icq", N_("ICQ")},
{ "irc", N_("IRC")},
{ "jabber", N_("Jabber")},
{ "local-xmpp", N_("Local network")},
{ "msn", N_("Windows Live Messenger")},
{ "myspace", N_("MySpace")},
{ "mxit", N_("MXit")},
{ "napster", N_("Napster")},
{ "qq", N_("Tencent QQ")},
{ "sametime", N_("IBM Lotus Sametime")},
{ "silc", N_("SILC")},
{ "sip", N_("sip")},
{ "skype", N_("Skype")},
{ "tel", N_("Telephony")},
{ "trepia", N_("Trepia")},
{ "yahoo", N_("Yahoo! Messenger")},
{ "yahoojp", N_("Yahoo! Messenger")},
{ "zephyr", N_("Zephyr")}
};
foreach (var d in data) {
if (d.service == service) {
display = d.display;
return dgettext (Config.GETTEXT_PACKAGE, d.display_name);
}
}
display = ImDisplay.DEFAULT;
return service;
}
public static string format_im_name (Tpf.Persona? persona,
string protocol, string id) {
string? service = null;
if (persona != null) {
var account = (persona.store as Tpf.PersonaStore).account;
service = account.service;
}
if (service == null || service == "")
service = protocol;
ImDisplay display;
var display_name = format_im_service (service, out display);
switch (display) {
default:
case ImDisplay.DEFAULT:
return id + " (" + display_name + ")";
case ImDisplay.ALIAS_SERVICE:
return persona.alias + " (" + display_name + ")";
}
}
public Widget? create_merged_presence_widget () {
return new ContactPresence (this);
}
public Widget? create_presence_widget (string protocol, string im_address) {
var tp = find_im_persona (protocol, im_address);
if (tp == null)
return null;
var i = new Image ();
i.set_from_icon_name (presence_to_icon_full (tp.presence_type), IconSize.MENU);
string last_class = Contact.presence_to_class (tp.presence_type);
i.get_style_context ().add_class (last_class);
i.set_tooltip_text (tp.presence_message);
var id1 = tp.notify["presence-type"].connect ((pspec) => {
i.set_from_icon_name (presence_to_icon_full (tp.presence_type), IconSize.MENU);
i.get_style_context ().remove_class (last_class);
last_class = Contact.presence_to_class (tp.presence_type);
i.get_style_context ().add_class (last_class);
});
var id2 = tp.notify["presence-message"].connect ( (pspec) => {
i.set_tooltip_text (tp.presence_message);
});
i.destroy.connect (() => {
tp.disconnect(id1);
tp.disconnect(id2);
});
return i;
}
private bool changed_cb () {
changed_id = 0;
var changed_personas = this.changed_personas;
this.changed_personas = false;
this.is_main = calc_is_main ();
update ();
changed ();
if (changed_personas)
personas_changed ();
return false;
}
private void unqueue_changed () {
if (changed_id != 0) {
Source.remove (changed_id);
changed_id = 0;
}
}
public void queue_changed (bool is_persona_change) {
_is_hidden_uptodate = false;
changed_personas |= is_persona_change;
if (changed_id != 0)
return;
changed_id = Idle.add (changed_cb);
}
private void notify_cb (ParamSpec pspec) {
if (pspec.get_name () == "avatar") {
_small_avatar = null;
_avatar_icon_data = null;
}
queue_changed (false);
}
private static bool get_is_phone (Persona persona) {
var tp = persona as Tpf.Persona;
if (tp == null || tp.contact == null)
return false;
unowned string[] types = tp.contact.client_types;
return (types != null && types[0] == "phone");
}
private void update_presence () {
presence_message = null;
presence_type = Folks.PresenceType.UNSET;
is_phone = false;
/* Choose the most available presence from our personas */
foreach (var p in individual.personas) {
if (p is PresenceDetails) {
unowned PresenceDetails presence = (PresenceDetails) p;
var p_is_phone = get_is_phone (p);
if (PresenceDetails.typecmp (presence.presence_type,
presence_type) > 0 ||
(presence.presence_type == presence_type &&
is_phone && !p_is_phone)) {
presence_type = presence.presence_type;
presence_message = presence.presence_message;
is_phone = p_is_phone;
}
}
}
if (presence_message == null)
presence_message = "";
}
private void update_filter_data () {
var builder = new StringBuilder ();
if (individual.alias != null) {
builder.append (Utils.canonicalize_for_search (individual.alias));
builder.append_unichar (' ');
}
if (individual.full_name != null) {
builder.append (Utils.canonicalize_for_search (individual.full_name));
builder.append_unichar (' ');
}
if (individual.nickname != null) {
builder.append (Utils.canonicalize_for_search (individual.nickname));
builder.append_unichar (' ');
}
var im_addresses = individual.im_addresses;
foreach (var detail in im_addresses.get_values ()) {
var addr = detail.value;
builder.append (addr.casefold ());
builder.append_unichar (' ');
}
var emails = individual.email_addresses;
foreach (var email in emails) {
builder.append (email.value.casefold ());
builder.append_unichar (' ');
}
filter_data = builder.str;
}
private void update () {
foreach (var email in individual.email_addresses) {
TypeSet.general.type_seen (email);
}
foreach (var phone in individual.phone_numbers) {
TypeSet.phone.type_seen (phone);
}
update_presence ();
update_filter_data ();
}
// TODO: This should be async, but the vala bindings are broken (bug #649875)
private Gdk.Pixbuf load_icon (LoadableIcon ?file, int size) {
Gdk.Pixbuf? res = null;
if (file != null) {
try {
Cancellable c = new Cancellable ();
var stream = file.load (size, null, c);
res = new Gdk.Pixbuf.from_stream_at_scale (stream, size, size, true, c);
} catch (GLib.Error e) {
warning ("error loading avatar %s\n", e.message);
}
}
return res;
}
public static Gdk.Pixbuf frame_icon (Gdk.Pixbuf icon) {
int w = icon.get_width ();
int h = icon.get_height ();
var cst = new Cairo.ImageSurface (Cairo.Format.ARGB32, w, h);
var cr = new Cairo.Context (cst);
cr.set_source_rgba (0, 0, 0, 0);
cr.rectangle (0, 0, w, h);
cr.fill ();
Gdk.cairo_set_source_pixbuf (cr, icon, 0, 0);
Utils.cairo_rounded_box (cr,
0, 0,
w, h, 4);
cr.fill ();
return Gdk.pixbuf_get_from_surface (cst, 0, 0, w, h);
}
private static Gdk.Pixbuf? fallback_pixbuf_default;
public static Gdk.Pixbuf draw_fallback_avatar (int size, Contact? contact) {
if (size == SMALL_AVATAR_SIZE && fallback_pixbuf_default != null)
return fallback_pixbuf_default;
Gdk.Pixbuf pixbuf = null;
try {
var cst = new Cairo.ImageSurface (Cairo.Format.ARGB32, size, size);
var cr = new Cairo.Context (cst);
var pat = new Cairo.Pattern.linear (0, 0, 0, size);
pat.add_color_stop_rgb (0, 0.937, 0.937, 0.937);
pat.add_color_stop_rgb (1, 0.969, 0.969, 0.969);
cr.set_source (pat);
cr.paint ();
int avatar_size = (int) (size * 0.3);
var icon_info = IconTheme.get_default ().lookup_icon ("avatar-default-symbolic", avatar_size,
IconLookupFlags.GENERIC_FALLBACK);
if (icon_info != null) {
Gdk.cairo_set_source_pixbuf (cr, icon_info.load_icon (), (size - avatar_size) / 2, (size - avatar_size) / 2);
cr.rectangle ((size - avatar_size) / 2, (size - avatar_size) / 2, avatar_size, avatar_size);
cr.fill ();
}
pixbuf = Gdk.pixbuf_get_from_surface (cst, 0, 0, size, size);
} catch {
}
if (size == SMALL_AVATAR_SIZE)
fallback_pixbuf_default = pixbuf;
if (pixbuf != null)
return pixbuf;
var cst = new Cairo.ImageSurface (Cairo.Format.ARGB32, size, size);
return Gdk.pixbuf_get_from_surface (cst, 0, 0, size, size);
}
/* We claim something is "removable" if at least one persona is removable,
that will typically unlink the rest. */
public bool can_remove_personas () {
foreach (var p in individual.personas) {
if (p.store.can_remove_personas == MaybeBool.TRUE &&
!(p is Tpf.Persona)) {
return true;
}
}
return false;
}
public async void remove_personas () throws Folks.PersonaStoreError {
var personas = new HashSet<Persona> ();
foreach (var p in individual.personas) {
if (p.store.can_remove_personas == MaybeBool.TRUE &&
!(p is Tpf.Persona)) {
personas.add (p);
}
}
foreach (var persona in personas) {
yield persona.store.remove_persona (persona);
}
}
public async Persona ensure_primary_persona () throws IndividualAggregatorError, ContactError, PropertyError {
Persona? p = find_primary_persona ();
if (p != null)
return p;
// There is no primary persona, so we link all the current personas
// together. This will create a new persona in the primary store
// that links all the personas together
// HACK-ATTACK:
// We need to create a fake persona since link_personas is a no-op
// for single-persona sets
var persona_set = new HashSet<Persona>();
persona_set.add_all (individual.personas);
if (persona_set.size == 1)
persona_set.add (new FakePersona (this));
yield store.aggregator.link_personas (persona_set);
p = find_primary_persona ();
if (p == null)
throw new ContactError.NO_PRIMARY (_("Unexpected internal error: created contact was not found"));
return p;
}
public Persona? find_persona_from_store (PersonaStore store) {
foreach (var p in individual.personas) {
if (p.store == store)
return p;
}
return null;
}
public Gee.List<Persona> get_personas_for_display () {
CompareDataFunc<Persona> compare_persona_by_store = (a, b) =>
{
Persona persona_a = (Persona *)a;
Persona persona_b = (Persona *)b;
var store_a = persona_a.store;
var store_b = persona_b.store;
if (store_a == store_b) {
if (persona_is_google (persona_a)) {
/* Non-other google personas rank before others */
if (persona_is_google_other (persona_a) && !persona_is_google_other (persona_b))
return 1;
if (!persona_is_google_other (persona_a) && persona_is_google_other (persona_b))
return -1;
}
return 0;
}
if (store_a.is_primary_store && store_b.is_primary_store)
return 0;
if (store_a.is_primary_store)
return -1;
if (store_b.is_primary_store)
return 1;
if (store_a.type_id == "eds" && store_b.type_id == "eds")
return strcmp (store_a.id, store_b.id);
if (store_a.type_id == "eds")
return -1;
if (store_b.type_id == "eds")
return 1;
return strcmp (store_a.id, store_b.id);
};
var persona_list = new ArrayList<Persona>();
int i = 0;
persona_list.add_all (individual.personas);
while (i < persona_list.size) {
if (persona_list[i].store.type_id == "key-file")
persona_list.remove_at (i);
else
i++;
}
persona_list.sort ((owned) compare_persona_by_store);
return persona_list;
}
public Persona? find_primary_persona () {
if (store.aggregator.primary_store == null)
return null;
return find_persona_from_store (store.aggregator.primary_store);
}
public Persona? find_persona_from_uid (string uid) {
foreach (var p in individual.personas) {
if (p.uid == uid)
return p;
}
if (uid == "uid-fake-persona" && this.fake_persona != null)
return this.fake_persona;
return null;
}
public string format_persona_stores () {
string stores = "";
bool first = true;
foreach (var p in individual.personas) {
if (!first)
stores += ", ";
stores += format_persona_store_name_for_contact (p);
first = false;
}
return stores;
}
public static string format_persona_store_name (PersonaStore store) {
if (store.type_id == "eds") {
unowned string? eds_name = lookup_esource_name_by_uid (store.id);
if (eds_name != null)
return eds_name;
}
if (store.type_id == "telepathy") {
var account = (store as Tpf.PersonaStore).account;
return format_im_service (account.service, null);
}
return store.display_name;
}
/* These are "regular" address book contacts, i.e. they contain a
persona that would be "main" if that persona was the primary store */
public bool has_mainable_persona () {
foreach (var p in individual.personas) {
if (p.store.type_id == "eds" &&
!persona_is_google_other (p))
return true;
}
return false;
}
/* We never want to suggest linking to google contacts that
are not My Contacts nor Profiles */
private bool non_linkable () {
bool all_unlinkable = true;
foreach (var p in individual.personas) {
if (!persona_is_google_other (p) ||
persona_is_google_profile (p))
all_unlinkable = false;
}
return all_unlinkable;
}
public bool suggest_link_to (Contact other) {
if (this.non_linkable () || other.non_linkable ())
return false;
if (!this.store.may_suggest_link (this, other))
return false;
/* Only connect main contacts with non-mainable contacts.
non-main contacts can link to any other */
return !this.is_main || !other.has_mainable_persona();
}
private static bool persona_is_google (Persona persona) {
var store = persona.store;
if (store.type_id == "eds" && esource_uid_is_google (store.id))
return true;
return false;
}
/**
* Return true only for personas which are in a Google address book, but which
* are not in the user's "My Contacts" group in the address book.
*/
public static bool persona_is_google_other (Persona persona) {
if (!persona_is_google (persona))
return false;
var p = persona as Edsf.Persona;
if (p != null)
return !p.in_google_personal_group;
return false;
}
public static bool persona_is_google_profile (Persona persona) {
if (!persona_is_google_other (persona))
return false;
var u = persona as UrlDetails;
if (u != null && u.urls.size == 1) {
foreach (var url in u.urls) {
if (/https?:\/\/www.google.com\/profiles\/[0-9]+$/.match(url.value))
return true;
}
}
return false;
}
public static string format_persona_store_name_for_contact (Persona persona) {
var store = persona.store;
if (store.type_id == "eds") {
if (persona_is_google_profile (persona))
return _("Google Circles");
else if (persona_is_google_other (persona))
return _("Google");
unowned string? eds_name = lookup_esource_name_by_uid_for_contact (store.id);
if (eds_name != null)
return eds_name;
}
if (store.type_id == "telepathy") {
var account = (store as Tpf.PersonaStore).account;
return format_im_service (account.service, null);
}
return store.display_name;
}
public static string[] sorted_properties = { "email-addresses" , "phone-numbers" , "im-addresses", "urls", "nickname", "birthday", "notes", "postal-addresses" };
public static string []sort_persona_properties (string [] props) {
CompareDataFunc<string> compare_properties = (a, b) =>
{
var sorted_map = new HashMap<string, int> ();
int i = 0;
foreach (var p in sorted_properties) {
sorted_map.set (p, ++i);
}
string a_str = (string) a;
string b_str = (string) b;
if (sorted_map.has_key (a_str) && sorted_map.has_key (b_str)) {
if (sorted_map[a_str] < sorted_map[b_str])
return -1;
if (sorted_map[a_str] > sorted_map[b_str])
return 1;
return 0;
} else if (sorted_map.has_key (a_str))
return -1;
else if (sorted_map.has_key (b_str))
return 1;
else {
if (a_str < b_str)
return -1;
if (a_str > b_str)
return 1;
return 0;
}
};
var sorted_props = new ArrayList<string> ();
foreach (var s in props) {
sorted_props.add (s);
}
sorted_props.sort ((owned) compare_properties);
return sorted_props.to_array ();
}
/* Tries to set the property on all persons that have it writeable, and
* if none, creates a new persona and writes to it, returning the new
* persona.
*/
public static async Persona? set_individual_property (Contact contact,
string property_name,
Value value) throws GLib.Error, PropertyError {
bool did_set = false;
// Need to make a copy here as it could change during the yields
var personas_copy = contact.individual.personas.to_array ();
foreach (var p in personas_copy) {
if (property_name in p.writeable_properties) {
did_set = true;
yield Contact.set_persona_property (p, property_name, value);
}
}
if (!did_set) {
var fake = new FakePersona (contact);
return yield fake.make_real_and_set (property_name, value);
}
return null;
}
public Account? is_callable (string proto, string id) {
Tpf.Persona? t_persona = this.find_im_persona (proto, id);
if (t_persona != null && t_persona.contact != null) {
unowned TelepathyGLib.Capabilities caps =
t_persona.contact.get_capabilities ();
if (caps.supports_audio_call (TelepathyGLib.HandleType.CONTACT))
return (t_persona.store as Tpf.PersonaStore).account;
}
return null;
}
public static async Persona? create_primary_persona_for_details (Folks.PersonaStore store, HashTable<string, Value?> details) throws GLib.Error {
var p = yield store.add_persona_from_details (details);
return p;
}
internal static async void set_persona_property (Persona persona,
string property_name, Value new_value) throws PropertyError, IndividualAggregatorError, ContactError, PropertyError {
if (persona is FakePersona) {
var fake = persona as FakePersona;
yield fake.make_real_and_set (property_name, new_value);
return;
}
/* FIXME: It should be possible to move these all to being delegates which are
* passed to the functions which currently call this one; but only once bgo#604827 is fixed. */
switch (property_name) {
case "alias":
yield (persona as AliasDetails).change_alias ((string) new_value);
break;
case "avatar":
yield (persona as AvatarDetails).change_avatar ((LoadableIcon?) new_value);
break;
case "birthday":
yield (persona as BirthdayDetails).change_birthday ((DateTime?) new_value);
break;
case "calendar-event-id":
yield (persona as BirthdayDetails).change_calendar_event_id ((string?) new_value);
break;
case "email-addresses":
yield (persona as EmailDetails).change_email_addresses ((Set<EmailFieldDetails>) new_value);
break;
case "is-favourite":
yield (persona as FavouriteDetails).change_is_favourite ((bool) new_value);
break;
case "gender":
yield (persona as GenderDetails).change_gender ((Gender) new_value);
break;
case "groups":
yield (persona as GroupDetails).change_groups ((Set<string>) new_value);
break;
case "im-addresses":
yield (persona as ImDetails).change_im_addresses ((MultiMap<string, ImFieldDetails>) new_value);
break;
case "local-ids":
yield (persona as LocalIdDetails).change_local_ids ((Set<string>) new_value);
break;
case "structured-name":
yield (persona as NameDetails).change_structured_name ((StructuredName?) new_value);
break;
case "full-name":
yield (persona as NameDetails).change_full_name ((string) new_value);
break;
case "nickname":
yield (persona as NameDetails).change_nickname ((string) new_value);
break;
case "notes":
yield (persona as NoteDetails).change_notes ((Set<NoteFieldDetails>) new_value);
break;
case "phone-numbers":
yield (persona as PhoneDetails).change_phone_numbers ((Set<PhoneFieldDetails>) new_value);
break;
case "postal-addresses":
yield (persona as PostalAddressDetails).change_postal_addresses ((Set<PostalAddressFieldDetails>) new_value);
break;
case "roles":
yield (persona as RoleDetails).change_roles ((Set<RoleFieldDetails>) new_value);
break;
case "urls":
yield (persona as UrlDetails).change_urls ((Set<UrlFieldDetails>) new_value);
break;
case "web-service-addresses":
yield (persona as WebServiceDetails).change_web_service_addresses ((MultiMap<string, WebServiceFieldDetails>) new_value);
break;
default:
critical ("Unknown property '%s' in Contact.set_persona_property().", property_name);
break;
}
}
public void keep_widget_uptodate (Widget w, owned Gtk.Callback callback) {
callback(w);
ulong id = this.changed.connect ( () => { callback(w); });
w.destroy.connect (() => { this.disconnect (id); });
}
public void fetch_contact_info () {
/* TODO: Ideally Folks should have API for this (#675131) */
foreach (var p in individual.personas) {
var tp = p as Tpf.Persona;
if (tp != null) {
tp.contact.request_contact_info_async.begin (null);
}
}
}
}
public class Contacts.FakePersonaStore : PersonaStore {
public static FakePersonaStore _the_store;
public static FakePersonaStore the_store() {
if (_the_store == null)
_the_store = new FakePersonaStore ();
return _the_store;
}
private HashMap<string, Persona> _personas;
private Map<string, Persona> _personas_ro;
public override string type_id { get { return "fake"; } }
public FakePersonaStore () {
Object (id: "uri", display_name: "fake store");
this._personas = new HashMap<string, Persona> ();
this._personas_ro = this._personas.read_only_view;
}
public override Map<string, Persona> personas
{
get { return this._personas_ro; }
}
public override MaybeBool can_add_personas { get { return MaybeBool.FALSE; } }
public override MaybeBool can_alias_personas { get { return MaybeBool.FALSE; } }
public override MaybeBool can_group_personas { get { return MaybeBool.FALSE; } }
public override MaybeBool can_remove_personas { get { return MaybeBool.FALSE; } }
public override bool is_prepared { get { return true; } }
public override bool is_quiescent { get { return true; } }
private string[] _always_writeable_properties = {};
public override string[] always_writeable_properties { get { return this._always_writeable_properties; } }
public override async void prepare () throws GLib.Error { }
public override async Persona? add_persona_from_details (HashTable<string, Value?> details) throws Folks.PersonaStoreError {
return null;
}
public override async void remove_persona (Persona persona) throws Folks.PersonaStoreError {
}
}
public class Contacts.FakePersona : Persona {
public Contact contact;
private class PropVal {
public string property;
public Value value;
}
private ArrayList<PropVal> prop_vals;
private bool now_real;
private bool has_full_name;
public static FakePersona? maybe_create_for (Contact contact) {
var primary_persona = contact.find_primary_persona ();
if (primary_persona != null)
return null;
foreach (var p in contact.individual.personas) {
// Don't fake a primary persona if we have an eds
// persona on a non-readonly store
if (p.store.type_id == "eds" &&
p.store.can_add_personas == MaybeBool.TRUE &&
p.store.can_remove_personas == MaybeBool.TRUE)
return null;
}
return new FakePersona (contact);
}
private const string[] _linkable_properties = {};
private const string[] _writeable_properties = {};
public override string[] linkable_properties
{
get { return _linkable_properties; }
}
public override string[] writeable_properties
{
get { return _writeable_properties; }
}
public async Persona? make_real_and_set (string property,
Value value) throws IndividualAggregatorError, ContactError, PropertyError {
var v = new PropVal ();
v.property = property;
v.value = value;
if (property == "full-name")
has_full_name = true;
if (prop_vals == null) {
prop_vals = new ArrayList<PropVal> ();
prop_vals.add (v);
Persona p = yield contact.ensure_primary_persona ();
if (!has_full_name)
p.set ("full-name", contact.display_name);
foreach (var pv in prop_vals) {
yield Contact.set_persona_property (p, pv.property, pv.value);
}
now_real = true;
return p;
} else {
assert (!now_real);
prop_vals.add (v);
return null;
}
}
public FakePersona (Contact contact) {
Object (display_id: "display_id",
uid: "uid-fake-persona",
iid: "iid",
store: contact.store.aggregator.primary_store ?? FakePersonaStore.the_store(),
is_user: false);
this.contact = contact;
this.contact.fake_persona = this;
}
}