]> git.0d.be Git - empathy.git/blob - libempathy-gtk/empathy-contact-list-store.c
Completely reworked ContactList API. Fixes bug #471611, bug #467280, bug #459540...
[empathy.git] / libempathy-gtk / empathy-contact-list-store.c
1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
3  * Copyright (C) 2005-2007 Imendio AB
4  * Copyright (C) 2007 Collabora Ltd.
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU General Public License as
8  * published by the Free Software Foundation; either version 2 of the
9  * License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14  * General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public
17  * License along with this program; if not, write to the
18  * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
19  * Boston, MA 02111-1307, USA.
20  *
21  * Authors: Mikael Hallendal <micke@imendio.com>
22  *          Martyn Russell <martyn@imendio.com>
23  *          Xavier Claessens <xclaesse@gmail.com>
24  */
25
26 #include "config.h"
27
28 #include <string.h>
29
30 #include <glib.h>
31 #include <gtk/gtk.h>
32
33 #include <libempathy/empathy-debug.h>
34
35 #include "empathy-contact-list-store.h"
36 #include "empathy-contact-groups.h"
37 #include "empathy-ui-utils.h"
38 #include "empathy-gtk-enum-types.h"
39
40 #define DEBUG_DOMAIN "ContactListStore"
41
42 /* Active users are those which have recently changed state
43  * (e.g. online, offline or from normal to a busy state).
44  */
45
46 /* Time user is shown as active */
47 #define ACTIVE_USER_SHOW_TIME 7000
48
49 /* Time after connecting which we wait before active users are enabled */
50 #define ACTIVE_USER_WAIT_TO_ENABLE_TIME 5000
51
52 #define GET_PRIV(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), EMPATHY_TYPE_CONTACT_LIST_STORE, EmpathyContactListStorePriv))
53
54 struct _EmpathyContactListStorePriv {
55         EmpathyContactList         *list;
56         gboolean                    show_offline;
57         gboolean                    show_avatars;
58         gboolean                    is_compact;
59         gboolean                    show_active;
60         EmpathyContactListStoreSort sort_criterium;
61         guint                       inhibit_active;
62
63         EmpathyContactGroupsFunc    get_contact_groups;
64         gpointer                    get_contact_groups_data;
65 };
66
67 typedef struct {
68         GtkTreeIter  iter;
69         const gchar *name;
70         gboolean     found;
71 } FindGroup;
72
73 typedef struct {
74         EmpathyContact *contact;
75         gboolean       found;
76         GList         *iters;
77 } FindContact;
78
79 typedef struct {
80         EmpathyContactListStore *store;
81         EmpathyContact          *contact;
82         gboolean                remove;
83 } ShowActiveData;
84
85 static void             empathy_contact_list_store_class_init         (EmpathyContactListStoreClass *klass);
86 static void             empathy_contact_list_store_init               (EmpathyContactListStore      *list);
87 static void             contact_list_store_finalize                  (GObject                       *object);
88 static void             contact_list_store_get_property              (GObject                       *object,
89                                                                       guint                          param_id,
90                                                                       GValue                        *value,
91                                                                       GParamSpec                    *pspec);
92 static void             contact_list_store_set_property              (GObject                       *object,
93                                                                       guint                          param_id,
94                                                                       const GValue                  *value,
95                                                                       GParamSpec                    *pspec);
96 static void             contact_list_store_setup                     (EmpathyContactListStore       *store);
97 static gboolean         contact_list_store_inibit_active_cb          (EmpathyContactListStore       *store);
98 static void             contact_list_store_members_changed_cb        (EmpathyContactList            *list_iface,
99                                                                       EmpathyContact                *contact,
100                                                                       EmpathyContact                *actor,
101                                                                       guint                          reason,
102                                                                       gchar                         *message,
103                                                                       gboolean                       is_member,
104                                                                       EmpathyContactListStore       *store);
105 static void             contact_list_store_groups_changed_cb         (EmpathyContactList            *list_iface,
106                                                                       EmpathyContact                *contact,
107                                                                       gchar                         *group,
108                                                                       gboolean                       is_member,
109                                                                       EmpathyContactListStore       *store);
110 static void             contact_list_store_add_contact               (EmpathyContactListStore       *store,
111                                                                       EmpathyContact                *contact);
112 static void             contact_list_store_remove_contact            (EmpathyContactListStore       *store,
113                                                                       EmpathyContact                *contact);
114 static void             contact_list_store_contact_update            (EmpathyContactListStore       *store,
115                                                                       EmpathyContact                *contact);
116 static void             contact_list_store_contact_updated_cb        (EmpathyContact                *contact,
117                                                                       GParamSpec                    *param,
118                                                                       EmpathyContactListStore       *store);
119 static void             contact_list_store_contact_set_active        (EmpathyContactListStore       *store,
120                                                                       EmpathyContact                *contact,
121                                                                       gboolean                       active,
122                                                                       gboolean                       set_changed);
123 static ShowActiveData * contact_list_store_contact_active_new        (EmpathyContactListStore       *store,
124                                                                       EmpathyContact                *contact,
125                                                                       gboolean                       remove);
126 static void             contact_list_store_contact_active_free       (ShowActiveData                *data);
127 static gboolean         contact_list_store_contact_active_cb         (ShowActiveData                *data);
128 static gboolean         contact_list_store_get_group_foreach         (GtkTreeModel                  *model,
129                                                                       GtkTreePath                   *path,
130                                                                       GtkTreeIter                   *iter,
131                                                                       FindGroup                     *fg);
132 static void             contact_list_store_get_group                 (EmpathyContactListStore       *store,
133                                                                       const gchar                   *name,
134                                                                       GtkTreeIter                   *iter_group_to_set,
135                                                                       GtkTreeIter                   *iter_separator_to_set,
136                                                                       gboolean                      *created);
137 static gint             contact_list_store_state_sort_func           (GtkTreeModel                  *model,
138                                                                       GtkTreeIter                   *iter_a,
139                                                                       GtkTreeIter                   *iter_b,
140                                                                       gpointer                       user_data);
141 static gint             contact_list_store_name_sort_func            (GtkTreeModel                  *model,
142                                                                       GtkTreeIter                   *iter_a,
143                                                                       GtkTreeIter                   *iter_b,
144                                                                       gpointer                       user_data);
145 static gboolean         contact_list_store_find_contact_foreach      (GtkTreeModel                  *model,
146                                                                       GtkTreePath                   *path,
147                                                                       GtkTreeIter                   *iter,
148                                                                       FindContact                   *fc);
149 static GList *          contact_list_store_find_contact              (EmpathyContactListStore       *store,
150                                                                       EmpathyContact                *contact);
151 static gboolean         contact_list_store_update_list_mode_foreach  (GtkTreeModel                  *model,
152                                                                       GtkTreePath                   *path,
153                                                                       GtkTreeIter                   *iter,
154                                                                       EmpathyContactListStore       *store);
155
156 enum {
157         PROP_0,
158         PROP_SHOW_OFFLINE,
159         PROP_SHOW_AVATARS,
160         PROP_IS_COMPACT,
161         PROP_SORT_CRITERIUM
162 };
163
164 G_DEFINE_TYPE (EmpathyContactListStore, empathy_contact_list_store, GTK_TYPE_TREE_STORE);
165
166 static void
167 empathy_contact_list_store_class_init (EmpathyContactListStoreClass *klass)
168 {
169         GObjectClass *object_class = G_OBJECT_CLASS (klass);
170
171         object_class->finalize = contact_list_store_finalize;
172         object_class->get_property = contact_list_store_get_property;
173         object_class->set_property = contact_list_store_set_property;
174
175         g_object_class_install_property (object_class,
176                                          PROP_SHOW_OFFLINE,
177                                          g_param_spec_boolean ("show-offline",
178                                                                "Show Offline",
179                                                                "Whether contact list should display "
180                                                                "offline contacts",
181                                                                FALSE,
182                                                                G_PARAM_READWRITE));
183          g_object_class_install_property (object_class,
184                                           PROP_SHOW_AVATARS,
185                                           g_param_spec_boolean ("show-avatars",
186                                                                 "Show Avatars",
187                                                                 "Whether contact list should display "
188                                                                 "avatars for contacts",
189                                                                 TRUE,
190                                                                 G_PARAM_READWRITE));
191         g_object_class_install_property (object_class,
192                                          PROP_IS_COMPACT,
193                                          g_param_spec_boolean ("is-compact",
194                                                                "Is Compact",
195                                                                "Whether the contact list is in compact mode or not",
196                                                                FALSE,
197                                                                G_PARAM_READWRITE));
198
199         g_object_class_install_property (object_class,
200                                          PROP_SORT_CRITERIUM,
201                                          g_param_spec_enum ("sort-criterium",
202                                                             "Sort citerium",
203                                                             "The sort criterium to use for sorting the contact list",
204                                                             EMPATHY_TYPE_CONTACT_LIST_STORE_SORT,
205                                                             EMPATHY_CONTACT_LIST_STORE_SORT_NAME,
206                                                             G_PARAM_READWRITE));
207
208         g_type_class_add_private (object_class, sizeof (EmpathyContactListStorePriv));
209 }
210
211 static void
212 empathy_contact_list_store_init (EmpathyContactListStore *store)
213 {
214         EmpathyContactListStorePriv *priv;
215
216         priv = GET_PRIV (store);
217
218         priv->show_avatars = TRUE;
219         priv->inhibit_active = g_timeout_add (ACTIVE_USER_WAIT_TO_ENABLE_TIME,
220                                               (GSourceFunc) contact_list_store_inibit_active_cb,
221                                               store);
222 }
223
224 static void
225 contact_list_store_finalize (GObject *object)
226 {
227         EmpathyContactListStorePriv *priv;
228
229         priv = GET_PRIV (object);
230
231         /* FIXME: disconnect all signals on the list and contacts */
232
233         if (priv->list) {
234                 g_object_unref (priv->list);
235         }
236
237         if (priv->inhibit_active) {
238                 g_source_remove (priv->inhibit_active);
239         }
240
241         G_OBJECT_CLASS (empathy_contact_list_store_parent_class)->finalize (object);
242 }
243
244 static void
245 contact_list_store_get_property (GObject    *object,
246                                  guint       param_id,
247                                  GValue     *value,
248                                  GParamSpec *pspec)
249 {
250         EmpathyContactListStorePriv *priv;
251
252         priv = GET_PRIV (object);
253
254         switch (param_id) {
255         case PROP_SHOW_OFFLINE:
256                 g_value_set_boolean (value, priv->show_offline);
257                 break;
258         case PROP_SHOW_AVATARS:
259                 g_value_set_boolean (value, priv->show_avatars);
260                 break;
261         case PROP_IS_COMPACT:
262                 g_value_set_boolean (value, priv->is_compact);
263                 break;
264         case PROP_SORT_CRITERIUM:
265                 g_value_set_enum (value, priv->sort_criterium);
266                 break;
267         default:
268                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
269                 break;
270         };
271 }
272
273 static void
274 contact_list_store_set_property (GObject      *object,
275                                  guint         param_id,
276                                  const GValue *value,
277                                  GParamSpec   *pspec)
278 {
279         EmpathyContactListStorePriv *priv;
280
281         priv = GET_PRIV (object);
282
283         switch (param_id) {
284         case PROP_SHOW_OFFLINE:
285                 empathy_contact_list_store_set_show_offline (EMPATHY_CONTACT_LIST_STORE (object),
286                                                             g_value_get_boolean (value));
287                 break;
288         case PROP_SHOW_AVATARS:
289                 empathy_contact_list_store_set_show_avatars (EMPATHY_CONTACT_LIST_STORE (object),
290                                                             g_value_get_boolean (value));
291                 break;
292         case PROP_IS_COMPACT:
293                 empathy_contact_list_store_set_is_compact (EMPATHY_CONTACT_LIST_STORE (object),
294                                                           g_value_get_boolean (value));
295                 break;
296         case PROP_SORT_CRITERIUM:
297                 empathy_contact_list_store_set_sort_criterium (EMPATHY_CONTACT_LIST_STORE (object),
298                                                               g_value_get_enum (value));
299                 break;
300         default:
301                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
302                 break;
303         };
304 }
305
306 EmpathyContactListStore *
307 empathy_contact_list_store_new (EmpathyContactList *list_iface)
308 {
309         EmpathyContactListStore     *store;
310         EmpathyContactListStorePriv *priv;
311         GList                      *contacts, *l;
312
313         g_return_val_if_fail (EMPATHY_IS_CONTACT_LIST (list_iface), NULL);
314
315         store = g_object_new (EMPATHY_TYPE_CONTACT_LIST_STORE, NULL);
316         priv = GET_PRIV (store);
317
318         contact_list_store_setup (store);
319         priv->list = g_object_ref (list_iface);
320
321         /* Signal connection. */
322         g_signal_connect (priv->list,
323                           "members-changed",
324                           G_CALLBACK (contact_list_store_members_changed_cb),
325                           store);
326         g_signal_connect (priv->list,
327                           "groups-changed",
328                           G_CALLBACK (contact_list_store_groups_changed_cb),
329                           store);
330
331         /* Add contacts already created. */
332         contacts = empathy_contact_list_get_members (priv->list);
333         for (l = contacts; l; l = l->next) {
334                 contact_list_store_members_changed_cb (priv->list, l->data,
335                                                        NULL, 0, NULL,
336                                                        TRUE,
337                                                        store);
338
339                 g_object_unref (l->data);
340         }
341         g_list_free (contacts);
342
343         return store;
344 }
345
346 EmpathyContactList *
347 empathy_contact_list_store_get_list_iface (EmpathyContactListStore *store)
348 {
349         EmpathyContactListStorePriv *priv;
350
351         g_return_val_if_fail (EMPATHY_IS_CONTACT_LIST_STORE (store), FALSE);
352
353         priv = GET_PRIV (store);
354
355         return priv->list;
356 }
357
358 gboolean
359 empathy_contact_list_store_get_show_offline (EmpathyContactListStore *store)
360 {
361         EmpathyContactListStorePriv *priv;
362
363         g_return_val_if_fail (EMPATHY_IS_CONTACT_LIST_STORE (store), FALSE);
364
365         priv = GET_PRIV (store);
366
367         return priv->show_offline;
368 }
369
370 void
371 empathy_contact_list_store_set_show_offline (EmpathyContactListStore *store,
372                                             gboolean                show_offline)
373 {
374         EmpathyContactListStorePriv *priv;
375         GList                      *contacts, *l;
376         gboolean                    show_active;
377
378         g_return_if_fail (EMPATHY_IS_CONTACT_LIST_STORE (store));
379
380         priv = GET_PRIV (store);
381
382         priv->show_offline = show_offline;
383         show_active = priv->show_active;
384
385         /* Disable temporarily. */
386         priv->show_active = FALSE;
387
388         contacts = empathy_contact_list_get_members (priv->list);
389         for (l = contacts; l; l = l->next) {
390                 contact_list_store_contact_update (store, l->data);
391
392                 g_object_unref (l->data);
393         }
394         g_list_free (contacts);
395
396         /* Restore to original setting. */
397         priv->show_active = show_active;
398 }
399
400 gboolean
401 empathy_contact_list_store_get_show_avatars (EmpathyContactListStore *store)
402 {
403         EmpathyContactListStorePriv *priv;
404
405         g_return_val_if_fail (EMPATHY_IS_CONTACT_LIST_STORE (store), TRUE);
406
407         priv = GET_PRIV (store);
408
409         return priv->show_avatars;
410 }
411
412 void
413 empathy_contact_list_store_set_show_avatars (EmpathyContactListStore *store,
414                                             gboolean                show_avatars)
415 {
416         EmpathyContactListStorePriv *priv;
417         GtkTreeModel               *model;
418
419         g_return_if_fail (EMPATHY_IS_CONTACT_LIST_STORE (store));
420
421         priv = GET_PRIV (store);
422
423         priv->show_avatars = show_avatars;
424
425         model = GTK_TREE_MODEL (store);
426
427         gtk_tree_model_foreach (model,
428                                 (GtkTreeModelForeachFunc)
429                                 contact_list_store_update_list_mode_foreach,
430                                 store);
431 }
432
433 gboolean
434 empathy_contact_list_store_get_is_compact (EmpathyContactListStore *store)
435 {
436         EmpathyContactListStorePriv *priv;
437
438         g_return_val_if_fail (EMPATHY_IS_CONTACT_LIST_STORE (store), TRUE);
439
440         priv = GET_PRIV (store);
441
442         return priv->is_compact;
443 }
444
445 void
446 empathy_contact_list_store_set_is_compact (EmpathyContactListStore *store,
447                                           gboolean                is_compact)
448 {
449         EmpathyContactListStorePriv *priv;
450         GtkTreeModel               *model;
451
452         g_return_if_fail (EMPATHY_IS_CONTACT_LIST_STORE (store));
453
454         priv = GET_PRIV (store);
455
456         priv->is_compact = is_compact;
457
458         model = GTK_TREE_MODEL (store);
459
460         gtk_tree_model_foreach (model,
461                                 (GtkTreeModelForeachFunc)
462                                 contact_list_store_update_list_mode_foreach,
463                                 store);
464 }
465
466 EmpathyContactListStoreSort
467 empathy_contact_list_store_get_sort_criterium (EmpathyContactListStore *store)
468 {
469         EmpathyContactListStorePriv *priv;
470
471         g_return_val_if_fail (EMPATHY_IS_CONTACT_LIST_STORE (store), 0);
472
473         priv = GET_PRIV (store);
474
475         return priv->sort_criterium;
476 }
477
478 void
479 empathy_contact_list_store_set_sort_criterium (EmpathyContactListStore     *store,
480                                               EmpathyContactListStoreSort  sort_criterium)
481 {
482         EmpathyContactListStorePriv *priv;
483
484         g_return_if_fail (EMPATHY_IS_CONTACT_LIST_STORE (store));
485
486         priv = GET_PRIV (store);
487
488         priv->sort_criterium = sort_criterium;
489
490         switch (sort_criterium) {
491         case EMPATHY_CONTACT_LIST_STORE_SORT_STATE:
492                 gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE (store),
493                                                       COL_STATUS,
494                                                       GTK_SORT_ASCENDING);
495                 break;
496                 
497         case EMPATHY_CONTACT_LIST_STORE_SORT_NAME:
498                 gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE (store),
499                                                       COL_NAME,
500                                                       GTK_SORT_ASCENDING);
501                 break;
502         }
503 }
504
505 gboolean
506 empathy_contact_list_store_row_separator_func (GtkTreeModel *model,
507                                               GtkTreeIter  *iter,
508                                               gpointer      data)
509 {
510         gboolean is_separator = FALSE;
511
512         g_return_val_if_fail (GTK_IS_TREE_MODEL (model), FALSE);
513
514         gtk_tree_model_get (model, iter,
515                             COL_IS_SEPARATOR, &is_separator,
516                             -1);
517
518         return is_separator;
519 }
520
521 gchar *
522 empathy_contact_list_store_get_parent_group (GtkTreeModel *model,
523                                             GtkTreePath  *path,
524                                             gboolean     *path_is_group)
525 {
526         GtkTreeIter  parent_iter, iter;
527         gchar       *name = NULL;
528         gboolean     is_group;
529
530         g_return_val_if_fail (GTK_IS_TREE_MODEL (model), NULL);
531
532         if (path_is_group) {
533                 *path_is_group = FALSE;
534         }
535
536         if (!gtk_tree_model_get_iter (model, &iter, path)) {
537                 return NULL;
538         }
539
540         gtk_tree_model_get (model, &iter,
541                             COL_IS_GROUP, &is_group,
542                             COL_NAME, &name,
543                             -1);
544
545         if (!is_group) {
546                 g_free (name);
547                 name = NULL;
548
549                 if (!gtk_tree_model_iter_parent (model, &parent_iter, &iter)) {
550                         return NULL;
551                 }
552
553                 iter = parent_iter;
554
555                 gtk_tree_model_get (model, &iter,
556                                     COL_IS_GROUP, &is_group,
557                                     COL_NAME, &name,
558                                     -1);
559                 if (!is_group) {
560                         g_free (name);
561                         return NULL;
562                 }
563         }
564
565         if (path_is_group) {
566                 *path_is_group = TRUE;
567         }
568
569         return name;
570 }
571
572 gboolean
573 empathy_contact_list_store_search_equal_func (GtkTreeModel *model,
574                                              gint          column,
575                                              const gchar  *key,
576                                              GtkTreeIter  *iter,
577                                              gpointer      search_data)
578 {
579         gchar    *name, *name_folded;
580         gchar    *key_folded;
581         gboolean  ret;
582
583         g_return_val_if_fail (GTK_IS_TREE_MODEL (model), FALSE);
584
585         if (!key) {
586                 return FALSE;
587         }
588
589         gtk_tree_model_get (model, iter, COL_NAME, &name, -1);
590
591         if (!name) {
592                 return FALSE;
593         }
594
595         name_folded = g_utf8_casefold (name, -1);
596         key_folded = g_utf8_casefold (key, -1);
597
598         if (name_folded && key_folded && 
599             strstr (name_folded, key_folded)) {
600                 ret = FALSE;
601         } else {
602                 ret = TRUE;
603         }
604
605         g_free (name);
606         g_free (name_folded);
607         g_free (key_folded);
608
609         return ret;
610 }
611
612 static void
613 contact_list_store_setup (EmpathyContactListStore *store)
614 {
615         EmpathyContactListStorePriv *priv;
616         GType                       types[] = {G_TYPE_STRING,       /* Status icon-name */
617                                                GDK_TYPE_PIXBUF,     /* Avatar pixbuf */
618                                                G_TYPE_BOOLEAN,      /* Avatar pixbuf visible */
619                                                G_TYPE_STRING,       /* Name */
620                                                G_TYPE_STRING,       /* Status string */
621                                                G_TYPE_BOOLEAN,      /* Show status */
622                                                EMPATHY_TYPE_CONTACT, /* Contact type */
623                                                G_TYPE_BOOLEAN,      /* Is group */
624                                                G_TYPE_BOOLEAN,      /* Is active */
625                                                G_TYPE_BOOLEAN,      /* Is online */
626                                                G_TYPE_BOOLEAN};     /* Is separator */
627         
628         priv = GET_PRIV (store);
629
630         gtk_tree_store_set_column_types (GTK_TREE_STORE (store), COL_COUNT, types);
631
632         /* Set up sorting */
633         gtk_tree_sortable_set_sort_func (GTK_TREE_SORTABLE (store),
634                                          COL_NAME,
635                                          contact_list_store_name_sort_func,
636                                          store, NULL);
637         gtk_tree_sortable_set_sort_func (GTK_TREE_SORTABLE (store),
638                                          COL_STATUS,
639                                          contact_list_store_state_sort_func,
640                                          store, NULL);
641
642         priv->sort_criterium = EMPATHY_CONTACT_LIST_STORE_SORT_NAME;
643         empathy_contact_list_store_set_sort_criterium (store, priv->sort_criterium);
644 }
645
646 static gboolean
647 contact_list_store_inibit_active_cb (EmpathyContactListStore *store)
648 {
649         EmpathyContactListStorePriv *priv;
650
651         priv = GET_PRIV (store);
652
653         priv->show_active = TRUE;
654         priv->inhibit_active = 0;
655
656         return FALSE;
657 }
658
659 static void
660 contact_list_store_members_changed_cb (EmpathyContactList      *list_iface,
661                                        EmpathyContact          *contact,
662                                        EmpathyContact          *actor,
663                                        guint                    reason,
664                                        gchar                   *message,
665                                        gboolean                 is_member,
666                                        EmpathyContactListStore *store)
667 {
668         EmpathyContactListStorePriv *priv;
669
670         priv = GET_PRIV (store);
671
672         empathy_debug (DEBUG_DOMAIN, 
673                       "Contact %s (%d) %s",
674                       empathy_contact_get_id (contact),
675                       empathy_contact_get_handle (contact),
676                       is_member ? "added" : "removed");
677
678         if (is_member) {
679                 g_signal_connect (contact, "notify::presence",
680                                   G_CALLBACK (contact_list_store_contact_updated_cb),
681                                   store);
682                 g_signal_connect (contact, "notify::name",
683                                   G_CALLBACK (contact_list_store_contact_updated_cb),
684                                   store);
685                 g_signal_connect (contact, "notify::avatar",
686                                   G_CALLBACK (contact_list_store_contact_updated_cb),
687                                   store);
688                 g_signal_connect (contact, "notify::type",
689                                   G_CALLBACK (contact_list_store_contact_updated_cb),
690                                   store);
691
692                 contact_list_store_add_contact (store, contact);
693         } else {
694                 g_signal_handlers_disconnect_by_func (contact,
695                                                       G_CALLBACK (contact_list_store_contact_updated_cb),
696                                                       store);
697
698                 contact_list_store_remove_contact (store, contact);
699         }
700 }
701
702 static void
703 contact_list_store_groups_changed_cb (EmpathyContactList      *list_iface,
704                                       EmpathyContact          *contact,
705                                       gchar                   *group,
706                                       gboolean                 is_member,
707                                       EmpathyContactListStore *store)
708 {
709         EmpathyContactListStorePriv *priv;
710         gboolean                     show_active;
711
712         priv = GET_PRIV (store);
713
714         empathy_debug (DEBUG_DOMAIN, "Updating groups for contact %s (%d)",
715                       empathy_contact_get_id (contact),
716                       empathy_contact_get_handle (contact));
717
718         /* We do this to make sure the groups are correct, if not, we
719          * would have to check the groups already set up for each
720          * contact and then see what has been updated.
721          */
722         show_active = priv->show_active;
723         priv->show_active = FALSE;
724         contact_list_store_remove_contact (store, contact);
725         contact_list_store_add_contact (store, contact);
726         priv->show_active = show_active;
727 }
728
729 static void
730 contact_list_store_add_contact (EmpathyContactListStore *store,
731                                 EmpathyContact          *contact)
732 {
733         EmpathyContactListStorePriv *priv;
734         GtkTreeIter                 iter;
735         GList                      *groups, *l;
736
737         priv = GET_PRIV (store);
738         
739         if (!priv->show_offline && !empathy_contact_is_online (contact)) {
740                 return;
741         }
742
743         groups = empathy_contact_list_get_groups (priv->list, contact);
744
745         /* If no groups just add it at the top level. */
746         if (!groups) {
747                 gtk_tree_store_append (GTK_TREE_STORE (store), &iter, NULL);
748                 gtk_tree_store_set (GTK_TREE_STORE (store), &iter,
749                                     COL_NAME, empathy_contact_get_name (contact),
750                                     COL_CONTACT, contact,
751                                     COL_IS_GROUP, FALSE,
752                                     COL_IS_SEPARATOR, FALSE,
753                                     -1);
754         }
755
756         /* Else add to each group. */
757         for (l = groups; l; l = l->next) {
758                 GtkTreeIter iter_group;
759
760                 contact_list_store_get_group (store, l->data, &iter_group, NULL, NULL);
761
762                 gtk_tree_store_insert_after (GTK_TREE_STORE (store), &iter,
763                                              &iter_group, NULL);
764                 gtk_tree_store_set (GTK_TREE_STORE (store), &iter,
765                                     COL_NAME, empathy_contact_get_name (contact),
766                                     COL_CONTACT, contact,
767                                     COL_IS_GROUP, FALSE,
768                                     COL_IS_SEPARATOR, FALSE,
769                                     -1);
770                 g_free (l->data);
771         }
772         g_list_free (groups);
773
774         contact_list_store_contact_update (store, contact);
775
776 }
777
778 static void
779 contact_list_store_remove_contact (EmpathyContactListStore *store,
780                                    EmpathyContact          *contact)
781 {
782         EmpathyContactListStorePriv *priv;
783         GtkTreeModel               *model;
784         GList                      *iters, *l;
785
786         priv = GET_PRIV (store);
787
788         iters = contact_list_store_find_contact (store, contact);
789         if (!iters) {
790                 return;
791         }
792         
793         /* Clean up model */
794         model = GTK_TREE_MODEL (store);
795
796         for (l = iters; l; l = l->next) {
797                 GtkTreeIter parent;
798
799                 /* NOTE: it is only <= 2 here because we have
800                  * separators after the group name, otherwise it
801                  * should be 1. 
802                  */
803                 if (gtk_tree_model_iter_parent (model, &parent, l->data) &&
804                     gtk_tree_model_iter_n_children (model, &parent) <= 2) {
805                         gtk_tree_store_remove (GTK_TREE_STORE (store), &parent);
806                 } else {
807                         gtk_tree_store_remove (GTK_TREE_STORE (store), l->data);
808                 }
809         }
810
811         g_list_foreach (iters, (GFunc) gtk_tree_iter_free, NULL);
812         g_list_free (iters);
813 }
814
815 static void
816 contact_list_store_contact_update (EmpathyContactListStore *store,
817                                    EmpathyContact          *contact)
818 {
819         EmpathyContactListStorePriv *priv;
820         ShowActiveData             *data;
821         GtkTreeModel               *model;
822         GList                      *iters, *l;
823         gboolean                    in_list;
824         gboolean                    should_be_in_list;
825         gboolean                    was_online = TRUE;
826         gboolean                    now_online = FALSE;
827         gboolean                    set_model = FALSE;
828         gboolean                    do_remove = FALSE;
829         gboolean                    do_set_active = FALSE;
830         gboolean                    do_set_refresh = FALSE;
831         GdkPixbuf                  *pixbuf_avatar;
832
833         priv = GET_PRIV (store);
834
835         model = GTK_TREE_MODEL (store);
836
837         iters = contact_list_store_find_contact (store, contact);
838         if (!iters) {
839                 in_list = FALSE;
840         } else {
841                 in_list = TRUE;
842         }
843
844         /* Get online state now. */
845         now_online = empathy_contact_is_online (contact);
846
847         if (priv->show_offline || now_online) {
848                 should_be_in_list = TRUE;
849         } else {
850                 should_be_in_list = FALSE;
851         }
852
853         if (!in_list && !should_be_in_list) {
854                 /* Nothing to do. */
855                 empathy_debug (DEBUG_DOMAIN,
856                               "Contact:'%s' in list:NO, should be:NO",
857                               empathy_contact_get_name (contact));
858
859                 g_list_foreach (iters, (GFunc) gtk_tree_iter_free, NULL);
860                 g_list_free (iters);
861                 return;
862         }
863         else if (in_list && !should_be_in_list) {
864                 empathy_debug (DEBUG_DOMAIN,
865                               "Contact:'%s' in list:YES, should be:NO",
866                               empathy_contact_get_name (contact));
867
868                 if (priv->show_active) {
869                         do_remove = TRUE;
870                         do_set_active = TRUE;
871                         do_set_refresh = TRUE;
872
873                         set_model = TRUE;
874                         empathy_debug (DEBUG_DOMAIN, "Remove item (after timeout)");
875                 } else {
876                         empathy_debug (DEBUG_DOMAIN, "Remove item (now)!");
877                         contact_list_store_remove_contact (store, contact);
878                 }
879         }
880         else if (!in_list && should_be_in_list) {
881                 empathy_debug (DEBUG_DOMAIN,
882                               "Contact:'%s' in list:NO, should be:YES",
883                               empathy_contact_get_name (contact));
884
885                 contact_list_store_add_contact (store, contact);
886
887                 if (priv->show_active) {
888                         do_set_active = TRUE;
889
890                         empathy_debug (DEBUG_DOMAIN, "Set active (contact added)");
891                 }
892         } else {
893                 empathy_debug (DEBUG_DOMAIN,
894                               "Contact:'%s' in list:YES, should be:YES",
895                               empathy_contact_get_name (contact));
896
897                 /* Get online state before. */
898                 if (iters && g_list_length (iters) > 0) {
899                         gtk_tree_model_get (model, iters->data,
900                                             COL_IS_ONLINE, &was_online,
901                                             -1);
902                 }
903
904                 /* Is this really an update or an online/offline. */
905                 if (priv->show_active) {
906                         if (was_online != now_online) {
907                                 do_set_active = TRUE;
908                                 do_set_refresh = TRUE;
909
910                                 empathy_debug (DEBUG_DOMAIN, "Set active (contact updated %s)",
911                                               was_online ? "online  -> offline" :
912                                                            "offline -> online");
913                         } else {
914                                 /* Was TRUE for presence updates. */
915                                 /* do_set_active = FALSE;  */
916                                 do_set_refresh = TRUE;
917
918                                 empathy_debug (DEBUG_DOMAIN, "Set active (contact updated)");
919                         }
920                 }
921
922                 set_model = TRUE;
923         }
924
925         pixbuf_avatar = empathy_pixbuf_avatar_from_contact_scaled (contact, 32, 32);
926         for (l = iters; l && set_model; l = l->next) {
927                 gtk_tree_store_set (GTK_TREE_STORE (store), l->data,
928                                     COL_ICON_STATUS, empathy_icon_name_for_contact (contact),
929                                     COL_PIXBUF_AVATAR, pixbuf_avatar,
930                                     COL_PIXBUF_AVATAR_VISIBLE, priv->show_avatars,
931                                     COL_NAME, empathy_contact_get_name (contact),
932                                     COL_STATUS, empathy_contact_get_status (contact),
933                                     COL_STATUS_VISIBLE, !priv->is_compact,
934                                     COL_IS_GROUP, FALSE,
935                                     COL_IS_ONLINE, now_online,
936                                     COL_IS_SEPARATOR, FALSE,
937                                     -1);
938         }
939
940         if (pixbuf_avatar) {
941                 g_object_unref (pixbuf_avatar);
942         }
943
944         if (priv->show_active && do_set_active) {
945                 contact_list_store_contact_set_active (store, contact, do_set_active, do_set_refresh);
946
947                 if (do_set_active) {
948                         data = contact_list_store_contact_active_new (store, contact, do_remove);
949                         g_timeout_add (ACTIVE_USER_SHOW_TIME,
950                                        (GSourceFunc) contact_list_store_contact_active_cb,
951                                        data);
952                 }
953         }
954
955         /* FIXME: when someone goes online then offline quickly, the
956          * first timeout sets the user to be inactive and the second
957          * timeout removes the user from the contact list, really we
958          * should remove the first timeout.
959          */
960         g_list_foreach (iters, (GFunc) gtk_tree_iter_free, NULL);
961         g_list_free (iters);
962 }
963
964 static void
965 contact_list_store_contact_updated_cb (EmpathyContact          *contact,
966                                        GParamSpec              *param,
967                                        EmpathyContactListStore *store)
968 {
969         empathy_debug (DEBUG_DOMAIN,
970                       "Contact:'%s' updated, checking roster is in sync...",
971                       empathy_contact_get_name (contact));
972
973         contact_list_store_contact_update (store, contact);
974 }
975
976 static void
977 contact_list_store_contact_set_active (EmpathyContactListStore *store,
978                                        EmpathyContact          *contact,
979                                        gboolean                active,
980                                        gboolean                set_changed)
981 {
982         EmpathyContactListStorePriv *priv;
983         GtkTreeModel               *model;
984         GList                      *iters, *l;
985
986         priv = GET_PRIV (store);
987         model = GTK_TREE_MODEL (store);
988
989         iters = contact_list_store_find_contact (store, contact);
990         for (l = iters; l; l = l->next) {
991                 GtkTreePath *path;
992
993                 gtk_tree_store_set (GTK_TREE_STORE (store), l->data,
994                                     COL_IS_ACTIVE, active,
995                                     -1);
996
997                 empathy_debug (DEBUG_DOMAIN, "Set item %s", active ? "active" : "inactive");
998
999                 if (set_changed) {
1000                         path = gtk_tree_model_get_path (model, l->data);
1001                         gtk_tree_model_row_changed (model, path, l->data);
1002                         gtk_tree_path_free (path);
1003                 }
1004         }
1005
1006         g_list_foreach (iters, (GFunc) gtk_tree_iter_free, NULL);
1007         g_list_free (iters);
1008
1009 }
1010
1011 static ShowActiveData *
1012 contact_list_store_contact_active_new (EmpathyContactListStore *store,
1013                                        EmpathyContact          *contact,
1014                                        gboolean                remove)
1015 {
1016         ShowActiveData *data;
1017
1018         empathy_debug (DEBUG_DOMAIN, 
1019                       "Contact:'%s' now active, and %s be removed",
1020                       empathy_contact_get_name (contact), 
1021                       remove ? "WILL" : "WILL NOT");
1022         
1023         data = g_slice_new0 (ShowActiveData);
1024
1025         data->store = g_object_ref (store);
1026         data->contact = g_object_ref (contact);
1027         data->remove = remove;
1028
1029         return data;
1030 }
1031
1032 static void
1033 contact_list_store_contact_active_free (ShowActiveData *data)
1034 {
1035         g_object_unref (data->contact);
1036         g_object_unref (data->store);
1037
1038         g_slice_free (ShowActiveData, data);
1039 }
1040
1041 static gboolean
1042 contact_list_store_contact_active_cb (ShowActiveData *data)
1043 {
1044         EmpathyContactListStorePriv *priv;
1045
1046         priv = GET_PRIV (data->store);
1047
1048         if (data->remove &&
1049             !priv->show_offline &&
1050             !empathy_contact_is_online (data->contact)) {
1051                 empathy_debug (DEBUG_DOMAIN, 
1052                               "Contact:'%s' active timeout, removing item",
1053                               empathy_contact_get_name (data->contact));
1054                 contact_list_store_remove_contact (data->store, data->contact);
1055         }
1056
1057         empathy_debug (DEBUG_DOMAIN, 
1058                       "Contact:'%s' no longer active",
1059                       empathy_contact_get_name (data->contact));
1060
1061         contact_list_store_contact_set_active (data->store,
1062                                                data->contact,
1063                                                FALSE,
1064                                                TRUE);
1065
1066         contact_list_store_contact_active_free (data);
1067
1068         return FALSE;
1069 }
1070
1071 static gboolean
1072 contact_list_store_get_group_foreach (GtkTreeModel *model,
1073                                       GtkTreePath  *path,
1074                                       GtkTreeIter  *iter,
1075                                       FindGroup    *fg)
1076 {
1077         gchar    *str;
1078         gboolean  is_group;
1079
1080         /* Groups are only at the top level. */
1081         if (gtk_tree_path_get_depth (path) != 1) {
1082                 return FALSE;
1083         }
1084
1085         gtk_tree_model_get (model, iter,
1086                             COL_NAME, &str,
1087                             COL_IS_GROUP, &is_group,
1088                             -1);
1089
1090         if (is_group && strcmp (str, fg->name) == 0) {
1091                 fg->found = TRUE;
1092                 fg->iter = *iter;
1093         }
1094
1095         g_free (str);
1096
1097         return fg->found;
1098 }
1099
1100 static void
1101 contact_list_store_get_group (EmpathyContactListStore *store,
1102                               const gchar            *name,
1103                               GtkTreeIter            *iter_group_to_set,
1104                               GtkTreeIter            *iter_separator_to_set,
1105                               gboolean               *created)
1106 {
1107         EmpathyContactListStorePriv *priv;
1108         GtkTreeModel                *model;
1109         GtkTreeIter                  iter_group;
1110         GtkTreeIter                  iter_separator;
1111         FindGroup                    fg;
1112
1113         priv = GET_PRIV (store);
1114
1115         memset (&fg, 0, sizeof (fg));
1116
1117         fg.name = name;
1118
1119         model = GTK_TREE_MODEL (store);
1120         gtk_tree_model_foreach (model,
1121                                 (GtkTreeModelForeachFunc) contact_list_store_get_group_foreach,
1122                                 &fg);
1123
1124         if (!fg.found) {
1125                 if (created) {
1126                         *created = TRUE;
1127                 }
1128
1129                 gtk_tree_store_append (GTK_TREE_STORE (store), &iter_group, NULL);
1130                 gtk_tree_store_set (GTK_TREE_STORE (store), &iter_group,
1131                                     COL_ICON_STATUS, NULL,
1132                                     COL_NAME, name,
1133                                     COL_IS_GROUP, TRUE,
1134                                     COL_IS_ACTIVE, FALSE,
1135                                     COL_IS_SEPARATOR, FALSE,
1136                                     -1);
1137
1138                 if (iter_group_to_set) {
1139                         *iter_group_to_set = iter_group;
1140                 }
1141
1142                 gtk_tree_store_append (GTK_TREE_STORE (store),
1143                                        &iter_separator, 
1144                                        &iter_group);
1145                 gtk_tree_store_set (GTK_TREE_STORE (store), &iter_separator,
1146                                     COL_IS_SEPARATOR, TRUE,
1147                                     -1);
1148
1149                 if (iter_separator_to_set) {
1150                         *iter_separator_to_set = iter_separator;
1151                 }
1152         } else {
1153                 if (created) {
1154                         *created = FALSE;
1155                 }
1156
1157                 if (iter_group_to_set) {
1158                         *iter_group_to_set = fg.iter;
1159                 }
1160
1161                 iter_separator = fg.iter;
1162
1163                 if (gtk_tree_model_iter_next (model, &iter_separator)) {
1164                         gboolean is_separator;
1165
1166                         gtk_tree_model_get (model, &iter_separator,
1167                                             COL_IS_SEPARATOR, &is_separator,
1168                                             -1);
1169
1170                         if (is_separator && iter_separator_to_set) {
1171                                 *iter_separator_to_set = iter_separator;
1172                         }
1173                 }
1174         }
1175 }
1176
1177 static gint
1178 contact_list_store_state_sort_func (GtkTreeModel *model,
1179                                     GtkTreeIter  *iter_a,
1180                                     GtkTreeIter  *iter_b,
1181                                     gpointer      user_data)
1182 {
1183         gint            ret_val = 0;
1184         gchar          *name_a, *name_b;
1185         gboolean        is_separator_a, is_separator_b;
1186         EmpathyContact  *contact_a, *contact_b;
1187         EmpathyPresence *presence_a, *presence_b;
1188         McPresence      state_a, state_b;
1189
1190         gtk_tree_model_get (model, iter_a,
1191                             COL_NAME, &name_a,
1192                             COL_CONTACT, &contact_a,
1193                             COL_IS_SEPARATOR, &is_separator_a,
1194                             -1);
1195         gtk_tree_model_get (model, iter_b,
1196                             COL_NAME, &name_b,
1197                             COL_CONTACT, &contact_b,
1198                             COL_IS_SEPARATOR, &is_separator_b,
1199                             -1);
1200
1201         /* Separator or group? */
1202         if (is_separator_a || is_separator_b) {
1203                 if (is_separator_a) {
1204                         ret_val = -1;
1205                 } else if (is_separator_b) {
1206                         ret_val = 1;
1207                 }
1208         } else if (!contact_a && contact_b) {
1209                 ret_val = 1;
1210         } else if (contact_a && !contact_b) {
1211                 ret_val = -1;
1212         } else if (!contact_a && !contact_b) {
1213                 /* Handle groups */
1214                 ret_val = g_utf8_collate (name_a, name_b);
1215         }
1216
1217         if (ret_val) {
1218                 goto free_and_out;
1219         }
1220
1221         /* If we managed to get this far, we can start looking at
1222          * the presences.
1223          */
1224         presence_a = empathy_contact_get_presence (EMPATHY_CONTACT (contact_a));
1225         presence_b = empathy_contact_get_presence (EMPATHY_CONTACT (contact_b));
1226
1227         if (!presence_a && presence_b) {
1228                 ret_val = 1;
1229         } else if (presence_a && !presence_b) {
1230                 ret_val = -1;
1231         } else if (!presence_a && !presence_b) {
1232                 /* Both offline, sort by name */
1233                 ret_val = g_utf8_collate (name_a, name_b);
1234         } else {
1235                 state_a = empathy_presence_get_state (presence_a);
1236                 state_b = empathy_presence_get_state (presence_b);
1237
1238                 if (state_a < state_b) {
1239                         ret_val = -1;
1240                 } else if (state_a > state_b) {
1241                         ret_val = 1;
1242                 } else {
1243                         /* Fallback: compare by name */
1244                         ret_val = g_utf8_collate (name_a, name_b);
1245                 }
1246         }
1247
1248 free_and_out:
1249         g_free (name_a);
1250         g_free (name_b);
1251
1252         if (contact_a) {
1253                 g_object_unref (contact_a);
1254         }
1255
1256         if (contact_b) {
1257                 g_object_unref (contact_b);
1258         }
1259
1260         return ret_val;
1261 }
1262
1263 static gint
1264 contact_list_store_name_sort_func (GtkTreeModel *model,
1265                                    GtkTreeIter  *iter_a,
1266                                    GtkTreeIter  *iter_b,
1267                                    gpointer      user_data)
1268 {
1269         gchar         *name_a, *name_b;
1270         EmpathyContact *contact_a, *contact_b;
1271         gboolean       is_separator_a, is_separator_b;
1272         gint           ret_val;
1273
1274         gtk_tree_model_get (model, iter_a,
1275                             COL_NAME, &name_a,
1276                             COL_CONTACT, &contact_a,
1277                             COL_IS_SEPARATOR, &is_separator_a,
1278                             -1);
1279         gtk_tree_model_get (model, iter_b,
1280                             COL_NAME, &name_b,
1281                             COL_CONTACT, &contact_b,
1282                             COL_IS_SEPARATOR, &is_separator_b,
1283                             -1);
1284
1285         /* If contact is NULL it means it's a group. */
1286
1287         if (is_separator_a || is_separator_b) {
1288                 if (is_separator_a) {
1289                         ret_val = -1;
1290                 } else if (is_separator_b) {
1291                         ret_val = 1;
1292                 }
1293         } else if (!contact_a && contact_b) {
1294                 ret_val = 1;
1295         } else if (contact_a && !contact_b) {
1296                 ret_val = -1;
1297         } else {
1298                 ret_val = g_utf8_collate (name_a, name_b);
1299         }
1300
1301         g_free (name_a);
1302         g_free (name_b);
1303
1304         if (contact_a) {
1305                 g_object_unref (contact_a);
1306         }
1307
1308         if (contact_b) {
1309                 g_object_unref (contact_b);
1310         }
1311
1312         return ret_val;
1313 }
1314
1315 static gboolean
1316 contact_list_store_find_contact_foreach (GtkTreeModel *model,
1317                                          GtkTreePath  *path,
1318                                          GtkTreeIter  *iter,
1319                                          FindContact  *fc)
1320 {
1321         EmpathyContact *contact;
1322
1323         gtk_tree_model_get (model, iter,
1324                             COL_CONTACT, &contact,
1325                             -1);
1326
1327         if (!contact) {
1328                 return FALSE;
1329         }
1330
1331         if (empathy_contact_equal (contact, fc->contact)) {
1332                 fc->found = TRUE;
1333                 fc->iters = g_list_append (fc->iters, gtk_tree_iter_copy (iter));
1334         }
1335         g_object_unref (contact);
1336
1337         return FALSE;
1338 }
1339
1340 static GList *
1341 contact_list_store_find_contact (EmpathyContactListStore *store,
1342                                  EmpathyContact          *contact)
1343 {
1344         EmpathyContactListStorePriv *priv;
1345         GtkTreeModel              *model;
1346         GList                     *l = NULL;
1347         FindContact                fc;
1348
1349         priv = GET_PRIV (store);
1350
1351         memset (&fc, 0, sizeof (fc));
1352
1353         fc.contact = contact;
1354
1355         model = GTK_TREE_MODEL (store);
1356         gtk_tree_model_foreach (model,
1357                                 (GtkTreeModelForeachFunc) contact_list_store_find_contact_foreach,
1358                                 &fc);
1359
1360         if (fc.found) {
1361                 l = fc.iters;
1362         }
1363
1364         return l;
1365 }
1366
1367 static gboolean
1368 contact_list_store_update_list_mode_foreach (GtkTreeModel           *model,
1369                                              GtkTreePath            *path,
1370                                              GtkTreeIter            *iter,
1371                                              EmpathyContactListStore *store)
1372 {
1373         EmpathyContactListStorePriv *priv;
1374         gboolean                    show_avatar = FALSE;
1375
1376         priv = GET_PRIV (store);
1377
1378         if (priv->show_avatars && !priv->is_compact) {
1379                 show_avatar = TRUE;
1380         }
1381
1382         gtk_tree_store_set (GTK_TREE_STORE (store), iter,
1383                             COL_PIXBUF_AVATAR_VISIBLE, show_avatar,
1384                             COL_STATUS_VISIBLE, !priv->is_compact,
1385                             -1);
1386
1387         return FALSE;
1388 }
1389