/* -*- Mode: vala; indent-tabs-mode: t; c-basic-offset: 2; tab-width: 8 -*- */ /* * Copyright (C) 2011 Alexander Larsson * * 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 . */ 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 ("%s", 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; } } public Icon? serializable_avatar_icon { get { if (individual.avatar != null && individual.avatar.to_string () != null) return individual.avatar; return null; } } private Variant? _avatar_icon_data; public Variant? avatar_icon_data { get { if (individual.avatar == null) return null; if (individual.avatar.to_string () != null) return null; if (_avatar_icon_data == null) { if (small_avatar == null) return null; var pixel_data = Variant.new_from_data (VariantType.BYTESTRING, small_avatar.get_pixels_with_length (), true, small_avatar); _avatar_icon_data = new Variant ("(iiibii@ay)", small_avatar.get_width (), small_avatar.get_height (), small_avatar.get_rowstride (), small_avatar.get_has_alpha (), small_avatar.get_bits_per_sample (), small_avatar.get_n_channels (), pixel_data); } 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 (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 (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 (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? 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; var bb = b as AbstractFieldDetails; return strcmp (aa.value, bb.value); } warning ("Unsupported AbstractFieldDetails value type"); return 0; } public static ArrayList sort_fields (Collection fields) { var res = new ArrayList(); 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(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 (' '); } var phone_numbers = individual.phone_numbers; foreach (var phone in phone_numbers) { builder.append (phone.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 (); 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_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 get_personas_for_display () { CompareDataFunc 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(); 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 compare_properties = (a, b) => { var sorted_map = new HashMap (); 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 (); 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 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) 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) new_value); break; case "im-addresses": yield (persona as ImDetails).change_im_addresses ((MultiMap) new_value); break; case "local-ids": yield (persona as LocalIdDetails).change_local_ids ((Set) 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) new_value); break; case "phone-numbers": yield (persona as PhoneDetails).change_phone_numbers ((Set) new_value); break; case "postal-addresses": yield (persona as PostalAddressDetails).change_postal_addresses ((Set) new_value); break; case "roles": yield (persona as RoleDetails).change_roles ((Set) new_value); break; case "urls": yield (persona as UrlDetails).change_urls ((Set) new_value); break; case "web-service-addresses": yield (persona as WebServiceDetails).change_web_service_addresses ((MultiMap) 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 _personas; private Map _personas_ro; public override string type_id { get { return "fake"; } } public FakePersonaStore () { Object (id: "uri", display_name: "fake store"); this._personas = new HashMap (); this._personas_ro = this._personas.read_only_view; } public override Map 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 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 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 (); 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; } }