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,
36 private HashTable<string, bool> _groups;
37 private GLib.List<Persona> _personas;
38 private HashTable<PersonaStore, HashSet<Persona>> stores;
39 private bool _is_favourite;
40 private string _alias;
45 public File avatar { get; private set; }
50 public Folks.PresenceType presence_type { get; private set; }
55 public string presence_message { get; private set; }
58 * A unique identifier for the Individual.
60 * This uniquely identifies the Individual, and persists across
61 * {@link IndividualAggregator} instances.
63 * FIXME: Will this.id actually be the persistent ID for storage?
65 public string id { get; private set; }
68 * Emitted when the last of the Individual's {@link Persona}s has been
71 * At this point, the Individual is invalid, so any client referencing it
72 * should unreference it and remove it from their UI.
74 public signal void removed ();
81 get { return this._alias; }
85 if (this._alias == value)
89 this._personas.foreach ((p) =>
92 ((Alias) p).alias = value;
98 * Whether this Individual is a user-defined favourite.
100 * This property is `true` if any of this Individual's {@link Persona}s are
103 * When set, the value is propagated to all of this Individual's
106 public bool is_favourite
108 get { return this._is_favourite; }
110 /* Propagate the new favourite status to every Persona, but only if it's
114 if (this._is_favourite == value)
117 this._is_favourite = value;
118 this._personas.foreach ((p) =>
121 ((Favourite) p).is_favourite = value;
129 public HashTable<string, bool> groups
131 get { return this._groups; }
133 /* Propagate the list of new groups to every Persona in the individual
134 * which implements the Groups interface */
137 this._personas.foreach ((p) =>
140 ((Groups) p).groups = value;
146 * The set of {@link Persona}s encapsulated by this Individual.
148 * Changing the set of personas may cause updates to the aggregated properties
149 * provided by the Individual, resulting in property notifications for them.
151 public GLib.List<Persona> personas
153 get { return this._personas; }
157 /* Disconnect from all our previous personas */
158 this._personas.foreach ((p) =>
160 unowned Persona persona = (Persona) p;
162 persona.notify["alias"].disconnect (this.notify_alias_cb);
163 persona.notify["avatar"].disconnect (this.notify_avatar_cb);
164 persona.notify["presence-message"].disconnect (
165 this.notify_presence_cb);
166 persona.notify["presence-type"].disconnect (
167 this.notify_presence_cb);
168 persona.notify["is-favourite"].disconnect (
169 this.notify_is_favourite_cb);
170 persona.notify["groups"].disconnect (this.notify_groups_cb);
174 ((Groups) p).group_changed.disconnect (
175 this.persona_group_changed_cb);
179 this._personas = new GLib.List<Persona> ();
180 value.foreach ((l) =>
182 this._personas.prepend ((Persona) l);
184 this._personas.reverse ();
186 /* If all the personas have been removed, remove the individual */
187 if (this._personas.length () < 1)
193 /* TODO: base this upon our ID in permanent storage, once we have that
195 if (this.id == null && this._personas.data != null)
196 this.id = this._personas.data.iid;
198 /* Connect to all the new personas */
199 this._personas.foreach ((p) =>
201 unowned Persona persona = (Persona) p;
203 persona.notify["alias"].connect (this.notify_alias_cb);
204 persona.notify["avatar"].connect (this.notify_avatar_cb);
205 persona.notify["presence-message"].connect (
206 this.notify_presence_cb);
207 persona.notify["presence-type"].connect (this.notify_presence_cb);
208 persona.notify["is-favourite"].connect (
209 this.notify_is_favourite_cb);
210 persona.notify["groups"].connect (this.notify_groups_cb);
214 ((Groups) p).group_changed.connect (
215 this.persona_group_changed_cb);
219 /* Update our aggregated fields and notify the changes */
220 this.update_fields ();
224 private void notify_groups_cb (Object obj, ParamSpec ps)
226 this.update_groups ();
229 private void notify_alias_cb (Object obj, ParamSpec ps)
231 this.update_alias ();
234 private void notify_avatar_cb (Object obj, ParamSpec ps)
236 this.update_avatar ();
239 private void persona_group_changed_cb (string group, bool is_member)
241 this.change_group.begin (group, is_member);
242 this.update_groups ();
246 * Add or remove the Individual from the specified group.
248 * If `is_member` is `true`, the Individual will be added to the `group`. If
249 * it is `false`, they will be removed from the `group`.
251 * The group membership change will propagate to every {@link Persona} in
254 * @param group a freeform group identifier
255 * @param is_member whether the Individual should be a member of the group
257 public async void change_group (string group, bool is_member)
259 this._personas.foreach ((p) =>
262 ((Groups) p).change_group.begin (group, is_member);
265 /* don't notify, since it hasn't happened in the persona backing stores
266 * yet; react to that directly */
269 private void notify_presence_cb (Object obj, ParamSpec ps)
271 this.update_presence ();
274 private void notify_is_favourite_cb (Object obj, ParamSpec ps)
276 this.update_is_favourite ();
280 * Create a new Individual.
282 * The Individual can optionally be seeded with the {@link Persona}s in
283 * `personas`. Otherwise, it will have to have personas added using the
284 * {@link Folks.Individual.personas} property after construction.
286 * @return a new Individual
288 public Individual (GLib.List<Persona>? personas)
290 Object (personas: personas);
292 this.stores = new HashTable<PersonaStore, HashSet<Persona>> (direct_hash,
294 this.stores_update ();
297 private void stores_update ()
299 this._personas.foreach ((p) =>
301 unowned Persona persona = (Persona) p;
302 var store_is_new = false;
303 var persona_set = this.stores.lookup (persona.store);
304 if (persona_set == null)
306 persona_set = new HashSet<Persona> (direct_hash, direct_equal);
310 persona_set.add (persona);
314 this.stores.insert (persona.store, persona_set);
316 persona.store.removed.connect (this.store_removed_cb);
317 persona.store.personas_changed.connect (
318 this.store_personas_changed_cb);
323 private void store_removed_cb (PersonaStore store)
325 var persona_set = this.stores.lookup (store);
326 if (persona_set != null)
328 foreach (var persona in persona_set)
330 this._personas.remove (persona);
331 /* FIXME: bgo#624249 means GLib.List leaks item references.
332 * We probably eventually want to transition away from GLib.List
333 * and use Gee.LinkedList, but that would mean exposing libgee
334 * in the public API. */
335 g_object_unref (persona);
339 this.stores.remove (store);
341 if (this._personas.length () < 1 || this.stores.size () < 1)
347 this.update_fields ();
350 private void store_personas_changed_cb (PersonaStore store,
351 GLib.List<Persona>? added,
352 GLib.List<Persona>? removed,
355 Groups.ChangeReason reason)
357 var persona_set = this.stores.lookup (store);
358 removed.foreach ((data) =>
360 unowned Persona p = (Persona) data;
362 if (persona_set.remove (p))
364 this._personas.remove (p);
365 /* FIXME: bgo#624249 means GLib.List leaks item references */
370 if (this._personas.length () < 1)
376 this.update_fields ();
379 private void update_fields ()
381 this.update_groups ();
382 this.update_presence ();
383 this.update_is_favourite ();
384 this.update_avatar ();
385 this.update_alias ();
388 private void update_groups ()
390 var new_groups = new HashTable<string, bool> (str_hash, str_equal);
392 /* this._groups is null during initial construction */
393 if (this._groups == null)
394 this._groups = new HashTable<string, bool> (str_hash, str_equal);
396 /* FIXME: this should partition the personas by store (maybe we should
397 * keep that mapping in general in this class), and execute
398 * "groups-changed" on the store (with the set of personas), to allow the
399 * back-end to optimize it (like Telepathy will for MembersChanged for the
400 * groups channel list) */
401 this._personas.foreach ((p) =>
405 unowned Groups persona = (Groups) p;
407 persona.groups.foreach ((k, v) =>
409 new_groups.insert ((string) k, true);
414 new_groups.foreach ((k, v) =>
416 var group = (string) k;
417 if (this._groups.lookup (group) != true)
419 this._groups.insert (group, true);
420 this._groups.foreach ((k, v) =>
426 this.group_changed (group, true);
430 /* buffer the removals, so we don't remove while iterating */
431 var removes = new GLib.List<string> ();
432 this._groups.foreach ((k, v) =>
434 var group = (string) k;
435 if (new_groups.lookup (group) != true)
436 removes.prepend (group);
439 removes.foreach ((l) =>
441 var group = (string) l;
442 this._groups.remove (group);
443 this.group_changed (group, false);
447 private void update_presence ()
449 var presence_message = "";
450 var presence_type = Folks.PresenceType.UNSET;
452 /* Choose the most available presence from our personas */
453 this._personas.foreach ((p) =>
457 unowned Presence presence = (Presence) p;
459 if (Presence.typecmp (presence.presence_type, presence_type) > 0)
461 presence_type = presence.presence_type;
462 presence_message = presence.presence_message;
467 if (presence_message == null)
468 presence_message = "";
470 /* only notify if the value has changed */
471 if (this.presence_message != presence_message)
472 this.presence_message = presence_message;
474 if (this.presence_type != presence_type)
475 this.presence_type = presence_type;
478 private void update_is_favourite ()
480 bool favourite = false;
482 this._personas.foreach ((p) =>
484 if (favourite == false && p is Favourite)
486 favourite = ((Favourite) p).is_favourite;
487 if (favourite == true)
492 /* Only notify if the value has changed */
493 if (this.is_favourite != favourite)
494 this.is_favourite = favourite;
497 private void update_alias ()
501 this._personas.foreach ((p) =>
505 unowned Alias a = (Alias) p;
507 if (alias == null && a.alias != null && a.alias.strip () != "")
514 /* We have to pick a UID, since none of the personas have an alias
515 * available. Pick the UID from the first persona in the list. */
516 alias = this._personas.data.uid;
517 warning ("No aliases available for individual; using UID instead: %s",
521 /* only notify if the value has changed */
522 if (this.alias != alias)
526 private void update_avatar ()
530 this._personas.foreach ((p) =>
532 if (avatar == null && p is Avatar)
534 avatar = ((Avatar) p).avatar;
539 /* only notify if the value has changed */
540 if (this.avatar != avatar)
541 this.avatar = avatar;
545 * GLib/C convenience functions (for built-in casting, etc.)
549 * Get the Individual's alias.
551 * The alias is a user-chosen name for the Individual; how the user wants that
552 * Individual to be represented in UIs.
554 * @return the Individual's alias
556 public unowned string get_alias ()
562 * Get a mapping of group ID to whether the Individual is a member.
564 * Freeform group IDs are mapped to a boolean which is `true` if the
565 * Individual is a member of the group, and `false` otherwise.
567 * @return a mapping of group ID to membership status
569 public HashTable<string, bool> get_groups ()
576 * Get the Individual's current presence message.
578 * The presence message returned is from the same {@link Persona} which
579 * provided the presence type returned by
580 * {@link Individual.get_presence_type}.
582 * If none of the {@link Persona}s in the Individual have a presence message
583 * set, an empty string is returned.
585 * @return the Individual's presence message
587 public unowned string get_presence_message ()
589 return this.presence_message;
593 * Get the Individual's current presence type.
595 * The presence type returned is from the same {@link Persona} which provided
596 * the presence message returned by {@link Individual.get_presence_message}.
598 * If none of the {@link Persona}s in the Individual have a presence type set,
599 * {@link PresenceType.UNSET} is returned.
601 * @return the Individual's presence type
603 public Folks.PresenceType get_presence_type ()
605 return this.presence_type;
609 * Whether the Individual is online.
611 * This will be `true` if any of the Individual's {@link Persona}s have a
612 * presence type higher than {@link PresenceType.OFFLINE}, as determined by
613 * {@link Presence.typecmp}.
615 * @return `true` if the Individual is online, `false` otherwise
617 public bool is_online ()
620 return p.is_online ();