2 * Copyright (C) 2010 Collabora Ltd.
4 * This library is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU Lesser General Public License as published by
6 * the Free Software Foundation, either version 2.1 of the License, or
7 * (at your option) any later version.
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU Lesser General Public License for more details.
14 * You should have received a copy of the GNU Lesser General Public License
15 * along with this library. If not, see <http://www.gnu.org/licenses/>.
18 * Travis Reitter <travis.reitter@collabora.co.uk>
26 * A physical person, aggregated from the various {@link Persona}s the person
27 * might have, such as their different IM addresses or vCard entries.
29 public class Folks.Individual : Object, Alias, Avatar, Capabilities, Groups,
32 private HashTable<string, bool> _groups;
33 private GLib.List<Persona> _personas;
34 private HashTable<PersonaStore, HashSet<Persona>> stores;
36 /* XXX: should setting this push it down into the Persona (to foward along to
37 * the actual store if possible?) */
38 public string alias { get; set; }
39 public File avatar { get; set; }
40 public CapabilitiesFlags capabilities { get; private set; }
41 public string id { get; private set; }
42 public Folks.PresenceType presence_type { get; private set; }
43 public string presence_message { get; private set; }
45 /* this individual has become invalid (eg, its last persona is removed) */
46 public signal void removed ();
48 public HashTable<string, bool> groups
50 get { return this._groups; }
52 /* Propagate the list of new groups to every Persona in the individual
53 * which implements the Groups interface */
56 this._personas.foreach ((p) =>
59 ((Groups) p).groups = value;
64 public GLib.List<Persona> personas
66 get { return this._personas; }
70 /* Disconnect from all our previous personas */
71 this._personas.foreach ((p) =>
73 var persona = (Persona) p;
74 var groups = (p is Groups) ? (Groups) p : null;
76 persona.notify["avatar"].disconnect (this.notify_avatar_cb);
77 persona.notify["presence-message"].disconnect (
78 this.notify_presence_cb);
79 persona.notify["presence-type"].disconnect (
80 this.notify_presence_cb);
81 groups.group_changed.disconnect (this.persona_group_changed_cb);
84 this._personas = value.copy ();
86 /* If all the personas have been removed, remove the individual */
87 if (this._personas.length () < 1)
93 /* TODO: base this upon our ID in permanent storage, once we have that
95 if (this.id == null && this._personas.data != null)
96 this.id = this._personas.data.iid;
98 /* Connect to all the new personas */
99 this._personas.foreach ((p) =>
101 var persona = (Persona) p;
102 var groups = (p is Groups) ? (Groups) p : null;
104 persona.notify["avatar"].connect (this.notify_avatar_cb);
105 persona.notify["presence-message"].connect (
106 this.notify_presence_cb);
107 persona.notify["presence-type"].connect (this.notify_presence_cb);
108 groups.group_changed.connect (this.persona_group_changed_cb);
111 /* Update our aggregated fields and notify the changes */
112 this.update_fields ();
116 private void notify_avatar_cb (Object obj, ParamSpec ps)
118 this.update_avatar ();
121 private void persona_group_changed_cb (string group, bool is_member)
123 this.change_group (group, is_member);
124 this.update_groups ();
127 public void change_group (string group, bool is_member)
129 this._personas.foreach ((p) =>
132 ((Groups) p).change_group (group, is_member);
135 /* don't notify, since it hasn't happened in the persona backing stores
136 * yet; react to that directly */
139 private void notify_presence_cb (Object obj, ParamSpec ps)
141 this.update_presence ();
144 public Individual (GLib.List<Persona>? personas)
146 Object (personas: personas);
148 this.stores = new HashTable<PersonaStore, HashSet<Persona>> (direct_hash,
150 this.stores_update ();
153 private void stores_update ()
155 this._personas.foreach ((p) =>
157 var persona = (Persona) p;
158 var store_is_new = false;
159 var persona_set = this.stores.lookup (persona.store);
160 if (persona_set == null)
162 persona_set = new HashSet<Persona> (direct_hash, direct_equal);
166 persona_set.add (persona);
170 this.stores.insert (persona.store, persona_set);
172 persona.store.removed.connect (this.store_removed_cb);
173 persona.store.personas_removed.connect (
174 this.store_personas_removed_cb);
179 private void store_removed_cb (PersonaStore store)
181 var persona_set = this.stores.lookup (store);
182 if (persona_set != null)
184 foreach (var persona in persona_set)
186 this._personas.remove (persona);
190 this.stores.remove (store);
192 if (this._personas.length () < 1 || this.stores.size () < 1)
198 this.update_fields ();
201 private void store_personas_removed_cb (PersonaStore store,
202 GLib.List<Persona> personas)
204 personas.foreach ((data) =>
206 this._personas.remove ((Persona) data);
209 if (this._personas.length () < 1)
215 this.update_fields ();
218 private void update_fields ()
220 /* Gather the first occurrence of each field. We assume that there is
221 * at least one persona in the list, since the Individual should've been
222 * destroyed before now otherwise. */
224 var caps = CapabilitiesFlags.NONE;
225 this._personas.foreach ((persona) =>
227 var p = (Persona) persona;
229 /* FIXME: also check to see if alias is just whitespace */
233 caps |= p.capabilities;
238 /* We have to pick a UID, since none of the personas have an alias
239 * available. Pick the UID from the first persona in the list. */
240 alias = this._personas.data.uid;
241 warning ("No aliases available for individual; using UID instead: %s",
245 /* only notify if the value has changed */
246 if (this.alias != alias)
249 if (this.capabilities != caps)
250 this.capabilities = caps;
252 this.update_groups ();
253 this.update_presence ();
254 this.update_avatar ();
257 private void update_groups ()
259 var new_groups = new HashTable<string, bool> (str_hash, str_equal);
261 /* this._groups is null during initial construction */
262 if (this._groups == null)
263 this._groups = new HashTable<string, bool> (str_hash, str_equal);
265 /* FIXME: this should partition the personas by store (maybe we should
266 * keep that mapping in general in this class), and execute
267 * "groups-changed" on the store (with the set of personas), to allow the
268 * back-end to optimize it (like Telepathy will for MembersChanged for the
269 * groups channel list) */
270 this._personas.foreach ((p) =>
274 var persona = (Groups) p;
276 persona.groups.foreach ((k, v) =>
278 new_groups.insert ((string) k, true);
283 new_groups.foreach ((k, v) =>
285 var group = (string) k;
286 if (this._groups.lookup (group) != true)
288 this._groups.insert (group, true);
289 this._groups.foreach ((k, v) =>
295 this.group_changed (group, true);
299 /* buffer the removals, so we don't remove while iterating */
300 var removes = new GLib.List<string> ();
301 this._groups.foreach ((k, v) =>
303 var group = (string) k;
304 if (new_groups.lookup (group) != true)
305 removes.prepend (group);
308 removes.foreach ((l) =>
310 var group = (string) l;
311 this._groups.remove (group);
312 this.group_changed (group, false);
316 private void update_presence ()
318 var presence_message = "";
319 var presence_type = Folks.PresenceType.UNSET;
321 /* Choose the most available presence from our personas */
322 this._personas.foreach ((p) =>
324 var persona = (Persona) p;
326 if (Presence.typecmp (persona.presence_type, presence_type) > 0)
328 presence_type = persona.presence_type;
329 presence_message = persona.presence_message;
333 if (presence_message == null)
334 presence_message = "";
336 /* only notify if the value has changed */
337 if (this.presence_message != presence_message)
338 this.presence_message = presence_message;
340 if (this.presence_type != presence_type)
341 this.presence_type = presence_type;
344 private void update_avatar ()
348 this._personas.foreach ((p) =>
350 var persona = (Persona) p;
354 avatar = persona.avatar;
359 /* only notify if the value has changed */
360 if (this.avatar != avatar)
361 this.avatar = avatar;
364 public CapabilitiesFlags get_capabilities ()
366 return this.capabilities;
370 * GLib/C convenience functions (for built-in casting, etc.)
372 public unowned string get_alias ()
377 public HashTable<string, bool> get_groups ()
383 public unowned string get_presence_message ()
385 return this.presence_message;
388 public Folks.PresenceType get_presence_type ()
390 return this.presence_type;
393 public bool is_online ()
396 return p.is_online ();