
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
1554 lines
43 KiB
Vala
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;
|
|
}
|
|
}
|