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