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