Merge branch 'gnome-3-8'
[empathy.git] / libempathy-gtk / empathy-user-info.c
1 /*
2  * empathy-user-info.c - Source for EmpathyUserInfo
3  *
4  * Copyright (C) 2012 - Collabora Ltd.
5  *
6  * This library is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU Lesser General Public
8  * License as published by the Free Software Foundation; either
9  * version 2.1 of the License, or (at your option) any later version.
10  *
11  * This library 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  * Lesser General Public License for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public License
17  * along with This library. If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 #include "config.h"
21 #include "empathy-user-info.h"
22
23 #include <glib/gi18n-lib.h>
24
25 #include "empathy-avatar-chooser.h"
26 #include "empathy-calendar-button.h"
27 #include "empathy-contactinfo-utils.h"
28 #include "empathy-time.h"
29 #include "empathy-utils.h"
30
31 #define DEBUG_FLAG EMPATHY_DEBUG_CONTACT
32 #include "empathy-debug.h"
33
34 G_DEFINE_TYPE (EmpathyUserInfo, empathy_user_info, GTK_TYPE_GRID)
35
36 struct _EmpathyUserInfoPrivate
37 {
38   TpAccount *account;
39
40   GtkWidget *avatar_chooser;
41   GtkWidget *nickname_entry;
42   GtkWidget *details_label;
43   GtkWidget *details_spinner;
44
45   GList *details_to_set;
46   gboolean details_changed;
47   GCancellable *details_cancellable;
48 };
49
50 enum
51 {
52   PROP_0,
53   PROP_ACCOUNT,
54 };
55
56 #define DATA_FIELD "contact-info-field"
57 #define DATA_IS_CONTACT_INFO "is-contact-info"
58
59 static void
60 contact_info_changed_cb (GtkEntry *entry,
61     EmpathyUserInfo *self)
62 {
63   const gchar *strv[] = { NULL, NULL };
64   TpContactInfoField *field;
65
66   self->priv->details_changed = TRUE;
67
68   field = g_object_get_data ((GObject *) entry, DATA_FIELD);
69   g_assert (field != NULL);
70
71   strv[0] = gtk_entry_get_text (entry);
72
73   if (field->field_value != NULL)
74     g_strfreev (field->field_value);
75   field->field_value = g_strdupv ((GStrv) strv);
76 }
77
78 static void
79 bday_changed_cb (EmpathyCalendarButton *button,
80     GDate *date,
81     EmpathyUserInfo *self)
82 {
83   const gchar *strv[] = { NULL, NULL };
84   TpContactInfoField *field;
85
86   self->priv->details_changed = TRUE;
87
88   field = g_object_get_data ((GObject *) button, DATA_FIELD);
89   g_assert (field != NULL);
90
91   if (date != NULL)
92     {
93       gchar tmp[255];
94
95       g_date_strftime (tmp, sizeof (tmp), EMPATHY_DATE_FORMAT_DISPLAY_SHORT,
96           date);
97       strv[0] = tmp;
98     }
99
100   if (field->field_value != NULL)
101     g_strfreev (field->field_value);
102   field->field_value = g_strdupv ((GStrv) strv);
103 }
104
105 static gboolean
106 field_name_in_field_list (GList *list,
107     const gchar *name)
108 {
109   GList *l;
110
111   for (l = list; l != NULL; l = g_list_next (l))
112     {
113       TpContactInfoField *field = l->data;
114
115       if (!tp_strdiff (field->field_name, name))
116         return TRUE;
117     }
118
119   return FALSE;
120 }
121
122 static TpContactInfoFieldSpec *
123 get_spec_from_list (GList *list,
124     const gchar *name)
125 {
126   GList *l;
127
128   for (l = list; l != NULL; l = g_list_next (l))
129     {
130       TpContactInfoFieldSpec *spec = l->data;
131
132       if (!tp_strdiff (spec->name, name))
133         return spec;
134     }
135
136   return NULL;
137 }
138
139 static void
140 add_row (GtkGrid *grid,
141     GtkWidget *title,
142     GtkWidget *value,
143     gboolean contact_info)
144 {
145   /* Title */
146   gtk_grid_attach_next_to (grid, title, NULL, GTK_POS_BOTTOM, 1, 1);
147   gtk_misc_set_alignment (GTK_MISC (title), 1, 0.5);
148   gtk_style_context_add_class (gtk_widget_get_style_context (title),
149       GTK_STYLE_CLASS_DIM_LABEL);
150   gtk_widget_show (title);
151
152   /* Value */
153   gtk_grid_attach_next_to (grid, value, title, GTK_POS_RIGHT,
154       contact_info ? 2 : 1, 1);
155   gtk_widget_set_hexpand (value, TRUE);
156   if (GTK_IS_LABEL (value))
157     {
158       gtk_misc_set_alignment (GTK_MISC (value), 0, 0.5);
159       gtk_label_set_selectable (GTK_LABEL (value), TRUE);
160     }
161   gtk_widget_show (value);
162
163   if (contact_info)
164     {
165       g_object_set_data (G_OBJECT (title),
166           DATA_IS_CONTACT_INFO, (gpointer) TRUE);
167       g_object_set_data (G_OBJECT (value),
168           DATA_IS_CONTACT_INFO, (gpointer) TRUE);
169     }
170 }
171
172 static guint
173 fill_contact_info_grid (EmpathyUserInfo *self)
174 {
175   TpConnection *connection;
176   TpContact *contact;
177   GList *specs, *l;
178   guint n_rows = 0;
179   GList *info;
180   const char **field_names = empathy_contact_info_get_field_names (NULL);
181   guint i;
182
183   g_assert (self->priv->details_to_set == NULL);
184
185   connection = tp_account_get_connection (self->priv->account);
186   contact = tp_connection_get_self_contact (connection);
187   specs = tp_connection_dup_contact_info_supported_fields (connection);
188   info = tp_contact_dup_contact_info (contact);
189
190   /* Look at the fields set in our vCard */
191   for (l = info; l != NULL; l = l->next)
192     {
193       TpContactInfoField *field = l->data;
194
195       /* For some reason it can happen that the vCard contains fields the CM
196        * claims to be not supported. This is a workaround for gabble bug
197        * https://bugs.freedesktop.org/show_bug.cgi?id=64319. But we shouldn't
198        * crash on buggy CM anyway. */
199       if (get_spec_from_list (specs, field->field_name) == NULL)
200         {
201           DEBUG ("Buggy CM: self's vCard contains %s field but it is not in "
202               "Connection' supported fields", field->field_name);
203           continue;
204         }
205
206       /* make a copy for the details_to_set list */
207       field = tp_contact_info_field_copy (field);
208       DEBUG ("Field %s is in our vCard", field->field_name);
209
210       self->priv->details_to_set = g_list_prepend (self->priv->details_to_set,
211           field);
212     }
213
214   /* Add fields which are supported but not in the vCard */
215   for (i = 0; field_names[i] != NULL; i++)
216     {
217       TpContactInfoFieldSpec *spec;
218       TpContactInfoField *field;
219
220       /* Check if the field was in the vCard */
221       if (field_name_in_field_list (self->priv->details_to_set,
222             field_names[i]))
223         continue;
224
225       /* Check if the CM supports the field */
226       spec = get_spec_from_list (specs, field_names[i]);
227       if (spec == NULL)
228         continue;
229
230       /* add an empty field so user can set a value */
231       field = tp_contact_info_field_new (spec->name, spec->parameters, NULL);
232
233       self->priv->details_to_set = g_list_prepend (self->priv->details_to_set,
234           field);
235     }
236
237   /* Add widgets for supported fields */
238   self->priv->details_to_set = g_list_sort (self->priv->details_to_set,
239       (GCompareFunc) empathy_contact_info_field_spec_cmp);
240
241   for (l = self->priv->details_to_set; l != NULL; l= g_list_next (l))
242     {
243       TpContactInfoField *field = l->data;
244       GtkWidget *label, *w;
245       TpContactInfoFieldSpec *spec;
246       gboolean has_field;
247       char *title;
248
249       has_field = empathy_contact_info_lookup_field (field->field_name,
250           NULL, NULL);
251       if (!has_field)
252         {
253           /* Empathy doesn't display this field so we can't change it.
254            * But we put it in the details_to_set list so it won't be erased
255            * when calling SetContactInfo (bgo #630427) */
256           DEBUG ("Unhandled ContactInfo field spec: %s", field->field_name);
257           continue;
258         }
259
260       spec = get_spec_from_list (specs, field->field_name);
261       /* We shouldn't have added the field to details_to_set if it's not
262        * supported by the CM */
263       g_assert (spec != NULL);
264
265       if (spec->flags & TP_CONTACT_INFO_FIELD_FLAG_OVERWRITTEN_BY_NICKNAME)
266         {
267           DEBUG ("Ignoring field '%s' due it to having the "
268               "Overwritten_By_Nickname flag", field->field_name);
269           continue;
270         }
271
272       /* Add Title */
273       title = empathy_contact_info_field_label (field->field_name,
274           field->parameters,
275           (spec->flags & TP_CONTACT_INFO_FIELD_FLAG_PARAMETERS_EXACT));
276       label = gtk_label_new (title);
277       g_free (title);
278
279       /* TODO: if TP_CONTACT_INFO_FIELD_FLAG_PARAMETERS_EXACT is not set we
280        * should allow user to tag the vCard fields (bgo#672034) */
281
282       /* Add Value */
283       if (!tp_strdiff (field->field_name, "bday"))
284         {
285           w = empathy_calendar_button_new ();
286
287           if (field->field_value[0])
288             {
289               GDate date;
290
291               g_date_set_parse (&date, field->field_value[0]);
292               if (g_date_valid (&date))
293                 {
294                   empathy_calendar_button_set_date (EMPATHY_CALENDAR_BUTTON (w),
295                       &date);
296                 }
297             }
298
299           g_signal_connect (w, "date-changed",
300             G_CALLBACK (bday_changed_cb), self);
301         }
302       else
303         {
304           w = gtk_entry_new ();
305           gtk_entry_set_text (GTK_ENTRY (w),
306               field->field_value[0] ? field->field_value[0] : "");
307           g_signal_connect (w, "changed",
308             G_CALLBACK (contact_info_changed_cb), self);
309         }
310
311       add_row (GTK_GRID (self), label, w, TRUE);
312
313       g_object_set_data ((GObject *) w, DATA_FIELD, field);
314
315       n_rows++;
316     }
317
318   tp_contact_info_spec_list_free (specs);
319   tp_contact_info_list_free (info);
320
321   return n_rows;
322 }
323
324 static void
325 grid_foreach_cb (GtkWidget *widget,
326     gpointer data)
327 {
328   if (g_object_get_data (G_OBJECT (widget), DATA_IS_CONTACT_INFO) != NULL)
329     gtk_widget_destroy (widget);
330 }
331
332 static void
333 request_contact_info_cb (GObject *object,
334     GAsyncResult *res,
335     gpointer user_data)
336 {
337   EmpathyUserInfo *self = user_data;
338   TpContact *contact = TP_CONTACT (object);
339   guint n_rows;
340   GError *error = NULL;
341
342   if (!tp_contact_request_contact_info_finish (contact, res, &error))
343     {
344       /* If the request got cancelled it could mean the contact widget is
345        * destroyed, so we should not dereference self */
346       if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
347         {
348           g_clear_error (&error);
349           return;
350         }
351       g_clear_error (&error);
352     }
353
354   n_rows = fill_contact_info_grid (self);
355
356   gtk_widget_set_visible (self->priv->details_label, n_rows > 0);
357   gtk_spinner_stop (GTK_SPINNER (self->priv->details_spinner));
358   gtk_widget_hide (self->priv->details_spinner);
359 }
360
361 static void
362 reload_contact_info (EmpathyUserInfo *self)
363 {
364   TpConnection *connection;
365   TpContact *contact = NULL;
366   TpContactInfoFlags flags;
367
368   /* Cancel previous RequestContactInfo, if any */
369   if (self->priv->details_cancellable != NULL)
370     g_cancellable_cancel (self->priv->details_cancellable);
371   g_clear_object (&self->priv->details_cancellable);
372
373   /* Remove current contact info widgets, if any */
374   gtk_container_foreach (GTK_CONTAINER (self), grid_foreach_cb, NULL);
375   gtk_widget_hide (self->priv->details_label);
376   gtk_widget_hide (self->priv->details_spinner);
377
378   tp_clear_pointer (&self->priv->details_to_set, tp_contact_info_list_free);
379   self->priv->details_changed = FALSE;
380
381   connection = tp_account_get_connection (self->priv->account);
382   if (connection != NULL)
383     contact = tp_connection_get_self_contact (connection);
384
385   /* Display infobar if we don't have a self contact (probably offline) */
386   if (contact == NULL)
387     {
388       GtkWidget *infobar;
389       GtkWidget *content;
390       GtkWidget *label;
391
392       infobar = gtk_info_bar_new ();
393       gtk_info_bar_set_message_type (GTK_INFO_BAR (infobar), GTK_MESSAGE_INFO);
394       content = gtk_info_bar_get_content_area (GTK_INFO_BAR (infobar));
395       label = gtk_label_new (_("Go online to edit your personal information."));
396       gtk_container_add (GTK_CONTAINER (content), label);
397       gtk_widget_show (label);
398
399       gtk_grid_attach_next_to ((GtkGrid *) self, infobar,
400           NULL, GTK_POS_BOTTOM, 3, 1);
401       gtk_widget_show (infobar);
402
403       g_object_set_data (G_OBJECT (infobar),
404           DATA_IS_CONTACT_INFO, (gpointer) TRUE);
405       return;
406     }
407
408   if (!tp_proxy_has_interface_by_id (connection,
409           TP_IFACE_QUARK_CONNECTION_INTERFACE_CONTACT_INFO))
410     return;
411
412   flags = tp_connection_get_contact_info_flags (connection);
413   if ((flags & TP_CONTACT_INFO_FLAG_CAN_SET) == 0)
414     return;
415
416   /* Request the contact's info */
417   gtk_widget_show (self->priv->details_spinner);
418   gtk_spinner_start (GTK_SPINNER (self->priv->details_spinner));
419
420   g_assert (self->priv->details_cancellable == NULL);
421   self->priv->details_cancellable = g_cancellable_new ();
422   tp_contact_request_contact_info_async (contact,
423       self->priv->details_cancellable, request_contact_info_cb,
424       self);
425 }
426
427 static void
428 connection_notify_cb (EmpathyUserInfo *self)
429 {
430   TpConnection *connection = tp_account_get_connection (self->priv->account);
431
432   if (connection != NULL)
433     {
434       tp_g_signal_connect_object (connection, "notify::self-contact",
435           G_CALLBACK (reload_contact_info), self, G_CONNECT_SWAPPED);
436     }
437
438   reload_contact_info (self);
439 }
440
441 static void
442 empathy_user_info_constructed (GObject *object)
443 {
444   EmpathyUserInfo *self = (EmpathyUserInfo *) object;
445   GtkGrid *grid = (GtkGrid *) self;
446   GtkWidget *title;
447   GtkWidget *value;
448
449   G_OBJECT_CLASS (empathy_user_info_parent_class)->constructed (object);
450
451   gtk_grid_set_column_spacing (grid, 6);
452   gtk_grid_set_row_spacing (grid, 6);
453
454   /* Setup id label */
455   title = gtk_label_new (_("Identifier"));
456   value = gtk_label_new (tp_account_get_normalized_name (self->priv->account));
457   add_row (grid, title, value, FALSE);
458
459   /* Setup nickname entry */
460   title = gtk_label_new (_("Alias"));
461   self->priv->nickname_entry = gtk_entry_new ();
462   gtk_entry_set_text (GTK_ENTRY (self->priv->nickname_entry),
463       tp_account_get_nickname (self->priv->account));
464   add_row (grid, title, self->priv->nickname_entry, FALSE);
465
466   /* Set up avatar chooser */
467   self->priv->avatar_chooser = empathy_avatar_chooser_new (self->priv->account);
468   gtk_grid_attach (grid, self->priv->avatar_chooser,
469       2, 0, 1, 3);
470   gtk_widget_show (self->priv->avatar_chooser);
471
472   /* Details label */
473   self->priv->details_label = gtk_label_new (NULL);
474   gtk_label_set_markup (GTK_LABEL (self->priv->details_label),
475       _("<b>Personal Details</b>"));
476   gtk_misc_set_alignment (GTK_MISC (self->priv->details_label), 0, 0.5);
477   gtk_grid_attach_next_to (grid, self->priv->details_label, NULL,
478       GTK_POS_BOTTOM, 3, 1);
479
480   /* Details spinner */
481   self->priv->details_spinner = gtk_spinner_new ();
482   gtk_widget_set_hexpand (self->priv->details_spinner, TRUE);
483   gtk_widget_set_vexpand (self->priv->details_spinner, TRUE);
484   gtk_grid_attach_next_to (grid, self->priv->details_spinner, NULL,
485       GTK_POS_BOTTOM, 3, 1);
486
487   g_signal_connect_swapped (self->priv->account, "notify::connection",
488       G_CALLBACK (connection_notify_cb), self);
489   connection_notify_cb (self);
490 }
491
492 static void
493 empathy_user_info_dispose (GObject *object)
494 {
495   EmpathyUserInfo *self = (EmpathyUserInfo *) object;
496
497   if (self->priv->account != NULL)
498     {
499       /* Disconnect the signal manually, because TpAccount::dispose will emit
500        * "notify::connection" signal before tp_g_signal_connect_object() had
501        * a chance to disconnect. */
502       g_signal_handlers_disconnect_by_func (self->priv->account,
503           connection_notify_cb, self);
504       g_clear_object (&self->priv->account);
505     }
506
507   if (self->priv->details_cancellable != NULL)
508     g_cancellable_cancel (self->priv->details_cancellable);
509   g_clear_object (&self->priv->details_cancellable);
510
511   G_OBJECT_CLASS (empathy_user_info_parent_class)->dispose (object);
512 }
513
514 static void
515 empathy_user_info_get_property (GObject *object,
516     guint property_id,
517     GValue *value,
518     GParamSpec *pspec)
519 {
520   EmpathyUserInfo *self = (EmpathyUserInfo *) object;
521
522   switch (property_id)
523     {
524       case PROP_ACCOUNT:
525         g_value_set_object (value, self->priv->account);
526         break;
527       default:
528         G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
529         break;
530     }
531 }
532
533 static void
534 empathy_user_info_set_property (GObject *object,
535     guint property_id,
536     const GValue *value,
537     GParamSpec *pspec)
538 {
539   EmpathyUserInfo *self = (EmpathyUserInfo *) object;
540
541   switch (property_id)
542     {
543       case PROP_ACCOUNT:
544         g_assert (self->priv->account == NULL); /* construct-only */
545         self->priv->account = g_value_dup_object (value);
546         break;
547       default:
548         G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
549         break;
550     }
551 }
552
553 static void
554 empathy_user_info_init (EmpathyUserInfo *self)
555 {
556   self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
557       EMPATHY_TYPE_USER_INFO, EmpathyUserInfoPrivate);
558 }
559
560 static void
561 empathy_user_info_class_init (EmpathyUserInfoClass *klass)
562 {
563   GObjectClass *object_class = G_OBJECT_CLASS (klass);
564   GParamSpec *param_spec;
565
566   object_class->constructed = empathy_user_info_constructed;
567   object_class->dispose = empathy_user_info_dispose;
568   object_class->get_property = empathy_user_info_get_property;
569   object_class->set_property = empathy_user_info_set_property;
570
571   g_type_class_add_private (object_class, sizeof (EmpathyUserInfoPrivate));
572
573   param_spec = g_param_spec_object ("account",
574       "account",
575       "The #TpAccount on which user info should be edited",
576       TP_TYPE_ACCOUNT,
577       G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
578   g_object_class_install_property (object_class, PROP_ACCOUNT, param_spec);
579 }
580
581 GtkWidget *
582 empathy_user_info_new (TpAccount *account)
583 {
584   g_return_val_if_fail (TP_IS_ACCOUNT (account), NULL);
585
586   return g_object_new (EMPATHY_TYPE_USER_INFO,
587       "account", account,
588       NULL);
589 }
590
591 void
592 empathy_user_info_discard (EmpathyUserInfo *self)
593 {
594   g_return_if_fail (EMPATHY_IS_USER_INFO (self));
595
596   reload_contact_info (self);
597   gtk_entry_set_text ((GtkEntry *) self->priv->nickname_entry,
598       tp_account_get_nickname (self->priv->account));
599 }
600
601 static void
602 apply_complete_one (GSimpleAsyncResult *result)
603 {
604   guint count;
605
606   count = g_simple_async_result_get_op_res_gssize (result);
607   count--;
608   g_simple_async_result_set_op_res_gssize (result, count);
609
610   if (count == 0)
611     g_simple_async_result_complete (result);
612 }
613
614 static void
615 avatar_chooser_apply_cb (GObject *source,
616     GAsyncResult *result,
617     gpointer user_data)
618 {
619   EmpathyAvatarChooser *avatar_chooser = (EmpathyAvatarChooser *) source;
620   GSimpleAsyncResult *my_result = user_data;
621   GError *error = NULL;
622
623   if (!empathy_avatar_chooser_apply_finish (avatar_chooser, result, &error))
624     g_simple_async_result_take_error (my_result, error);
625
626   apply_complete_one (my_result);
627   g_object_unref (my_result);
628 }
629
630 static void
631 set_nickname_cb (GObject *source,
632     GAsyncResult *result,
633     gpointer user_data)
634 {
635   TpAccount *account = (TpAccount *) source;
636   GSimpleAsyncResult *my_result = user_data;
637   GError *error = NULL;
638
639   if (!tp_account_set_nickname_finish (account, result, &error))
640     g_simple_async_result_take_error (my_result, error);
641
642   apply_complete_one (my_result);
643   g_object_unref (my_result);
644 }
645
646 static void
647 set_contact_info_cb (GObject *source,
648     GAsyncResult *result,
649     gpointer user_data)
650 {
651   TpConnection *connection = (TpConnection *) source;
652   GSimpleAsyncResult *my_result = user_data;
653   GError *error = NULL;
654
655   if (!tp_connection_set_contact_info_finish (connection, result, &error))
656     g_simple_async_result_take_error (my_result, error);
657
658   apply_complete_one (my_result);
659   g_object_unref (my_result);
660 }
661
662 static gboolean
663 field_value_is_empty (TpContactInfoField *field)
664 {
665   guint i;
666
667   if (field->field_value == NULL)
668     return TRUE;
669
670   /* Field is empty if all its values are empty */
671   for (i = 0; field->field_value[i] != NULL; i++)
672     {
673       if (!tp_str_empty (field->field_value[i]))
674         return FALSE;
675     }
676
677   return TRUE;
678 }
679
680 void
681 empathy_user_info_apply_async (EmpathyUserInfo *self,
682     GAsyncReadyCallback callback,
683     gpointer user_data)
684 {
685   GSimpleAsyncResult *result;
686   const gchar *new_nickname;
687   guint count = 0;
688   GList *l, *next;
689
690   g_return_if_fail (EMPATHY_IS_USER_INFO (self));
691
692   result = g_simple_async_result_new ((GObject *) self, callback, user_data,
693       empathy_user_info_apply_async);
694
695   /* Apply avatar */
696   empathy_avatar_chooser_apply_async (
697       (EmpathyAvatarChooser *) self->priv->avatar_chooser,
698       avatar_chooser_apply_cb, g_object_ref (result));
699   count++;
700
701   /* Apply nickname */
702   new_nickname = gtk_entry_get_text (GTK_ENTRY (self->priv->nickname_entry));
703   if (tp_strdiff (new_nickname, tp_account_get_nickname (self->priv->account)))
704     {
705       tp_account_set_nickname_async (self->priv->account, new_nickname,
706           set_nickname_cb, g_object_ref (result));
707       count++;
708     }
709
710   /* Remove empty fields */
711   for (l = self->priv->details_to_set; l != NULL; l = next)
712     {
713       TpContactInfoField *field = l->data;
714
715       next = l->next;
716       if (field_value_is_empty (field))
717         {
718           DEBUG ("Drop empty field: %s", field->field_name);
719           tp_contact_info_field_free (field);
720           self->priv->details_to_set =
721               g_list_delete_link (self->priv->details_to_set, l);
722         }
723     }
724
725   if (self->priv->details_to_set != NULL)
726     {
727       if (self->priv->details_changed)
728         {
729           tp_connection_set_contact_info_async (
730               tp_account_get_connection (self->priv->account),
731               self->priv->details_to_set, set_contact_info_cb,
732               g_object_ref (result));
733           count++;
734         }
735
736       tp_contact_info_list_free (self->priv->details_to_set);
737       self->priv->details_to_set = NULL;
738     }
739
740   self->priv->details_changed = FALSE;
741
742   g_simple_async_result_set_op_res_gssize (result, count);
743
744   g_object_unref (result);
745 }
746
747 gboolean
748 empathy_user_info_apply_finish (EmpathyUserInfo *self,
749     GAsyncResult *result,
750     GError **error)
751 {
752   empathy_implement_finish_void (self, empathy_user_info_apply_async);
753 }