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,
37 private HashTable<string, bool> _groups;
38 private GLib.List<Persona> _personas;
39 private HashTable<PersonaStore, HashSet<Persona>> stores;
40 private bool _is_favourite;
41 private string _alias;
46 public File avatar { get; private set; }
51 public CapabilitiesFlags capabilities { get; private set; }
56 public Folks.PresenceType presence_type { get; private set; }
61 public string presence_message { get; private set; }
64 * A unique identifier for the Individual.
66 * This uniquely identifies the Individual, and persists across
67 * {@link IndividualAggregator} instances.
69 * FIXME: Will this.id actually be the persistent ID for storage?
71 public string id { get; private set; }
74 * Emitted when the last of the Individual's {@link Persona}s has been
77 * At this point, the Individual is invalid, so any client referencing it
78 * should unreference it and remove it from their UI.
80 public signal void removed ();
87 get { return this._alias; }
91 if (this._alias == value)
95 this._personas.foreach ((p) =>
98 ((Alias) p).alias = value;
104 * Whether this Individual is a user-defined favourite.
106 * This property is `true` if any of this Individual's {@link Persona}s are
109 * When set, the value is propagated to all of this Individual's
112 public bool is_favourite
114 get { return this._is_favourite; }
116 /* Propagate the new favourite status to every Persona, but only if it's
120 if (this._is_favourite == value)
123 this._is_favourite = value;
124 this._personas.foreach ((p) =>
127 ((Favourite) p).is_favourite = value;
135 public HashTable<string, bool> groups
137 get { return this._groups; }
139 /* Propagate the list of new groups to every Persona in the individual
140 * which implements the Groups interface */
143 this._personas.foreach ((p) =>
146 ((Groups) p).groups = value;
152 * The set of {@link Persona}s encapsulated by this Individual.
154 * Changing the set of personas may cause updates to the aggregated properties
155 * provided by the Individual, resulting in property notifications for them.
157 public GLib.List<Persona> personas
159 get { return this._personas; }
163 /* Disconnect from all our previous personas */
164 this._personas.foreach ((p) =>
166 var persona = (Persona) p;
167 var groups = (p is Groups) ? (Groups) p : null;
169 persona.notify["avatar"].disconnect (this.notify_avatar_cb);
170 persona.notify["presence-message"].disconnect (
171 this.notify_presence_cb);
172 persona.notify["presence-type"].disconnect (
173 this.notify_presence_cb);
174 persona.notify["is-favourite"].disconnect (
175 this.notify_is_favourite_cb);
176 groups.group_changed.disconnect (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 var persona = (Persona) p;
202 var groups = (p is Groups) ? (Groups) p : null;
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 groups.group_changed.connect (this.persona_group_changed_cb);
213 /* Update our aggregated fields and notify the changes */
214 this.update_fields ();
218 private void notify_avatar_cb (Object obj, ParamSpec ps)
220 this.update_avatar ();
223 private void persona_group_changed_cb (string group, bool is_member)
225 this.change_group (group, is_member);
226 this.update_groups ();
230 * Add or remove the Individual from the specified group.
232 * If `is_member` is `true`, the Individual will be added to the `group`. If
233 * it is `false`, they will be removed from the `group`.
235 * The group membership change will propagate to every {@link Persona} in
238 * @param group a freeform group identifier
239 * @param is_member whether the Individual should be a member of the group
241 public void change_group (string group, bool is_member)
243 this._personas.foreach ((p) =>
246 ((Groups) p).change_group (group, is_member);
249 /* don't notify, since it hasn't happened in the persona backing stores
250 * yet; react to that directly */
253 private void notify_presence_cb (Object obj, ParamSpec ps)
255 this.update_presence ();
258 private void notify_is_favourite_cb (Object obj, ParamSpec ps)
260 this.update_is_favourite ();
264 * Create a new Individual.
266 * The Individual can optionally be seeded with the {@link Persona}s in
267 * `personas`. Otherwise, it will have to have personas added using the
268 * {@link Folks.Individual.personas} property after construction.
270 * @return a new Individual
272 public Individual (GLib.List<Persona>? personas)
274 Object (personas: personas);
276 this.stores = new HashTable<PersonaStore, HashSet<Persona>> (direct_hash,
278 this.stores_update ();
281 private void stores_update ()
283 this._personas.foreach ((p) =>
285 var persona = (Persona) p;
286 var store_is_new = false;
287 var persona_set = this.stores.lookup (persona.store);
288 if (persona_set == null)
290 persona_set = new HashSet<Persona> (direct_hash, direct_equal);
294 persona_set.add (persona);
298 this.stores.insert (persona.store, persona_set);
300 persona.store.removed.connect (this.store_removed_cb);
301 persona.store.personas_removed.connect (
302 this.store_personas_removed_cb);
307 private void store_removed_cb (PersonaStore store)
309 var persona_set = this.stores.lookup (store);
310 if (persona_set != null)
312 foreach (var persona in persona_set)
314 this._personas.remove (persona);
318 this.stores.remove (store);
320 if (this._personas.length () < 1 || this.stores.size () < 1)
326 this.update_fields ();
329 private void store_personas_removed_cb (PersonaStore store,
330 GLib.List<Persona> personas)
332 var persona_set = this.stores.lookup (store);
333 personas.foreach ((data) =>
335 var p = (Persona) data;
337 persona_set.remove (p);
338 this._personas.remove (p);
341 if (this._personas.length () < 1)
347 this.update_fields ();
350 private void update_fields ()
352 /* Gather the first occurrence of each field. We assume that there is
353 * at least one persona in the list, since the Individual should've been
354 * destroyed before now otherwise. */
356 var caps = CapabilitiesFlags.NONE;
357 this._personas.foreach ((p) =>
363 if (alias == null || alias.strip () == "")
367 if (p is Capabilities)
368 caps |= ((Capabilities) p).capabilities;
373 /* We have to pick a UID, since none of the personas have an alias
374 * available. Pick the UID from the first persona in the list. */
375 alias = this._personas.data.uid;
376 warning ("No aliases available for individual; using UID instead: %s",
380 /* only notify if the value has changed */
381 if (this.alias != alias)
384 if (this.capabilities != caps)
385 this.capabilities = caps;
387 this.update_groups ();
388 this.update_presence ();
389 this.update_is_favourite ();
390 this.update_avatar ();
393 private void update_groups ()
395 var new_groups = new HashTable<string, bool> (str_hash, str_equal);
397 /* this._groups is null during initial construction */
398 if (this._groups == null)
399 this._groups = new HashTable<string, bool> (str_hash, str_equal);
401 /* FIXME: this should partition the personas by store (maybe we should
402 * keep that mapping in general in this class), and execute
403 * "groups-changed" on the store (with the set of personas), to allow the
404 * back-end to optimize it (like Telepathy will for MembersChanged for the
405 * groups channel list) */
406 this._personas.foreach ((p) =>
410 var persona = (Groups) p;
412 persona.groups.foreach ((k, v) =>
414 new_groups.insert ((string) k, true);
419 new_groups.foreach ((k, v) =>
421 var group = (string) k;
422 if (this._groups.lookup (group) != true)
424 this._groups.insert (group, true);
425 this._groups.foreach ((k, v) =>
431 this.group_changed (group, true);
435 /* buffer the removals, so we don't remove while iterating */
436 var removes = new GLib.List<string> ();
437 this._groups.foreach ((k, v) =>
439 var group = (string) k;
440 if (new_groups.lookup (group) != true)
441 removes.prepend (group);
444 removes.foreach ((l) =>
446 var group = (string) l;
447 this._groups.remove (group);
448 this.group_changed (group, false);
452 private void update_presence ()
454 var presence_message = "";
455 var presence_type = Folks.PresenceType.UNSET;
457 /* Choose the most available presence from our personas */
458 this._personas.foreach ((p) =>
462 var presence = (Presence) p;
464 if (Presence.typecmp (presence.presence_type, presence_type) > 0)
466 presence_type = presence.presence_type;
467 presence_message = presence.presence_message;
472 if (presence_message == null)
473 presence_message = "";
475 /* only notify if the value has changed */
476 if (this.presence_message != presence_message)
477 this.presence_message = presence_message;
479 if (this.presence_type != presence_type)
480 this.presence_type = presence_type;
483 private void update_is_favourite ()
485 bool favourite = false;
487 this._personas.foreach ((p) =>
489 if (favourite == false && p is Favourite)
491 favourite = ((Favourite) p).is_favourite;
492 if (favourite == true)
497 /* Only notify if the value has changed */
498 if (this._is_favourite != favourite)
499 this._is_favourite = favourite;
502 private void update_avatar ()
506 this._personas.foreach ((p) =>
508 if (avatar == null && p is Avatar)
510 avatar = ((Avatar) p).avatar;
515 /* only notify if the value has changed */
516 if (this.avatar != avatar)
517 this.avatar = avatar;
521 * Get a bitmask of the capabilities of this Individual.
523 * The capabilities is the union of the sets of capabilities of all the
524 * {@link Persona}s in the Individual.
526 * @return bitmask of the Individual's capabilities
528 public CapabilitiesFlags get_capabilities ()
530 return this.capabilities;
534 * GLib/C convenience functions (for built-in casting, etc.)
538 * Get the Individual's alias.
540 * The alias is a user-chosen name for the Individual; how the user wants that
541 * Individual to be represented in UIs.
543 * @return the Individual's alias
545 public unowned string get_alias ()
551 * Get a mapping of group ID to whether the Individual is a member.
553 * Freeform group IDs are mapped to a boolean which is `true` if the
554 * Individual is a member of the group, and `false` otherwise.
556 * @return a mapping of group ID to membership status
558 public HashTable<string, bool> get_groups ()
565 * Get the Individual's current presence message.
567 * The presence message returned is from the same {@link Persona} which
568 * provided the presence type returned by
569 * {@link Individual.get_presence_type}.
571 * If none of the {@link Persona}s in the Individual have a presence message
572 * set, an empty string is returned.
574 * @return the Individual's presence message
576 public unowned string get_presence_message ()
578 return this.presence_message;
582 * Get the Individual's current presence type.
584 * The presence type returned is from the same {@link Persona} which provided
585 * the presence message returned by {@link Individual.get_presence_message}.
587 * If none of the {@link Persona}s in the Individual have a presence type set,
588 * {@link PresenceType.UNSET} is returned.
590 * @return the Individual's presence type
592 public Folks.PresenceType get_presence_type ()
594 return this.presence_type;
598 * Whether the Individual is online.
600 * This will be `true` if any of the Individual's {@link Persona}s have a
601 * presence type higher than {@link PresenceType.OFFLINE}, as determined by
602 * {@link Presence.typecmp}.
604 * @return `true` if the Individual is online, `false` otherwise
606 public bool is_online ()
609 return p.is_online ();