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 * @param replacement_individual the individual which has replaced this one
75 * due to linking, or `null` if this individual was removed for another reason
77 public signal void removed (Individual? replacement_individual);
84 get { return this._alias; }
88 if (this._alias == value)
92 this._personas.foreach ((p) =>
94 if (p is Alias && ((Persona) p).store.is_writeable == true)
95 ((Alias) p).alias = value;
101 * Whether this Individual is a user-defined favourite.
103 * This property is `true` if any of this Individual's {@link Persona}s are
106 public bool is_favourite
108 get { return this._is_favourite; }
112 if (this._is_favourite == value)
115 this._is_favourite = value;
116 this._personas.foreach ((p) =>
118 if (p is Favourite && ((Persona) p).store.is_writeable == true)
119 ((Favourite) p).is_favourite = value;
127 public HashTable<string, bool> groups
129 get { return this._groups; }
133 this._personas.foreach ((p) =>
135 if (p is Groups && ((Persona) p).store.is_writeable == true)
136 ((Groups) p).groups = value;
142 * The set of {@link Persona}s encapsulated by this Individual.
144 * Changing the set of personas may cause updates to the aggregated properties
145 * provided by the Individual, resulting in property notifications for them.
147 * Changing the set of personas will not cause permanent linking/unlinking of
148 * the added/removed personas to/from this Individual. To do that, call
149 * {@link IndividualAggregator.link_personas} or
150 * {@link IndividualAggregator.unlink_individual}, which will ensure the link
151 * changes are written to the appropriate backend.
153 public GLib.List<Persona> personas
155 get { return this._personas; }
156 set { this._set_personas (value, null); }
159 private void notify_groups_cb (Object obj, ParamSpec ps)
161 this.update_groups ();
164 private void notify_alias_cb (Object obj, ParamSpec ps)
166 this.update_alias ();
169 private void notify_avatar_cb (Object obj, ParamSpec ps)
171 this.update_avatar ();
174 private void persona_group_changed_cb (string group, bool is_member)
176 this.change_group.begin (group, is_member);
177 this.update_groups ();
181 * Add or remove the Individual from the specified group.
183 * If `is_member` is `true`, the Individual will be added to the `group`. If
184 * it is `false`, they will be removed from the `group`.
186 * The group membership change will propagate to every {@link Persona} in
189 * @param group a freeform group identifier
190 * @param is_member whether the Individual should be a member of the group
192 public async void change_group (string group, bool is_member)
194 this._personas.foreach ((p) =>
197 ((Groups) p).change_group.begin (group, is_member);
200 /* don't notify, since it hasn't happened in the persona backing stores
201 * yet; react to that directly */
204 private void notify_presence_cb (Object obj, ParamSpec ps)
206 this.update_presence ();
209 private void notify_is_favourite_cb (Object obj, ParamSpec ps)
211 this.update_is_favourite ();
215 * Create a new Individual.
217 * The Individual can optionally be seeded with the {@link Persona}s in
218 * `personas`. Otherwise, it will have to have personas added using the
219 * {@link Folks.Individual.personas} property after construction.
221 * @return a new Individual
223 public Individual (GLib.List<Persona>? personas)
225 Object (personas: personas);
227 this.stores = new HashTable<PersonaStore, HashSet<Persona>> (direct_hash,
229 this.stores_update ();
232 private void stores_update ()
234 this._personas.foreach ((p) =>
236 unowned Persona persona = (Persona) p;
237 var store_is_new = false;
238 var persona_set = this.stores.lookup (persona.store);
239 if (persona_set == null)
241 persona_set = new HashSet<Persona> (direct_hash, direct_equal);
245 persona_set.add (persona);
249 this.stores.insert (persona.store, persona_set);
251 persona.store.removed.connect (this.store_removed_cb);
252 persona.store.personas_changed.connect (
253 this.store_personas_changed_cb);
258 private void store_removed_cb (PersonaStore store)
260 var persona_set = this.stores.lookup (store);
261 if (persona_set != null)
263 foreach (var persona in persona_set)
265 this._personas.remove (persona);
266 /* FIXME: bgo#624249 means GLib.List leaks item references.
267 * We probably eventually want to transition away from GLib.List
268 * and use Gee.LinkedList, but that would mean exposing libgee
269 * in the public API. */
270 g_object_unref (persona);
274 this.stores.remove (store);
276 if (this._personas.length () < 1 || this.stores.size () < 1)
282 this.update_fields ();
285 private void store_personas_changed_cb (PersonaStore store,
286 GLib.List<Persona>? added,
287 GLib.List<Persona>? removed,
290 Groups.ChangeReason reason)
292 var persona_set = this.stores.lookup (store);
293 removed.foreach ((data) =>
295 unowned Persona p = (Persona) data;
297 if (persona_set.remove (p))
299 this._personas.remove (p);
300 /* FIXME: bgo#624249 means GLib.List leaks item references */
305 if (this._personas.length () < 1)
311 this.update_fields ();
314 private void update_fields ()
316 this.update_groups ();
317 this.update_presence ();
318 this.update_is_favourite ();
319 this.update_avatar ();
320 this.update_alias ();
323 private void update_groups ()
325 var new_groups = new HashTable<string, bool> (str_hash, str_equal);
327 /* this._groups is null during initial construction */
328 if (this._groups == null)
329 this._groups = new HashTable<string, bool> (str_hash, str_equal);
331 /* FIXME: this should partition the personas by store (maybe we should
332 * keep that mapping in general in this class), and execute
333 * "groups-changed" on the store (with the set of personas), to allow the
334 * back-end to optimize it (like Telepathy will for MembersChanged for the
335 * groups channel list) */
336 this._personas.foreach ((p) =>
340 unowned Groups persona = (Groups) p;
342 persona.groups.foreach ((k, v) =>
344 new_groups.insert ((string) k, true);
349 new_groups.foreach ((k, v) =>
351 var group = (string) k;
352 if (this._groups.lookup (group) != true)
354 this._groups.insert (group, true);
355 this._groups.foreach ((k, v) =>
361 this.group_changed (group, true);
365 /* buffer the removals, so we don't remove while iterating */
366 var removes = new GLib.List<string> ();
367 this._groups.foreach ((k, v) =>
369 var group = (string) k;
370 if (new_groups.lookup (group) != true)
371 removes.prepend (group);
374 removes.foreach ((l) =>
376 var group = (string) l;
377 this._groups.remove (group);
378 this.group_changed (group, false);
382 private void update_presence ()
384 var presence_message = "";
385 var presence_type = Folks.PresenceType.UNSET;
387 /* Choose the most available presence from our personas */
388 this._personas.foreach ((p) =>
392 unowned Presence presence = (Presence) p;
394 if (Presence.typecmp (presence.presence_type, presence_type) > 0)
396 presence_type = presence.presence_type;
397 presence_message = presence.presence_message;
402 if (presence_message == null)
403 presence_message = "";
405 /* only notify if the value has changed */
406 if (this.presence_message != presence_message)
407 this.presence_message = presence_message;
409 if (this.presence_type != presence_type)
410 this.presence_type = presence_type;
413 private void update_is_favourite ()
415 bool favourite = false;
417 this._personas.foreach ((p) =>
419 if (favourite == false && p is Favourite)
421 favourite = ((Favourite) p).is_favourite;
422 if (favourite == true)
427 /* Only notify if the value has changed */
428 if (this.is_favourite != favourite)
429 this.is_favourite = favourite;
432 private void update_alias ()
435 bool alias_is_display_id = false;
437 foreach (Persona p in this._personas)
441 unowned Alias a = (Alias) p;
443 if (a.alias == null || a.alias.strip () == "")
446 if (alias == null || alias_is_display_id == true)
448 /* We prefer to not have an alias which is the same as the
449 * Persona's display-id, since having such an alias implies
450 * that it's the default. However, we prefer using such an
451 * alias to using the Persona's UID, which is our ultimate
452 * fallback (below). */
455 if (a.alias == p.display_id)
456 alias_is_display_id = true;
457 else if (alias != null)
465 /* We have to pick a UID, since none of the personas have an alias
466 * available. Pick the UID from the first persona in the list. */
467 alias = this._personas.data.uid;
468 message ("No aliases available for individual; using UID instead: %s",
472 /* only notify if the value has changed */
473 if (this.alias != alias)
477 private void update_avatar ()
481 this._personas.foreach ((p) =>
483 if (avatar == null && p is Avatar)
485 avatar = ((Avatar) p).avatar;
490 /* only notify if the value has changed */
491 if (this.avatar != avatar)
492 this.avatar = avatar;
496 * GLib/C convenience functions (for built-in casting, etc.)
500 * Get the Individual's alias.
502 * The alias is a user-chosen name for the Individual; how the user wants that
503 * Individual to be represented in UIs.
505 * @return the Individual's alias
507 public unowned string get_alias ()
513 * Get a mapping of group ID to whether the Individual is a member.
515 * Freeform group IDs are mapped to a boolean which is `true` if the
516 * Individual is a member of the group, and `false` otherwise.
518 * @return a mapping of group ID to membership status
520 public HashTable<string, bool> get_groups ()
527 * Get the Individual's current presence message.
529 * The presence message returned is from the same {@link Persona} which
530 * provided the presence type returned by
531 * {@link Individual.get_presence_type}.
533 * If none of the {@link Persona}s in the Individual have a presence message
534 * set, an empty string is returned.
536 * @return the Individual's presence message
538 public unowned string get_presence_message ()
540 return this.presence_message;
544 * Get the Individual's current presence type.
546 * The presence type returned is from the same {@link Persona} which provided
547 * the presence message returned by {@link Individual.get_presence_message}.
549 * If none of the {@link Persona}s in the Individual have a presence type set,
550 * {@link PresenceType.UNSET} is returned.
552 * @return the Individual's presence type
554 public Folks.PresenceType get_presence_type ()
556 return this.presence_type;
560 * Whether the Individual is online.
562 * This will be `true` if any of the Individual's {@link Persona}s have a
563 * presence type higher than {@link PresenceType.OFFLINE}, as determined by
564 * {@link Presence.typecmp}.
566 * @return `true` if the Individual is online, `false` otherwise
568 public bool is_online ()
571 return p.is_online ();
574 private void _set_personas (GLib.List<Persona>? personas,
575 Individual? replacement_individual)
577 /* Disconnect from all our previous personas */
578 this._personas.foreach ((p) =>
580 unowned Persona persona = (Persona) p;
582 persona.notify["alias"].disconnect (this.notify_alias_cb);
583 persona.notify["avatar"].disconnect (this.notify_avatar_cb);
584 persona.notify["presence-message"].disconnect (
585 this.notify_presence_cb);
586 persona.notify["presence-type"].disconnect (this.notify_presence_cb);
587 persona.notify["is-favourite"].disconnect (
588 this.notify_is_favourite_cb);
589 persona.notify["groups"].disconnect (this.notify_groups_cb);
593 ((Groups) p).group_changed.disconnect (
594 this.persona_group_changed_cb);
598 this._personas = new GLib.List<Persona> ();
599 personas.foreach ((l) =>
601 this._personas.prepend ((Persona) l);
603 this._personas.reverse ();
605 /* If all the personas have been removed, remove the individual */
606 if (this._personas.length () < 1)
608 this.removed (replacement_individual);
612 /* TODO: base this upon our ID in permanent storage, once we have that
614 if (this.id == null && this._personas.data != null)
615 this.id = this._personas.data.uid;
617 /* Connect to all the new personas */
618 this._personas.foreach ((p) =>
620 unowned Persona persona = (Persona) p;
622 persona.notify["alias"].connect (this.notify_alias_cb);
623 persona.notify["avatar"].connect (this.notify_avatar_cb);
624 persona.notify["presence-message"].connect (this.notify_presence_cb);
625 persona.notify["presence-type"].connect (this.notify_presence_cb);
626 persona.notify["is-favourite"].connect (this.notify_is_favourite_cb);
627 persona.notify["groups"].connect (this.notify_groups_cb);
631 ((Groups) p).group_changed.connect (
632 this.persona_group_changed_cb);
636 /* Update our aggregated fields and notify the changes */
637 this.update_fields ();
640 internal void replace (Individual replacement_individual)
642 this._set_personas (null, replacement_individual);