Add some comments to the code
[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 public class Folks.Individual : Object, Alias, Avatar, Capabilities, Groups,
26        Presence
27 {
28   private HashTable<string, bool> _groups;
29   private GLib.List<Persona> _personas;
30   private HashTable<PersonaStore, HashSet<Persona>> stores;
31
32   /* XXX: should setting this push it down into the Persona (to foward along to
33    * the actual store if possible?) */
34   public string alias { get; set; }
35   public File avatar { get; set; }
36   public CapabilitiesFlags capabilities { get; private set; }
37   public string id { get; private set; }
38   public Folks.PresenceType presence_type { get; private set; }
39   public string presence_message { get; private set; }
40
41   /* the last of this individuals personas has been removed, so it is invalid */
42   public signal void removed ();
43
44   public HashTable<string, bool> groups
45     {
46       get { return this._groups; }
47
48       /* Propagate the list of new groups to every Persona in the individual
49        * which implements the Groups interface */
50       set
51         {
52           this._personas.foreach ((p) =>
53             {
54               if (p is Groups)
55                 ((Groups) p).groups = value;
56             });
57         }
58     }
59
60   public GLib.List<Persona> personas
61     {
62       get { return this._personas; }
63
64       set
65         {
66           /* Disconnect from all our previous personas */
67           this._personas.foreach ((p) =>
68             {
69               var persona = (Persona) p;
70               var groups = (p is Groups) ? (Groups) p : null;
71
72               persona.notify["avatar"].disconnect (this.notify_avatar_cb);
73               persona.notify["presence-message"].disconnect (
74                   this.notify_presence_cb);
75               persona.notify["presence-type"].disconnect (
76                   this.notify_presence_cb);
77               groups.group_changed.disconnect (this.persona_group_changed_cb);
78             });
79
80           this._personas = value.copy ();
81
82           /* TODO: base this upon our ID in permanent storage, once we have that
83            */
84           if (this.id == null && this._personas.data != null)
85             this.id = this._personas.data.iid;
86
87           /* Connect to all the new personas */
88           this._personas.foreach ((p) =>
89             {
90               var persona = (Persona) p;
91               var groups = (p is Groups) ? (Groups) p : null;
92
93               persona.notify["avatar"].connect (this.notify_avatar_cb);
94               persona.notify["presence-message"].connect (
95                   this.notify_presence_cb);
96               persona.notify["presence-type"].connect (this.notify_presence_cb);
97               groups.group_changed.connect (this.persona_group_changed_cb);
98             });
99
100           /* Update our aggregated fields and notify the changes */
101           this.update_fields ();
102         }
103     }
104
105   private void notify_avatar_cb (Object obj, ParamSpec ps)
106     {
107       this.update_avatar ();
108     }
109
110   private void persona_group_changed_cb (string group, bool is_member)
111     {
112       this.change_group (group, is_member);
113       this.update_groups ();
114     }
115
116   public void change_group (string group, bool is_member)
117     {
118       this._personas.foreach ((p) =>
119         {
120           if (p is Groups)
121             ((Groups) p).change_group (group, is_member);
122         });
123
124       /* don't notify, since it hasn't happened in the persona backing stores
125        * yet; react to that directly */
126     }
127
128   private void notify_presence_cb (Object obj, ParamSpec ps)
129     {
130       this.update_presence ();
131     }
132
133   public Individual (GLib.List<Persona>? personas)
134     {
135       Object (personas: personas);
136
137       this.stores = new HashTable<PersonaStore, HashSet<Persona>> (direct_hash,
138           direct_equal);
139       this.stores_update ();
140     }
141
142   private void stores_update ()
143     {
144       this._personas.foreach ((p) =>
145         {
146           var persona = (Persona) p;
147           var store_is_new = false;
148           var persona_set = this.stores.lookup (persona.store);
149           if (persona_set == null)
150             {
151               persona_set = new HashSet<Persona> (direct_hash, direct_equal);
152               store_is_new = true;
153             }
154
155           persona_set.add (persona);
156
157           if (store_is_new)
158             {
159               this.stores.insert (persona.store, persona_set);
160
161               persona.store.removed.connect (this.store_removed_cb);
162             }
163         });
164     }
165
166   private void store_removed_cb (PersonaStore store)
167     {
168       var persona_set = this.stores.lookup (store);
169       if (persona_set != null)
170         {
171           foreach (var persona in persona_set)
172             {
173               this._personas.remove (persona);
174             }
175         }
176       if (store != null)
177         this.stores.remove (store);
178
179       if (this._personas.length () < 1 || this.stores.size () < 1)
180         {
181           this.removed ();
182           return;
183         }
184
185       this.update_fields ();
186     }
187
188   private void update_fields ()
189     {
190       /* gather the first occurrence of each field */
191       string alias = null;
192       var caps = CapabilitiesFlags.NONE;
193       this._personas.foreach ((persona) =>
194         {
195           var p = (Persona) persona;
196
197           /* FIXME: also check to see if alias is just whitespace */
198           if (alias == null)
199             alias = p.alias;
200
201           caps |= p.capabilities;
202         });
203
204       if (alias == null)
205         {
206           /* FIXME: pick a UID or similar instead */
207           alias = "Name Unknown";
208         }
209
210       /* only notify if the value has changed */
211       if (this.alias != alias)
212         this.alias = alias;
213
214       if (this.capabilities != caps)
215         this.capabilities = caps;
216
217       this.update_groups ();
218       this.update_presence ();
219       this.update_avatar ();
220     }
221
222   private void update_groups ()
223     {
224       var new_groups = new HashTable<string, bool> (str_hash, str_equal);
225
226       /* this._groups is null during initial construction */
227       if (this._groups == null)
228         this._groups = new HashTable<string, bool> (str_hash, str_equal);
229
230       /* FIXME: this should partition the personas by store (maybe we should
231        * keep that mapping in general in this class), and execute
232        * "groups-changed" on the store (with the set of personas), to allow the
233        * back-end to optimize it (like Telepathy will for MembersChanged for the
234        * groups channel list) */
235       this._personas.foreach ((p) =>
236         {
237           if (p is Groups)
238             {
239               var persona = (Groups) p;
240
241               persona.groups.foreach ((k, v) =>
242                 {
243                   new_groups.insert ((string) k, true);
244                 });
245             }
246         });
247
248       new_groups.foreach ((k, v) =>
249         {
250           var group = (string) k;
251           if (this._groups.lookup (group) != true)
252             {
253               this._groups.insert (group, true);
254               this._groups.foreach ((k, v) =>
255                 {
256                   var g = (string) k;
257                   debug ("   %s", g);
258                 });
259
260               this.group_changed (group, true);
261             }
262         });
263
264       /* buffer the removals, so we don't remove while iterating */
265       var removes = new GLib.List<string> ();
266       this._groups.foreach ((k, v) =>
267         {
268           var group = (string) k;
269           if (new_groups.lookup (group) != true)
270             removes.prepend (group);
271         });
272
273       removes.foreach ((l) =>
274         {
275           var group = (string) l;
276           this._groups.remove (group);
277           this.group_changed (group, false);
278         });
279     }
280
281   private void update_presence ()
282     {
283       var presence_message = "";
284       var presence_type = Folks.PresenceType.UNSET;
285
286       /* Choose the most available presence from our personas */
287       this._personas.foreach ((p) =>
288         {
289           var persona = (Persona) p;
290
291           if (presence_message == null || presence_message == "")
292             presence_message = persona.presence_message;
293
294           if (Presence.typecmp (persona.presence_type, presence_type) > 0)
295             presence_type = persona.presence_type;
296         });
297
298       if (presence_message == null)
299         presence_message = "";
300
301       /* only notify if the value has changed */
302       if (this.presence_message != presence_message)
303         this.presence_message = presence_message;
304
305       if (this.presence_type != presence_type)
306         this.presence_type = presence_type;
307     }
308
309   private void update_avatar ()
310     {
311       File avatar = null;
312
313       this._personas.foreach ((p) =>
314         {
315           var persona = (Persona) p;
316
317           if (avatar == null)
318             {
319               avatar = persona.avatar;
320               return;
321             }
322         });
323
324       /* only notify if the value has changed */
325       if (this.avatar != avatar)
326         this.avatar = avatar;
327     }
328
329   public CapabilitiesFlags get_capabilities ()
330     {
331       return this.capabilities;
332     }
333
334   /*
335    * GLib/C convenience functions (for built-in casting, etc.)
336    */
337   public unowned string get_alias ()
338     {
339       return this.alias;
340     }
341
342   public HashTable<string, bool> get_groups ()
343     {
344       Groups g = this;
345       return g.groups;
346     }
347
348   public string get_presence_message ()
349     {
350       return this.presence_message;
351     }
352
353   public Folks.PresenceType get_presence_type ()
354     {
355       return this.presence_type;
356     }
357
358   public bool is_online ()
359     {
360       Presence p = this;
361       return p.is_online ();
362     }
363 }