Pass changes to Personas' aliases on to Telepathy
[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,
30     Alias,
31     Avatar,
32     Capabilities,
33     Favourite,
34     Groups,
35     Presence
36 {
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;
42
43   /**
44    * {@inheritDoc}
45    */
46   public File avatar { get; private 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 string alias
86     {
87       get { return this._alias; }
88
89       set
90         {
91           if (this._alias == value)
92             return;
93
94           this._alias = value;
95           this._personas.foreach ((p) =>
96             {
97               if (p is Alias)
98                 ((Alias) p).alias = value;
99             });
100         }
101     }
102
103   /**
104    * Whether this Individual is a user-defined favourite.
105    *
106    * This property is `true` if any of this Individual's {@link Persona}s are
107    * favourites).
108    *
109    * When set, the value is propagated to all of this Individual's
110    * {@link Persona}s.
111    */
112   public bool is_favourite
113     {
114       get { return this._is_favourite; }
115
116       /* Propagate the new favourite status to every Persona, but only if it's
117        * changed. */
118       set
119         {
120           if (this._is_favourite == value)
121             return;
122
123           this._is_favourite = value;
124           this._personas.foreach ((p) =>
125             {
126               if (p is Favourite)
127                 ((Favourite) p).is_favourite = value;
128             });
129         }
130     }
131
132   /**
133    * {@inheritDoc}
134    */
135   public HashTable<string, bool> groups
136     {
137       get { return this._groups; }
138
139       /* Propagate the list of new groups to every Persona in the individual
140        * which implements the Groups interface */
141       set
142         {
143           this._personas.foreach ((p) =>
144             {
145               if (p is Groups)
146                 ((Groups) p).groups = value;
147             });
148         }
149     }
150
151   /**
152    * The set of {@link Persona}s encapsulated by this Individual.
153    *
154    * Changing the set of personas may cause updates to the aggregated properties
155    * provided by the Individual, resulting in property notifications for them.
156    */
157   public GLib.List<Persona> personas
158     {
159       get { return this._personas; }
160
161       set
162         {
163           /* Disconnect from all our previous personas */
164           this._personas.foreach ((p) =>
165             {
166               var persona = (Persona) p;
167               var groups = (p is Groups) ? (Groups) p : null;
168
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);
177             });
178
179           this._personas = new GLib.List<Persona> ();
180           value.foreach ((l) =>
181             {
182               this._personas.prepend ((Persona) l);
183             });
184           this._personas.reverse ();
185
186           /* If all the personas have been removed, remove the individual */
187           if (this._personas.length () < 1)
188             {
189               this.removed ();
190               return;
191             }
192
193           /* TODO: base this upon our ID in permanent storage, once we have that
194            */
195           if (this.id == null && this._personas.data != null)
196             this.id = this._personas.data.iid;
197
198           /* Connect to all the new personas */
199           this._personas.foreach ((p) =>
200             {
201               var persona = (Persona) p;
202               var groups = (p is Groups) ? (Groups) p : null;
203
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);
211             });
212
213           /* Update our aggregated fields and notify the changes */
214           this.update_fields ();
215         }
216     }
217
218   private void notify_avatar_cb (Object obj, ParamSpec ps)
219     {
220       this.update_avatar ();
221     }
222
223   private void persona_group_changed_cb (string group, bool is_member)
224     {
225       this.change_group (group, is_member);
226       this.update_groups ();
227     }
228
229   /**
230    * Add or remove the Individual from the specified group.
231    *
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`.
234    *
235    * The group membership change will propagate to every {@link Persona} in
236    * the Individual.
237    *
238    * @param group a freeform group identifier
239    * @param is_member whether the Individual should be a member of the group
240    */
241   public void change_group (string group, bool is_member)
242     {
243       this._personas.foreach ((p) =>
244         {
245           if (p is Groups)
246             ((Groups) p).change_group (group, is_member);
247         });
248
249       /* don't notify, since it hasn't happened in the persona backing stores
250        * yet; react to that directly */
251     }
252
253   private void notify_presence_cb (Object obj, ParamSpec ps)
254     {
255       this.update_presence ();
256     }
257
258   private void notify_is_favourite_cb (Object obj, ParamSpec ps)
259     {
260       this.update_is_favourite ();
261     }
262
263   /**
264    * Create a new Individual.
265    *
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.
269    *
270    * @return a new Individual
271    */
272   public Individual (GLib.List<Persona>? personas)
273     {
274       Object (personas: personas);
275
276       this.stores = new HashTable<PersonaStore, HashSet<Persona>> (direct_hash,
277           direct_equal);
278       this.stores_update ();
279     }
280
281   private void stores_update ()
282     {
283       this._personas.foreach ((p) =>
284         {
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)
289             {
290               persona_set = new HashSet<Persona> (direct_hash, direct_equal);
291               store_is_new = true;
292             }
293
294           persona_set.add (persona);
295
296           if (store_is_new)
297             {
298               this.stores.insert (persona.store, persona_set);
299
300               persona.store.removed.connect (this.store_removed_cb);
301               persona.store.personas_removed.connect (
302                 this.store_personas_removed_cb);
303             }
304         });
305     }
306
307   private void store_removed_cb (PersonaStore store)
308     {
309       var persona_set = this.stores.lookup (store);
310       if (persona_set != null)
311         {
312           foreach (var persona in persona_set)
313             {
314               this._personas.remove (persona);
315             }
316         }
317       if (store != null)
318         this.stores.remove (store);
319
320       if (this._personas.length () < 1 || this.stores.size () < 1)
321         {
322           this.removed ();
323           return;
324         }
325
326       this.update_fields ();
327     }
328
329   private void store_personas_removed_cb (PersonaStore store,
330       GLib.List<Persona> personas)
331     {
332       var persona_set = this.stores.lookup (store);
333       personas.foreach ((data) =>
334         {
335           var p = (Persona) data;
336
337           persona_set.remove (p);
338           this._personas.remove (p);
339         });
340
341       if (this._personas.length () < 1)
342         {
343           this.removed ();
344           return;
345         }
346
347       this.update_fields ();
348     }
349
350   private void update_fields ()
351     {
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. */
355       string alias = null;
356       var caps = CapabilitiesFlags.NONE;
357       this._personas.foreach ((p) =>
358         {
359           if (p is Alias)
360             {
361               var a = (Alias) p;
362
363               if (alias == null || alias.strip () == "")
364                 alias = a.alias;
365             }
366
367           if (p is Capabilities)
368             caps |= ((Capabilities) p).capabilities;
369         });
370
371       if (alias == null)
372         {
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",
377                    alias);
378         }
379
380       /* only notify if the value has changed */
381       if (this.alias != alias)
382         this.alias = alias;
383
384       if (this.capabilities != caps)
385         this.capabilities = caps;
386
387       this.update_groups ();
388       this.update_presence ();
389       this.update_is_favourite ();
390       this.update_avatar ();
391     }
392
393   private void update_groups ()
394     {
395       var new_groups = new HashTable<string, bool> (str_hash, str_equal);
396
397       /* this._groups is null during initial construction */
398       if (this._groups == null)
399         this._groups = new HashTable<string, bool> (str_hash, str_equal);
400
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) =>
407         {
408           if (p is Groups)
409             {
410               var persona = (Groups) p;
411
412               persona.groups.foreach ((k, v) =>
413                 {
414                   new_groups.insert ((string) k, true);
415                 });
416             }
417         });
418
419       new_groups.foreach ((k, v) =>
420         {
421           var group = (string) k;
422           if (this._groups.lookup (group) != true)
423             {
424               this._groups.insert (group, true);
425               this._groups.foreach ((k, v) =>
426                 {
427                   var g = (string) k;
428                   debug ("   %s", g);
429                 });
430
431               this.group_changed (group, true);
432             }
433         });
434
435       /* buffer the removals, so we don't remove while iterating */
436       var removes = new GLib.List<string> ();
437       this._groups.foreach ((k, v) =>
438         {
439           var group = (string) k;
440           if (new_groups.lookup (group) != true)
441             removes.prepend (group);
442         });
443
444       removes.foreach ((l) =>
445         {
446           var group = (string) l;
447           this._groups.remove (group);
448           this.group_changed (group, false);
449         });
450     }
451
452   private void update_presence ()
453     {
454       var presence_message = "";
455       var presence_type = Folks.PresenceType.UNSET;
456
457       /* Choose the most available presence from our personas */
458       this._personas.foreach ((p) =>
459         {
460           if (p is Presence)
461             {
462               var presence = (Presence) p; 
463
464               if (Presence.typecmp (presence.presence_type, presence_type) > 0)
465                 {
466                   presence_type = presence.presence_type;
467                   presence_message = presence.presence_message;
468                 }
469             }
470         });
471
472       if (presence_message == null)
473         presence_message = "";
474
475       /* only notify if the value has changed */
476       if (this.presence_message != presence_message)
477         this.presence_message = presence_message;
478
479       if (this.presence_type != presence_type)
480         this.presence_type = presence_type;
481     }
482
483   private void update_is_favourite ()
484     {
485       bool favourite = false;
486
487       this._personas.foreach ((p) =>
488         {
489           if (favourite == false && p is Favourite)
490             {
491               favourite = ((Favourite) p).is_favourite;
492               if (favourite == true)
493                 return;
494             }
495         });
496
497       /* Only notify if the value has changed */
498       if (this._is_favourite != favourite)
499         this._is_favourite = favourite;
500     }
501
502   private void update_avatar ()
503     {
504       File avatar = null;
505
506       this._personas.foreach ((p) =>
507         {
508           if (avatar == null && p is Avatar)
509             {
510               avatar = ((Avatar) p).avatar;
511               return;
512             }
513         });
514
515       /* only notify if the value has changed */
516       if (this.avatar != avatar)
517         this.avatar = avatar;
518     }
519
520   /**
521    * Get a bitmask of the capabilities of this Individual.
522    *
523    * The capabilities is the union of the sets of capabilities of all the
524    * {@link Persona}s in the Individual.
525    *
526    * @return bitmask of the Individual's capabilities
527    */
528   public CapabilitiesFlags get_capabilities ()
529     {
530       return this.capabilities;
531     }
532
533   /*
534    * GLib/C convenience functions (for built-in casting, etc.)
535    */
536
537   /**
538    * Get the Individual's alias.
539    *
540    * The alias is a user-chosen name for the Individual; how the user wants that
541    * Individual to be represented in UIs.
542    *
543    * @return the Individual's alias
544    */
545   public unowned string get_alias ()
546     {
547       return this.alias;
548     }
549
550   /**
551    * Get a mapping of group ID to whether the Individual is a member.
552    *
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.
555    *
556    * @return a mapping of group ID to membership status
557    */
558   public HashTable<string, bool> get_groups ()
559     {
560       Groups g = this;
561       return g.groups;
562     }
563
564   /**
565    * Get the Individual's current presence message.
566    *
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}.
570    *
571    * If none of the {@link Persona}s in the Individual have a presence message
572    * set, an empty string is returned.
573    *
574    * @return the Individual's presence message
575    */
576   public unowned string get_presence_message ()
577     {
578       return this.presence_message;
579     }
580
581   /**
582    * Get the Individual's current presence type.
583    *
584    * The presence type returned is from the same {@link Persona} which provided
585    * the presence message returned by {@link Individual.get_presence_message}.
586    *
587    * If none of the {@link Persona}s in the Individual have a presence type set,
588    * {@link PresenceType.UNSET} is returned.
589    *
590    * @return the Individual's presence type
591    */
592   public Folks.PresenceType get_presence_type ()
593     {
594       return this.presence_type;
595     }
596
597   /**
598    * Whether the Individual is online.
599    *
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}.
603    *
604    * @return `true` if the Individual is online, `false` otherwise
605    */
606   public bool is_online ()
607     {
608       Presence p = this;
609       return p.is_online ();
610     }
611 }