sort contacts by most recent event
[empathy.git] / libempathy-gtk / empathy-roster-contact.c
1 #include "config.h"
2 #include "empathy-roster-contact.h"
3
4 #include <glib/gi18n-lib.h>
5 #include <tp-account-widgets/tpaw-images.h>
6 #include <tp-account-widgets/tpaw-pixbuf-utils.h>
7
8 #include "empathy-ui-utils.h"
9 #include "empathy-utils.h"
10
11 G_DEFINE_TYPE (EmpathyRosterContact, empathy_roster_contact, GTK_TYPE_LIST_BOX_ROW)
12
13 #define AVATAR_SIZE 48
14
15 enum
16 {
17   PROP_INDIVIDIUAL = 1,
18   PROP_GROUP,
19   PROP_ONLINE,
20   PROP_ALIAS,
21   PROP_MOST_RECENT_EVENT,
22   N_PROPS
23 };
24
25 /*
26 enum
27 {
28   LAST_SIGNAL
29 };
30
31 static guint signals[LAST_SIGNAL];
32 */
33
34 struct _EmpathyRosterContactPriv
35 {
36   FolksIndividual *individual;
37   EmpathyContact *contact;
38   gchar *group;
39
40   TplLogManager *log_manager;
41   TplEvent *most_recent_event;
42
43   GtkWidget *avatar;
44   GtkWidget *first_line_alig;
45   GtkWidget *alias;
46   GtkWidget *presence_msg;
47   GtkWidget *most_recent_msg;
48   GtkWidget *presence_icon;
49   GtkWidget *phone_icon;
50
51   /* If not NULL, used instead of the individual's presence icon */
52   gchar *event_icon;
53
54   gboolean online;
55 };
56
57 static const gchar *
58 get_alias (EmpathyRosterContact *self)
59 {
60   return folks_alias_details_get_alias (FOLKS_ALIAS_DETAILS (
61         self->priv->individual));
62 }
63
64 static void
65 empathy_roster_contact_get_property (GObject *object,
66     guint property_id,
67     GValue *value,
68     GParamSpec *pspec)
69 {
70   EmpathyRosterContact *self = EMPATHY_ROSTER_CONTACT (object);
71
72   switch (property_id)
73     {
74       case PROP_INDIVIDIUAL:
75         g_value_set_object (value, self->priv->individual);
76         break;
77       case PROP_GROUP:
78         g_value_set_string (value, self->priv->group);
79         break;
80       case PROP_ONLINE:
81         g_value_set_boolean (value, self->priv->online);
82         break;
83       case PROP_ALIAS:
84         g_value_set_string (value, get_alias (self));
85         break;
86       case PROP_MOST_RECENT_EVENT:
87         g_value_set_object (value, self->priv->most_recent_event);
88         break;
89       default:
90         G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
91         break;
92     }
93 }
94
95 static void
96 empathy_roster_contact_set_property (GObject *object,
97     guint property_id,
98     const GValue *value,
99     GParamSpec *pspec)
100 {
101   EmpathyRosterContact *self = EMPATHY_ROSTER_CONTACT (object);
102
103   switch (property_id)
104     {
105       case PROP_INDIVIDIUAL:
106         g_assert (self->priv->individual == NULL); /* construct only */
107         self->priv->individual = g_value_dup_object (value);
108         break;
109       case PROP_GROUP:
110         g_assert (self->priv->group == NULL); /* construct only */
111         self->priv->group = g_value_dup_string (value);
112         break;
113       default:
114         G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
115         break;
116     }
117 }
118
119 gint64
120 empathy_roster_contact_get_most_recent_timestamp (EmpathyRosterContact *contact)
121 {
122   if (contact->priv->most_recent_event) {
123     return tpl_event_get_timestamp (contact->priv->most_recent_event);
124   }
125   return 0;
126 }
127
128 static const gchar*
129 get_most_recent_message (EmpathyRosterContact *contact)
130 {
131   if (contact->priv->most_recent_event) {
132     return tpl_text_event_get_message (TPL_TEXT_EVENT(contact->priv->most_recent_event));
133   }
134   return NULL;
135 }
136
137 static void
138 avatar_loaded_cb (GObject *source,
139     GAsyncResult *result,
140     gpointer user_data)
141 {
142   TpWeakRef *wr = user_data;
143   EmpathyRosterContact *self;
144   GdkPixbuf *pixbuf;
145
146   self = tp_weak_ref_dup_object (wr);
147   if (self == NULL)
148     goto out;
149
150   pixbuf = empathy_pixbuf_avatar_from_individual_scaled_finish (
151       FOLKS_INDIVIDUAL (source), result, NULL);
152
153   if (pixbuf == NULL)
154     {
155       pixbuf = tpaw_pixbuf_from_icon_name_sized (
156           TPAW_IMAGE_AVATAR_DEFAULT, AVATAR_SIZE);
157     }
158
159   gtk_image_set_from_pixbuf (GTK_IMAGE (self->priv->avatar), pixbuf);
160   g_object_unref (pixbuf);
161
162   g_object_unref (self);
163
164 out:
165   tp_weak_ref_destroy (wr);
166 }
167
168 static void
169 update_most_recent_msg (EmpathyRosterContact *self)
170 {
171   const gchar* msg = get_most_recent_message (self);
172
173   if (tp_str_empty (msg))
174     {
175       gtk_alignment_set (GTK_ALIGNMENT (self->priv->first_line_alig),
176           0, 0.5, 1, 1);
177       gtk_widget_hide (self->priv->most_recent_msg);
178     }
179   else
180     {
181       gchar *tmp = g_strdup (msg);
182       if (strchr(tmp, '\n')) strchr(tmp, '\n')[0] = 0;
183       gtk_label_set_text (GTK_LABEL (self->priv->most_recent_msg), tmp);
184       gtk_alignment_set (GTK_ALIGNMENT (self->priv->first_line_alig),
185           0, 0.75, 1, 1);
186       gtk_misc_set_alignment (GTK_MISC (self->priv->most_recent_msg), 0, 0.25);
187       g_free (tmp);
188     }
189 }
190
191 static void
192 update_avatar (EmpathyRosterContact *self)
193 {
194   empathy_pixbuf_avatar_from_individual_scaled_async (self->priv->individual,
195       AVATAR_SIZE, AVATAR_SIZE, NULL, avatar_loaded_cb,
196       tp_weak_ref_new (self, NULL, NULL));
197 }
198
199 static void
200 avatar_changed_cb (FolksIndividual *individual,
201     GParamSpec *spec,
202     EmpathyRosterContact *self)
203 {
204   update_avatar (self);
205 }
206
207 static void
208 update_alias (EmpathyRosterContact *self)
209 {
210   gtk_label_set_text (GTK_LABEL (self->priv->alias), get_alias (self));
211
212   g_object_notify (G_OBJECT (self), "alias");
213 }
214
215 static void
216 alias_changed_cb (FolksIndividual *individual,
217     GParamSpec *spec,
218     EmpathyRosterContact *self)
219 {
220   update_alias (self);
221 }
222
223 static void
224 update_presence_msg (EmpathyRosterContact *self)
225 {
226   const gchar *msg;
227   GStrv types;
228
229   msg = folks_presence_details_get_presence_message (
230       FOLKS_PRESENCE_DETAILS (self->priv->individual));
231
232   if (tp_str_empty (msg))
233     {
234       /* Just display the alias in the center of the row */
235       gtk_alignment_set (GTK_ALIGNMENT (self->priv->first_line_alig),
236           0, 0.5, 1, 1);
237
238       gtk_widget_hide (self->priv->presence_msg);
239     }
240   else
241     {
242       FolksPresenceType type;
243
244       type = folks_presence_details_get_presence_type (
245           FOLKS_PRESENCE_DETAILS (self->priv->individual));
246       if (type == FOLKS_PRESENCE_TYPE_ERROR)
247         {
248           gchar *tmp;
249
250           /* Add a prefix explaining that something goes wrong when trying to
251            * fetch contact's presence. */
252           tmp = g_strdup_printf (_("Server cannot find contact: %s"), msg);
253           gtk_label_set_text (GTK_LABEL (self->priv->presence_msg), tmp);
254
255           g_free (tmp);
256         }
257       else
258         {
259           gtk_label_set_text (GTK_LABEL (self->priv->presence_msg), msg);
260         }
261
262       gtk_alignment_set (GTK_ALIGNMENT (self->priv->first_line_alig),
263           0, 0.75, 1, 1);
264       gtk_misc_set_alignment (GTK_MISC (self->priv->presence_msg), 0, 0.25);
265
266       gtk_widget_show (self->priv->presence_msg);
267     }
268
269   types = (GStrv) empathy_individual_get_client_types (self->priv->individual);
270
271   gtk_widget_set_visible (self->priv->phone_icon,
272       empathy_client_types_contains_mobile_device (types));
273 }
274
275 static void
276 presence_message_changed_cb (FolksIndividual *individual,
277     GParamSpec *spec,
278     EmpathyRosterContact *self)
279 {
280   update_presence_msg (self);
281 }
282
283 static void
284 update_presence_icon (EmpathyRosterContact *self)
285 {
286   const gchar *icon;
287
288   if (self->priv->event_icon == NULL)
289     icon = empathy_icon_name_for_individual (self->priv->individual);
290   else
291     icon = self->priv->event_icon;
292
293   gtk_image_set_from_icon_name (GTK_IMAGE (self->priv->presence_icon), icon,
294       GTK_ICON_SIZE_MENU);
295 }
296
297 static void
298 update_online (EmpathyRosterContact *self)
299 {
300   FolksPresenceType presence;
301   gboolean online;
302
303   presence = folks_presence_details_get_presence_type (
304       FOLKS_PRESENCE_DETAILS (self->priv->individual));
305
306   switch (presence)
307     {
308       case FOLKS_PRESENCE_TYPE_UNSET:
309       case FOLKS_PRESENCE_TYPE_OFFLINE:
310       case FOLKS_PRESENCE_TYPE_UNKNOWN:
311       case FOLKS_PRESENCE_TYPE_ERROR:
312         online = FALSE;
313         break;
314
315       case FOLKS_PRESENCE_TYPE_AVAILABLE:
316       case FOLKS_PRESENCE_TYPE_AWAY:
317       case FOLKS_PRESENCE_TYPE_EXTENDED_AWAY:
318       case FOLKS_PRESENCE_TYPE_HIDDEN:
319       case FOLKS_PRESENCE_TYPE_BUSY:
320         online = TRUE;
321         break;
322
323       default:
324         g_warning ("Unknown FolksPresenceType: %d", presence);
325         online = FALSE;
326     }
327
328   if (self->priv->online == online)
329     return;
330
331   self->priv->online = online;
332   g_object_notify (G_OBJECT (self), "online");
333 }
334
335 static void
336 presence_status_changed_cb (FolksIndividual *individual,
337     GParamSpec *spec,
338     EmpathyRosterContact *self)
339 {
340   update_presence_icon (self);
341   update_online (self);
342 }
343
344 static void
345 get_filtered_events (GObject *source_object, GAsyncResult *res, gpointer user_data)
346 {
347   EmpathyRosterContact *contact = EMPATHY_ROSTER_CONTACT (user_data);
348   GError *error;
349   GList *events;
350
351   error = NULL;
352   if (!tpl_log_manager_get_filtered_events_finish (contact->priv->log_manager, res, &events, &error))
353     {
354       g_warning ("Unable to get events: %s", error->message);
355       g_error_free (error);
356       goto out;
357     }
358
359   if (events) {
360     contact->priv->most_recent_event = TPL_EVENT (events->data);
361     g_object_notify (G_OBJECT (contact), "most-recent-event");
362     update_most_recent_msg (contact);
363   }
364
365  out:
366   return;
367 }
368
369 static void
370 empathy_roster_contact_constructed (GObject *object)
371 {
372   EmpathyRosterContact *self = EMPATHY_ROSTER_CONTACT (object);
373   TplEntity *tpl_entity;
374   void (*chain_up) (GObject *) =
375       ((GObjectClass *) empathy_roster_contact_parent_class)->constructed;
376
377   if (chain_up != NULL)
378     chain_up (object);
379
380   g_assert (FOLKS_IS_INDIVIDUAL (self->priv->individual));
381
382   self->priv->contact = empathy_contact_dup_best_for_action (
383                   self->priv->individual,
384                   EMPATHY_ACTION_CHAT);
385
386   self->priv->log_manager = tpl_log_manager_dup_singleton ();
387
388   tpl_entity = tpl_entity_new_from_tp_contact (
389                   empathy_contact_get_tp_contact (self->priv->contact),
390                   TPL_ENTITY_CONTACT);
391   tpl_log_manager_get_filtered_events_async(
392                   self->priv->log_manager,
393                   empathy_contact_get_account (self->priv->contact),
394                   tpl_entity,
395                   TPL_EVENT_MASK_TEXT,
396                   1,
397                   NULL,
398                   NULL,
399                   get_filtered_events,
400                   object);
401
402   tp_g_signal_connect_object (self->priv->individual, "notify::avatar",
403       G_CALLBACK (avatar_changed_cb), self, 0);
404   tp_g_signal_connect_object (self->priv->individual, "notify::alias",
405       G_CALLBACK (alias_changed_cb), self, 0);
406   tp_g_signal_connect_object (self->priv->individual,
407       "notify::presence-message",
408       G_CALLBACK (presence_message_changed_cb), self, 0);
409   tp_g_signal_connect_object (self->priv->individual, "notify::presence-status",
410       G_CALLBACK (presence_status_changed_cb), self, 0);
411
412   update_avatar (self);
413   update_alias (self);
414   update_presence_msg (self);
415   update_presence_icon (self);
416
417   update_online (self);
418 }
419
420 static void
421 empathy_roster_contact_dispose (GObject *object)
422 {
423   EmpathyRosterContact *self = EMPATHY_ROSTER_CONTACT (object);
424   void (*chain_up) (GObject *) =
425       ((GObjectClass *) empathy_roster_contact_parent_class)->dispose;
426
427   g_clear_object (&self->priv->individual);
428
429   if (chain_up != NULL)
430     chain_up (object);
431 }
432
433 static void
434 empathy_roster_contact_finalize (GObject *object)
435 {
436   EmpathyRosterContact *self = EMPATHY_ROSTER_CONTACT (object);
437   void (*chain_up) (GObject *) =
438       ((GObjectClass *) empathy_roster_contact_parent_class)->finalize;
439
440   g_free (self->priv->group);
441   g_free (self->priv->event_icon);
442   g_object_unref (self->priv->log_manager);
443
444   if (chain_up != NULL)
445     chain_up (object);
446 }
447
448 static void
449 empathy_roster_contact_class_init (
450     EmpathyRosterContactClass *klass)
451 {
452   GObjectClass *oclass = G_OBJECT_CLASS (klass);
453   GParamSpec *spec;
454
455   oclass->get_property = empathy_roster_contact_get_property;
456   oclass->set_property = empathy_roster_contact_set_property;
457   oclass->constructed = empathy_roster_contact_constructed;
458   oclass->dispose = empathy_roster_contact_dispose;
459   oclass->finalize = empathy_roster_contact_finalize;
460
461   spec = g_param_spec_object ("individual", "Individual",
462       "FolksIndividual",
463       FOLKS_TYPE_INDIVIDUAL,
464       G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
465   g_object_class_install_property (oclass, PROP_INDIVIDIUAL, spec);
466
467   spec = g_param_spec_string ("group", "Group",
468       "Group of this widget, or NULL",
469       NULL,
470       G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
471   g_object_class_install_property (oclass, PROP_GROUP, spec);
472
473   spec = g_param_spec_boolean ("online", "Online",
474       "TRUE if Individual is online",
475       FALSE,
476       G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
477   g_object_class_install_property (oclass, PROP_ONLINE, spec);
478
479   spec = g_param_spec_string ("alias", "Alias",
480       "The Alias of the individual displayed in the widget",
481       NULL,
482       G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
483   g_object_class_install_property (oclass, PROP_ALIAS, spec);
484
485   spec = g_param_spec_object ("most-recent-event", "Most recent event",
486       "Most recent event",
487       TPL_TYPE_EVENT,
488       G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
489   g_object_class_install_property (oclass, PROP_MOST_RECENT_EVENT, spec);
490
491   g_type_class_add_private (klass, sizeof (EmpathyRosterContactPriv));
492 }
493
494 static void
495 empathy_roster_contact_init (EmpathyRosterContact *self)
496 {
497   GtkWidget *alig, *main_box, *box, *first_line_box;
498   GtkStyleContext *context;
499
500   self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
501       EMPATHY_TYPE_ROSTER_CONTACT, EmpathyRosterContactPriv);
502
503   alig = gtk_alignment_new (0.5, 0.5, 1, 1);
504   gtk_widget_show (alig);
505   gtk_alignment_set_padding (GTK_ALIGNMENT (alig), 4, 4, 4, 12);
506
507   main_box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 8);
508
509   /* Avatar */
510   self->priv->avatar = gtk_image_new ();
511
512   gtk_widget_set_size_request (self->priv->avatar, AVATAR_SIZE, AVATAR_SIZE);
513
514   gtk_box_pack_start (GTK_BOX (main_box), self->priv->avatar, FALSE, FALSE, 0);
515   gtk_widget_show (self->priv->avatar);
516
517   box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
518
519   /* Alias and phone icon */
520   self->priv->first_line_alig = gtk_alignment_new (0, 0.5, 1, 1);
521   first_line_box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
522
523   self->priv->alias = gtk_label_new (NULL);
524   gtk_label_set_ellipsize (GTK_LABEL (self->priv->alias), PANGO_ELLIPSIZE_END);
525   gtk_box_pack_start (GTK_BOX (first_line_box), self->priv->alias,
526       FALSE, FALSE, 0);
527   gtk_misc_set_alignment (GTK_MISC (self->priv->alias), 0, 0.5);
528   gtk_widget_show (self->priv->alias);
529
530   self->priv->phone_icon = gtk_image_new_from_icon_name ("phone-symbolic",
531       GTK_ICON_SIZE_MENU);
532   gtk_misc_set_alignment (GTK_MISC (self->priv->phone_icon), 0, 0.5);
533   gtk_box_pack_start (GTK_BOX (first_line_box), self->priv->phone_icon,
534       TRUE, TRUE, 0);
535
536   gtk_container_add (GTK_CONTAINER (self->priv->first_line_alig),
537       first_line_box);
538   gtk_widget_show (self->priv->first_line_alig);
539
540   gtk_box_pack_start (GTK_BOX (box), self->priv->first_line_alig,
541       TRUE, TRUE, 0);
542   gtk_widget_show (first_line_box);
543
544   gtk_box_pack_start (GTK_BOX (main_box), box, TRUE, TRUE, 0);
545   gtk_widget_show (box);
546
547   /* Presence */
548   self->priv->presence_msg = gtk_label_new (NULL);
549   gtk_label_set_ellipsize (GTK_LABEL (self->priv->presence_msg),
550       PANGO_ELLIPSIZE_END);
551   /*
552   gtk_box_pack_start (GTK_BOX (box), self->priv->presence_msg, TRUE, TRUE, 0);
553   gtk_widget_show (self->priv->presence_msg);
554   */
555
556   context = gtk_widget_get_style_context (self->priv->presence_msg);
557   gtk_style_context_add_class (context, GTK_STYLE_CLASS_DIM_LABEL);
558
559   /* Most recent message */
560   self->priv->most_recent_msg = gtk_label_new (NULL);
561   gtk_label_set_ellipsize (GTK_LABEL (self->priv->most_recent_msg),
562       PANGO_ELLIPSIZE_END);
563   gtk_box_pack_start (GTK_BOX (box), self->priv->most_recent_msg, TRUE, TRUE, 0);
564   gtk_widget_show (self->priv->most_recent_msg);
565
566   context = gtk_widget_get_style_context (self->priv->most_recent_msg);
567   gtk_style_context_add_class (context, GTK_STYLE_CLASS_DIM_LABEL);
568
569   /* Presence icon */
570   self->priv->presence_icon = gtk_image_new ();
571
572   gtk_box_pack_start (GTK_BOX (main_box), self->priv->presence_icon,
573       FALSE, FALSE, 0);
574   gtk_widget_show (self->priv->presence_icon);
575
576   gtk_container_add (GTK_CONTAINER (self), alig);
577   gtk_container_add (GTK_CONTAINER (alig), main_box);
578   gtk_widget_show (main_box);
579 }
580
581 GtkWidget *
582 empathy_roster_contact_new (FolksIndividual *individual,
583     const gchar *group)
584 {
585   g_return_val_if_fail (FOLKS_IS_INDIVIDUAL (individual), NULL);
586
587   return g_object_new (EMPATHY_TYPE_ROSTER_CONTACT,
588       "individual", individual,
589       "group", group,
590       NULL);
591 }
592
593 FolksIndividual *
594 empathy_roster_contact_get_individual (EmpathyRosterContact *self)
595 {
596   return self->priv->individual;
597 }
598
599 EmpathyContact *
600 empathy_roster_contact_get_contact (EmpathyRosterContact *self)
601 {
602   return self->priv->contact;
603 }
604
605 gboolean
606 empathy_roster_contact_is_online (EmpathyRosterContact *self)
607 {
608   return self->priv->online;
609 }
610
611 const gchar *
612 empathy_roster_contact_get_group (EmpathyRosterContact *self)
613 {
614   return self->priv->group;
615 }
616
617 void
618 empathy_roster_contact_set_event_icon (EmpathyRosterContact *self,
619     const gchar *icon)
620 {
621   if (!tp_strdiff (self->priv->event_icon, icon))
622     return;
623
624   g_free (self->priv->event_icon);
625   self->priv->event_icon = g_strdup (icon);
626
627   update_presence_icon (self);
628 }
629
630 GdkPixbuf *
631 empathy_roster_contact_get_avatar_pixbuf (EmpathyRosterContact *self)
632 {
633   return gtk_image_get_pixbuf (GTK_IMAGE (self->priv->avatar));
634 }