Add documentation for every public signal, property and member
[platform/upstream/folks.git] / folks / individual.vala
1 /*
2  * Copyright (C) 2010 Collabora Ltd.
3  *
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.
8  *
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.
13  *
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/>.
16  *
17  * Authors:
18  *       Travis Reitter <travis.reitter@collabora.co.uk>
19  */
20
21 using Gee;
22 using GLib;
23 using Folks;
24
25 /**
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.
28  */
29 public class Folks.Individual : Object, Alias, Avatar, Capabilities, Groups,
30        Presence
31 {
32   private HashTable<string, bool> _groups;
33   private GLib.List<Persona> _personas;
34   private HashTable<PersonaStore, HashSet<Persona>> stores;
35
36   /* XXX: should setting this push it down into the Persona (to foward along to
37    * the actual store if possible?) */
38   /**
39    * {@inheritDoc}
40    */
41   public string alias { get; set; }
42
43   /**
44    * {@inheritDoc}
45    */
46   public File avatar { get; set; }
47
48   /**
49    * {@inheritDoc}
50    */
51   public CapabilitiesFlags capabilities { get; private set; }
52
53   /**
54    * {@inheritDoc}
55    */
56   public Folks.PresenceType presence_type { get; private set; }
57
58   /**
59    * {@inheritDoc}
60    */
61   public string presence_message { get; private set; }
62
63   /**
64    * A unique identifier for the Individual.
65    *
66    * This uniquely identifies the Individual, and persists across
67    * {@link IndividualAggregator} instances.
68    *
69    * FIXME: Will this.id actually be the persistent ID for storage?
70    */
71   public string id { get; private set; }
72
73   /**
74    * Emitted when the last of the Individual's {@link Persona}s has been
75    * removed.
76    *
77    * At this point, the Individual is invalid, so any client referencing it
78    * should unreference it and remove it from their UI.
79    */
80   public signal void removed ();
81
82   /**
83    * {@inheritDoc}
84    */
85   public HashTable<string, bool> groups
86     {
87       get { return this._groups; }
88
89       /* Propagate the list of new groups to every Persona in the individual
90        * which implements the Groups interface */
91       set
92         {
93           this._personas.foreach ((p) =>
94             {
95               if (p is Groups)
96                 ((Groups) p).groups = value;
97             });
98         }
99     }
100
101   /**
102    * The set of {@link Persona}s encapsulated by this Individual.
103    *
104    * Changing the set of personas may cause updates to the aggregated properties
105    * provided by the Individual, resulting in property notifications for them.
106    */
107   public GLib.List<Persona> personas
108     {
109       get { return this._personas; }
110
111       set
112         {
113           /* Disconnect from all our previous personas */
114           this._personas.foreach ((p) =>
115             {
116               var persona = (Persona) p;
117               var groups = (p is Groups) ? (Groups) p : null;
118
119               persona.notify["avatar"].disconnect (this.notify_avatar_cb);
120               persona.notify["presence-message"].disconnect (
121                   this.notify_presence_cb);
122               persona.notify["presence-type"].disconnect (
123                   this.notify_presence_cb);
124               groups.group_changed.disconnect (this.persona_group_changed_cb);
125             });
126
127           this._personas = value.copy ();
128
129           /* If all the personas have been removed, remove the individual */
130           if (this._personas.length () < 1)
131             {
132               this.removed ();
133               return;
134             }
135
136           /* TODO: base this upon our ID in permanent storage, once we have that
137            */
138           if (this.id == null && this._personas.data != null)
139             this.id = this._personas.data.iid;
140
141           /* Connect to all the new personas */
142           this._personas.foreach ((p) =>
143             {
144               var persona = (Persona) p;
145               var groups = (p is Groups) ? (Groups) p : null;
146
147               persona.notify["avatar"].connect (this.notify_avatar_cb);
148               persona.notify["presence-message"].connect (
149                   this.notify_presence_cb);
150               persona.notify["presence-type"].connect (this.notify_presence_cb);
151               groups.group_changed.connect (this.persona_group_changed_cb);
152             });
153
154           /* Update our aggregated fields and notify the changes */
155           this.update_fields ();
156         }
157     }
158
159   private void notify_avatar_cb (Object obj, ParamSpec ps)
160     {
161       this.update_avatar ();
162     }
163
164   private void persona_group_changed_cb (string group, bool is_member)
165     {
166       this.change_group (group, is_member);
167       this.update_groups ();
168     }
169
170   /**
171    * Add or remove the Individual from the specified group.
172    *
173    * If `is_member` is `true`, the Individual will be added to the `group`. If
174    * it is `false`, they will be removed from the `group`.
175    *
176    * The group membership change will propagate to every {@link Persona} in
177    * the Individual.
178    *
179    * @param group a freeform group identifier
180    * @param is_member whether the Individual should be a member of the group
181    */
182   public void change_group (string group, bool is_member)
183     {
184       this._personas.foreach ((p) =>
185         {
186           if (p is Groups)
187             ((Groups) p).change_group (group, is_member);
188         });
189
190       /* don't notify, since it hasn't happened in the persona backing stores
191        * yet; react to that directly */
192     }
193
194   private void notify_presence_cb (Object obj, ParamSpec ps)
195     {
196       this.update_presence ();
197     }
198
199   /**
200    * Create a new Individual.
201    *
202    * The Individual can optionally be seeded with the {@link Persona}s in
203    * `personas`. Otherwise, it will have to have personas added using the
204    * {@link Folks.Individual.personas} property after construction.
205    *
206    * @return a new Individual
207    */
208   public Individual (GLib.List<Persona>? personas)
209     {
210       Object (personas: personas);
211
212       this.stores = new HashTable<PersonaStore, HashSet<Persona>> (direct_hash,
213           direct_equal);
214       this.stores_update ();
215     }
216
217   private void stores_update ()
218     {
219       this._personas.foreach ((p) =>
220         {
221           var persona = (Persona) p;
222           var store_is_new = false;
223           var persona_set = this.stores.lookup (persona.store);
224           if (persona_set == null)
225             {
226               persona_set = new HashSet<Persona> (direct_hash, direct_equal);
227               store_is_new = true;
228             }
229
230           persona_set.add (persona);
231
232           if (store_is_new)
233             {
234               this.stores.insert (persona.store, persona_set);
235
236               persona.store.removed.connect (this.store_removed_cb);
237               persona.store.personas_removed.connect (
238                 this.store_personas_removed_cb);
239             }
240         });
241     }
242
243   private void store_removed_cb (PersonaStore store)
244     {
245       var persona_set = this.stores.lookup (store);
246       if (persona_set != null)
247         {
248           foreach (var persona in persona_set)
249             {
250               this._personas.remove (persona);
251             }
252         }
253       if (store != null)
254         this.stores.remove (store);
255
256       if (this._personas.length () < 1 || this.stores.size () < 1)
257         {
258           this.removed ();
259           return;
260         }
261
262       this.update_fields ();
263     }
264
265   private void store_personas_removed_cb (PersonaStore store,
266       GLib.List<Persona> personas)
267     {
268       personas.foreach ((data) =>
269         {
270           this._personas.remove ((Persona) data);
271         });
272
273       if (this._personas.length () < 1)
274         {
275           this.removed ();
276           return;
277         }
278
279       this.update_fields ();
280     }
281
282   private void update_fields ()
283     {
284       /* Gather the first occurrence of each field. We assume that there is
285        * at least one persona in the list, since the Individual should've been
286        * destroyed before now otherwise. */
287       string alias = null;
288       var caps = CapabilitiesFlags.NONE;
289       this._personas.foreach ((persona) =>
290         {
291           var p = (Persona) persona;
292
293           /* FIXME: also check to see if alias is just whitespace */
294           if (alias == null)
295             alias = p.alias;
296
297           caps |= p.capabilities;
298         });
299
300       if (alias == null)
301         {
302           /* We have to pick a UID, since none of the personas have an alias
303            * available. Pick the UID from the first persona in the list. */
304           alias = this._personas.data.uid;
305           warning ("No aliases available for individual; using UID instead: %s",
306                    alias);
307         }
308
309       /* only notify if the value has changed */
310       if (this.alias != alias)
311         this.alias = alias;
312
313       if (this.capabilities != caps)
314         this.capabilities = caps;
315
316       this.update_groups ();
317       this.update_presence ();
318       this.update_avatar ();
319     }
320
321   private void update_groups ()
322     {
323       var new_groups = new HashTable<string, bool> (str_hash, str_equal);
324
325       /* this._groups is null during initial construction */
326       if (this._groups == null)
327         this._groups = new HashTable<string, bool> (str_hash, str_equal);
328
329       /* FIXME: this should partition the personas by store (maybe we should
330        * keep that mapping in general in this class), and execute
331        * "groups-changed" on the store (with the set of personas), to allow the
332        * back-end to optimize it (like Telepathy will for MembersChanged for the
333        * groups channel list) */
334       this._personas.foreach ((p) =>
335         {
336           if (p is Groups)
337             {
338               var persona = (Groups) p;
339
340               persona.groups.foreach ((k, v) =>
341                 {
342                   new_groups.insert ((string) k, true);
343                 });
344             }
345         });
346
347       new_groups.foreach ((k, v) =>
348         {
349           var group = (string) k;
350           if (this._groups.lookup (group) != true)
351             {
352               this._groups.insert (group, true);
353               this._groups.foreach ((k, v) =>
354                 {
355                   var g = (string) k;
356                   debug ("   %s", g);
357                 });
358
359               this.group_changed (group, true);
360             }
361         });
362
363       /* buffer the removals, so we don't remove while iterating */
364       var removes = new GLib.List<string> ();
365       this._groups.foreach ((k, v) =>
366         {
367           var group = (string) k;
368           if (new_groups.lookup (group) != true)
369             removes.prepend (group);
370         });
371
372       removes.foreach ((l) =>
373         {
374           var group = (string) l;
375           this._groups.remove (group);
376           this.group_changed (group, false);
377         });
378     }
379
380   private void update_presence ()
381     {
382       var presence_message = "";
383       var presence_type = Folks.PresenceType.UNSET;
384
385       /* Choose the most available presence from our personas */
386       this._personas.foreach ((p) =>
387         {
388           var persona = (Persona) p;
389
390           if (Presence.typecmp (persona.presence_type, presence_type) > 0)
391             {
392               presence_type = persona.presence_type;
393               presence_message = persona.presence_message;
394             }
395         });
396
397       if (presence_message == null)
398         presence_message = "";
399
400       /* only notify if the value has changed */
401       if (this.presence_message != presence_message)
402         this.presence_message = presence_message;
403
404       if (this.presence_type != presence_type)
405         this.presence_type = presence_type;
406     }
407
408   private void update_avatar ()
409     {
410       File avatar = null;
411
412       this._personas.foreach ((p) =>
413         {
414           var persona = (Persona) p;
415
416           if (avatar == null)
417             {
418               avatar = persona.avatar;
419               return;
420             }
421         });
422
423       /* only notify if the value has changed */
424       if (this.avatar != avatar)
425         this.avatar = avatar;
426     }
427
428   /**
429    * Get a bitmask of the capabilities of this Individual.
430    *
431    * The capabilities is the union of the sets of capabilities of all the
432    * {@link Persona}s in the Individual.
433    *
434    * @return bitmask of the Individual's capabilities
435    */
436   public CapabilitiesFlags get_capabilities ()
437     {
438       return this.capabilities;
439     }
440
441   /*
442    * GLib/C convenience functions (for built-in casting, etc.)
443    */
444
445   /**
446    * Get the Individual's alias.
447    *
448    * The alias is a user-chosen name for the Individual; how the user wants that
449    * Individual to be represented in UIs.
450    *
451    * @return the Individual's alias
452    */
453   public unowned string get_alias ()
454     {
455       return this.alias;
456     }
457
458   /**
459    * Get a mapping of group ID to whether the Individual is a member.
460    *
461    * Freeform group IDs are mapped to a boolean which is `true` if the
462    * Individual is a member of the group, and `false` otherwise.
463    *
464    * @return a mapping of group ID to membership status
465    */
466   public HashTable<string, bool> get_groups ()
467     {
468       Groups g = this;
469       return g.groups;
470     }
471
472   /**
473    * Get the Individual's current presence message.
474    *
475    * The presence message returned is from the same {@link Persona} which
476    * provided the presence type returned by
477    * {@link Individual.get_presence_type}.
478    *
479    * If none of the {@link Persona}s in the Individual have a presence message
480    * set, an empty string is returned.
481    *
482    * @return the Individual's presence message
483    */
484   public unowned string get_presence_message ()
485     {
486       return this.presence_message;
487     }
488
489   /**
490    * Get the Individual's current presence type.
491    *
492    * The presence type returned is from the same {@link Persona} which provided
493    * the presence message returned by {@link Individual.get_presence_message}.
494    *
495    * If none of the {@link Persona}s in the Individual have a presence type set,
496    * {@link PresenceType.UNSET} is returned.
497    *
498    * @return the Individual's presence type
499    */
500   public Folks.PresenceType get_presence_type ()
501     {
502       return this.presence_type;
503     }
504
505   /**
506    * Whether the Individual is online.
507    *
508    * This will be `true` if any of the Individual's {@link Persona}s have a
509    * presence type higher than {@link PresenceType.OFFLINE}, as determined by
510    * {@link Presence.typecmp}.
511    *
512    * @return `true` if the Individual is online, `false` otherwise
513    */
514   public bool is_online ()
515     {
516       Presence p = this;
517       return p.is_online ();
518     }
519 }