]> git.0d.be Git - empathy.git/blob - libempathy-gtk/empathy-chat.c
Don't unhighlight chat tabs when more messages are received
[empathy.git] / libempathy-gtk / empathy-chat.c
1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
3  * Copyright (C) 2002-2007 Imendio AB
4  * Copyright (C) 2007-2010 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., 51 Franklin St, Fifth Floor,
19  * Boston, MA  02110-1301  USA
20  *
21  * Authors: Mikael Hallendal <micke@imendio.com>
22  *          Richard Hult <richard@imendio.com>
23  *          Martyn Russell <martyn@imendio.com>
24  *          Geert-Jan Van den Bogaerde <geertjan@gnome.org>
25  *          Xavier Claessens <xclaesse@gmail.com>
26  */
27
28 #include <config.h>
29
30 #include <string.h>
31 #include <stdlib.h>
32
33 #undef G_DISABLE_DEPRECATED /* for GCompletion */
34 #include <gdk/gdkkeysyms.h>
35 #include <glib/gi18n-lib.h>
36 #include <gtk/gtk.h>
37
38 #include <telepathy-glib/account-manager.h>
39 #include <telepathy-glib/util.h>
40 #include <telepathy-logger/log-manager.h>
41 #include <telepathy-logger/text-event.h>
42 #include <libempathy/empathy-contact-list.h>
43 #include <libempathy/empathy-gsettings.h>
44 #include <libempathy/empathy-keyring.h>
45 #include <libempathy/empathy-utils.h>
46 #include <libempathy/empathy-request-util.h>
47 #include <libempathy/empathy-chatroom-manager.h>
48
49 #include "empathy-chat.h"
50 #include "empathy-spell.h"
51 #include "empathy-contact-dialogs.h"
52 #include "empathy-individual-store-channel.h"
53 #include "empathy-individual-view.h"
54 #include "empathy-input-text-view.h"
55 #include "empathy-search-bar.h"
56 #include "empathy-theme-manager.h"
57 #include "empathy-theme-adium.h"
58 #include "empathy-smiley-manager.h"
59 #include "empathy-ui-utils.h"
60 #include "empathy-string-parser.h"
61 #include "extensions/extensions.h"
62
63 #define DEBUG_FLAG EMPATHY_DEBUG_CHAT
64 #include <libempathy/empathy-debug.h>
65
66 #define IS_ENTER(v) (v == GDK_KEY_Return || v == GDK_KEY_ISO_Enter || v == GDK_KEY_KP_Enter)
67 #define COMPOSING_STOP_TIMEOUT 5
68
69 #define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyChat)
70 struct _EmpathyChatPriv {
71         EmpathyTpChat     *tp_chat;
72         TpAccount         *account;
73         gchar             *id;
74         gchar             *name;
75         gchar             *subject;
76         EmpathyContact    *self_contact;
77         EmpathyContact    *remote_contact;
78         gboolean           show_contacts;
79
80         GSettings         *gsettings_chat;
81         GSettings         *gsettings_ui;
82
83         TplLogManager     *log_manager;
84         TpAccountManager  *account_manager;
85         GList             *input_history;
86         GList             *input_history_current;
87         GList             *compositors;
88         GCompletion       *completion;
89         guint              composing_stop_timeout_id;
90         guint              block_events_timeout_id;
91         TpHandleType       handle_type;
92         gint               contacts_width;
93         gboolean           has_input_vscroll;
94
95         /* TRUE if spell checking is enabled, FALSE otherwise.
96          * This is to keep track of the last state of spell checking
97          * when it changes. */
98         gboolean           spell_checking_enabled;
99
100         /* These store the signal handler ids for the enclosed text entry. */
101         gulong             insert_text_id;
102         gulong             delete_range_id;
103         gulong             notify_cursor_position_id;
104
105         /* Source func ID for update_misspelled_words () */
106         guint              update_misspelled_words_id;
107         /* Source func ID for save_paned_pos_timeout () */
108         guint              save_paned_pos_id;
109         /* Source func ID for chat_contacts_visible_timeout_cb () */
110         guint              contacts_visible_id;
111
112         GtkWidget         *widget;
113         GtkWidget         *hpaned;
114         GtkWidget         *vbox_left;
115         GtkWidget         *scrolled_window_chat;
116         GtkWidget         *scrolled_window_input;
117         GtkWidget         *scrolled_window_contacts;
118         GtkWidget         *hbox_topic;
119         GtkWidget         *expander_topic;
120         GtkWidget         *label_topic;
121         GtkWidget         *contact_list_view;
122         GtkWidget         *info_bar_vbox;
123         GtkWidget         *search_bar;
124
125         guint              unread_messages;
126         guint              unread_messages_when_offline;
127         /* TRUE if the pending messages can be displayed. This is to avoid to show
128          * pending messages *before* messages from logs. (#603980) */
129         gboolean           can_show_pending;
130
131         /* FIXME: retrieving_backlogs flag is a workaround for Bug#610994 and should
132          * be differently handled since it introduces another race condition, which
133          * is really hard to occur, but still possible.
134          *
135          * With the current workaround (which has the race above), we need to be
136          * sure to ACK any pending messages only when the retrieval of backlogs is
137          * finished, that's why using retrieving_backlogs flag.
138          * empathy_chat_messages_read () will check this variable and not ACK
139          * anything when TRUE. It will be set TRUE at chat_constructed () and set
140          * back to FALSE when the backlog has been retrieved and the pending
141          * messages actually showed to the user.
142          *
143          * Race condition introduced with this workaround:
144          * Scenario: a message is pending, the user is notified and selects the tab.
145          * the tab with a pending message is focused before the messages are properly
146          * shown (since the preparation of the window is slower AND async WRT the
147          * tab showing), which means the user won't see any new messages (rare but
148          * possible), if he/she will change tab focus before the messages are
149          * properly shown, the tab will be set as 'seen' and the user won't be
150          * notified again about the already notified pending messages when the
151          * messages in tab will be properly shown */
152         gboolean           retrieving_backlogs;
153         gboolean           sms_channel;
154
155         /* we need to know whether populate-popup happened in response to
156          * the keyboard or the mouse. We can't ask GTK for the most recent
157          * event, because it will be a notify event. Instead we track it here */
158         GdkEventType       most_recent_event_type;
159
160         /* A regex matching our own current nickname in the room, or %NULL if
161          * !empathy_chat_is_room (). */
162         GRegex            *highlight_regex;
163
164         /* TRUE if empathy_chat_is_room() and there are unread highlighted messages.
165          * Cleared by empathy_chat_messages_read(). */
166         gboolean           highlighted;
167 };
168
169 typedef struct {
170         gchar *text; /* Original message that was specified
171                       * upon entry creation. */
172         gchar *modified_text; /* Message that was modified by user.
173                                * When no modifications were made, it is NULL */
174 } InputHistoryEntry;
175
176 enum {
177         COMPOSING,
178         NEW_MESSAGE,
179         PART_COMMAND_ENTERED,
180         LAST_SIGNAL
181 };
182
183 enum {
184         PROP_0,
185         PROP_TP_CHAT,
186         PROP_ACCOUNT,
187         PROP_ID,
188         PROP_NAME,
189         PROP_SUBJECT,
190         PROP_REMOTE_CONTACT,
191         PROP_SHOW_CONTACTS,
192         PROP_SMS_CHANNEL,
193         PROP_N_MESSAGES_SENDING,
194         PROP_NB_UNREAD_MESSAGES,
195 };
196
197 static guint signals[LAST_SIGNAL] = { 0 };
198
199 G_DEFINE_TYPE (EmpathyChat, empathy_chat, GTK_TYPE_BOX);
200
201 static gboolean update_misspelled_words (gpointer data);
202
203 static void
204 chat_get_property (GObject    *object,
205                    guint       param_id,
206                    GValue     *value,
207                    GParamSpec *pspec)
208 {
209         EmpathyChat *chat = EMPATHY_CHAT (object);
210         EmpathyChatPriv *priv = GET_PRIV (object);
211
212         switch (param_id) {
213         case PROP_TP_CHAT:
214                 g_value_set_object (value, priv->tp_chat);
215                 break;
216         case PROP_ACCOUNT:
217                 g_value_set_object (value, priv->account);
218                 break;
219         case PROP_NAME:
220                 g_value_take_string (value, empathy_chat_dup_name (chat));
221                 break;
222         case PROP_ID:
223                 g_value_set_string (value, priv->id);
224                 break;
225         case PROP_SUBJECT:
226                 g_value_set_string (value, priv->subject);
227                 break;
228         case PROP_REMOTE_CONTACT:
229                 g_value_set_object (value, priv->remote_contact);
230                 break;
231         case PROP_SHOW_CONTACTS:
232                 g_value_set_boolean (value, priv->show_contacts);
233                 break;
234         case PROP_SMS_CHANNEL:
235                 g_value_set_boolean (value, priv->sms_channel);
236                 break;
237         case PROP_N_MESSAGES_SENDING:
238                 g_value_set_uint (value,
239                         empathy_chat_get_n_messages_sending (chat));
240                 break;
241         case PROP_NB_UNREAD_MESSAGES:
242                 g_value_set_uint (value,
243                         empathy_chat_get_nb_unread_messages (chat));
244                 break;
245         default:
246                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
247                 break;
248         };
249 }
250
251 static void
252 chat_set_property (GObject      *object,
253                    guint         param_id,
254                    const GValue *value,
255                    GParamSpec   *pspec)
256 {
257         EmpathyChat *chat = EMPATHY_CHAT (object);
258
259         switch (param_id) {
260         case PROP_TP_CHAT:
261                 empathy_chat_set_tp_chat (chat, EMPATHY_TP_CHAT (g_value_get_object (value)));
262                 break;
263         case PROP_SHOW_CONTACTS:
264                 empathy_chat_set_show_contacts (chat, g_value_get_boolean (value));
265                 break;
266         default:
267                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
268                 break;
269         };
270 }
271
272 static void
273 account_reconnected (EmpathyChat *chat,
274                         TpAccount *account)
275 {
276         EmpathyChatPriv *priv = GET_PRIV (chat);
277
278         DEBUG ("Account reconnected, request a new Text channel");
279
280         /* FIXME: Ideally we should ask to handle ourself the channel so we can
281         * report the error if any but this is blocked by
282         * https://bugs.freedesktop.org/show_bug.cgi?id=13422 */
283         switch (priv->handle_type) {
284                 case TP_HANDLE_TYPE_CONTACT:
285                         if (priv->sms_channel)
286                                 empathy_sms_contact_id (
287                                         account, priv->id,
288                                         TP_USER_ACTION_TIME_NOT_USER_ACTION,
289                                         NULL, NULL);
290                         else
291                                 empathy_chat_with_contact_id (
292                                         account, priv->id,
293                                         TP_USER_ACTION_TIME_NOT_USER_ACTION,
294                                         NULL, NULL);
295                         break;
296                 case TP_HANDLE_TYPE_ROOM:
297                         empathy_join_muc (account, priv->id,
298                                 TP_USER_ACTION_TIME_NOT_USER_ACTION);
299                         break;
300                 case TP_HANDLE_TYPE_NONE:
301                 case TP_HANDLE_TYPE_LIST:
302                 case TP_HANDLE_TYPE_GROUP:
303                 default:
304                         g_assert_not_reached ();
305                         break;
306         }
307
308         g_object_unref (chat);
309 }
310
311 static void
312 chat_new_connection_cb (TpAccount   *account,
313                         guint        old_status,
314                         guint        new_status,
315                         guint        reason,
316                         gchar       *dbus_error_name,
317                         GHashTable  *details,
318                         EmpathyChat *chat)
319 {
320         EmpathyChatPriv *priv = GET_PRIV (chat);
321
322         if (new_status != TP_CONNECTION_STATUS_CONNECTED)
323                 return;
324
325         if (priv->tp_chat != NULL || account != priv->account ||
326             priv->handle_type == TP_HANDLE_TYPE_NONE ||
327             EMP_STR_EMPTY (priv->id))
328                 return;
329
330         g_object_ref (chat);
331
332         account_reconnected (chat, account);
333 }
334
335 static void
336 chat_composing_remove_timeout (EmpathyChat *chat)
337 {
338         EmpathyChatPriv *priv;
339
340         priv = GET_PRIV (chat);
341
342         if (priv->composing_stop_timeout_id) {
343                 g_source_remove (priv->composing_stop_timeout_id);
344                 priv->composing_stop_timeout_id = 0;
345         }
346 }
347
348 static void
349 set_chate_state_cb (GObject *source,
350                     GAsyncResult *result,
351                     gpointer user_data)
352 {
353         GError *error = NULL;
354
355         if (!tp_text_channel_set_chat_state_finish (TP_TEXT_CHANNEL (source), result,
356                 &error)) {
357                 DEBUG ("Failed to set chat state: %s", error->message);
358                 g_error_free (error);
359         }
360 }
361
362 static void
363 set_chat_state (EmpathyChat *self,
364                 TpChannelChatState state)
365 {
366         EmpathyChatPriv *priv = GET_PRIV (self);
367
368         if (!tp_proxy_has_interface_by_id (priv->tp_chat,
369                 TP_IFACE_QUARK_CHANNEL_INTERFACE_CHAT_STATE))
370                 return;
371
372         tp_text_channel_set_chat_state_async (TP_TEXT_CHANNEL (priv->tp_chat), state,
373                 set_chate_state_cb, self);
374 }
375
376 static gboolean
377 chat_composing_stop_timeout_cb (EmpathyChat *chat)
378 {
379         EmpathyChatPriv *priv;
380
381         priv = GET_PRIV (chat);
382
383         priv->composing_stop_timeout_id = 0;
384         set_chat_state (chat, TP_CHANNEL_CHAT_STATE_PAUSED);
385
386         return FALSE;
387 }
388
389 static void
390 chat_composing_start (EmpathyChat *chat)
391 {
392         EmpathyChatPriv *priv;
393
394         priv = GET_PRIV (chat);
395
396         if (priv->composing_stop_timeout_id) {
397                 /* Just restart the timeout */
398                 chat_composing_remove_timeout (chat);
399         } else {
400                 set_chat_state (chat, TP_CHANNEL_CHAT_STATE_COMPOSING);
401         }
402
403         priv->composing_stop_timeout_id = g_timeout_add_seconds (
404                 COMPOSING_STOP_TIMEOUT,
405                 (GSourceFunc) chat_composing_stop_timeout_cb,
406                 chat);
407 }
408
409 static void
410 chat_composing_stop (EmpathyChat *chat)
411 {
412         chat_composing_remove_timeout (chat);
413         set_chat_state (chat, TP_CHANNEL_CHAT_STATE_ACTIVE);
414 }
415
416 static gint
417 chat_input_history_entry_cmp (InputHistoryEntry *entry,
418                               const gchar *text)
419 {
420         if (!tp_strdiff (entry->text, text)) {
421                 if (entry->modified_text != NULL) {
422                         /* Modified entry and single string cannot be equal. */
423                         return 1;
424                 }
425                 return 0;
426         }
427         return 1;
428 }
429
430 static InputHistoryEntry *
431 chat_input_history_entry_new_with_text (const gchar *text)
432 {
433         InputHistoryEntry *entry;
434         entry = g_slice_new0 (InputHistoryEntry);
435         entry->text = g_strdup (text);
436
437         return entry;
438 }
439
440 static void
441 chat_input_history_entry_free (InputHistoryEntry *entry)
442 {
443         g_free (entry->text);
444         g_free (entry->modified_text);
445         g_slice_free (InputHistoryEntry, entry);
446 }
447
448 static void
449 chat_input_history_entry_revert (InputHistoryEntry *entry)
450 {
451         g_free (entry->modified_text);
452         entry->modified_text = NULL;
453 }
454
455 static void
456 chat_input_history_entry_update_text (InputHistoryEntry *entry,
457                                       const gchar *text)
458 {
459         gchar *old;
460
461         if (!tp_strdiff (text, entry->text)) {
462                 g_free (entry->modified_text);
463                 entry->modified_text = NULL;
464                 return;
465         }
466
467         old = entry->modified_text;
468         entry->modified_text = g_strdup (text);
469         g_free (old);
470 }
471
472 static const gchar *
473 chat_input_history_entry_get_text (InputHistoryEntry *entry)
474 {
475         if (entry == NULL) {
476                 return NULL;
477         }
478
479         if (entry->modified_text != NULL) {
480                 return entry->modified_text;
481         }
482         return entry->text;
483 }
484
485 static GList *
486 chat_input_history_remove_item (GList *list,
487                                 GList *item)
488 {
489         list = g_list_remove_link (list, item);
490         chat_input_history_entry_free (item->data);
491         g_list_free_1 (item);
492         return list;
493 }
494
495 static void
496 chat_input_history_revert (EmpathyChat *chat)
497 {
498         EmpathyChatPriv   *priv;
499         GList             *list;
500         GList             *item1;
501         GList             *item2;
502         InputHistoryEntry *entry;
503
504         priv = GET_PRIV (chat);
505         list = priv->input_history;
506
507         if (list == NULL) {
508                 DEBUG ("No input history");
509                 return;
510         }
511
512         /* Delete temporary entry */
513         if (priv->input_history_current != NULL) {
514                 item1 = list;
515                 list = chat_input_history_remove_item (list, item1);
516                 if (priv->input_history_current == item1) {
517                         /* Removed temporary entry was current entry */
518                         priv->input_history = list;
519                         priv->input_history_current = NULL;
520                         return;
521                 }
522         }
523         else {
524                 /* There is no entry to revert */
525                 return;
526         }
527
528         /* Restore the current history entry to original value */
529         item1 = priv->input_history_current;
530         entry = item1->data;
531         chat_input_history_entry_revert (entry);
532
533         /* Remove restored entry if there is other occurance before this entry */
534         item2 = g_list_find_custom (list, chat_input_history_entry_get_text (entry),
535                                     (GCompareFunc) chat_input_history_entry_cmp);
536         if (item2 != item1) {
537                 list = chat_input_history_remove_item (list, item1);
538         }
539         else {
540                 /* Remove other occurance of the restored entry */
541                 item2 = g_list_find_custom (item1->next,
542                                             chat_input_history_entry_get_text (entry),
543                                             (GCompareFunc) chat_input_history_entry_cmp);
544                 if (item2 != NULL) {
545                         list = chat_input_history_remove_item (list, item2);
546                 }
547         }
548
549         priv->input_history_current = NULL;
550         priv->input_history = list;
551 }
552
553 static void
554 chat_input_history_add (EmpathyChat  *chat,
555                         const gchar *str,
556                         gboolean temporary)
557 {
558         EmpathyChatPriv   *priv;
559         GList             *list;
560         GList             *item;
561         InputHistoryEntry *entry;
562
563         priv = GET_PRIV (chat);
564
565         list = priv->input_history;
566
567         /* Remove any other occurances of this entry, if not temporary */
568         if (!temporary) {
569                 while ((item = g_list_find_custom (list, str,
570                     (GCompareFunc) chat_input_history_entry_cmp)) != NULL) {
571                         list = chat_input_history_remove_item (list, item);
572                 }
573
574                 /* Trim the list to the last 10 items */
575                 while (g_list_length (list) > 10) {
576                         item = g_list_last (list);
577                         if (item != NULL) {
578                                 list = chat_input_history_remove_item (list, item);
579                         }
580                 }
581         }
582
583
584
585         /* Add new entry */
586         entry = chat_input_history_entry_new_with_text (str);
587         list = g_list_prepend (list, entry);
588
589         /* Set the list and the current item pointer */
590         priv->input_history = list;
591         if (temporary) {
592                 priv->input_history_current = list;
593         }
594         else {
595                 priv->input_history_current = NULL;
596         }
597 }
598
599 static const gchar *
600 chat_input_history_get_next (EmpathyChat *chat)
601 {
602         EmpathyChatPriv *priv;
603         GList           *item;
604         const gchar     *msg;
605
606         priv = GET_PRIV (chat);
607
608         if (priv->input_history == NULL) {
609                 DEBUG ("No input history, next entry is NULL");
610                 return NULL;
611         }
612         g_assert (priv->input_history_current != NULL);
613
614         if ((item = g_list_next (priv->input_history_current)) == NULL)
615         {
616                 item = priv->input_history_current;
617         }
618
619         msg = chat_input_history_entry_get_text (item->data);
620
621         DEBUG ("Returning next entry: '%s'", msg);
622
623         priv->input_history_current = item;
624
625         return msg;
626 }
627
628 static const gchar *
629 chat_input_history_get_prev (EmpathyChat *chat)
630 {
631         EmpathyChatPriv *priv;
632         GList           *item;
633         const gchar     *msg;
634
635         g_return_val_if_fail (EMPATHY_IS_CHAT (chat), NULL);
636
637         priv = GET_PRIV (chat);
638
639         if (priv->input_history == NULL) {
640                 DEBUG ("No input history, previous entry is NULL");
641                 return NULL;
642         }
643
644         if (priv->input_history_current == NULL)
645         {
646                 return NULL;
647         }
648         else if ((item = g_list_previous (priv->input_history_current)) == NULL)
649         {
650                 item = priv->input_history_current;
651         }
652
653         msg = chat_input_history_entry_get_text (item->data);
654
655         DEBUG ("Returning previous entry: '%s'", msg);
656
657         priv->input_history_current = item;
658
659         return msg;
660 }
661
662 static void
663 chat_input_history_update (EmpathyChat *chat,
664                            GtkTextBuffer *buffer)
665 {
666         EmpathyChatPriv      *priv;
667         GtkTextIter           start, end;
668         gchar                *text;
669         InputHistoryEntry    *entry;
670
671         priv = GET_PRIV (chat);
672
673         gtk_text_buffer_get_bounds (buffer, &start, &end);
674         text = gtk_text_buffer_get_text (buffer, &start, &end, FALSE);
675
676         if (priv->input_history_current == NULL) {
677                 /* Add the current text temporarily to the history */
678                 chat_input_history_add (chat, text, TRUE);
679                 g_free (text);
680                 return;
681         }
682
683         /* Save the changes in the history */
684         entry = priv->input_history_current->data;
685         if (tp_strdiff (chat_input_history_entry_get_text (entry), text)) {
686                 chat_input_history_entry_update_text (entry, text);
687         }
688
689         g_free (text);
690 }
691
692 typedef struct {
693         EmpathyChat *chat;
694         gchar *message;
695 } ChatCommandMsgData;
696
697 static void
698 chat_command_msg_cb (GObject *source,
699                               GAsyncResult *result,
700                               gpointer                  user_data)
701 {
702         ChatCommandMsgData *data = user_data;
703         GError *error = NULL;
704         TpChannel *channel;
705
706         channel = tp_account_channel_request_ensure_and_observe_channel_finish (
707                                         TP_ACCOUNT_CHANNEL_REQUEST (source), result, &error);
708
709         if (channel == NULL) {
710                 DEBUG ("Failed to get channel: %s", error->message);
711                 g_error_free (error);
712
713                 empathy_chat_view_append_event (data->chat->view,
714                         _("Failed to open private chat"));
715                 goto OUT;
716         }
717
718         if (!EMP_STR_EMPTY (data->message) && TP_IS_TEXT_CHANNEL (channel)) {
719                 TpTextChannel *text = (TpTextChannel *) channel;
720                 TpMessage *msg;
721
722                 msg = tp_client_message_new_text (TP_CHANNEL_TEXT_MESSAGE_TYPE_NORMAL,
723                         data->message);
724
725                 tp_text_channel_send_message_async (text, msg, 0, NULL, NULL);
726
727                 g_object_unref (msg);
728         }
729
730         g_object_unref (channel);
731
732 OUT:
733         g_free (data->message);
734         g_slice_free (ChatCommandMsgData, data);
735 }
736
737 static gboolean
738 nick_command_supported (EmpathyChat *chat)
739 {
740         EmpathyChatPriv * priv = GET_PRIV (chat);
741         TpConnection    *connection;
742
743         connection = tp_channel_borrow_connection (TP_CHANNEL (priv->tp_chat));
744         return tp_proxy_has_interface_by_id (connection,
745                 EMP_IFACE_QUARK_CONNECTION_INTERFACE_RENAMING);
746 }
747
748 static gboolean
749 part_command_supported (EmpathyChat *chat)
750 {
751         EmpathyChatPriv * priv = GET_PRIV (chat);
752
753         return tp_proxy_has_interface_by_id (priv->tp_chat,
754                         TP_IFACE_QUARK_CHANNEL_INTERFACE_GROUP);
755 }
756
757 static void
758 chat_command_clear (EmpathyChat *chat,
759                     GStrv        strv)
760 {
761         empathy_chat_view_clear (chat->view);
762 }
763
764 static void
765 chat_command_topic (EmpathyChat *chat,
766                     GStrv        strv)
767 {
768         EmpathyChatPriv *priv = GET_PRIV (chat);
769
770         if (!empathy_tp_chat_supports_subject (priv->tp_chat)) {
771                 empathy_chat_view_append_event (chat->view,
772                         _("Topic not supported on this conversation"));
773                 return;
774         }
775
776         if (!empathy_tp_chat_can_set_subject (priv->tp_chat)) {
777                 empathy_chat_view_append_event (chat->view,
778                         _("You are not allowed to change the topic"));
779                 return;
780         }
781
782         empathy_tp_chat_set_subject (priv->tp_chat, strv[1]);
783 }
784
785 void
786 empathy_chat_join_muc (EmpathyChat *chat,
787                    const gchar   *room)
788 {
789         EmpathyChatPriv *priv = GET_PRIV (chat);
790
791         empathy_join_muc (priv->account, room,
792                 empathy_get_current_action_time ());
793 }
794
795 static void
796 chat_command_join (EmpathyChat *chat,
797                    GStrv        strv)
798 {
799         guint i = 0;
800
801         GStrv rooms = g_strsplit_set (strv[1], ", ", -1);
802
803         /* FIXME: Ideally we should ask to handle ourself the channel so we can
804         * report the error if any but this is blocked by
805         * https://bugs.freedesktop.org/show_bug.cgi?id=13422 */
806         while (rooms[i] != NULL) {
807                 /* ignore empty strings */
808                 if (!EMP_STR_EMPTY (rooms[i])) {
809                         empathy_chat_join_muc (chat, rooms[i]);
810                 }
811                 i++;
812         }
813         g_strfreev (rooms);
814 }
815
816 static void
817 chat_command_part (EmpathyChat *chat,
818                            GStrv        strv)
819 {
820         g_signal_emit (chat, signals[PART_COMMAND_ENTERED], 0, strv);
821 }
822
823 static void
824 chat_command_msg_internal (EmpathyChat *chat,
825                            const gchar *contact_id,
826                            const gchar *message)
827 {
828         EmpathyChatPriv *priv = GET_PRIV (chat);
829         ChatCommandMsgData *data;
830         TpAccountChannelRequest *req;
831         GHashTable *request;
832
833         request = tp_asv_new (
834                 TP_PROP_CHANNEL_CHANNEL_TYPE, G_TYPE_STRING, TP_IFACE_CHANNEL_TYPE_TEXT,
835                 TP_PROP_CHANNEL_TARGET_HANDLE_TYPE, G_TYPE_UINT, TP_HANDLE_TYPE_CONTACT,
836                 TP_PROP_CHANNEL_TARGET_ID, G_TYPE_STRING, contact_id,
837                 NULL);
838
839         req = tp_account_channel_request_new (priv->account, request,
840                 empathy_get_current_action_time ());
841
842         /* FIXME: We should probably search in members alias. But this
843          * is enough for IRC */
844         data = g_slice_new (ChatCommandMsgData);
845         data->chat = chat;
846         data->message = g_strdup (message);
847
848         tp_account_channel_request_ensure_and_observe_channel_async (req,
849                 EMPATHY_CHAT_BUS_NAME, NULL, chat_command_msg_cb, data);
850
851         g_object_unref (req);
852         g_hash_table_unref (request);
853 }
854
855 static void
856 chat_command_query (EmpathyChat *chat,
857                     GStrv        strv)
858 {
859         /* If <message> part is not defined,
860          * strv[2] will be the terminal NULL */
861         chat_command_msg_internal (chat, strv[1], strv[2]);
862 }
863
864 static void
865 chat_command_msg (EmpathyChat *chat,
866                   GStrv        strv)
867 {
868         chat_command_msg_internal (chat, strv[1], strv[2]);
869 }
870
871 static void
872 callback_for_request_rename (TpProxy *proxy,
873                   const GError *error,
874                   gpointer user_data,
875                   GObject *weak_object)
876 {
877         if (error != NULL) {
878                 DEBUG ("Call to RequestRename method failed: %s",error->message);
879         }
880 }
881
882 static void
883 chat_command_nick (EmpathyChat *chat,
884                    GStrv        strv)
885 {
886         EmpathyChatPriv *priv = GET_PRIV (chat);
887         TpProxy *proxy;
888
889         proxy = TP_PROXY (tp_account_get_connection (priv->account));
890
891         emp_cli_connection_interface_renaming_call_request_rename (proxy, -1,
892                 strv[1], callback_for_request_rename, NULL, NULL, NULL);
893 }
894
895 static void
896 chat_command_me (EmpathyChat *chat,
897                   GStrv        strv)
898 {
899         EmpathyChatPriv *priv = GET_PRIV (chat);
900         TpMessage *message;
901         TpTextChannel *channel;
902
903         channel = (TpTextChannel *) (priv->tp_chat);
904
905         if (!tp_text_channel_supports_message_type (channel,
906                         TP_CHANNEL_TEXT_MESSAGE_TYPE_ACTION)) {
907                 /* Action message are not supported, 'simulate' the action */
908                 gchar *tmp;
909
910                 /* The TpChat can't be ready if it doesn't have the self contact */
911                 g_assert (priv->self_contact != NULL);
912
913                 tmp = g_strdup_printf ("%s %s", empathy_contact_get_alias (priv->self_contact),
914                         strv[1]);
915                 message = tp_client_message_new_text (TP_CHANNEL_TEXT_MESSAGE_TYPE_NORMAL,
916                         tmp);
917                 g_free (tmp);
918         }
919         else {
920                 message = tp_client_message_new_text (TP_CHANNEL_TEXT_MESSAGE_TYPE_ACTION,
921                         strv[1]);
922         }
923
924         empathy_tp_chat_send (priv->tp_chat, message);
925         g_object_unref (message);
926 }
927
928 static void
929 chat_command_say (EmpathyChat *chat,
930                   GStrv        strv)
931 {
932         EmpathyChatPriv *priv = GET_PRIV (chat);
933         TpMessage *message;
934
935         message = tp_client_message_new_text (TP_CHANNEL_TEXT_MESSAGE_TYPE_NORMAL,
936                 strv[1]);
937         empathy_tp_chat_send (priv->tp_chat, message);
938         g_object_unref (message);
939 }
940
941 static void
942 whois_got_contact_cb (TpConnection *connection,
943                       guint n_contacts,
944                       TpContact * const *contacts,
945                       const gchar * const *requested_ids,
946                       GHashTable *failed_id_errors,
947                       const GError *error,
948                       gpointer user_data,
949                       GObject *weak_object)
950 {
951         EmpathyChat *chat = EMPATHY_CHAT (weak_object);
952
953         g_return_if_fail (n_contacts <= 1);
954
955         if (n_contacts == 0) {
956                 GHashTableIter iter;
957                 gpointer key = NULL, value = NULL;
958                 gchar *id;
959                 GError *id_error;
960
961                 /* tp-glib guarantees that the contacts you requested will be
962                  * in failed_id_errors regardless of whether the individual
963                  * contact was invalid or the whole operation failed.
964                  */
965                 g_hash_table_iter_init (&iter, failed_id_errors);
966                 g_hash_table_iter_next (&iter, &key, &value);
967                 id = key;
968                 id_error = value;
969
970                 DEBUG ("Error getting TpContact for '%s': %s %u %s",
971                         id, g_quark_to_string (id_error->domain),
972                         id_error->code, id_error->message);
973
974                 if (error == NULL) {
975                         /* The specific ID failed. */
976                         gchar *event = g_strdup_printf (
977                                 _("“%s” is not a valid contact ID"), id);
978                         empathy_chat_view_append_event (chat->view, event);
979                         g_free (event);
980                 }
981                 /* Otherwise we're disconnected or something; so the window
982                  * will already say ‘Disconnected’, so let's not show anything.
983                  */
984         } else {
985                 EmpathyContact *empathy_contact;
986                 GtkWidget *window;
987
988                 g_return_if_fail (contacts[0] != NULL);
989                 empathy_contact = empathy_contact_dup_from_tp_contact (
990                         contacts[0]);
991
992                 window = gtk_widget_get_toplevel (GTK_WIDGET (chat));
993                 /* If we're alive and this command is running, we'd better be
994                  * in a window. */
995                 g_return_if_fail (window != NULL);
996                 g_return_if_fail (gtk_widget_is_toplevel (window));
997                 empathy_contact_information_dialog_show (empathy_contact,
998                         GTK_WINDOW (window));
999                 g_object_unref (empathy_contact);
1000         }
1001 }
1002
1003 static void
1004 chat_command_whois (EmpathyChat *chat,
1005                     GStrv strv)
1006 {
1007         EmpathyChatPriv *priv = GET_PRIV (chat);
1008         TpConnection *conn;
1009
1010         conn = tp_channel_borrow_connection ((TpChannel *) priv->tp_chat);
1011         tp_connection_get_contacts_by_id (conn,
1012                 /* Element 0 of 'strv' is "whois"; element 1 is the contact ID
1013                  * entered by the user (including spaces, if any). */
1014                 1, (const gchar * const *) strv + 1,
1015                 0, NULL,
1016                 whois_got_contact_cb, NULL, NULL, G_OBJECT (chat));
1017 }
1018
1019 static void
1020 chat_command_whale (EmpathyChat *chat,
1021                     GStrv        strv)
1022 {
1023         EmpathyChatPriv *priv = GET_PRIV (chat);
1024         TpMessage *message;
1025
1026         message = tp_client_message_new_text (TP_CHANNEL_TEXT_MESSAGE_TYPE_NORMAL,
1027                 "\n\n\n"
1028                 "•_______________•");
1029         empathy_tp_chat_send (priv->tp_chat, message);
1030         g_object_unref (message);
1031 }
1032
1033 static void
1034 chat_command_babywhale (EmpathyChat *chat,
1035                         GStrv        strv)
1036 {
1037         EmpathyChatPriv *priv = GET_PRIV (chat);
1038         TpMessage *message;
1039
1040         message = tp_client_message_new_text (TP_CHANNEL_TEXT_MESSAGE_TYPE_NORMAL,
1041                 "\n"
1042                 "•_____•");
1043         empathy_tp_chat_send (priv->tp_chat, message);
1044         g_object_unref (message);
1045 }
1046
1047 static void
1048 chat_command_inspector (EmpathyChat *chat,
1049                     GStrv        strv)
1050 {
1051         if (EMPATHY_IS_THEME_ADIUM (chat->view)) {
1052                 empathy_theme_adium_show_inspector (
1053                         EMPATHY_THEME_ADIUM (chat->view));
1054         }
1055 }
1056
1057 static void chat_command_help (EmpathyChat *chat, GStrv strv);
1058
1059 typedef void (*ChatCommandFunc) (EmpathyChat *chat, GStrv strv);
1060
1061 typedef struct {
1062         const gchar *prefix;
1063         guint min_parts;
1064         guint max_parts;
1065         ChatCommandFunc func;
1066         gboolean (*is_supported)(EmpathyChat *chat);
1067         const gchar *help;
1068 } ChatCommandItem;
1069
1070 static ChatCommandItem commands[] = {
1071         {"clear", 1, 1, chat_command_clear, NULL,
1072          N_("/clear: clear all messages from the current conversation")},
1073
1074         {"topic", 2, 2, chat_command_topic, NULL,
1075          N_("/topic <topic>: set the topic of the current conversation")},
1076
1077         {"join", 2, 2, chat_command_join, NULL,
1078          N_("/join <chat room ID>: join a new chat room")},
1079
1080         {"j", 2, 2, chat_command_join, NULL,
1081          N_("/j <chat room ID>: join a new chat room")},
1082
1083         /* FIXME: https://bugzilla.gnome.org/show_bug.cgi?id=643295 */
1084         {"part", 1, 3, chat_command_part, part_command_supported,
1085          N_("/part [<chat room ID>] [<reason>]: leave the chat room, "
1086             "by default the current one")},
1087
1088         {"query", 2, 3, chat_command_query, NULL,
1089          N_("/query <contact ID> [<message>]: open a private chat")},
1090
1091         {"msg", 3, 3, chat_command_msg, NULL,
1092          N_("/msg <contact ID> <message>: open a private chat")},
1093
1094         {"nick", 2, 2, chat_command_nick, nick_command_supported,
1095          N_("/nick <nickname>: change your nickname on the current server")},
1096
1097         {"me", 2, 2, chat_command_me, NULL,
1098          N_("/me <message>: send an ACTION message to the current conversation")},
1099
1100         {"say", 2, 2, chat_command_say, NULL,
1101          N_("/say <message>: send <message> to the current conversation. "
1102             "This is used to send a message starting with a '/'. For example: "
1103             "\"/say /join is used to join a new chat room\"")},
1104
1105         {"whois", 2, 2, chat_command_whois, NULL,
1106          N_("/whois <contact ID>: display information about a contact")},
1107
1108         {"help", 1, 2, chat_command_help, NULL,
1109          N_("/help [<command>]: show all supported commands. "
1110             "If <command> is defined, show its usage.")},
1111
1112         {"inspector", 1, 1, chat_command_inspector, NULL, NULL},
1113
1114         {"whale", 1, 1, chat_command_whale, NULL, NULL},
1115         {"babywhale", 1, 1, chat_command_babywhale, NULL, NULL},
1116 };
1117
1118 static void
1119 chat_command_show_help (EmpathyChat     *chat,
1120                         ChatCommandItem *item)
1121 {
1122         gchar *str;
1123
1124         if (item->help == NULL) {
1125                 return;
1126         }
1127
1128         str = g_strdup_printf (_("Usage: %s"), _(item->help));
1129         empathy_chat_view_append_event (chat->view, str);
1130         g_free (str);
1131 }
1132
1133 static void
1134 chat_command_help (EmpathyChat *chat,
1135                    GStrv        strv)
1136 {
1137         guint i;
1138
1139         /* If <command> part is not defined,
1140          * strv[1] will be the terminal NULL */
1141         if (strv[1] == NULL) {
1142                 for (i = 0; i < G_N_ELEMENTS (commands); i++) {
1143                         if (commands[i].is_supported != NULL) {
1144                                 if (!commands[i].is_supported (chat)) {
1145                                         continue;
1146                                 }
1147                         }
1148                         if (commands[i].help == NULL) {
1149                                 continue;
1150                         }
1151                         empathy_chat_view_append_event (chat->view,
1152                                 _(commands[i].help));
1153                 }
1154                 return;
1155         }
1156
1157         for (i = 0; i < G_N_ELEMENTS (commands); i++) {
1158                 if (g_ascii_strcasecmp (strv[1], commands[i].prefix) == 0) {
1159                         if (commands[i].is_supported != NULL) {
1160                                 if (!commands[i].is_supported (chat)) {
1161                                         break;
1162                                 }
1163                         }
1164                         if (commands[i].help == NULL) {
1165                                 break;
1166                         }
1167                         chat_command_show_help (chat, &commands[i]);
1168                         return;
1169                 }
1170         }
1171
1172         empathy_chat_view_append_event (chat->view,
1173                 _("Unknown command"));
1174 }
1175
1176 static GStrv
1177 chat_command_parse (const gchar *text, guint max_parts)
1178 {
1179         GPtrArray *array;
1180         gchar *item;
1181
1182         DEBUG ("Parse command, parts=%d text=\"%s\":", max_parts, text);
1183
1184         array = g_ptr_array_sized_new (max_parts + 1);
1185         while (max_parts > 1) {
1186                 const gchar *end;
1187
1188                 /* Skip white spaces */
1189                 while (g_ascii_isspace (*text)) {
1190                         text++;
1191                 }
1192
1193                 /* Search the end of this part, until first space. */
1194                 for (end = text; *end != '\0' && !g_ascii_isspace (*end); end++)
1195                         /* Do nothing */;
1196                 if (*end == '\0') {
1197                         break;
1198                 }
1199
1200                 item = g_strndup (text, end - text);
1201                 g_ptr_array_add (array, item);
1202                 DEBUG ("\tITEM: \"%s\"", item);
1203
1204                 text = end;
1205                 max_parts--;
1206         }
1207
1208         /* Append last part if not empty */
1209         item = g_strstrip (g_strdup (text));
1210         if (!EMP_STR_EMPTY (item)) {
1211                 g_ptr_array_add (array, item);
1212                 DEBUG ("\tITEM: \"%s\"", item);
1213         } else {
1214                 g_free (item);
1215         }
1216
1217         /* Make the array NULL-terminated */
1218         g_ptr_array_add (array, NULL);
1219
1220         return (GStrv) g_ptr_array_free (array, FALSE);
1221 }
1222
1223 static gboolean
1224 has_prefix_case (const gchar *s,
1225                   const gchar *prefix)
1226 {
1227         return g_ascii_strncasecmp (s, prefix, strlen (prefix)) == 0;
1228 }
1229
1230 static void
1231 chat_send (EmpathyChat  *chat,
1232            const gchar *msg)
1233 {
1234         EmpathyChatPriv *priv;
1235         TpMessage  *message;
1236         guint            i;
1237
1238         if (EMP_STR_EMPTY (msg)) {
1239                 return;
1240         }
1241
1242         priv = GET_PRIV (chat);
1243
1244         chat_input_history_add (chat, msg, FALSE);
1245
1246         if (msg[0] == '/') {
1247                 gboolean second_slash = FALSE;
1248                 const gchar *iter = msg + 1;
1249
1250                 for (i = 0; i < G_N_ELEMENTS (commands); i++) {
1251                         GStrv strv;
1252                         guint strv_len;
1253                         gchar c;
1254
1255                         if (!has_prefix_case (msg + 1, commands[i].prefix)) {
1256                                 continue;
1257                         }
1258                         c = *(msg + 1 + strlen (commands[i].prefix));
1259                         if (c != '\0' && !g_ascii_isspace (c)) {
1260                                 continue;
1261                         }
1262                         if (commands[i].is_supported != NULL) {
1263                                 if (!commands[i].is_supported (chat)) {
1264                                         continue;
1265                                 }
1266                         }
1267
1268                         /* We can't use g_strsplit here because it does
1269                          * not deal correctly if we have more than one space
1270                          * between args */
1271                         strv = chat_command_parse (msg + 1, commands[i].max_parts);
1272
1273                         strv_len = g_strv_length (strv);
1274                         if (strv_len < commands[i].min_parts ||
1275                             strv_len > commands[i].max_parts) {
1276                                 chat_command_show_help (chat, &commands[i]);
1277                                 g_strfreev (strv);
1278                                 return;
1279                         }
1280
1281                         commands[i].func (chat, strv);
1282                         g_strfreev (strv);
1283                         return;
1284                 }
1285
1286                 /* Also allow messages with two slashes before the
1287                  * first space, so it is possible to send a /unix/path.
1288                  * This heuristic is kind of crap. */
1289                 while (*iter != '\0' && !g_ascii_isspace (*iter)) {
1290                         if (*iter == '/') {
1291                                 second_slash = TRUE;
1292                                 break;
1293                         }
1294                         iter++;
1295                 }
1296
1297                 if (!second_slash) {
1298                         empathy_chat_view_append_event (chat->view,
1299                                 _("Unknown command; see /help for the available"
1300                                   " commands"));
1301                         return;
1302                 }
1303         }
1304
1305         message = tp_client_message_new_text (TP_CHANNEL_TEXT_MESSAGE_TYPE_NORMAL,
1306                 msg);
1307         empathy_tp_chat_send (priv->tp_chat, message);
1308         g_object_unref (message);
1309 }
1310
1311 static void
1312 chat_input_text_view_send (EmpathyChat *chat)
1313 {
1314         GtkTextBuffer  *buffer;
1315         GtkTextIter     start, end;
1316         gchar          *msg;
1317
1318         buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (chat->input_text_view));
1319
1320         gtk_text_buffer_get_bounds (buffer, &start, &end);
1321         msg = gtk_text_buffer_get_text (buffer, &start, &end, FALSE);
1322
1323         /* clear the input field */
1324         gtk_text_buffer_set_text (buffer, "", -1);
1325         /* delete input history modifications */
1326         chat_input_history_revert (chat);
1327
1328         chat_send (chat, msg);
1329         g_free (msg);
1330 }
1331
1332 static void
1333 chat_state_changed_cb (EmpathyTpChat      *tp_chat,
1334                        EmpathyContact     *contact,
1335                        TpChannelChatState  state,
1336                        EmpathyChat        *chat)
1337 {
1338         EmpathyChatPriv *priv;
1339         GList          *l;
1340         gboolean        was_composing;
1341
1342         priv = GET_PRIV (chat);
1343
1344         if (empathy_contact_is_user (contact)) {
1345                 /* We don't care about our own chat state */
1346                 return;
1347         }
1348
1349         was_composing = (priv->compositors != NULL);
1350
1351         /* Find the contact in the list. After that l is the list elem or NULL */
1352         for (l = priv->compositors; l; l = l->next) {
1353                 if (contact == l->data) {
1354                         break;
1355                 }
1356         }
1357
1358         switch (state) {
1359         case TP_CHANNEL_CHAT_STATE_GONE:
1360         case TP_CHANNEL_CHAT_STATE_INACTIVE:
1361         case TP_CHANNEL_CHAT_STATE_PAUSED:
1362         case TP_CHANNEL_CHAT_STATE_ACTIVE:
1363                 /* Contact is not composing */
1364                 if (l) {
1365                         priv->compositors = g_list_remove_link (priv->compositors, l);
1366                         g_object_unref (l->data);
1367                         g_list_free1 (l);
1368                 }
1369                 break;
1370         case TP_CHANNEL_CHAT_STATE_COMPOSING:
1371                 /* Contact is composing */
1372                 if (!l) {
1373                         priv->compositors = g_list_prepend (priv->compositors,
1374                                                             g_object_ref (contact));
1375                 }
1376                 break;
1377         default:
1378                 g_assert_not_reached ();
1379         }
1380
1381         DEBUG ("Was composing: %s now composing: %s",
1382                 was_composing ? "yes" : "no",
1383                 priv->compositors ? "yes" : "no");
1384
1385         if ((was_composing && !priv->compositors) ||
1386             (!was_composing && priv->compositors)) {
1387                 /* Composing state changed */
1388                 g_signal_emit (chat, signals[COMPOSING], 0,
1389                                priv->compositors != NULL);
1390         }
1391 }
1392
1393 static GRegex *
1394 get_highlight_regex_for (const gchar *name)
1395 {
1396         GRegex *regex;
1397         gchar *name_esc, *pattern;
1398         GError *error = NULL;
1399
1400         name_esc = g_regex_escape_string (name, -1);
1401         pattern = g_strdup_printf ("\\b%s\\b", name_esc);
1402         regex = g_regex_new (pattern, G_REGEX_CASELESS | G_REGEX_OPTIMIZE, 0,
1403                 &error);
1404
1405         if (regex == NULL) {
1406                 DEBUG ("couldn't compile regex /%s/: %s", pattern,
1407                         error->message);
1408
1409                 g_error_free (error);
1410         }
1411
1412         g_free (pattern);
1413         g_free (name_esc);
1414
1415         return regex;
1416 }
1417
1418 /* Called when priv->self_contact changes, or priv->self_contact:alias changes.
1419  * Only connected if empathy_chat_is_room() is TRUE, for obvious-ish reasons.
1420  */
1421 static void
1422 chat_self_contact_alias_changed_cb (EmpathyChat *chat)
1423 {
1424         EmpathyChatPriv *priv = GET_PRIV (chat);
1425
1426         tp_clear_pointer (&priv->highlight_regex, g_regex_unref);
1427
1428         if (priv->self_contact != NULL) {
1429                 const gchar *alias = empathy_contact_get_alias (priv->self_contact);
1430
1431                 g_return_if_fail (alias != NULL);
1432                 priv->highlight_regex = get_highlight_regex_for (alias);
1433         }
1434 }
1435
1436 static gboolean
1437 chat_should_highlight (EmpathyChat *chat,
1438         EmpathyMessage *message)
1439 {
1440         EmpathyChatPriv *priv = GET_PRIV (chat);
1441         const gchar   *msg;
1442         TpChannelTextMessageFlags flags;
1443
1444         g_return_val_if_fail (EMPATHY_IS_MESSAGE (message), FALSE);
1445
1446         if (!empathy_chat_is_room (chat)) {
1447                 return FALSE;
1448         }
1449
1450         if (!empathy_message_is_incoming (message)) {
1451                 return FALSE;
1452         }
1453
1454         msg = empathy_message_get_body (message);
1455         if (!msg) {
1456                 return FALSE;
1457         }
1458
1459         flags = empathy_message_get_flags (message);
1460         if (flags & TP_CHANNEL_TEXT_MESSAGE_FLAG_SCROLLBACK) {
1461                 /* FIXME: Ideally we shouldn't highlight scrollback messages only if they
1462                  * have already been received by the user before (and so are in the logs) */
1463                 return FALSE;
1464         }
1465
1466         if (priv->highlight_regex == NULL) {
1467                 return FALSE;
1468         }
1469
1470         return g_regex_match (priv->highlight_regex, msg, 0, NULL);
1471 }
1472
1473 static void
1474 chat_message_received (EmpathyChat *chat,
1475         EmpathyMessage *message,
1476         gboolean pending)
1477 {
1478         EmpathyChatPriv *priv = GET_PRIV (chat);
1479         EmpathyContact  *sender;
1480
1481         sender = empathy_message_get_sender (message);
1482
1483         if (empathy_message_is_edit (message)) {
1484                 DEBUG ("Editing message '%s' to '%s'",
1485                         empathy_message_get_supersedes (message),
1486                         empathy_message_get_body (message));
1487
1488                 empathy_chat_view_edit_message (chat->view, message);
1489         } else {
1490                 gboolean should_highlight = chat_should_highlight (chat, message);
1491
1492                 if (should_highlight) {
1493                         priv->highlighted = TRUE;
1494                 }
1495
1496                 DEBUG ("Appending new message '%s' from %s (%d)",
1497                         empathy_message_get_token (message),
1498                         empathy_contact_get_alias (sender),
1499                         empathy_contact_get_handle (sender));
1500
1501                 empathy_chat_view_append_message (chat->view, message, should_highlight);
1502
1503                 if (empathy_message_is_incoming (message)) {
1504                         priv->unread_messages++;
1505                         g_object_notify (G_OBJECT (chat), "nb-unread-messages");
1506                 }
1507
1508                 g_signal_emit (chat, signals[NEW_MESSAGE], 0, message, pending,
1509                                should_highlight);
1510         }
1511
1512         /* We received a message so the contact is no longer
1513          * composing */
1514         chat_state_changed_cb (priv->tp_chat, sender,
1515                                TP_CHANNEL_CHAT_STATE_ACTIVE,
1516                                chat);
1517 }
1518
1519 static void
1520 chat_message_received_cb (EmpathyTpChat  *tp_chat,
1521                           EmpathyMessage *message,
1522                           EmpathyChat    *chat)
1523 {
1524         chat_message_received (chat, message, FALSE);
1525 }
1526
1527 static void
1528 chat_message_acknowledged_cb (EmpathyTpChat  *tp_chat,
1529                               EmpathyMessage *message,
1530                               EmpathyChat    *chat)
1531 {
1532         EmpathyChatPriv *priv = GET_PRIV (chat);
1533
1534         empathy_chat_view_message_acknowledged (chat->view,
1535             message);
1536
1537         if (!empathy_message_is_edit (message)) {
1538                 priv->unread_messages--;
1539                 g_object_notify (G_OBJECT (chat), "nb-unread-messages");
1540         }
1541 }
1542
1543 static void
1544 append_balance_error (EmpathyChat *chat,
1545                       const gchar *message_body)
1546 {
1547         EmpathyChatPriv *priv = GET_PRIV (chat);
1548         TpConnection *conn = tp_channel_borrow_connection (TP_CHANNEL (priv->tp_chat));
1549         const gchar *uri = tp_connection_get_balance_uri (conn);
1550         const gchar *error = _("insufficient balance to send message");
1551         gchar *str, *str_markup = NULL;
1552
1553         if (message_body != NULL) {
1554                 str = g_strdup_printf (_("Error sending message '%s': %s"), message_body, error);
1555         } else {
1556                 str = g_strdup_printf (_("Error sending message: %s"), error);
1557         }
1558
1559         if (!tp_str_empty (uri)) {
1560                 /* translators: error used when user doesn't have enough credit on his
1561                  * account to send the message. */
1562                 gchar *markup_error = g_strdup_printf (_("insufficient balance to send message."
1563                                                          " <a href='%s'>Top up</a>."), uri);
1564
1565                 if (message_body != NULL) {
1566                         gchar *escaped_body = g_markup_escape_text (message_body, -1);
1567
1568                         str_markup = g_strdup_printf (_("Error sending message '%s': %s"),
1569                                 escaped_body, markup_error);
1570
1571                         g_free (escaped_body);
1572                 } else {
1573                         str_markup = g_strdup_printf (_("Error sending message: %s"), markup_error);
1574                 }
1575
1576                 g_free (markup_error);
1577         }
1578
1579         if (str_markup != NULL)
1580                 empathy_chat_view_append_event_markup (chat->view, str_markup, str);
1581         else
1582                 empathy_chat_view_append_event (chat->view, str);
1583
1584         g_free (str);
1585         g_free (str_markup);
1586 }
1587
1588 static void
1589 chat_send_error_cb (EmpathyTpChat          *tp_chat,
1590                     const gchar            *message_body,
1591                     TpChannelTextSendError  error_code,
1592                     const gchar            *dbus_error,
1593                     EmpathyChat            *chat)
1594 {
1595         const gchar *error = NULL;
1596         gchar       *str;
1597
1598         if (!tp_strdiff (dbus_error, TP_ERROR_STR_INSUFFICIENT_BALANCE)) {
1599                 append_balance_error (chat, message_body);
1600                 return;
1601         } else if (!tp_strdiff (dbus_error, TP_ERROR_STR_NOT_CAPABLE)) {
1602                 error = _("not capable");
1603         }
1604
1605         if (error == NULL) {
1606                 /* if we didn't find a dbus-error, try the old error */
1607                 switch (error_code) {
1608                 case TP_CHANNEL_TEXT_SEND_ERROR_OFFLINE:
1609                         error = _("offline");
1610                         break;
1611                 case TP_CHANNEL_TEXT_SEND_ERROR_INVALID_CONTACT:
1612                         error = _("invalid contact");
1613                         break;
1614                 case TP_CHANNEL_TEXT_SEND_ERROR_PERMISSION_DENIED:
1615                         error = _("permission denied");
1616                         break;
1617                 case TP_CHANNEL_TEXT_SEND_ERROR_TOO_LONG:
1618                         error = _("too long message");
1619                         break;
1620                 case TP_CHANNEL_TEXT_SEND_ERROR_NOT_IMPLEMENTED:
1621                         error = _("not implemented");
1622                         break;
1623                 case TP_CHANNEL_TEXT_SEND_ERROR_UNKNOWN:
1624                 default:
1625                         error = _("unknown");
1626                         break;
1627                 }
1628         }
1629
1630         if (message_body != NULL) {
1631                         str = g_strdup_printf (_("Error sending message '%s': %s"),
1632                                 message_body, error);
1633         }
1634         else {
1635                         str = g_strdup_printf (_("Error sending message: %s"), error);
1636         }
1637
1638         empathy_chat_view_append_event (chat->view, str);
1639         g_free (str);
1640 }
1641
1642 static void
1643 chat_topic_label_size_allocate_cb (GtkLabel *label,
1644                                    GtkAllocation *allocation,
1645                                    EmpathyChat *chat)
1646 {
1647         EmpathyChatPriv *priv = GET_PRIV (chat);
1648
1649         if (!gtk_label_get_line_wrap (label)) {
1650                 if (pango_layout_is_ellipsized (gtk_label_get_layout (label)))
1651                         gtk_widget_show (priv->expander_topic);
1652                 else
1653                         gtk_widget_hide (priv->expander_topic);
1654
1655                 return;
1656         }
1657 }
1658
1659 static void
1660 chat_topic_expander_activate_cb (GtkExpander *expander,
1661                                  GParamSpec *param_spec,
1662                                  EmpathyChat *chat)
1663 {
1664         EmpathyChatPriv *priv = GET_PRIV (chat);
1665
1666         if (gtk_expander_get_expanded (expander)) {
1667                 gtk_label_set_ellipsize (GTK_LABEL (priv->label_topic), PANGO_ELLIPSIZE_NONE);
1668                 gtk_label_set_line_wrap (GTK_LABEL (priv->label_topic), TRUE);
1669         } else {
1670                 gtk_label_set_ellipsize (GTK_LABEL (priv->label_topic), PANGO_ELLIPSIZE_END);
1671                 gtk_label_set_line_wrap (GTK_LABEL (priv->label_topic), FALSE);
1672         }
1673 }
1674
1675 static void
1676 chat_subject_changed_cb (EmpathyChat *chat)
1677 {
1678         EmpathyChatPriv *priv = GET_PRIV (chat);
1679
1680                 g_free (priv->subject);
1681                 priv->subject = g_strdup (empathy_tp_chat_get_subject (priv->tp_chat));
1682                 g_object_notify (G_OBJECT (chat), "subject");
1683
1684                 if (EMP_STR_EMPTY (priv->subject)) {
1685                         gtk_widget_hide (priv->hbox_topic);
1686                 } else {
1687                         gchar *markup_topic;
1688                         gchar *markup_text;
1689
1690                         markup_topic = empathy_add_link_markup (priv->subject);
1691                         markup_text = g_strdup_printf ("<span weight=\"bold\">%s</span> %s",
1692                                 _("Topic:"), markup_topic);
1693
1694                         gtk_label_set_markup (GTK_LABEL (priv->label_topic), markup_text);
1695                         g_free (markup_text);
1696                         g_free (markup_topic);
1697
1698                         gtk_widget_show (priv->hbox_topic);
1699                 }
1700                 if (priv->block_events_timeout_id == 0) {
1701                         gchar *str = NULL;
1702
1703                         if (!EMP_STR_EMPTY (priv->subject)) {
1704                                 const gchar *actor = empathy_tp_chat_get_subject_actor (priv->tp_chat);
1705
1706                                 if (tp_str_empty (actor)) {
1707                                         str = g_strdup_printf (_("Topic set to: %s"), priv->subject);
1708                                 } else {
1709                                         str = g_strdup_printf (_("Topic set by %s to: %s"),
1710                                                                actor, priv->subject);
1711                                 }
1712                         } else if (empathy_tp_chat_supports_subject (priv->tp_chat)) {
1713                                 /* No need to display this 'event' is no topic can be defined anyway */
1714                                 str = g_strdup (_("No topic defined"));
1715                         }
1716
1717                         if (str != NULL) {
1718                                 empathy_chat_view_append_event (EMPATHY_CHAT (chat)->view, str);
1719                                 g_free (str);
1720                         }
1721                 }
1722 }
1723
1724 static void
1725 chat_title_changed_cb (EmpathyChat *chat)
1726 {
1727         EmpathyChatPriv *priv = GET_PRIV (chat);
1728
1729                 g_free (priv->name);
1730                 priv->name = g_strdup (empathy_tp_chat_get_title (priv->tp_chat));
1731                 g_object_notify (G_OBJECT (chat), "name");
1732 }
1733
1734 static gboolean
1735 chat_input_text_get_word_from_iter (GtkTextIter   *iter,
1736                                     GtkTextIter   *start,
1737                                     GtkTextIter   *end)
1738 {
1739         GtkTextIter word_start = *iter;
1740         GtkTextIter word_end = *iter;
1741         GtkTextIter tmp;
1742
1743         if (gtk_text_iter_inside_word (&word_end) &&
1744                         !gtk_text_iter_ends_word (&word_end)) {
1745                 gtk_text_iter_forward_word_end (&word_end);
1746         }
1747
1748         tmp = word_end;
1749
1750         if (gtk_text_iter_get_char (&tmp) == '\'') {
1751                 gtk_text_iter_forward_char (&tmp);
1752
1753                 if (g_unichar_isalpha (gtk_text_iter_get_char (&tmp))) {
1754                         gtk_text_iter_forward_word_end (&word_end);
1755                 }
1756         }
1757
1758
1759         if (gtk_text_iter_inside_word (&word_start) ||
1760                         gtk_text_iter_ends_word (&word_start)) {
1761                 if (!gtk_text_iter_starts_word (&word_start) ||
1762                                 gtk_text_iter_equal (&word_start, &word_end)) {
1763                         gtk_text_iter_backward_word_start (&word_start);
1764                 }
1765
1766                 tmp = word_start;
1767                 gtk_text_iter_backward_char (&tmp);
1768
1769                 if (gtk_text_iter_get_char (&tmp) == '\'') {
1770                         gtk_text_iter_backward_char (&tmp);
1771
1772                         if (g_unichar_isalpha (gtk_text_iter_get_char (&tmp))) {
1773                                 gtk_text_iter_backward_word_start (&word_start);
1774                         }
1775                 }
1776         }
1777
1778         *start = word_start;
1779         *end = word_end;
1780         return TRUE;
1781 }
1782
1783 static void
1784 chat_input_text_buffer_insert_text_cb (GtkTextBuffer *buffer,
1785                                        GtkTextIter   *location,
1786                                        gchar         *text,
1787                                        gint           len,
1788                                        EmpathyChat   *chat)
1789 {
1790         GtkTextIter iter, pos;
1791
1792         /* Remove all misspelled tags in the inserted text.
1793          * This happens when text is inserted within a misspelled word. */
1794         gtk_text_buffer_get_iter_at_offset (buffer, &iter,
1795                                             gtk_text_iter_get_offset (location) - len);
1796         gtk_text_buffer_remove_tag_by_name (buffer, "misspelled",
1797                                             &iter, location);
1798
1799         gtk_text_buffer_get_iter_at_mark (buffer, &pos, gtk_text_buffer_get_insert (buffer));
1800
1801         do {
1802                 GtkTextIter start, end;
1803                 gchar *str;
1804
1805                 if (!chat_input_text_get_word_from_iter (&iter, &start, &end))
1806                         continue;
1807
1808                 str = gtk_text_buffer_get_text (buffer, &start, &end, FALSE);
1809
1810                 if (gtk_text_iter_in_range (&pos, &start, &end) ||
1811                                 gtk_text_iter_equal (&pos, &end) ||
1812                                 empathy_spell_check (str)) {
1813                         gtk_text_buffer_remove_tag_by_name (buffer, "misspelled", &start, &end);
1814                 } else {
1815                         gtk_text_buffer_apply_tag_by_name (buffer, "misspelled", &start, &end);
1816                 }
1817
1818                 g_free (str);
1819
1820         } while (gtk_text_iter_forward_word_end (&iter) &&
1821                  gtk_text_iter_compare (&iter, location) <= 0);
1822 }
1823
1824 static void
1825 chat_input_text_buffer_delete_range_cb (GtkTextBuffer *buffer,
1826                                         GtkTextIter   *start,
1827                                         GtkTextIter   *end,
1828                                         EmpathyChat   *chat)
1829 {
1830         GtkTextIter word_start, word_end;
1831
1832         if (chat_input_text_get_word_from_iter (start, &word_start, &word_end)) {
1833                 gtk_text_buffer_remove_tag_by_name (buffer, "misspelled",
1834                                                     &word_start, &word_end);
1835         }
1836 }
1837
1838 static void
1839 chat_input_text_buffer_changed_cb (GtkTextBuffer *buffer,
1840                                    EmpathyChat    *chat)
1841 {
1842         if (gtk_text_buffer_get_char_count (buffer) == 0) {
1843                 chat_composing_stop (chat);
1844         } else {
1845                 chat_composing_start (chat);
1846         }
1847 }
1848
1849 static void
1850 chat_input_text_buffer_notify_cursor_position_cb (GtkTextBuffer *buffer,
1851                                                   GParamSpec    *pspec,
1852                                                   EmpathyChat    *chat)
1853 {
1854         GtkTextIter pos;
1855         GtkTextIter prev_pos;
1856         GtkTextIter word_start;
1857         GtkTextIter word_end;
1858         GtkTextMark *mark;
1859         gchar *str;
1860
1861         mark = gtk_text_buffer_get_mark (buffer, "previous-cursor-position");
1862
1863         gtk_text_buffer_get_iter_at_mark (buffer, &pos,
1864                                           gtk_text_buffer_get_insert (buffer));
1865         gtk_text_buffer_get_iter_at_mark (buffer, &prev_pos, mark);
1866
1867         if (!chat_input_text_get_word_from_iter (&prev_pos, &word_start, &word_end))
1868                 goto out;
1869
1870         if (!gtk_text_iter_in_range (&pos, &word_start, &word_end) &&
1871                         !gtk_text_iter_equal (&pos, &word_end)) {
1872                 str = gtk_text_buffer_get_text (buffer,
1873                                         &word_start, &word_end, FALSE);
1874
1875                 if (!empathy_spell_check (str)) {
1876                         gtk_text_buffer_apply_tag_by_name (buffer,
1877                                         "misspelled", &word_start, &word_end);
1878                 } else {
1879                         gtk_text_buffer_remove_tag_by_name (buffer,
1880                                         "misspelled", &word_start, &word_end);
1881                 }
1882
1883                 g_free (str);
1884         }
1885
1886 out:
1887         gtk_text_buffer_move_mark (buffer, mark, &pos);
1888 }
1889
1890 static gboolean
1891 empathy_isspace_cb (gunichar c,
1892                  gpointer data)
1893 {
1894         return g_unichar_isspace (c);
1895 }
1896
1897 static gboolean
1898 chat_input_key_press_event_cb (GtkWidget   *widget,
1899                                GdkEventKey *event,
1900                                EmpathyChat *chat)
1901 {
1902         EmpathyChatPriv *priv;
1903         GtkAdjustment  *adj;
1904         gdouble         val;
1905         GtkWidget      *text_view_sw;
1906
1907         priv = GET_PRIV (chat);
1908
1909         priv->most_recent_event_type = event->type;
1910
1911         /* Catch ctrl+up/down so we can traverse messages we sent */
1912         if ((event->state & GDK_CONTROL_MASK) &&
1913             (event->keyval == GDK_KEY_Up ||
1914              event->keyval == GDK_KEY_Down)) {
1915                 GtkTextBuffer *buffer;
1916                 const gchar   *str;
1917
1918                 buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (chat->input_text_view));
1919                 chat_input_history_update (chat, buffer);
1920
1921                 if (event->keyval == GDK_KEY_Up) {
1922                         str = chat_input_history_get_next (chat);
1923                 } else {
1924                         str = chat_input_history_get_prev (chat);
1925                 }
1926
1927                 g_signal_handlers_block_by_func (buffer,
1928                                                  chat_input_text_buffer_changed_cb,
1929                                                  chat);
1930                 gtk_text_buffer_set_text (buffer, str ? str : "", -1);
1931                 g_signal_handlers_unblock_by_func (buffer,
1932                                                    chat_input_text_buffer_changed_cb,
1933                                                    chat);
1934
1935                 return TRUE;
1936         }
1937
1938         /* Catch enter but not ctrl/shift-enter */
1939         if (IS_ENTER (event->keyval) &&
1940             !(event->state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK))) {
1941                 GtkTextView *view;
1942
1943                 /* This is to make sure that kinput2 gets the enter. And if
1944                  * it's handled there we shouldn't send on it. This is because
1945                  * kinput2 uses Enter to commit letters. See:
1946                  * http://bugzilla.redhat.com/bugzilla/show_bug.cgi?id=104299
1947                  */
1948
1949                 view = GTK_TEXT_VIEW (chat->input_text_view);
1950                 if (gtk_text_view_im_context_filter_keypress (view, event)) {
1951                         gtk_text_view_reset_im_context (view);
1952                         return TRUE;
1953                 }
1954
1955                 chat_input_text_view_send (chat);
1956                 return TRUE;
1957         }
1958
1959         text_view_sw = gtk_widget_get_parent (GTK_WIDGET (chat->view));
1960
1961         if (IS_ENTER (event->keyval) &&
1962             (event->state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK))) {
1963                 /* Newline for shift/control-enter. */
1964                 return FALSE;
1965         }
1966         if (!(event->state & GDK_CONTROL_MASK) &&
1967             event->keyval == GDK_KEY_Page_Up) {
1968                 adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (text_view_sw));
1969                 gtk_adjustment_set_value (adj, gtk_adjustment_get_value (adj) - gtk_adjustment_get_page_size (adj));
1970                 return TRUE;
1971         }
1972         if ((event->state & GDK_CONTROL_MASK) != GDK_CONTROL_MASK &&
1973             event->keyval == GDK_KEY_Page_Down) {
1974                 adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (text_view_sw));
1975                 val = MIN (gtk_adjustment_get_value (adj) + gtk_adjustment_get_page_size (adj),
1976                            gtk_adjustment_get_upper (adj) - gtk_adjustment_get_page_size (adj));
1977                 gtk_adjustment_set_value (adj, val);
1978                 return TRUE;
1979         }
1980         if (event->keyval == GDK_KEY_Escape) {
1981                 empathy_search_bar_hide (EMPATHY_SEARCH_BAR (priv->search_bar));
1982         }
1983         if (!(event->state & (GDK_CONTROL_MASK | GDK_SHIFT_MASK)) &&
1984             event->keyval == GDK_KEY_Tab) {
1985                 GtkTextBuffer *buffer;
1986                 GtkTextIter    start, current;
1987                 gchar         *nick, *completed;
1988                 GList         *list, *completed_list;
1989                 gboolean       is_start_of_buffer;
1990
1991                 buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (EMPATHY_CHAT (chat)->input_text_view));
1992                 gtk_text_buffer_get_iter_at_mark (buffer, &current, gtk_text_buffer_get_insert (buffer));
1993
1994                 /* Get the start of the nick to complete. */
1995                 gtk_text_buffer_get_iter_at_mark (buffer, &start, gtk_text_buffer_get_insert (buffer));
1996                 if (gtk_text_iter_backward_find_char (&start, &empathy_isspace_cb, NULL, NULL)) {
1997                         gtk_text_iter_set_offset (&start, gtk_text_iter_get_offset (&start) + 1);
1998                 }
1999                 is_start_of_buffer = gtk_text_iter_is_start (&start);
2000
2001                 list = empathy_contact_list_get_members (EMPATHY_CONTACT_LIST (priv->tp_chat));
2002                 g_completion_add_items (priv->completion, list);
2003
2004                 nick = gtk_text_buffer_get_text (buffer, &start, &current, FALSE);
2005                 completed_list = g_completion_complete (priv->completion,
2006                                                         nick,
2007                                                         &completed);
2008
2009                 g_free (nick);
2010
2011                 if (completed) {
2012                         guint        len;
2013                         const gchar *text;
2014                         GString     *message = NULL;
2015                         GList       *l;
2016
2017                         gtk_text_buffer_delete (buffer, &start, &current);
2018
2019                         len = g_list_length (completed_list);
2020
2021                         if (len == 1) {
2022                                 /* If we only have one hit, use that text
2023                                  * instead of the text in completed since the
2024                                  * completed text will use the typed string
2025                                  * which might be cased all wrong.
2026                                  * Fixes #120876
2027                                  * */
2028                                 text = empathy_contact_get_alias (completed_list->data);
2029                         } else {
2030                                 text = completed;
2031
2032                                 /* Print all hits to the scrollback view, so the
2033                                  * user knows what possibilities he has.
2034                                  * Fixes #599779
2035                                  * */
2036                                  message = g_string_new ("");
2037                                  for (l = completed_list; l != NULL; l = l->next) {
2038                                         g_string_append (message, empathy_contact_get_alias (l->data));
2039                                         g_string_append (message, " - ");
2040                                  }
2041                                  empathy_chat_view_append_event (chat->view, message->str);
2042                                  g_string_free (message, TRUE);
2043                         }
2044
2045                         gtk_text_buffer_insert_at_cursor (buffer, text, strlen (text));
2046
2047                         if (len == 1 && is_start_of_buffer) {
2048                             gchar *complete_char;
2049
2050                             complete_char = g_settings_get_string (
2051                                     priv->gsettings_chat,
2052                                     EMPATHY_PREFS_CHAT_NICK_COMPLETION_CHAR);
2053
2054                             if (complete_char != NULL) {
2055                                 gtk_text_buffer_insert_at_cursor (buffer,
2056                                                                   complete_char,
2057                                                                   strlen (complete_char));
2058                                 gtk_text_buffer_insert_at_cursor (buffer, " ", 1);
2059                                 g_free (complete_char);
2060                             }
2061                         }
2062
2063                         g_free (completed);
2064                 }
2065
2066                 g_completion_clear_items (priv->completion);
2067
2068                 g_list_foreach (list, (GFunc) g_object_unref, NULL);
2069                 g_list_free (list);
2070
2071                 return TRUE;
2072         }
2073
2074         return FALSE;
2075 }
2076
2077 static gboolean
2078 chat_text_view_focus_in_event_cb (GtkWidget  *widget,
2079                                   GdkEvent   *event,
2080                                   EmpathyChat *chat)
2081 {
2082         gtk_widget_grab_focus (chat->input_text_view);
2083
2084         return TRUE;
2085 }
2086
2087 static void
2088 chat_input_realize_cb (GtkWidget   *widget,
2089                        EmpathyChat *chat)
2090 {
2091         DEBUG ("Setting focus to the input text view");
2092         if (gtk_widget_is_sensitive (widget)) {
2093                 gtk_widget_grab_focus (widget);
2094         }
2095 }
2096
2097 static void
2098 chat_input_has_focus_notify_cb (GtkWidget   *widget,
2099                                 GParamSpec  *pspec,
2100                                 EmpathyChat *chat)
2101 {
2102         empathy_chat_view_focus_toggled (chat->view, gtk_widget_has_focus (widget));
2103 }
2104
2105 static void
2106 chat_insert_smiley_activate_cb (EmpathySmileyManager *manager,
2107                                 EmpathySmiley        *smiley,
2108                                 gpointer              user_data)
2109 {
2110         EmpathyChat   *chat = EMPATHY_CHAT (user_data);
2111         GtkTextBuffer *buffer;
2112         GtkTextIter    iter;
2113
2114         buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (chat->input_text_view));
2115
2116         gtk_text_buffer_get_end_iter (buffer, &iter);
2117         gtk_text_buffer_insert (buffer, &iter, smiley->str, -1);
2118
2119         gtk_text_buffer_get_end_iter (buffer, &iter);
2120         gtk_text_buffer_insert (buffer, &iter, " ", -1);
2121 }
2122
2123 typedef struct {
2124         EmpathyChat  *chat;
2125         gchar       *word;
2126
2127         GtkTextIter  start;
2128         GtkTextIter  end;
2129 } EmpathyChatSpell;
2130
2131 static EmpathyChatSpell *
2132 chat_spell_new (EmpathyChat  *chat,
2133                 const gchar *word,
2134                 GtkTextIter  start,
2135                 GtkTextIter  end)
2136 {
2137         EmpathyChatSpell *chat_spell;
2138
2139         chat_spell = g_slice_new0 (EmpathyChatSpell);
2140
2141         chat_spell->chat = g_object_ref (chat);
2142         chat_spell->word = g_strdup (word);
2143         chat_spell->start = start;
2144         chat_spell->end = end;
2145
2146         return chat_spell;
2147 }
2148
2149 static void
2150 chat_spell_free (EmpathyChatSpell *chat_spell)
2151 {
2152         g_object_unref (chat_spell->chat);
2153         g_free (chat_spell->word);
2154         g_slice_free (EmpathyChatSpell, chat_spell);
2155 }
2156
2157 static void
2158 chat_spelling_menu_activate_cb (GtkMenuItem     *menu_item,
2159                                                 EmpathyChatSpell *chat_spell)
2160 {
2161     empathy_chat_correct_word (chat_spell->chat,
2162                                &(chat_spell->start),
2163                                &(chat_spell->end),
2164                                gtk_menu_item_get_label (menu_item));
2165 }
2166
2167
2168 static GtkWidget *
2169 chat_spelling_build_suggestions_menu (const gchar *code,
2170                                       EmpathyChatSpell *chat_spell)
2171 {
2172         GList     *suggestions, *l;
2173         GtkWidget *menu, *menu_item;
2174
2175         suggestions = empathy_spell_get_suggestions (code, chat_spell->word);
2176         if (suggestions == NULL)
2177                 return NULL;
2178
2179         menu = gtk_menu_new ();
2180         for (l = suggestions; l; l = l->next) {
2181                 menu_item = gtk_menu_item_new_with_label (l->data);
2182                 g_signal_connect (G_OBJECT (menu_item), "activate",
2183                                   G_CALLBACK (chat_spelling_menu_activate_cb),
2184                                   chat_spell);
2185                 gtk_menu_shell_append (GTK_MENU_SHELL (menu), menu_item);
2186         }
2187         empathy_spell_free_suggestions (suggestions);
2188
2189         gtk_widget_show_all (menu);
2190
2191         return menu;
2192 }
2193
2194 static GtkWidget *
2195 chat_spelling_build_menu (EmpathyChatSpell *chat_spell)
2196 {
2197         GtkWidget *menu, *submenu, *item;
2198         GList     *codes, *l;
2199
2200         codes = empathy_spell_get_enabled_language_codes ();
2201         g_assert (codes != NULL);
2202
2203         if (g_list_length (codes) > 1) {
2204                 menu = gtk_menu_new ();
2205
2206                 for (l = codes; l; l = l->next) {
2207                         const gchar *code, *name;
2208
2209                         code = l->data;
2210                         name = empathy_spell_get_language_name (code);
2211                         if (!name)
2212                                 continue;
2213
2214                         item = gtk_image_menu_item_new_with_label (name);
2215
2216                         submenu = chat_spelling_build_suggestions_menu (
2217                                         code, chat_spell);
2218                         if (submenu == NULL)
2219                                 gtk_widget_set_sensitive (item, FALSE);
2220                         else
2221                                 gtk_menu_item_set_submenu (GTK_MENU_ITEM (item),
2222                                                            submenu);
2223                         gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
2224                 }
2225         } else {
2226                 menu = chat_spelling_build_suggestions_menu (codes->data,
2227                                                              chat_spell);
2228                 if (menu == NULL) {
2229                         menu = gtk_menu_new ();
2230                         item = gtk_menu_item_new_with_label (_("(No Suggestions)"));
2231                         gtk_widget_set_sensitive (item, FALSE);
2232                         gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
2233                 }
2234         }
2235         g_list_free (codes);
2236
2237         gtk_widget_show_all (menu);
2238
2239         return menu;
2240 }
2241
2242 typedef struct {
2243         EmpathyChat  *chat;
2244         gchar        *word;
2245         gchar        *code;
2246 } EmpathyChatWord;
2247
2248 static EmpathyChatWord *
2249 chat_word_new (EmpathyChat  *chat,
2250                 const gchar  *word,
2251                 const gchar  *code)
2252 {
2253         EmpathyChatWord *chat_word;
2254
2255         chat_word = g_slice_new0 (EmpathyChatWord);
2256
2257         chat_word->chat = g_object_ref (chat);
2258         chat_word->word = g_strdup (word);
2259         chat_word->code = g_strdup (code);
2260
2261         return chat_word;
2262 }
2263
2264 static void
2265 chat_word_free (EmpathyChatWord *chat_word)
2266 {
2267         g_object_unref (chat_word->chat);
2268         g_free (chat_word->word);
2269         g_free (chat_word->code);
2270         g_slice_free (EmpathyChatWord, chat_word);
2271 }
2272
2273 static void
2274 chat_add_to_dictionary_activate_cb (GtkMenuItem     *menu_item,
2275                                     EmpathyChatWord *chat_word)
2276 {
2277         EmpathyChatPriv *priv = GET_PRIV (chat_word->chat);
2278
2279         empathy_spell_add_to_dictionary (chat_word->code,
2280                                          chat_word->word);
2281         priv->update_misspelled_words_id =
2282                 g_idle_add (update_misspelled_words, chat_word->chat);
2283 }
2284
2285 static GtkWidget *
2286 chat_spelling_build_add_to_dictionary_item (EmpathyChatSpell *chat_spell)
2287 {
2288         GtkWidget       *menu, *item, *lang_item, *image;
2289         GList           *codes, *l;
2290         gchar           *label;
2291         const gchar     *code, *name;
2292         EmpathyChatWord *chat_word;
2293
2294         codes = empathy_spell_get_enabled_language_codes ();
2295         g_assert (codes != NULL);
2296         if (g_list_length (codes) > 1) {
2297                 /* translators: %s is the selected word */
2298                 label = g_strdup_printf (_("Add '%s' to Dictionary"),
2299                                         chat_spell->word);
2300                 item = gtk_image_menu_item_new_with_mnemonic (label);
2301                 g_free (label);
2302                 image = gtk_image_new_from_icon_name (GTK_STOCK_ADD,
2303                                                       GTK_ICON_SIZE_MENU);
2304                 gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item),
2305                                                image);
2306
2307                 menu = gtk_menu_new ();
2308
2309                 for (l = codes; l; l = l->next) {
2310                         code = l->data;
2311                         name = empathy_spell_get_language_name (code);
2312                         if (name == NULL)
2313                                 continue;
2314
2315                         lang_item = gtk_image_menu_item_new_with_label (name);
2316
2317                         chat_word= chat_word_new (chat_spell->chat,
2318                                                   chat_spell->word, code);
2319                         g_object_set_data_full (G_OBJECT (lang_item),
2320                                 "chat-word", chat_word,
2321                                 (GDestroyNotify) chat_word_free);
2322
2323                         g_signal_connect (G_OBJECT (lang_item), "activate",
2324                                 G_CALLBACK (chat_add_to_dictionary_activate_cb),
2325                                 chat_word);
2326                         gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), lang_item);
2327                 }
2328                 gtk_menu_item_set_submenu (GTK_MENU_ITEM (item), menu);
2329         } else {
2330                 code = codes->data;
2331                 name = empathy_spell_get_language_name (code);
2332                 g_assert (name != NULL);
2333                 /* translators: first %s is the selected word,
2334                  * second %s is the language name of the target dictionary */
2335                 label = g_strdup_printf (_("Add '%s' to %s Dictionary"),
2336                                         chat_spell->word, name);
2337                 item = gtk_image_menu_item_new_with_mnemonic (label);
2338                 g_free (label);
2339                 image = gtk_image_new_from_icon_name (GTK_STOCK_ADD,
2340                                                       GTK_ICON_SIZE_MENU);
2341                 gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), image);
2342
2343                 chat_word = chat_word_new (chat_spell->chat, chat_spell->word,
2344                                            code);
2345                 g_object_set_data_full (G_OBJECT (item), "chat-word", chat_word,
2346                                         (GDestroyNotify) chat_word_free);
2347
2348                 g_signal_connect (G_OBJECT (item), "activate",
2349                                   G_CALLBACK (chat_add_to_dictionary_activate_cb),
2350                                   chat_word);
2351         }
2352         g_list_free (codes);
2353
2354         gtk_widget_show_all (item);
2355
2356         return item;
2357 }
2358
2359 static void
2360 chat_text_send_cb (GtkMenuItem *menuitem,
2361                    EmpathyChat *chat)
2362 {
2363         chat_input_text_view_send (chat);
2364 }
2365
2366 static gboolean
2367 chat_input_button_press_event_cb (GtkTextView    *view,
2368                                   GdkEventButton *event,
2369                                   EmpathyChat    *chat)
2370 {
2371         EmpathyChatPriv *priv = GET_PRIV (chat);
2372
2373         priv->most_recent_event_type = event->type;
2374
2375         return FALSE;
2376 }
2377
2378 static void
2379 chat_input_populate_popup_cb (GtkTextView *view,
2380                               GtkMenu     *menu,
2381                               EmpathyChat *chat)
2382 {
2383         EmpathyChatPriv      *priv = GET_PRIV (chat);
2384         GtkTextBuffer        *buffer;
2385         GtkTextTagTable      *table;
2386         GtkTextTag           *tag;
2387         gint                  x, y;
2388         GtkTextIter           iter, start, end;
2389         GtkWidget            *item;
2390         gchar                *str = NULL;
2391         EmpathyChatSpell     *chat_spell;
2392         GtkWidget            *spell_menu;
2393         GtkWidget            *spell_item;
2394         EmpathySmileyManager *smiley_manager;
2395         GtkWidget            *smiley_menu;
2396         GtkWidget            *image;
2397
2398         buffer = gtk_text_view_get_buffer (view);
2399
2400         /* Add the emoticon menu. */
2401         item = gtk_separator_menu_item_new ();
2402         gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
2403         gtk_widget_show (item);
2404
2405         item = gtk_image_menu_item_new_with_mnemonic (_("Insert Smiley"));
2406         image = gtk_image_new_from_icon_name ("face-smile",
2407                                               GTK_ICON_SIZE_MENU);
2408         gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), image);
2409         gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
2410         gtk_widget_show (item);
2411
2412         smiley_manager = empathy_smiley_manager_dup_singleton ();
2413         smiley_menu = empathy_smiley_menu_new (smiley_manager,
2414                                                chat_insert_smiley_activate_cb,
2415                                                chat);
2416         gtk_menu_item_set_submenu (GTK_MENU_ITEM (item), smiley_menu);
2417         g_object_unref (smiley_manager);
2418
2419         /* Add the Send menu item. */
2420         gtk_text_buffer_get_bounds (buffer, &start, &end);
2421         str = gtk_text_buffer_get_text (buffer, &start, &end, FALSE);
2422         if (!EMP_STR_EMPTY (str)) {
2423                 item = gtk_menu_item_new_with_mnemonic (_("_Send"));
2424                 g_signal_connect (G_OBJECT (item), "activate",
2425                                   G_CALLBACK (chat_text_send_cb), chat);
2426                 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
2427                 gtk_widget_show (item);
2428         }
2429         str = NULL;
2430
2431         /* Add the spell check menu item. */
2432         table = gtk_text_buffer_get_tag_table (buffer);
2433         tag = gtk_text_tag_table_lookup (table, "misspelled");
2434
2435         switch (priv->most_recent_event_type) {
2436             case GDK_BUTTON_PRESS:
2437                 /* get the location from the pointer */
2438                 gdk_window_get_device_position (gtk_widget_get_window (GTK_WIDGET (view)),
2439                         gdk_device_manager_get_client_pointer (gdk_display_get_device_manager (
2440                                 gtk_widget_get_display (GTK_WIDGET (view)))), &x, &y, NULL);
2441
2442                 gtk_text_view_window_to_buffer_coords (GTK_TEXT_VIEW (view),
2443                                                        GTK_TEXT_WINDOW_WIDGET,
2444                                                        x, y,
2445                                                        &x, &y);
2446                 gtk_text_view_get_iter_at_location (GTK_TEXT_VIEW (view),
2447                                                     &iter, x, y);
2448                 break;
2449
2450             default:
2451                 g_warn_if_reached ();
2452                 /* assume the KEY_PRESS case */
2453
2454             case GDK_KEY_PRESS:
2455                 /* get the location from the cursor */
2456                 gtk_text_buffer_get_iter_at_mark (buffer, &iter,
2457                                 gtk_text_buffer_get_insert (buffer));
2458                 break;
2459
2460         }
2461
2462         start = end = iter;
2463         if (gtk_text_iter_backward_to_tag_toggle (&start, tag) &&
2464             gtk_text_iter_forward_to_tag_toggle (&end, tag)) {
2465
2466                 str = gtk_text_buffer_get_text (buffer,
2467                                                 &start, &end, FALSE);
2468         }
2469         if (!EMP_STR_EMPTY (str)) {
2470                 chat_spell = chat_spell_new (chat, str, start, end);
2471                 g_object_set_data_full (G_OBJECT (menu),
2472                                         "chat-spell", chat_spell,
2473                                         (GDestroyNotify) chat_spell_free);
2474
2475                 item = gtk_separator_menu_item_new ();
2476                 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
2477                 gtk_widget_show (item);
2478
2479                 /* Spelling suggestions */
2480                 item = gtk_image_menu_item_new_with_mnemonic (_("_Spelling Suggestions"));
2481                 image = gtk_image_new_from_icon_name (GTK_STOCK_SPELL_CHECK,
2482                                                       GTK_ICON_SIZE_MENU);
2483                 gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), image);
2484
2485                 spell_menu = chat_spelling_build_menu (chat_spell);
2486                 gtk_menu_item_set_submenu (GTK_MENU_ITEM (item), spell_menu);
2487
2488
2489                 spell_item = gtk_separator_menu_item_new ();
2490                 gtk_menu_shell_append (GTK_MENU_SHELL (spell_menu), spell_item);
2491                 gtk_widget_show (spell_item);
2492
2493                 /* Add to dictionary */
2494                 spell_item = chat_spelling_build_add_to_dictionary_item (chat_spell);
2495
2496                 gtk_menu_shell_append (GTK_MENU_SHELL (spell_menu), spell_item);
2497                 gtk_widget_show (spell_item);
2498
2499                 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
2500                 gtk_widget_show (item);
2501         }
2502 }
2503
2504
2505 static gboolean
2506 chat_log_filter (TplEvent *event,
2507                  gpointer user_data)
2508 {
2509         EmpathyChat *chat = user_data;
2510         EmpathyMessage *message;
2511         EmpathyChatPriv *priv = GET_PRIV (chat);
2512         const GList *pending;
2513
2514         g_return_val_if_fail (TPL_IS_EVENT (event), FALSE);
2515         g_return_val_if_fail (EMPATHY_IS_CHAT (chat), FALSE);
2516
2517         pending = empathy_tp_chat_get_pending_messages (priv->tp_chat);
2518         message = empathy_message_from_tpl_log_event (event);
2519
2520         for (; pending; pending = g_list_next (pending)) {
2521                 if (empathy_message_equal (message, pending->data)) {
2522                         g_object_unref (message);
2523                         return FALSE;
2524                 }
2525         }
2526
2527         g_object_unref (message);
2528         return TRUE;
2529 }
2530
2531
2532 static void
2533 show_pending_messages (EmpathyChat *chat) {
2534         EmpathyChatPriv *priv = GET_PRIV (chat);
2535         const GList *messages, *l;
2536
2537         g_return_if_fail (EMPATHY_IS_CHAT (chat));
2538
2539         if (chat->view == NULL || priv->tp_chat == NULL)
2540                 return;
2541
2542         if (!priv->can_show_pending)
2543                 return;
2544
2545         messages = empathy_tp_chat_get_pending_messages (priv->tp_chat);
2546
2547         for (l = messages; l != NULL ; l = g_list_next (l)) {
2548                 EmpathyMessage *message = EMPATHY_MESSAGE (l->data);
2549                 chat_message_received (chat, message, TRUE);
2550         }
2551 }
2552
2553
2554 static void
2555 got_filtered_messages_cb (GObject *manager,
2556                 GAsyncResult *result,
2557                 gpointer user_data)
2558 {
2559         GList *l;
2560         GList *messages;
2561         EmpathyChat *chat = EMPATHY_CHAT (user_data);
2562         EmpathyChatPriv *priv = GET_PRIV (chat);
2563         GError *error = NULL;
2564
2565         if (!tpl_log_manager_get_filtered_events_finish (TPL_LOG_MANAGER (manager),
2566                 result, &messages, &error)) {
2567                 DEBUG ("%s. Aborting.", error->message);
2568                 empathy_chat_view_append_event (chat->view,
2569                         _("Failed to retrieve recent logs"));
2570                 g_error_free (error);
2571                 goto out;
2572         }
2573
2574         for (l = messages; l; l = g_list_next (l)) {
2575                 EmpathyMessage *message;
2576
2577                 g_assert (TPL_IS_EVENT (l->data));
2578
2579                 message = empathy_message_from_tpl_log_event (l->data);
2580                 g_object_unref (l->data);
2581
2582                 if (empathy_message_is_edit (message)) {
2583                         /* this is an edited message, create a synthetic event
2584                          * using the supersedes token and
2585                          * original-message-sent timestamp, that we can then
2586                          * replace */
2587                         EmpathyMessage *syn_msg = g_object_new (
2588                                 EMPATHY_TYPE_MESSAGE,
2589                                 "body", "",
2590                                 "token", empathy_message_get_supersedes (message),
2591                                 "type", empathy_message_get_tptype (message),
2592                                 "timestamp", empathy_message_get_original_timestamp (message),
2593                                 "incoming", empathy_message_is_incoming (message),
2594                                 "is-backlog", TRUE,
2595                                 "receiver", empathy_message_get_receiver (message),
2596                                 "sender", empathy_message_get_sender (message),
2597                                 NULL);
2598
2599                         empathy_chat_view_append_message (chat->view, syn_msg,
2600                                                           chat_should_highlight (chat, syn_msg));
2601                         empathy_chat_view_edit_message (chat->view, message);
2602
2603                         g_object_unref (syn_msg);
2604                 } else {
2605                         /* append the latest message */
2606                         empathy_chat_view_append_message (chat->view, message,
2607                                                           chat_should_highlight (chat, message));
2608                 }
2609
2610                 g_object_unref (message);
2611         }
2612         g_list_free (messages);
2613
2614 out:
2615         /* in case of TPL error, skip backlog and show pending messages */
2616         priv->can_show_pending = TRUE;
2617         show_pending_messages (chat);
2618
2619         /* FIXME: See Bug#610994, we are forcing the ACK of the queue. See comments
2620          * about it in EmpathyChatPriv definition */
2621         priv->retrieving_backlogs = FALSE;
2622         empathy_chat_messages_read (chat);
2623
2624         /* Turn back on scrolling */
2625         empathy_chat_view_scroll (chat->view, TRUE);
2626 }
2627
2628 static void
2629 chat_add_logs (EmpathyChat *chat)
2630 {
2631         EmpathyChatPriv *priv = GET_PRIV (chat);
2632         TplEntity       *target;
2633
2634         if (!priv->id) {
2635                 return;
2636         }
2637
2638         /* Turn off scrolling temporarily */
2639         empathy_chat_view_scroll (chat->view, FALSE);
2640
2641         /* Add messages from last conversation */
2642         if (priv->handle_type == TP_HANDLE_TYPE_ROOM)
2643           target = tpl_entity_new_from_room_id (priv->id);
2644         else
2645           target = tpl_entity_new (priv->id, TPL_ENTITY_CONTACT, NULL, NULL);
2646
2647         priv->retrieving_backlogs = TRUE;
2648         tpl_log_manager_get_filtered_events_async (priv->log_manager,
2649                                                    priv->account,
2650                                                    target,
2651                                                    TPL_EVENT_MASK_TEXT,
2652                                                    5,
2653                                                    chat_log_filter,
2654                                                    chat,
2655                                                    got_filtered_messages_cb,
2656                                                    (gpointer) chat);
2657
2658         g_object_unref (target);
2659 }
2660
2661 static gint
2662 chat_contacts_completion_func (const gchar *s1,
2663                                const gchar *s2,
2664                                gsize        n)
2665 {
2666         gchar *tmp, *nick1, *nick2;
2667         gint   ret;
2668
2669         if (s1 == s2) {
2670                 return 0;
2671         }
2672         if (!s1 || !s2) {
2673                 return s1 ? -1 : +1;
2674         }
2675
2676         tmp = g_utf8_normalize (s1, -1, G_NORMALIZE_DEFAULT);
2677         nick1 = g_utf8_casefold (tmp, -1);
2678         g_free (tmp);
2679
2680         tmp = g_utf8_normalize (s2, -1, G_NORMALIZE_DEFAULT);
2681         nick2 = g_utf8_casefold (tmp, -1);
2682         g_free (tmp);
2683
2684         ret = strncmp (nick1, nick2, n);
2685
2686         g_free (nick1);
2687         g_free (nick2);
2688
2689         return ret;
2690 }
2691
2692 static gchar *
2693 build_part_message (guint           reason,
2694                     const gchar    *name,
2695                     EmpathyContact *actor,
2696                     const gchar    *message)
2697 {
2698         GString *s = g_string_new ("");
2699         const gchar *actor_name = NULL;
2700
2701         if (actor != NULL) {
2702                 actor_name = empathy_contact_get_alias (actor);
2703         }
2704
2705         /* Having an actor only really makes sense for a few actions... */
2706         switch (reason) {
2707         case TP_CHANNEL_GROUP_CHANGE_REASON_OFFLINE:
2708                 g_string_append_printf (s, _("%s has disconnected"), name);
2709                 break;
2710         case TP_CHANNEL_GROUP_CHANGE_REASON_KICKED:
2711                 if (actor_name != NULL) {
2712                         /* translators: reverse the order of these arguments
2713                          * if the kicked should come before the kicker in your locale.
2714                          */
2715                         g_string_append_printf (s, _("%1$s was kicked by %2$s"),
2716                                 name, actor_name);
2717                 } else {
2718                         g_string_append_printf (s, _("%s was kicked"), name);
2719                 }
2720                 break;
2721         case TP_CHANNEL_GROUP_CHANGE_REASON_BANNED:
2722                 if (actor_name != NULL) {
2723                         /* translators: reverse the order of these arguments
2724                          * if the banned should come before the banner in your locale.
2725                          */
2726                         g_string_append_printf (s, _("%1$s was banned by %2$s"),
2727                                 name, actor_name);
2728                 } else {
2729                         g_string_append_printf (s, _("%s was banned"), name);
2730                 }
2731                 break;
2732         default:
2733                 g_string_append_printf (s, _("%s has left the room"), name);
2734         }
2735
2736         if (!EMP_STR_EMPTY (message)) {
2737                 /* Note to translators: this string is appended to
2738                  * notifications like "foo has left the room", with the message
2739                  * given by the user living the room. If this poses a problem,
2740                  * please let us know. :-)
2741                  */
2742                 g_string_append_printf (s, _(" (%s)"), message);
2743         }
2744
2745         return g_string_free (s, FALSE);
2746 }
2747
2748 static void
2749 chat_members_changed_cb (EmpathyTpChat  *tp_chat,
2750                          EmpathyContact *contact,
2751                          EmpathyContact *actor,
2752                          guint           reason,
2753                          gchar          *message,
2754                          gboolean        is_member,
2755                          EmpathyChat    *chat)
2756 {
2757         EmpathyChatPriv *priv = GET_PRIV (chat);
2758         const gchar *name = empathy_contact_get_alias (contact);
2759         gchar *str;
2760
2761         g_return_if_fail (TP_CHANNEL_GROUP_CHANGE_REASON_RENAMED != reason);
2762
2763         if (priv->block_events_timeout_id != 0)
2764                 return;
2765
2766         if (is_member) {
2767                 str = g_strdup_printf (_("%s has joined the room"),
2768                                        name);
2769         } else {
2770                 str = build_part_message (reason, name, actor, message);
2771         }
2772
2773         empathy_chat_view_append_event (chat->view, str);
2774         g_free (str);
2775 }
2776
2777 static void
2778 chat_member_renamed_cb (EmpathyTpChat  *tp_chat,
2779                          EmpathyContact *old_contact,
2780                          EmpathyContact *new_contact,
2781                          guint           reason,
2782                          gchar          *message,
2783                          EmpathyChat    *chat)
2784 {
2785         EmpathyChatPriv *priv = GET_PRIV (chat);
2786
2787         g_return_if_fail (TP_CHANNEL_GROUP_CHANGE_REASON_RENAMED == reason);
2788
2789         if (priv->block_events_timeout_id == 0) {
2790                 gchar *str;
2791
2792                 str = g_strdup_printf (_("%s is now known as %s"),
2793                                        empathy_contact_get_alias (old_contact),
2794                                        empathy_contact_get_alias (new_contact));
2795                 empathy_chat_view_append_event (chat->view, str);
2796                 g_free (str);
2797         }
2798
2799 }
2800
2801 static gboolean
2802 chat_contacts_visible_timeout_cb (gpointer chat)
2803 {
2804         EmpathyChatPriv *priv = GET_PRIV (chat);
2805
2806         /* Relax the size request */
2807         gtk_widget_set_size_request (priv->vbox_left, -1, -1);
2808
2809         /* Set the position of the slider. This must be done here because
2810          * GtkPaned need to know its size allocation and it will be settled only
2811          * after the gtk_window_resize () tough effect. */
2812         if (priv->contacts_width > 0) {
2813                 gtk_paned_set_position (GTK_PANED (priv->hpaned),
2814                                         priv->contacts_width);
2815         }
2816
2817         priv->contacts_visible_id = 0;
2818
2819         return FALSE;
2820 }
2821
2822 static void
2823 chat_update_contacts_visibility (EmpathyChat *chat,
2824                          gboolean show)
2825 {
2826         EmpathyChatPriv *priv = GET_PRIV (chat);
2827
2828         if (!priv->scrolled_window_contacts) {
2829                 return;
2830         }
2831
2832         if (priv->remote_contact != NULL) {
2833                 show = FALSE;
2834         }
2835
2836         if (show && priv->contact_list_view == NULL) {
2837                 EmpathyIndividualStore *store;
2838                 gint                     min_width;
2839                 GtkAllocation            allocation;
2840
2841                 /* We are adding the contact list to the chat, we don't want the
2842                  * chat view to become too small. If the chat view is already
2843                  * smaller than 250 make sure that size won't change. If the
2844                  * chat view is bigger the contact list will take some space on
2845                  * it but we make sure the chat view don't become smaller than
2846                  * 250. Relax the size request once the resize is done */
2847                 gtk_widget_get_allocation (priv->vbox_left, &allocation);
2848                 min_width = MIN (allocation.width, 250);
2849                 gtk_widget_set_size_request (priv->vbox_left, min_width, -1);
2850
2851                 /* There is no way to know when the window resize will happen
2852                  * since it is WM's decision. Let's hope it won't be longer. */
2853                 if (priv->contacts_visible_id != 0)
2854                         g_source_remove (priv->contacts_visible_id);
2855                 priv->contacts_visible_id = g_timeout_add (500,
2856                         chat_contacts_visible_timeout_cb, chat);
2857
2858                 store = EMPATHY_INDIVIDUAL_STORE (
2859                                 empathy_individual_store_channel_new ((TpChannel *) priv->tp_chat));
2860
2861                 empathy_individual_store_set_show_groups (store, FALSE);
2862
2863                 priv->contact_list_view = GTK_WIDGET (empathy_individual_view_new (store,
2864                         EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_TOOLTIP,
2865                         EMPATHY_INDIVIDUAL_FEATURE_ADD_CONTACT |
2866                         EMPATHY_INDIVIDUAL_FEATURE_CHAT |
2867                         EMPATHY_INDIVIDUAL_FEATURE_CALL |
2868                         EMPATHY_INDIVIDUAL_FEATURE_LOG |
2869                         EMPATHY_INDIVIDUAL_FEATURE_INFO));
2870
2871                 empathy_individual_view_set_show_offline (
2872                         EMPATHY_INDIVIDUAL_VIEW (priv->contact_list_view), TRUE);
2873                 empathy_individual_view_set_show_uninteresting (
2874                         EMPATHY_INDIVIDUAL_VIEW (priv->contact_list_view), TRUE);
2875
2876                 gtk_container_add (GTK_CONTAINER (priv->scrolled_window_contacts),
2877                                    priv->contact_list_view);
2878
2879                 gtk_widget_show (priv->contact_list_view);
2880                 gtk_widget_show (priv->scrolled_window_contacts);
2881                 g_object_unref (store);
2882         } else if (!show) {
2883                 priv->contacts_width = gtk_paned_get_position (GTK_PANED (priv->hpaned));
2884                 gtk_widget_hide (priv->scrolled_window_contacts);
2885                 if (priv->contact_list_view != NULL) {
2886                         gtk_widget_destroy (priv->contact_list_view);
2887                         priv->contact_list_view = NULL;
2888                 }
2889         }
2890 }
2891
2892 void
2893 empathy_chat_set_show_contacts (EmpathyChat *chat,
2894                                 gboolean     show)
2895 {
2896         EmpathyChatPriv *priv = GET_PRIV (chat);
2897
2898         priv->show_contacts = show;
2899
2900         chat_update_contacts_visibility (chat, show);
2901
2902         g_object_notify (G_OBJECT (chat), "show-contacts");
2903 }
2904
2905 static void
2906 chat_self_contact_changed_cb (EmpathyChat *chat)
2907 {
2908         EmpathyChatPriv *priv = GET_PRIV (chat);
2909
2910         if (priv->self_contact != NULL) {
2911                 g_signal_handlers_disconnect_by_func (priv->self_contact,
2912                                                       chat_self_contact_alias_changed_cb,
2913                                                       chat);
2914         }
2915         g_clear_object (&priv->self_contact);
2916
2917         priv->self_contact = empathy_tp_chat_get_self_contact (priv->tp_chat);
2918         if (priv->self_contact != NULL) {
2919                 g_object_ref (priv->self_contact);
2920
2921                 if (empathy_chat_is_room (chat)) {
2922                         g_signal_connect_swapped (priv->self_contact, "notify::alias",
2923                                           G_CALLBACK (chat_self_contact_alias_changed_cb),
2924                                           chat);
2925                 }
2926         }
2927
2928         chat_self_contact_alias_changed_cb (chat);
2929 }
2930
2931 static void
2932 chat_remote_contact_changed_cb (EmpathyChat *chat)
2933 {
2934         EmpathyChatPriv *priv = GET_PRIV (chat);
2935
2936         if (priv->remote_contact != NULL) {
2937                 g_object_unref (priv->remote_contact);
2938                 priv->remote_contact = NULL;
2939         }
2940
2941         g_free (priv->id);
2942
2943         priv->id = g_strdup (empathy_tp_chat_get_id (priv->tp_chat));
2944         priv->remote_contact = empathy_tp_chat_get_remote_contact (priv->tp_chat);
2945         if (priv->remote_contact != NULL) {
2946                 g_object_ref (priv->remote_contact);
2947                 priv->handle_type = TP_HANDLE_TYPE_CONTACT;
2948         }
2949         else if (priv->tp_chat != NULL) {
2950                 tp_channel_get_handle ((TpChannel *) priv->tp_chat, &priv->handle_type);
2951         }
2952
2953         chat_update_contacts_visibility (chat, priv->show_contacts);
2954
2955         g_object_notify (G_OBJECT (chat), "remote-contact");
2956         g_object_notify (G_OBJECT (chat), "id");
2957 }
2958
2959 static void
2960 chat_invalidated_cb (EmpathyTpChat *tp_chat,
2961                  guint domain,
2962                  gint code,
2963                  gchar *message,
2964                  EmpathyChat   *chat)
2965 {
2966         EmpathyChatPriv *priv;
2967
2968         priv = GET_PRIV (chat);
2969
2970         if (!priv->tp_chat) {
2971                 return;
2972         }
2973
2974         chat_composing_remove_timeout (chat);
2975         g_object_unref (priv->tp_chat);
2976         priv->tp_chat = NULL;
2977         g_object_notify (G_OBJECT (chat), "tp-chat");
2978
2979         empathy_chat_view_append_event (chat->view, _("Disconnected"));
2980         gtk_widget_set_sensitive (chat->input_text_view, FALSE);
2981
2982         chat_update_contacts_visibility (chat, FALSE);
2983
2984         priv->unread_messages_when_offline = priv->unread_messages;
2985 }
2986
2987 static gboolean
2988 update_misspelled_words (gpointer data)
2989 {
2990         EmpathyChat *chat = EMPATHY_CHAT (data);
2991         EmpathyChatPriv *priv = GET_PRIV (chat);
2992         GtkTextBuffer *buffer;
2993         GtkTextIter iter;
2994         gint length;
2995
2996         buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (chat->input_text_view));
2997
2998         gtk_text_buffer_get_end_iter (buffer, &iter);
2999         length = gtk_text_iter_get_offset (&iter);
3000         chat_input_text_buffer_insert_text_cb (buffer, &iter,
3001                                                NULL, length, chat);
3002
3003         priv->update_misspelled_words_id = 0;
3004
3005         return FALSE;
3006 }
3007
3008 static void
3009 conf_spell_checking_cb (GSettings *gsettings_chat,
3010                         const gchar *key,
3011                         gpointer user_data)
3012 {
3013         EmpathyChat *chat = EMPATHY_CHAT (user_data);
3014         EmpathyChatPriv *priv = GET_PRIV (chat);
3015         gboolean spell_checker;
3016         GtkTextBuffer *buffer;
3017
3018         if (strcmp (key, EMPATHY_PREFS_CHAT_SPELL_CHECKER_ENABLED) != 0)
3019                 return;
3020
3021         spell_checker = g_settings_get_boolean (gsettings_chat,
3022                         EMPATHY_PREFS_CHAT_SPELL_CHECKER_ENABLED);
3023
3024         if (!empathy_spell_supported ()) {
3025                 spell_checker = FALSE;
3026         }
3027
3028         buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (chat->input_text_view));
3029
3030         if (spell_checker == priv->spell_checking_enabled) {
3031                 if (spell_checker) {
3032                         /* Possibly changed dictionaries,
3033                          * update misspelled words. Need to do so in idle
3034                          * so the spell checker is updated. */
3035                         priv->update_misspelled_words_id =
3036                                 g_idle_add (update_misspelled_words, chat);
3037                 }
3038
3039                 return;
3040         }
3041
3042         if (spell_checker) {
3043                 GtkTextIter iter;
3044
3045                 priv->notify_cursor_position_id = tp_g_signal_connect_object  (
3046                                 buffer, "notify::cursor-position",
3047                                 G_CALLBACK (chat_input_text_buffer_notify_cursor_position_cb),
3048                                 chat, 0);
3049                 priv->insert_text_id = tp_g_signal_connect_object  (
3050                                 buffer, "insert-text",
3051                                 G_CALLBACK (chat_input_text_buffer_insert_text_cb),
3052                                 chat, G_CONNECT_AFTER);
3053                 priv->delete_range_id = tp_g_signal_connect_object  (
3054                                 buffer, "delete-range",
3055                                 G_CALLBACK (chat_input_text_buffer_delete_range_cb),
3056                                 chat, G_CONNECT_AFTER);
3057
3058                 gtk_text_buffer_create_tag (buffer, "misspelled",
3059                                             "underline", PANGO_UNDERLINE_ERROR,
3060                                             NULL);
3061
3062                 gtk_text_buffer_get_iter_at_mark (buffer, &iter,
3063                                                   gtk_text_buffer_get_insert (buffer));
3064                 gtk_text_buffer_create_mark (buffer, "previous-cursor-position",
3065                                              &iter, TRUE);
3066
3067                 /* Mark misspelled words in the existing buffer.
3068                  * Need to do so in idle so the spell checker is updated. */
3069                 priv->update_misspelled_words_id =
3070                         g_idle_add (update_misspelled_words, chat);
3071         } else {
3072                 GtkTextTagTable *table;
3073                 GtkTextTag *tag;
3074
3075                 g_signal_handler_disconnect (buffer, priv->notify_cursor_position_id);
3076                 priv->notify_cursor_position_id = 0;
3077                 g_signal_handler_disconnect (buffer, priv->insert_text_id);
3078                 priv->insert_text_id = 0;
3079                 g_signal_handler_disconnect (buffer, priv->delete_range_id);
3080                 priv->delete_range_id = 0;
3081
3082                 table = gtk_text_buffer_get_tag_table (buffer);
3083                 tag = gtk_text_tag_table_lookup (table, "misspelled");
3084                 gtk_text_tag_table_remove (table, tag);
3085
3086                 gtk_text_buffer_delete_mark_by_name (buffer,
3087                                                      "previous-cursor-position");
3088         }
3089
3090         priv->spell_checking_enabled = spell_checker;
3091 }
3092
3093 static gboolean
3094 save_paned_pos_timeout (gpointer data)
3095 {
3096         EmpathyChat *self = data;
3097         gint hpaned_pos;
3098
3099         hpaned_pos = gtk_paned_get_position (GTK_PANED (self->priv->hpaned));
3100
3101         g_settings_set_int (self->priv->gsettings_ui,
3102                             EMPATHY_PREFS_UI_CHAT_WINDOW_PANED_POS,
3103                             hpaned_pos);
3104
3105         return FALSE;
3106 }
3107
3108 static gboolean
3109 chat_hpaned_pos_changed_cb (GtkWidget* hpaned,
3110                 GParamSpec *spec,
3111                 gpointer user_data)
3112 {
3113         EmpathyChat *chat = EMPATHY_CHAT (user_data);
3114
3115         if (chat->priv->save_paned_pos_id != 0)
3116                 g_source_remove (chat->priv->save_paned_pos_id);
3117
3118         chat->priv->save_paned_pos_id = g_timeout_add_seconds (1,
3119                 save_paned_pos_timeout, chat);
3120
3121         return TRUE;
3122 }
3123
3124 static void
3125 chat_create_ui (EmpathyChat *chat)
3126 {
3127         EmpathyChatPriv *priv = GET_PRIV (chat);
3128         GtkBuilder      *gui;
3129         GList           *list = NULL;
3130         gchar           *filename;
3131         GtkTextBuffer   *buffer;
3132         EmpathyThemeManager *theme_mgr;
3133
3134         filename = empathy_file_lookup ("empathy-chat.ui",
3135                                         "libempathy-gtk");
3136         gui = empathy_builder_get_file (filename,
3137                                         "chat_widget", &priv->widget,
3138                                         "hpaned", &priv->hpaned,
3139                                         "vbox_left", &priv->vbox_left,
3140                                         "scrolled_window_chat", &priv->scrolled_window_chat,
3141                                         "scrolled_window_input", &priv->scrolled_window_input,
3142                                         "hbox_topic", &priv->hbox_topic,
3143                                         "expander_topic", &priv->expander_topic,
3144                                         "label_topic", &priv->label_topic,
3145                                         "scrolled_window_contacts", &priv->scrolled_window_contacts,
3146                                         "info_bar_vbox", &priv->info_bar_vbox,
3147                                         NULL);
3148
3149         empathy_builder_connect (gui, chat,
3150                 "expander_topic", "notify::expanded", chat_topic_expander_activate_cb,
3151                 "label_topic", "size-allocate", chat_topic_label_size_allocate_cb,
3152                 NULL);
3153
3154         g_free (filename);
3155
3156         /* Add message view. */
3157         theme_mgr = empathy_theme_manager_dup_singleton ();
3158         chat->view = empathy_theme_manager_create_view (theme_mgr);
3159         g_object_unref (theme_mgr);
3160         /* If this is a GtkTextView, it's set as a drag destination for text/plain
3161            and other types, even though it's non-editable and doesn't accept any
3162            drags.  This steals drag motion for anything inside the scrollbars,
3163            making drag destinations on chat windows far less useful.
3164          */
3165         gtk_drag_dest_unset (GTK_WIDGET (chat->view));
3166         g_signal_connect (chat->view, "focus_in_event",
3167                           G_CALLBACK (chat_text_view_focus_in_event_cb),
3168                           chat);
3169         gtk_container_add (GTK_CONTAINER (priv->scrolled_window_chat),
3170                            GTK_WIDGET (chat->view));
3171         gtk_widget_show (GTK_WIDGET (chat->view));
3172
3173         /* Add input GtkTextView */
3174         chat->input_text_view = empathy_input_text_view_new ();
3175         g_signal_connect (chat->input_text_view, "notify::has-focus",
3176                           G_CALLBACK (chat_input_has_focus_notify_cb),
3177                           chat);
3178         g_signal_connect (chat->input_text_view, "key-press-event",
3179                           G_CALLBACK (chat_input_key_press_event_cb),
3180                           chat);
3181         g_signal_connect (chat->input_text_view, "realize",
3182                           G_CALLBACK (chat_input_realize_cb),
3183                           chat);
3184         g_signal_connect (chat->input_text_view, "button-press-event",
3185                           G_CALLBACK (chat_input_button_press_event_cb),
3186                           chat);
3187         g_signal_connect (chat->input_text_view, "populate-popup",
3188                           G_CALLBACK (chat_input_populate_popup_cb),
3189                           chat);
3190         buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (chat->input_text_view));
3191         tp_g_signal_connect_object  (buffer, "changed",
3192                           G_CALLBACK (chat_input_text_buffer_changed_cb),
3193                           chat, 0);
3194         tp_g_signal_connect_object (priv->gsettings_chat,
3195                         "changed::" EMPATHY_PREFS_CHAT_SPELL_CHECKER_ENABLED,
3196                         G_CALLBACK (conf_spell_checking_cb), chat, 0);
3197         conf_spell_checking_cb (priv->gsettings_chat,
3198                                 EMPATHY_PREFS_CHAT_SPELL_CHECKER_ENABLED, chat);
3199         gtk_container_add (GTK_CONTAINER (priv->scrolled_window_input),
3200                            chat->input_text_view);
3201         gtk_widget_show (chat->input_text_view);
3202
3203         /* Add the (invisible) search bar */
3204         priv->search_bar = empathy_search_bar_new (chat->view);
3205         gtk_box_pack_start (GTK_BOX(priv->vbox_left),
3206                             priv->search_bar,
3207                             FALSE, FALSE, 0);
3208         gtk_box_reorder_child (GTK_BOX(priv->vbox_left), priv->search_bar, 1);
3209
3210         /* Initialy hide the topic, will be shown if not empty */
3211         gtk_widget_hide (priv->hbox_topic);
3212
3213         g_signal_connect (priv->hpaned, "notify::position",
3214                           G_CALLBACK (chat_hpaned_pos_changed_cb),
3215                           chat);
3216
3217         /* Set widget focus order */
3218         list = g_list_append (NULL, priv->search_bar);
3219         list = g_list_append (list, priv->scrolled_window_input);
3220         gtk_container_set_focus_chain (GTK_CONTAINER (priv->vbox_left), list);
3221         g_list_free (list);
3222
3223         list = g_list_append (NULL, priv->vbox_left);
3224         list = g_list_append (list, priv->scrolled_window_contacts);
3225         gtk_container_set_focus_chain (GTK_CONTAINER (priv->hpaned), list);
3226         g_list_free (list);
3227
3228         list = g_list_append (NULL, priv->hpaned);
3229         list = g_list_append (list, priv->hbox_topic);
3230         gtk_container_set_focus_chain (GTK_CONTAINER (priv->widget), list);
3231         g_list_free (list);
3232
3233         /* Add the main widget in the chat widget */
3234         gtk_box_pack_start (GTK_BOX (chat), priv->widget, TRUE, TRUE, 0);
3235         g_object_unref (gui);
3236 }
3237
3238 static void
3239 chat_finalize (GObject *object)
3240 {
3241         EmpathyChat     *chat;
3242         EmpathyChatPriv *priv;
3243
3244         chat = EMPATHY_CHAT (object);
3245         priv = GET_PRIV (chat);
3246
3247         DEBUG ("Finalized: %p", object);
3248
3249         if (priv->update_misspelled_words_id != 0)
3250                 g_source_remove (priv->update_misspelled_words_id);
3251
3252         if (priv->save_paned_pos_id != 0)
3253                 g_source_remove (priv->save_paned_pos_id);
3254
3255         if (priv->contacts_visible_id != 0)
3256                 g_source_remove (priv->contacts_visible_id);
3257
3258         g_object_unref (priv->gsettings_chat);
3259         g_object_unref (priv->gsettings_ui);
3260
3261         g_list_foreach (priv->input_history, (GFunc) chat_input_history_entry_free, NULL);
3262         g_list_free (priv->input_history);
3263
3264         g_list_foreach (priv->compositors, (GFunc) g_object_unref, NULL);
3265         g_list_free (priv->compositors);
3266
3267         chat_composing_remove_timeout (chat);
3268
3269         g_object_unref (priv->account_manager);
3270         g_object_unref (priv->log_manager);
3271
3272         if (priv->tp_chat) {
3273                 g_signal_handlers_disconnect_by_func (priv->tp_chat,
3274                         chat_invalidated_cb, chat);
3275                 g_signal_handlers_disconnect_by_func (priv->tp_chat,
3276                         chat_message_received_cb, chat);
3277                 g_signal_handlers_disconnect_by_func (priv->tp_chat,
3278                         chat_message_acknowledged_cb, chat);
3279                 g_signal_handlers_disconnect_by_func (priv->tp_chat,
3280                         chat_send_error_cb, chat);
3281                 g_signal_handlers_disconnect_by_func (priv->tp_chat,
3282                         chat_state_changed_cb, chat);
3283                 g_signal_handlers_disconnect_by_func (priv->tp_chat,
3284                         chat_members_changed_cb, chat);
3285                 g_signal_handlers_disconnect_by_func (priv->tp_chat,
3286                         chat_self_contact_changed_cb, chat);
3287                 g_signal_handlers_disconnect_by_func (priv->tp_chat,
3288                         chat_remote_contact_changed_cb, chat);
3289                 g_signal_handlers_disconnect_by_func (priv->tp_chat,
3290                         chat_title_changed_cb, chat);
3291                 g_signal_handlers_disconnect_by_func (priv->tp_chat,
3292                         chat_subject_changed_cb, chat);
3293                 empathy_tp_chat_leave (priv->tp_chat, "");
3294                 g_object_unref (priv->tp_chat);
3295         }
3296         if (priv->account) {
3297                 g_object_unref (priv->account);
3298         }
3299         if (priv->self_contact) {
3300                 g_signal_handlers_disconnect_by_func (priv->self_contact,
3301                                                       chat_self_contact_alias_changed_cb,
3302                                                       chat);
3303                 g_object_unref (priv->self_contact);
3304         }
3305         if (priv->remote_contact) {
3306                 g_object_unref (priv->remote_contact);
3307         }
3308
3309         if (priv->block_events_timeout_id) {
3310                 g_source_remove (priv->block_events_timeout_id);
3311         }
3312
3313         g_free (priv->id);
3314         g_free (priv->name);
3315         g_free (priv->subject);
3316         g_completion_free (priv->completion);
3317
3318         tp_clear_pointer (&priv->highlight_regex, g_regex_unref);
3319
3320         G_OBJECT_CLASS (empathy_chat_parent_class)->finalize (object);
3321 }
3322
3323 static void
3324 chat_constructed (GObject *object)
3325 {
3326         EmpathyChat *chat = EMPATHY_CHAT (object);
3327         EmpathyChatPriv *priv = GET_PRIV (chat);
3328
3329         if (priv->handle_type != TP_HANDLE_TYPE_ROOM) {
3330                 /* First display logs from the logger and then display pending messages */
3331                 chat_add_logs (chat);
3332         }
3333          else {
3334                 /* Just display pending messages for rooms */
3335                 priv->can_show_pending = TRUE;
3336                 show_pending_messages (chat);
3337         }
3338 }
3339
3340 static void
3341 empathy_chat_class_init (EmpathyChatClass *klass)
3342 {
3343         GObjectClass   *object_class = G_OBJECT_CLASS (klass);
3344
3345         object_class->finalize = chat_finalize;
3346         object_class->get_property = chat_get_property;
3347         object_class->set_property = chat_set_property;
3348         object_class->constructed = chat_constructed;
3349
3350         g_object_class_install_property (object_class,
3351                                          PROP_TP_CHAT,
3352                                          g_param_spec_object ("tp-chat",
3353                                                               "Empathy tp chat",
3354                                                               "The tp chat object",
3355                                                               EMPATHY_TYPE_TP_CHAT,
3356                                                               G_PARAM_CONSTRUCT |
3357                                                               G_PARAM_READWRITE |
3358                                                               G_PARAM_STATIC_STRINGS));
3359         g_object_class_install_property (object_class,
3360                                          PROP_ACCOUNT,
3361                                          g_param_spec_object ("account",
3362                                                               "Account of the chat",
3363                                                               "The account of the chat",
3364                                                               TP_TYPE_ACCOUNT,
3365                                                               G_PARAM_READABLE |
3366                                                               G_PARAM_STATIC_STRINGS));
3367         g_object_class_install_property (object_class,
3368                                          PROP_ID,
3369                                          g_param_spec_string ("id",
3370                                                               "Chat's id",
3371                                                               "The id of the chat",
3372                                                               NULL,
3373                                                               G_PARAM_READABLE |
3374                                                               G_PARAM_STATIC_STRINGS));
3375         g_object_class_install_property (object_class,
3376                                          PROP_NAME,
3377                                          g_param_spec_string ("name",
3378                                                               "Chat's name",
3379                                                               "The name of the chat",
3380                                                               NULL,
3381                                                               G_PARAM_READABLE |
3382                                                               G_PARAM_STATIC_STRINGS));
3383         g_object_class_install_property (object_class,
3384                                          PROP_SUBJECT,
3385                                          g_param_spec_string ("subject",
3386                                                               "Chat's subject",
3387                                                               "The subject or topic of the chat",
3388                                                               NULL,
3389                                                               G_PARAM_READABLE |
3390                                                               G_PARAM_STATIC_STRINGS));
3391         g_object_class_install_property (object_class,
3392                                          PROP_REMOTE_CONTACT,
3393                                          g_param_spec_object ("remote-contact",
3394                                                               "The remote contact",
3395                                                               "The remote contact is any",
3396                                                               EMPATHY_TYPE_CONTACT,
3397                                                               G_PARAM_READABLE |
3398                                                               G_PARAM_STATIC_STRINGS));
3399         g_object_class_install_property (object_class,
3400                                          PROP_SHOW_CONTACTS,
3401                                          g_param_spec_boolean ("show-contacts",
3402                                                                "Contacts' visibility",
3403                                                                "The visibility of the contacts' list",
3404                                                                TRUE,
3405                                                                G_PARAM_READWRITE |
3406                                                                G_PARAM_STATIC_STRINGS));
3407
3408         g_object_class_install_property (object_class,
3409                                          PROP_SMS_CHANNEL,
3410                                          g_param_spec_boolean ("sms-channel",
3411                                                                "SMS Channel",
3412                                                                "TRUE if this channel is for sending SMSes",
3413                                                                FALSE,
3414                                                                G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
3415
3416         g_object_class_install_property (object_class,
3417                                          PROP_N_MESSAGES_SENDING,
3418                                          g_param_spec_uint ("n-messages-sending",
3419                                                             "Num Messages Sending",
3420                                                             "The number of messages being sent",
3421                                                             0, G_MAXUINT, 0,
3422                                                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
3423
3424         g_object_class_install_property (object_class,
3425                                          PROP_NB_UNREAD_MESSAGES,
3426                                          g_param_spec_uint ("nb-unread-messages",
3427                                                             "Num Unread Messages",
3428                                                             "The number of unread messages",
3429                                                             0, G_MAXUINT, 0,
3430                                                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
3431
3432         signals[COMPOSING] =
3433                 g_signal_new ("composing",
3434                               G_OBJECT_CLASS_TYPE (object_class),
3435                               G_SIGNAL_RUN_LAST,
3436                               0,
3437                               NULL, NULL,
3438                               g_cclosure_marshal_generic,
3439                               G_TYPE_NONE,
3440                               1, G_TYPE_BOOLEAN);
3441
3442         /**
3443          * EmpathyChat::new-message:
3444          * @self: the #EmpathyChat
3445          * @message: the new message
3446          * @pending: whether the message was in the pending queue when @self
3447          *  was created
3448          * @should_highlight: %TRUE if the message mentions the local user
3449          */
3450         signals[NEW_MESSAGE] =
3451                 g_signal_new ("new-message",
3452                               G_OBJECT_CLASS_TYPE (object_class),
3453                               G_SIGNAL_RUN_LAST,
3454                               0,
3455                               NULL, NULL,
3456                               g_cclosure_marshal_generic,
3457                               G_TYPE_NONE,
3458                               3, EMPATHY_TYPE_MESSAGE, G_TYPE_BOOLEAN, G_TYPE_BOOLEAN);
3459
3460         signals[PART_COMMAND_ENTERED] =
3461                         g_signal_new ("part-command-entered",
3462                                   G_OBJECT_CLASS_TYPE (object_class),
3463                                   G_SIGNAL_RUN_LAST,
3464                                   0,
3465                                   NULL, NULL,
3466                                   g_cclosure_marshal_generic,
3467                                   G_TYPE_NONE,
3468                                   1, G_TYPE_STRV);
3469
3470         g_type_class_add_private (object_class, sizeof (EmpathyChatPriv));
3471 }
3472
3473 static gboolean
3474 chat_block_events_timeout_cb (gpointer data)
3475 {
3476         EmpathyChatPriv *priv = GET_PRIV (data);
3477
3478         priv->block_events_timeout_id = 0;
3479
3480         return FALSE;
3481 }
3482
3483 static void
3484 account_manager_prepared_cb (GObject *source_object,
3485                              GAsyncResult *result,
3486                              gpointer user_data)
3487 {
3488         GList *accounts, *l;
3489         TpAccountManager *account_manager = TP_ACCOUNT_MANAGER (source_object);
3490         EmpathyChat *chat = user_data;
3491         GError *error = NULL;
3492
3493         if (!tp_proxy_prepare_finish (account_manager, result, &error)) {
3494                 DEBUG ("Failed to prepare the account manager: %s", error->message);
3495                 g_error_free (error);
3496                 return;
3497         }
3498
3499         accounts = tp_account_manager_get_valid_accounts (account_manager);
3500
3501         for (l = accounts; l != NULL; l = l->next) {
3502                 TpAccount *account = l->data;
3503                 tp_g_signal_connect_object (account, "status-changed",
3504                                              G_CALLBACK (chat_new_connection_cb),
3505                                              chat, 0);
3506         }
3507
3508         g_list_free (accounts);
3509 }
3510
3511 static void
3512 empathy_chat_init (EmpathyChat *chat)
3513 {
3514         EmpathyChatPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (chat,
3515                 EMPATHY_TYPE_CHAT, EmpathyChatPriv);
3516
3517         chat->priv = priv;
3518         priv->log_manager = tpl_log_manager_dup_singleton ();
3519         priv->gsettings_chat = g_settings_new (EMPATHY_PREFS_CHAT_SCHEMA);
3520         priv->gsettings_ui = g_settings_new (EMPATHY_PREFS_UI_SCHEMA);
3521
3522         priv->contacts_width = g_settings_get_int (priv->gsettings_ui,
3523                 EMPATHY_PREFS_UI_CHAT_WINDOW_PANED_POS);
3524         priv->input_history = NULL;
3525         priv->input_history_current = NULL;
3526         priv->account_manager = tp_account_manager_dup ();
3527
3528         tp_proxy_prepare_async (priv->account_manager, NULL,
3529                                           account_manager_prepared_cb, chat);
3530
3531         priv->show_contacts = g_settings_get_boolean (priv->gsettings_chat,
3532                         EMPATHY_PREFS_CHAT_SHOW_CONTACTS_IN_ROOMS);
3533
3534         /* Block events for some time to avoid having "has come online" or
3535          * "joined" messages. */
3536         priv->block_events_timeout_id =
3537                 g_timeout_add_seconds (1, chat_block_events_timeout_cb, chat);
3538
3539         /* Add nick name completion */
3540         priv->completion = g_completion_new ((GCompletionFunc) empathy_contact_get_alias);
3541         g_completion_set_compare (priv->completion, chat_contacts_completion_func);
3542
3543         chat_create_ui (chat);
3544 }
3545
3546 EmpathyChat *
3547 empathy_chat_new (EmpathyTpChat *tp_chat)
3548 {
3549         return g_object_new (EMPATHY_TYPE_CHAT, "tp-chat", tp_chat, NULL);
3550 }
3551
3552 EmpathyTpChat *
3553 empathy_chat_get_tp_chat (EmpathyChat *chat)
3554 {
3555         EmpathyChatPriv *priv = GET_PRIV (chat);
3556
3557         g_return_val_if_fail (EMPATHY_IS_CHAT (chat), NULL);
3558
3559         return priv->tp_chat;
3560 }
3561
3562 typedef struct
3563 {
3564         EmpathyChat *self;
3565         GtkWidget *info_bar;
3566         gulong response_id;
3567         GtkWidget *button;
3568         GtkWidget *label;
3569         GtkWidget *entry;
3570         GtkWidget *spinner;
3571         gchar *password;
3572 } PasswordData;
3573
3574 static void
3575 passwd_remember_button_cb (GtkButton *button,
3576                           PasswordData *data)
3577 {
3578         gtk_info_bar_response (GTK_INFO_BAR (data->info_bar), GTK_RESPONSE_OK);
3579 }
3580
3581 static void
3582 passwd_not_now_button_cb (GtkButton *button,
3583                           PasswordData *data)
3584 {
3585         gtk_info_bar_response (GTK_INFO_BAR (data->info_bar), GTK_RESPONSE_NO);
3586 }
3587
3588 static void
3589 remember_password_infobar_response_cb (GtkWidget *info_bar,
3590                                        gint response_id,
3591                                        PasswordData *data)
3592 {
3593         EmpathyChatPriv *priv = GET_PRIV (data->self);
3594
3595         if (response_id == GTK_RESPONSE_OK) {
3596                 DEBUG ("Saving room password");
3597                 empathy_keyring_set_room_password_async (priv->account,
3598                                                          empathy_tp_chat_get_id (priv->tp_chat),
3599                                                          data->password,
3600                                                          NULL, NULL);
3601         }
3602
3603         gtk_widget_destroy (info_bar);
3604         g_free (data->password);
3605         g_slice_free (PasswordData, data);
3606 }
3607
3608 static void
3609 chat_prompt_to_save_password (EmpathyChat *self,
3610                               PasswordData *data)
3611 {
3612         GtkWidget *content_area;
3613         GtkWidget *hbox;
3614         GtkWidget *image;
3615         GtkWidget *label;
3616         GtkWidget *alig;
3617         GtkWidget *button;
3618
3619         /* save the password in case it needs to be saved */
3620         data->password = g_strdup (gtk_entry_get_text (GTK_ENTRY (data->entry)));
3621
3622         /* Remove all previous widgets */
3623         content_area = gtk_info_bar_get_content_area (GTK_INFO_BAR (data->info_bar));
3624         gtk_container_forall (GTK_CONTAINER (content_area),
3625                               (GtkCallback) gtk_widget_destroy, NULL);
3626         data->button = NULL;
3627         data->label = NULL;
3628         data->entry = NULL;
3629         data->spinner = NULL;
3630
3631         gtk_info_bar_set_message_type (GTK_INFO_BAR (data->info_bar),
3632                                        GTK_MESSAGE_QUESTION);
3633
3634         hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 5);
3635         gtk_box_pack_start (GTK_BOX (content_area), hbox, TRUE, TRUE, 0);
3636
3637         /* Add image */
3638         image = gtk_image_new_from_stock (GTK_STOCK_DIALOG_AUTHENTICATION,
3639                                           GTK_ICON_SIZE_DIALOG);
3640         gtk_box_pack_start (GTK_BOX (hbox), image, FALSE, FALSE, 0);
3641
3642         /* Add message */
3643         label = gtk_label_new (_("Would you like to store this password?"));
3644         gtk_box_pack_start (GTK_BOX (hbox), label, TRUE, TRUE, 0);
3645
3646         /* Add 'Remember' button */
3647         alig = gtk_alignment_new (0, 0.5, 1, 0);
3648
3649         button = gtk_button_new_with_label (_("Remember"));
3650         gtk_container_add (GTK_CONTAINER (alig), button);
3651         gtk_box_pack_start (GTK_BOX (hbox), alig, FALSE, FALSE, 0);
3652
3653         g_signal_connect (button, "clicked", G_CALLBACK (passwd_remember_button_cb),
3654                           data);
3655
3656         /* Add 'Not now' button */
3657         alig = gtk_alignment_new (0, 0.5, 1, 0);
3658
3659         button = gtk_button_new_with_label (_("Not now"));
3660         gtk_container_add (GTK_CONTAINER (alig), button);
3661         gtk_box_pack_start (GTK_BOX (hbox), alig, FALSE, FALSE, 0);
3662
3663         g_signal_connect (button, "clicked", G_CALLBACK (passwd_not_now_button_cb),
3664                           data);
3665
3666         /* go! */
3667         g_signal_handler_disconnect (data->info_bar, data->response_id);
3668         g_signal_connect (data->info_bar, "response",
3669                           G_CALLBACK (remember_password_infobar_response_cb), data);
3670
3671         gtk_widget_show_all (data->info_bar);
3672 }
3673
3674 static void
3675 provide_password_cb (GObject *tp_chat,
3676                      GAsyncResult *res,
3677                      gpointer user_data)
3678 {
3679         PasswordData *data = user_data;
3680         EmpathyChat *self = data->self;
3681         EmpathyChatPriv *priv = GET_PRIV (self);
3682         GError *error = NULL;
3683
3684         if (!tp_channel_provide_password_finish (TP_CHANNEL (tp_chat), res,
3685                                                       &error)) {
3686                 DEBUG ("error: %s", error->message);
3687                 /* FIXME: what should we do if that's another error? Close the channel?
3688                  * Display the raw D-Bus error to the user isn't very useful */
3689                 if (g_error_matches (error, TP_ERRORS, TP_ERROR_AUTHENTICATION_FAILED)) {
3690                         /* entry */
3691                         gtk_entry_set_text (GTK_ENTRY (data->entry), "");
3692                         gtk_widget_set_sensitive (data->entry, TRUE);
3693                         gtk_widget_grab_focus (data->entry);
3694
3695                         /* info bar */
3696                         gtk_info_bar_set_message_type (
3697                             GTK_INFO_BAR (data->info_bar),
3698                             GTK_MESSAGE_ERROR);
3699
3700                         /* button */
3701                         gtk_widget_set_sensitive (data->button, TRUE);
3702                         gtk_button_set_label (GTK_BUTTON (data->button),
3703                             _("Retry"));
3704
3705                         /* label */
3706                         gtk_label_set_text (GTK_LABEL (data->label),
3707                             _("Wrong password; please try again:"));
3708
3709                         /* spinner */
3710                         gtk_spinner_stop (GTK_SPINNER (data->spinner));
3711                         gtk_widget_hide (data->spinner);
3712                 }
3713                 g_error_free (error);
3714                 return;
3715         }
3716
3717         if (empathy_keyring_is_available ()) {
3718                 /* ask whether they want to save the password */
3719                 chat_prompt_to_save_password (self, data);
3720         } else {
3721                 /* Get rid of the password info bar finally */
3722                 gtk_widget_destroy (data->info_bar);
3723                 g_slice_free (PasswordData, data);
3724         }
3725
3726         /* Room joined */
3727         gtk_widget_set_sensitive (priv->hpaned, TRUE);
3728         gtk_widget_set_sensitive (self->input_text_view, TRUE);
3729         gtk_widget_grab_focus (self->input_text_view);
3730 }
3731
3732 static void
3733 password_infobar_response_cb (GtkWidget *info_bar,
3734                               gint response_id,
3735                               PasswordData *data)
3736 {
3737         EmpathyChatPriv *priv = GET_PRIV (data->self);
3738         const gchar *password;
3739
3740         if (response_id != GTK_RESPONSE_OK) {
3741                 gtk_widget_destroy (info_bar);
3742                 g_slice_free (PasswordData, data);
3743                 return;
3744         }
3745
3746         password = gtk_entry_get_text (GTK_ENTRY (data->entry));
3747
3748         tp_channel_provide_password_async (TP_CHANNEL (priv->tp_chat), password,
3749                                                 provide_password_cb, data);
3750
3751         gtk_widget_set_sensitive (data->button, FALSE);
3752         gtk_widget_set_sensitive (data->entry, FALSE);
3753
3754         gtk_spinner_start (GTK_SPINNER (data->spinner));
3755         gtk_widget_show (data->spinner);
3756 }
3757
3758 static void
3759 password_entry_activate_cb (GtkWidget *entry,
3760                           PasswordData *data)
3761 {
3762         gtk_info_bar_response (GTK_INFO_BAR (data->info_bar), GTK_RESPONSE_OK);
3763 }
3764
3765 static void
3766 passwd_join_button_cb (GtkButton *button,
3767                           PasswordData *data)
3768 {
3769         gtk_info_bar_response (GTK_INFO_BAR (data->info_bar), GTK_RESPONSE_OK);
3770 }
3771
3772 static void
3773 clear_icon_released_cb (GtkEntry *entry,
3774                         GtkEntryIconPosition icon_pos,
3775                         GdkEvent *event,
3776                         PasswordData *data)
3777 {
3778         gtk_entry_set_text (entry, "");
3779 }
3780
3781 static void
3782 password_entry_changed_cb (GtkEditable *entry,
3783                            PasswordData *data)
3784 {
3785         const gchar *str;
3786
3787         str = gtk_entry_get_text (GTK_ENTRY (entry));
3788
3789         gtk_entry_set_icon_sensitive (GTK_ENTRY (entry),
3790             GTK_ENTRY_ICON_SECONDARY, !EMP_STR_EMPTY (str));
3791 }
3792
3793 static void
3794 infobar_chat_invalidated_cb (TpProxy       *proxy,
3795                            guint          domain,
3796                            gint           code,
3797                            gchar         *message,
3798                            gpointer       password_infobar)
3799 {
3800         /* Destroy the password infobar whenever a channel is invalidated
3801          * so we don't have multiple infobars when the MUC is rejoined */
3802         gtk_widget_destroy (GTK_WIDGET (password_infobar));
3803 }
3804
3805 static void
3806 display_password_info_bar (EmpathyChat *self)
3807 {
3808         EmpathyChatPriv *priv = GET_PRIV (self);
3809         GtkWidget *info_bar;
3810         GtkWidget *content_area;
3811         GtkWidget *hbox;
3812         GtkWidget *image;
3813         GtkWidget *label;
3814         GtkWidget *entry;
3815         GtkWidget *alig;
3816         GtkWidget *button;
3817         GtkWidget *spinner;
3818         PasswordData *data;
3819
3820         data = g_slice_new0 (PasswordData);
3821
3822         info_bar = gtk_info_bar_new ();
3823         gtk_info_bar_set_message_type (GTK_INFO_BAR (info_bar),
3824             GTK_MESSAGE_QUESTION);
3825
3826         content_area = gtk_info_bar_get_content_area (GTK_INFO_BAR (info_bar));
3827
3828         hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 5);
3829         gtk_box_pack_start (GTK_BOX (content_area), hbox, TRUE, TRUE, 0);
3830
3831         /* Add image */
3832         image = gtk_image_new_from_stock (GTK_STOCK_DIALOG_AUTHENTICATION,
3833                                           GTK_ICON_SIZE_DIALOG);
3834         gtk_box_pack_start (GTK_BOX (hbox), image, FALSE, FALSE, 0);
3835
3836         /* Add message */
3837         label = gtk_label_new (_("This room is protected by a password:"));
3838         gtk_box_pack_start (GTK_BOX (hbox), label, FALSE, FALSE, 0);
3839
3840         /* Add password entry */
3841         entry = gtk_entry_new ();
3842         gtk_entry_set_visibility (GTK_ENTRY (entry), FALSE);
3843         gtk_box_pack_start (GTK_BOX (hbox), entry, TRUE, TRUE, 0);
3844
3845         gtk_entry_set_icon_from_stock (GTK_ENTRY (entry),
3846                                        GTK_ENTRY_ICON_SECONDARY, GTK_STOCK_CLEAR);
3847         gtk_entry_set_icon_sensitive (GTK_ENTRY (entry),
3848                                       GTK_ENTRY_ICON_SECONDARY, FALSE);
3849
3850         g_signal_connect (entry, "icon-release",
3851                           G_CALLBACK (clear_icon_released_cb), data);
3852         g_signal_connect (entry, "changed",
3853                           G_CALLBACK (password_entry_changed_cb), data);
3854
3855         g_signal_connect (entry, "activate",
3856                           G_CALLBACK (password_entry_activate_cb), data);
3857
3858         /* Focus the password entry once it's realized */
3859         g_signal_connect (entry, "realize", G_CALLBACK (gtk_widget_grab_focus), NULL);
3860
3861         /* Add 'Join' button */
3862         alig = gtk_alignment_new (0, 0.5, 1, 0);
3863
3864         button = gtk_button_new_with_label (_("Join"));
3865         gtk_container_add (GTK_CONTAINER (alig), button);
3866         gtk_box_pack_start (GTK_BOX (hbox), alig, FALSE, FALSE, 0);
3867
3868         g_signal_connect (button, "clicked", G_CALLBACK (passwd_join_button_cb),
3869                           data);
3870
3871         /* Add spinner */
3872         spinner = gtk_spinner_new ();
3873         gtk_box_pack_end (GTK_BOX (hbox), spinner, FALSE, FALSE, 0);
3874
3875         /* Save some data for messing around with later */
3876         data->self = self;
3877         data->info_bar = info_bar;
3878         data->button = button;
3879         data->label = label;
3880         data->entry = entry;
3881         data->spinner = spinner;
3882
3883         gtk_box_pack_start (GTK_BOX (priv->info_bar_vbox), info_bar,
3884                             TRUE, TRUE, 3);
3885         gtk_widget_show_all (hbox);
3886
3887         tp_g_signal_connect_object (priv->tp_chat,
3888                                   "invalidated", G_CALLBACK (infobar_chat_invalidated_cb),
3889                                   info_bar, 0);
3890
3891         data->response_id = g_signal_connect (info_bar, "response",
3892                                               G_CALLBACK (password_infobar_response_cb), data);
3893
3894         gtk_widget_show_all (info_bar);
3895         /* ... but hide the spinner */
3896         gtk_widget_hide (spinner);
3897
3898         /* prevent the user from typing anything */
3899         gtk_widget_set_sensitive (self->input_text_view, FALSE);
3900 }
3901
3902 static void
3903 provide_saved_password_cb (GObject *tp_chat,
3904                            GAsyncResult *res,
3905                            gpointer user_data)
3906 {
3907         EmpathyChat *self = user_data;
3908         EmpathyChatPriv *priv = GET_PRIV (self);
3909         GError *error = NULL;
3910
3911         if (!tp_channel_provide_password_finish (TP_CHANNEL (tp_chat), res,
3912                                                       &error)) {
3913                 DEBUG ("error: %s", error->message);
3914                 /* FIXME: what should we do if that's another error? Close the channel?
3915                  * Display the raw D-Bus error to the user isn't very useful */
3916                 if (g_error_matches (error, TP_ERRORS, TP_ERROR_AUTHENTICATION_FAILED)) {
3917                         display_password_info_bar (self);
3918                         gtk_widget_set_sensitive (priv->hpaned, FALSE);
3919                 }
3920                 g_error_free (error);
3921                 return;
3922         }
3923
3924         /* Room joined */
3925         gtk_widget_set_sensitive (priv->hpaned, TRUE);
3926         gtk_widget_grab_focus (self->input_text_view);
3927 }
3928
3929 static void
3930 chat_room_got_password_cb (GObject *source,
3931                            GAsyncResult *result,
3932                            gpointer user_data)
3933 {
3934         EmpathyChat *self = user_data;
3935         EmpathyChatPriv *priv = GET_PRIV (self);
3936         const gchar *password;
3937         GError *error = NULL;
3938
3939         password = empathy_keyring_get_room_password_finish (priv->account,
3940             result, &error);
3941
3942         if (error != NULL) {
3943                 DEBUG ("Couldn't get room password: %s\n", error->message);
3944                 g_clear_error (&error);
3945
3946                 display_password_info_bar (self);
3947                 gtk_widget_set_sensitive (priv->hpaned, FALSE);
3948                 return;
3949         }
3950
3951         tp_channel_provide_password_async (TP_CHANNEL (priv->tp_chat), password,
3952                                                 provide_saved_password_cb, self);
3953 }
3954
3955 static void
3956 chat_password_needed_changed_cb (EmpathyChat *self)
3957 {
3958         EmpathyChatPriv *priv = GET_PRIV (self);
3959
3960         if (tp_channel_password_needed (TP_CHANNEL (priv->tp_chat))) {
3961                 empathy_keyring_get_room_password_async (priv->account,
3962                                                          empathy_tp_chat_get_id (priv->tp_chat),
3963                                                          chat_room_got_password_cb, self);
3964         }
3965 }
3966
3967 static void
3968 chat_sms_channel_changed_cb (EmpathyChat *self)
3969 {
3970         EmpathyChatPriv *priv = GET_PRIV (self);
3971
3972         priv->sms_channel = tp_text_channel_is_sms_channel (
3973                 (TpTextChannel *) priv->tp_chat);
3974         g_object_notify (G_OBJECT (self), "sms-channel");
3975 }
3976
3977 static void
3978 chat_n_messages_sending_changed_cb (EmpathyChat *self)
3979 {
3980         g_object_notify (G_OBJECT (self), "n-messages-sending");
3981 }
3982
3983 void
3984 empathy_chat_set_tp_chat (EmpathyChat   *chat,
3985                           EmpathyTpChat *tp_chat)
3986 {
3987         EmpathyChatPriv *priv = GET_PRIV (chat);
3988
3989         g_return_if_fail (EMPATHY_IS_CHAT (chat));
3990         g_return_if_fail (EMPATHY_IS_TP_CHAT (tp_chat));
3991
3992         if (priv->tp_chat) {
3993                 return;
3994         }
3995
3996         if (priv->account) {
3997                 g_object_unref (priv->account);
3998         }
3999
4000         priv->tp_chat = g_object_ref (tp_chat);
4001         priv->account = g_object_ref (empathy_tp_chat_get_account (priv->tp_chat));
4002
4003         g_signal_connect (tp_chat, "invalidated",
4004                           G_CALLBACK (chat_invalidated_cb),
4005                           chat);
4006         g_signal_connect (tp_chat, "message-received-empathy",
4007                           G_CALLBACK (chat_message_received_cb),
4008                           chat);
4009         g_signal_connect (tp_chat, "message_acknowledged",
4010                           G_CALLBACK (chat_message_acknowledged_cb),
4011                           chat);
4012         g_signal_connect (tp_chat, "send-error",
4013                           G_CALLBACK (chat_send_error_cb),
4014                           chat);
4015         g_signal_connect (tp_chat, "chat-state-changed-empathy",
4016                           G_CALLBACK (chat_state_changed_cb),
4017                           chat);
4018         g_signal_connect (tp_chat, "members-changed",
4019                           G_CALLBACK (chat_members_changed_cb),
4020                           chat);
4021         g_signal_connect (tp_chat, "member-renamed",
4022                           G_CALLBACK (chat_member_renamed_cb),
4023                           chat);
4024         g_signal_connect_swapped (tp_chat, "notify::self-contact",
4025                                   G_CALLBACK (chat_self_contact_changed_cb),
4026                                   chat);
4027         g_signal_connect_swapped (tp_chat, "notify::remote-contact",
4028                                   G_CALLBACK (chat_remote_contact_changed_cb),
4029                                   chat);
4030         g_signal_connect_swapped (tp_chat, "notify::password-needed",
4031                                   G_CALLBACK (chat_password_needed_changed_cb),
4032                                   chat);
4033         g_signal_connect_swapped (tp_chat, "notify::is-sms-channel",
4034                                   G_CALLBACK (chat_sms_channel_changed_cb),
4035                                   chat);
4036         g_signal_connect_swapped (tp_chat, "notify::n-messages-sending",
4037                                   G_CALLBACK (chat_n_messages_sending_changed_cb),
4038                                   chat);
4039         g_signal_connect_swapped (tp_chat, "notify::title",
4040                                   G_CALLBACK (chat_title_changed_cb),
4041                                   chat);
4042         g_signal_connect_swapped (tp_chat, "notify::subject",
4043                                   G_CALLBACK (chat_subject_changed_cb),
4044                                   chat);
4045
4046         /* Get initial value of properties */
4047         chat_sms_channel_changed_cb (chat);
4048         chat_self_contact_changed_cb (chat);
4049         chat_remote_contact_changed_cb (chat);
4050         chat_title_changed_cb (chat);
4051         chat_subject_changed_cb (chat);
4052
4053         if (chat->input_text_view) {
4054                 gtk_widget_set_sensitive (chat->input_text_view, TRUE);
4055                 if (priv->block_events_timeout_id == 0) {
4056                         empathy_chat_view_append_event (chat->view, _("Connected"));
4057                 }
4058         }
4059
4060         g_object_notify (G_OBJECT (chat), "tp-chat");
4061         g_object_notify (G_OBJECT (chat), "id");
4062         g_object_notify (G_OBJECT (chat), "account");
4063
4064         /* This is a noop when tp-chat is set at object construction time and causes
4065          * the pending messages to be show when it's set on the object after it has
4066          * been created */
4067         show_pending_messages (chat);
4068
4069         /* check if a password is needed */
4070         chat_password_needed_changed_cb (chat);
4071 }
4072
4073 TpAccount *
4074 empathy_chat_get_account (EmpathyChat *chat)
4075 {
4076         EmpathyChatPriv *priv = GET_PRIV (chat);
4077
4078         g_return_val_if_fail (EMPATHY_IS_CHAT (chat), NULL);
4079
4080         return priv->account;
4081 }
4082
4083 const gchar *
4084 empathy_chat_get_id (EmpathyChat *chat)
4085 {
4086         EmpathyChatPriv *priv = GET_PRIV (chat);
4087
4088         g_return_val_if_fail (EMPATHY_IS_CHAT (chat), NULL);
4089
4090         return priv->id;
4091 }
4092
4093 gchar *
4094 empathy_chat_dup_name (EmpathyChat *chat)
4095 {
4096         EmpathyChatPriv *priv = GET_PRIV (chat);
4097         const gchar *ret;
4098
4099         g_return_val_if_fail (EMPATHY_IS_CHAT (chat), NULL);
4100
4101         ret = priv->name;
4102
4103         if (!ret && priv->remote_contact) {
4104                 ret = empathy_contact_get_alias (priv->remote_contact);
4105         }
4106
4107         if (!ret)
4108                 ret = priv->id;
4109
4110         if (!ret)
4111                 ret = _("Conversation");
4112
4113         if (priv->sms_channel)
4114                 /* Translators: this string is a something like
4115                  * "Escher Cat (SMS)" */
4116                 return g_strdup_printf (_("%s (SMS)"), ret);
4117         else
4118                 return g_strdup (ret);
4119 }
4120
4121 const gchar *
4122 empathy_chat_get_subject (EmpathyChat *chat)
4123 {
4124         EmpathyChatPriv *priv = GET_PRIV (chat);
4125
4126         g_return_val_if_fail (EMPATHY_IS_CHAT (chat), NULL);
4127
4128         return priv->subject;
4129 }
4130
4131 EmpathyContact *
4132 empathy_chat_get_remote_contact (EmpathyChat *chat)
4133 {
4134         EmpathyChatPriv *priv = GET_PRIV (chat);
4135
4136         g_return_val_if_fail (EMPATHY_IS_CHAT (chat), NULL);
4137
4138         return priv->remote_contact;
4139 }
4140
4141 GtkWidget *
4142 empathy_chat_get_contact_menu (EmpathyChat *chat)
4143 {
4144         EmpathyChatPriv *priv = GET_PRIV (chat);
4145         GtkWidget       *menu = NULL;
4146         FolksIndividual *individual;
4147         TpContact *contact;
4148
4149         g_return_val_if_fail (EMPATHY_IS_CHAT (chat), NULL);
4150
4151         if (priv->remote_contact == NULL)
4152                 return NULL;
4153
4154         contact = empathy_contact_get_tp_contact (priv->remote_contact);
4155         if (contact == NULL)
4156                 return NULL;
4157
4158         individual = empathy_create_individual_from_tp_contact (contact);
4159         if (individual == NULL)
4160                 return NULL;
4161
4162         menu = empathy_individual_menu_new (individual,
4163                                          EMPATHY_INDIVIDUAL_FEATURE_CALL |
4164                                          EMPATHY_INDIVIDUAL_FEATURE_LOG |
4165                                          EMPATHY_INDIVIDUAL_FEATURE_INFO |
4166                                          EMPATHY_INDIVIDUAL_FEATURE_BLOCK, NULL);
4167
4168         g_object_unref (individual);
4169
4170         return menu;
4171 }
4172
4173 void
4174 empathy_chat_clear (EmpathyChat *chat)
4175 {
4176         g_return_if_fail (EMPATHY_IS_CHAT (chat));
4177
4178         empathy_chat_view_clear (chat->view);
4179 }
4180
4181 void
4182 empathy_chat_scroll_down (EmpathyChat *chat)
4183 {
4184         g_return_if_fail (EMPATHY_IS_CHAT (chat));
4185
4186         empathy_chat_view_scroll_down (chat->view);
4187 }
4188
4189 void
4190 empathy_chat_cut (EmpathyChat *chat)
4191 {
4192         GtkTextBuffer *buffer;
4193
4194         g_return_if_fail (EMPATHY_IS_CHAT (chat));
4195
4196         buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (chat->input_text_view));
4197         if (gtk_text_buffer_get_has_selection (buffer)) {
4198                 GtkClipboard *clipboard;
4199
4200                 clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD);
4201
4202                 gtk_text_buffer_cut_clipboard (buffer, clipboard, TRUE);
4203         }
4204 }
4205
4206 void
4207 empathy_chat_copy (EmpathyChat *chat)
4208 {
4209         GtkTextBuffer *buffer;
4210
4211         g_return_if_fail (EMPATHY_IS_CHAT (chat));
4212
4213         if (empathy_chat_view_get_has_selection (chat->view)) {
4214                 empathy_chat_view_copy_clipboard (chat->view);
4215                 return;
4216         }
4217
4218         buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (chat->input_text_view));
4219         if (gtk_text_buffer_get_has_selection (buffer)) {
4220                 GtkClipboard *clipboard;
4221
4222                 clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD);
4223
4224                 gtk_text_buffer_copy_clipboard (buffer, clipboard);
4225         }
4226         else {
4227                 gint start_offset;
4228                 gint end_offset;
4229                 EmpathyChatPriv *priv = GET_PRIV (chat);
4230
4231                 if (gtk_label_get_selection_bounds (GTK_LABEL (priv->label_topic),
4232                                                                &start_offset,
4233                                                                &end_offset)) {
4234                         gchar *start;
4235                         gchar *end;
4236                         gchar *selection;
4237                         const gchar *topic;
4238                         GtkClipboard *clipboard;
4239
4240                         topic = gtk_label_get_text (GTK_LABEL (priv->label_topic));
4241                         start = g_utf8_offset_to_pointer (topic, start_offset);
4242                         end = g_utf8_offset_to_pointer (topic, end_offset);
4243                         selection = g_strndup (start, end - start);
4244
4245                         clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD);
4246                         gtk_clipboard_set_text (clipboard, selection, -1);
4247
4248                         g_free (selection);
4249                 }
4250         }
4251 }
4252
4253 void
4254 empathy_chat_paste (EmpathyChat *chat)
4255 {
4256         GtkTextBuffer *buffer;
4257         GtkClipboard  *clipboard;
4258         EmpathyChatPriv *priv;
4259
4260         g_return_if_fail (EMPATHY_IS_CHAT (chat));
4261
4262         priv = GET_PRIV (chat);
4263
4264         if (gtk_widget_get_visible (priv->search_bar)) {
4265                 empathy_search_bar_paste_clipboard (EMPATHY_SEARCH_BAR (priv->search_bar));
4266                 return;
4267         }
4268
4269         if (priv->tp_chat == NULL ||
4270             !gtk_widget_is_sensitive (chat->input_text_view))
4271                 return;
4272
4273         buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (chat->input_text_view));
4274         clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD);
4275
4276         gtk_text_buffer_paste_clipboard (buffer, clipboard, NULL, TRUE);
4277 }
4278
4279 void
4280 empathy_chat_find (EmpathyChat *chat)
4281 {
4282         EmpathyChatPriv *priv;
4283
4284         g_return_if_fail (EMPATHY_IS_CHAT (chat));
4285
4286         priv = GET_PRIV (chat);
4287
4288         empathy_search_bar_show (EMPATHY_SEARCH_BAR (priv->search_bar));
4289 }
4290
4291 void
4292 empathy_chat_correct_word (EmpathyChat  *chat,
4293                           GtkTextIter *start,
4294                           GtkTextIter *end,
4295                           const gchar *new_word)
4296 {
4297         GtkTextBuffer *buffer;
4298
4299         g_return_if_fail (chat != NULL);
4300         g_return_if_fail (new_word != NULL);
4301
4302         buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (chat->input_text_view));
4303
4304         gtk_text_buffer_delete (buffer, start, end);
4305         gtk_text_buffer_insert (buffer, start,
4306                                 new_word,
4307                                 -1);
4308 }
4309
4310 gboolean
4311 empathy_chat_is_room (EmpathyChat *chat)
4312 {
4313         EmpathyChatPriv *priv = GET_PRIV (chat);
4314
4315         g_return_val_if_fail (EMPATHY_IS_CHAT (chat), FALSE);
4316
4317         return (priv->handle_type == TP_HANDLE_TYPE_ROOM);
4318 }
4319
4320 gboolean
4321 empathy_chat_is_highlighted (EmpathyChat *chat)
4322 {
4323         EmpathyChatPriv *priv = GET_PRIV (chat);
4324
4325         g_return_val_if_fail (EMPATHY_IS_CHAT (chat), FALSE);
4326
4327         return priv->highlighted;
4328 }
4329
4330 guint
4331 empathy_chat_get_nb_unread_messages (EmpathyChat *self)
4332 {
4333         EmpathyChatPriv *priv = GET_PRIV (self);
4334
4335         g_return_val_if_fail (EMPATHY_IS_CHAT (self), 0);
4336
4337         return priv->unread_messages;
4338 }
4339
4340 /* called when the messages have been read by user */
4341 void
4342 empathy_chat_messages_read (EmpathyChat *self)
4343 {
4344         EmpathyChatPriv *priv = GET_PRIV (self);
4345
4346         g_return_if_fail (EMPATHY_IS_CHAT (self));
4347
4348         /* FIXME: See Bug#610994, See comments about it in EmpathyChatPriv
4349          * definition. If we are still retrieving the backlogs, do not ACK */
4350         if (priv->retrieving_backlogs)
4351                 return;
4352
4353         if (priv->tp_chat != NULL) {
4354                 tp_text_channel_ack_all_pending_messages_async (
4355                         TP_TEXT_CHANNEL (priv->tp_chat), NULL, NULL);
4356         }
4357
4358         priv->highlighted = FALSE;
4359
4360         if (priv->unread_messages_when_offline > 0) {
4361                 /* We can't ack those as the connection has gone away so just consider
4362                 * them as read. */
4363                 priv->unread_messages -= priv->unread_messages_when_offline;
4364                 g_object_notify (G_OBJECT (self), "nb-unread-messages");
4365                 priv->unread_messages_when_offline = 0;
4366         }
4367 }
4368
4369 /* Return TRUE if on of the contacts in this chat is composing */
4370 gboolean
4371 empathy_chat_is_composing (EmpathyChat *chat)
4372 {
4373   return chat->priv->compositors != NULL;
4374 }
4375
4376 gboolean
4377 empathy_chat_is_sms_channel (EmpathyChat *self)
4378 {
4379         EmpathyChatPriv *priv = GET_PRIV (self);
4380
4381         g_return_val_if_fail (EMPATHY_IS_CHAT (self), 0);
4382
4383         return priv->sms_channel;
4384 }
4385
4386 guint
4387 empathy_chat_get_n_messages_sending (EmpathyChat *self)
4388 {
4389         EmpathyChatPriv *priv;
4390
4391         g_return_val_if_fail (EMPATHY_IS_CHAT (self), 0);
4392
4393         priv = GET_PRIV (self);
4394
4395         if (priv->tp_chat == NULL) {
4396                 return 0;
4397         } else {
4398                 guint n_messages;
4399
4400                 g_object_get (priv->tp_chat,
4401                         "n-messages-sending", &n_messages,
4402                         NULL);
4403
4404                 return n_messages;
4405         }
4406 }
4407
4408 gchar *
4409 empathy_chat_dup_text (EmpathyChat *self)
4410 {
4411         GtkTextBuffer *buffer;
4412         GtkTextIter     start, end;
4413
4414         buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (self->input_text_view));
4415
4416         gtk_text_buffer_get_bounds (buffer, &start, &end);
4417         return gtk_text_buffer_get_text (buffer, &start, &end, FALSE);
4418 }
4419
4420 void
4421 empathy_chat_set_text (EmpathyChat *self,
4422                        const gchar *text)
4423 {
4424         GtkTextBuffer *buffer;
4425
4426         buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (self->input_text_view));
4427
4428         gtk_text_buffer_set_text (buffer, text, -1);
4429 }