/* pidgin * * Pidgin is the legal property of its developers, whose names are too numerous * to list here. Please refer to the COPYRIGHT file distributed with this * source distribution. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA * */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include "gtkblist.h" #include "gtkconv.h" #include "gtkdialogs.h" #include "gtkutils.h" #include "pidginavatar.h" #include "pidgincolor.h" #include "pidgincore.h" #include "pidgindisplaywindow.h" #include "pidgininfopane.h" #include "pidgininvitedialog.h" #include "pidginmessage.h" #include "pidginpresenceicon.h" enum { CHAT_USERS_ICON_COLUMN, CHAT_USERS_ALIAS_COLUMN, CHAT_USERS_ALIAS_KEY_COLUMN, CHAT_USERS_NAME_COLUMN, CHAT_USERS_FLAGS_COLUMN, CHAT_USERS_COLOR_COLUMN, CHAT_USERS_WEIGHT_COLUMN, CHAT_USERS_ICON_NAME_COLUMN, CHAT_USERS_COLUMNS }; typedef enum { PIDGIN_CONV_SET_TITLE = 1 << 0, PIDGIN_CONV_BUDDY_ICON = 1 << 1, PIDGIN_CONV_MENU = 1 << 2, PIDGIN_CONV_TAB_ICON = 1 << 3, PIDGIN_CONV_TOPIC = 1 << 4, PIDGIN_CONV_COLORIZE_TITLE = 1 << 6, }PidginConvFields; #define PIDGIN_CONV_ALL ((1 << 7) - 1) /* Prototypes. <-- because Paco-Paco hates this comment. */ static void pidgin_conv_updated(PurpleConversation *conv, PurpleConversationUpdateType type); gboolean pidgin_conv_has_focus(PurpleConversation *conv); static void pidgin_conv_update_fields(PurpleConversation *conv, PidginConvFields fields); static void pidgin_conv_placement_place(PidginConversation *conv); /************************************************************************** * Callbacks **************************************************************************/ static gboolean close_this_sucker(gpointer data) { PidginConversation *gtkconv = data; GList *list = g_list_copy(gtkconv->convs); purple_debug_misc("gtkconv", "closing %s", purple_conversation_get_name(list->data)); g_list_free_full(list, g_object_unref); return FALSE; } static gboolean close_conv_cb(G_GNUC_UNUSED GtkButton *button, PidginConversation *gtkconv) { /* We are going to destroy the conversations immediately only if the 'close immediately' * preference is selected. Otherwise, close the conversation after a reasonable timeout * (I am going to consider 10 minutes as a 'reasonable timeout' here. * For chats, close immediately if the chat is not in the buddylist, or if the chat is * not marked 'Persistent' */ PurpleConversation *conv = gtkconv->active_conv; if(PURPLE_IS_IM_CONVERSATION(conv) || PURPLE_IS_CHAT_CONVERSATION(conv)) { close_this_sucker(gtkconv); } return TRUE; } static gboolean check_for_and_do_command(PurpleConversation *conv) { PidginConversation *gtkconv; GtkWidget *input = NULL; GtkTextBuffer *buffer = NULL; gchar *cmd; const gchar *prefix; gboolean retval = FALSE; gtkconv = PIDGIN_CONVERSATION(conv); prefix = "/"; input = talkatu_editor_get_input(TALKATU_EDITOR(gtkconv->editor)); buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(input)); cmd = talkatu_buffer_get_plain_text(TALKATU_BUFFER(buffer)); if (cmd && g_str_has_prefix(cmd, prefix)) { PurpleCmdStatus status; char *error, *cmdline, *markup; cmdline = cmd + strlen(prefix); if (purple_strequal(cmdline, "xyzzy")) { purple_conversation_write_system_message(conv, "Nothing happens", PURPLE_MESSAGE_NO_LOG); g_free(cmd); return TRUE; } /* Docs are unclear on whether or not prefix should be removed from * the markup so, ignoring for now. Notably if the markup is * `/foo arg1` we now have to move the bold tag around? * - gk 20190709 */ markup = talkatu_markup_get_html(buffer, NULL); status = purple_cmd_do_command(conv, cmdline, markup, &error); g_free(markup); switch (status) { case PURPLE_CMD_STATUS_OK: retval = TRUE; break; case PURPLE_CMD_STATUS_NOT_FOUND: { PurpleProtocol *protocol = NULL; PurpleConnection *gc; if ((gc = purple_conversation_get_connection(conv))) protocol = purple_connection_get_protocol(gc); if ((protocol != NULL) && (purple_protocol_get_options(protocol) & OPT_PROTO_SLASH_COMMANDS_NATIVE)) { char *spaceslash; /* If the first word in the entered text has a '/' in it, then the user * probably didn't mean it as a command. So send the text as message. */ spaceslash = cmdline; while (*spaceslash && *spaceslash != ' ' && *spaceslash != '/') spaceslash++; if (*spaceslash != '/') { purple_conversation_write_system_message(conv, _("Unknown command."), PURPLE_MESSAGE_NO_LOG); retval = TRUE; } } break; } case PURPLE_CMD_STATUS_WRONG_ARGS: purple_conversation_write_system_message(conv, _("Syntax Error: You typed the wrong " "number of arguments to that command."), PURPLE_MESSAGE_NO_LOG); retval = TRUE; break; case PURPLE_CMD_STATUS_FAILED: purple_conversation_write_system_message(conv, error ? error : _("Your command failed for an unknown reason."), PURPLE_MESSAGE_NO_LOG); g_free(error); retval = TRUE; break; case PURPLE_CMD_STATUS_WRONG_TYPE: if(PURPLE_IS_IM_CONVERSATION(conv)) purple_conversation_write_system_message(conv, _("That command only works in chats, not IMs."), PURPLE_MESSAGE_NO_LOG); else purple_conversation_write_system_message(conv, _("That command only works in IMs, not chats."), PURPLE_MESSAGE_NO_LOG); retval = TRUE; break; case PURPLE_CMD_STATUS_WRONG_PROTOCOL: purple_conversation_write_system_message(conv, _("That command doesn't work on this protocol."), PURPLE_MESSAGE_NO_LOG); retval = TRUE; break; } } g_free(cmd); return retval; } static void send_cb(G_GNUC_UNUSED GtkWidget *widget, PidginConversation *gtkconv) { PurpleConversation *conv = gtkconv->active_conv; PurpleAccount *account; PurpleMessageFlags flags = 0; GtkTextBuffer *buffer = NULL; gchar *content; account = purple_conversation_get_account(conv); buffer = talkatu_editor_get_buffer(TALKATU_EDITOR(gtkconv->editor)); if (check_for_and_do_command(conv)) { talkatu_buffer_clear(TALKATU_BUFFER(buffer)); return; } if (PURPLE_IS_CHAT_CONVERSATION(conv) && purple_chat_conversation_has_left(PURPLE_CHAT_CONVERSATION(conv))) { return; } if (!purple_account_is_connected(account)) { return; } content = talkatu_markup_get_html(buffer, NULL); if (purple_strequal(content, "")) { g_free(content); return; } purple_idle_touch(); /* XXX: is there a better way to tell if the message has images? */ // if (strstr(buf, "entry, gtkconv->history}, {gtkconv->history, gtkconv->entry}, {NULL, NULL} }, *ptr; for (ptr = transitions; !next && ptr->from; ptr++) { GtkWidget *from, *to; if (dir == GTK_DIR_TAB_FORWARD) { from = ptr->from; to = ptr->to; } else { from = ptr->to; to = ptr->from; } if (gtk_widget_is_focus(from)) next = to; } if (next) gtk_widget_grab_focus(next); return !!next; } static gboolean conv_keypress_common(PidginConversation *gtkconv, guint keyval, GdkModifierType state) { /* If CTRL was held down... */ if (state & GDK_CONTROL_MASK) { switch (keyval) { case GDK_KEY_F6: if (gtkconv_cycle_focus(gtkconv, state & GDK_SHIFT_MASK ? GTK_DIR_TAB_BACKWARD : GTK_DIR_TAB_FORWARD)) { return TRUE; } break; } /* End of switch */ /* If ALT (or whatever) was held down... */ } else if (state & GDK_ALT_MASK) { /* If neither CTRL nor ALT were held down... */ } else { switch (keyval) { case GDK_KEY_F6: if (gtkconv_cycle_focus(gtkconv, state & GDK_SHIFT_MASK ? GTK_DIR_TAB_BACKWARD : GTK_DIR_TAB_FORWARD)) { return TRUE; } break; case GDK_KEY_Page_Up: case GDK_KEY_KP_Page_Up: talkatu_auto_scroller_decrement(TALKATU_AUTO_SCROLLER(gtkconv->vadjustment)); return TRUE; break; case GDK_KEY_Page_Down: case GDK_KEY_KP_Page_Down: talkatu_auto_scroller_increment(TALKATU_AUTO_SCROLLER(gtkconv->vadjustment)); return TRUE; break; } } return FALSE; } static gboolean is_valid_conversation_key(guint keyval, GdkModifierType state) { if (state & GDK_CONTROL_MASK) { return TRUE; } switch (keyval) { case GDK_KEY_F6: case GDK_KEY_F10: case GDK_KEY_Menu: case GDK_KEY_Shift_L: case GDK_KEY_Shift_R: case GDK_KEY_Control_L: case GDK_KEY_Control_R: case GDK_KEY_Escape: case GDK_KEY_Up: case GDK_KEY_Down: case GDK_KEY_Left: case GDK_KEY_Right: case GDK_KEY_Page_Up: case GDK_KEY_KP_Page_Up: case GDK_KEY_Page_Down: case GDK_KEY_KP_Page_Down: case GDK_KEY_Home: case GDK_KEY_End: case GDK_KEY_Tab: case GDK_KEY_KP_Tab: case GDK_KEY_ISO_Left_Tab: return TRUE; default: return FALSE; } } /* * If someone tries to type into the conversation backlog of a * conversation window then we yank focus from the conversation backlog * and give it to the text entry box so that people can type * all the live long day and it will get entered into the entry box. */ static gboolean refocus_entry_cb(GtkEventControllerKey *controller, guint keyval, G_GNUC_UNUSED guint keycode, GdkModifierType state, gpointer data) { PidginConversation *gtkconv = data; GtkWidget *input = NULL; GdkEvent *event = NULL; /* If we have a valid key for the conversation display, then exit */ event = gtk_event_controller_get_current_event(GTK_EVENT_CONTROLLER(controller)); if (is_valid_conversation_key(keyval, state)) { if (gdk_event_get_event_type(event) == GDK_KEY_PRESS) { return conv_keypress_common(gtkconv, keyval, state); } return FALSE; } input = talkatu_editor_get_input(TALKATU_EDITOR(gtkconv->editor)); gtk_widget_grab_focus(input); gtk_event_controller_key_forward(controller, input); return TRUE; } void pidgin_conv_switch_active_conversation(PurpleConversation *conv) { PidginConversation *gtkconv; PurpleConversation *old_conv; g_return_if_fail(conv != NULL); gtkconv = PIDGIN_CONVERSATION(conv); old_conv = gtkconv->active_conv; purple_debug_info("gtkconv", "setting active conversation on toolbar %p\n", conv); if (old_conv == conv) return; gtkconv->active_conv = conv; purple_signal_emit(pidgin_conversations_get_handle(), "conversation-switched", conv); g_object_set_data(G_OBJECT(gtkconv->entry), "transient_buddy", NULL); } /************************************************************************** * Utility functions **************************************************************************/ static GtkWidget * setup_common_pane(PidginConversation *gtkconv) { GSimpleActionGroup *ag = NULL; GtkTextBuffer *buffer = NULL; GtkWidget *vbox, *input, *hpaned, *sw; GtkEventController *key = NULL; PurpleConversation *conv = gtkconv->active_conv; /* Setup the top part of the pane */ vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6); /* Setup the info pane */ gtkconv->infopane = pidgin_info_pane_new(conv); gtk_widget_set_vexpand(gtkconv->infopane, FALSE); gtk_box_append(GTK_BOX(vbox), gtkconv->infopane); /* Setup the history widget */ sw = gtk_scrolled_window_new(); gtkconv->vadjustment = talkatu_auto_scroller_new(); gtk_scrolled_window_set_vadjustment(GTK_SCROLLED_WINDOW(sw), gtkconv->vadjustment); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(sw), GTK_POLICY_NEVER, GTK_POLICY_ALWAYS); gtkconv->history = talkatu_history_new(); gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(sw), gtkconv->history); /* Add the talkatu history */ hpaned = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL); gtk_widget_set_vexpand(hpaned, TRUE); gtk_box_append(GTK_BOX(vbox), hpaned); gtk_paned_set_start_child(GTK_PANED(hpaned), sw); gtk_paned_set_resize_start_child(GTK_PANED(hpaned), TRUE); gtk_paned_set_shrink_start_child(GTK_PANED(hpaned), TRUE); g_object_set_data(G_OBJECT(gtkconv->history), "gtkconv", gtkconv); key = gtk_event_controller_key_new(); g_signal_connect(key, "key-pressed", G_CALLBACK(refocus_entry_cb), gtkconv); g_signal_connect(key, "key-released", G_CALLBACK(refocus_entry_cb), gtkconv); gtk_widget_add_controller(gtkconv->history, key); /* Setup the entry widget and all signals */ gtkconv->editor = talkatu_editor_new(); ag = talkatu_action_group_new(TALKATU_FORMAT_HTML); buffer = talkatu_buffer_new(ag); talkatu_action_group_set_buffer(TALKATU_ACTION_GROUP(ag), buffer); g_clear_object(&ag); talkatu_editor_set_buffer(TALKATU_EDITOR(gtkconv->editor), buffer); g_clear_object(&buffer); gtk_box_append(GTK_BOX(vbox), gtkconv->editor); input = talkatu_editor_get_input(TALKATU_EDITOR(gtkconv->editor)); gtk_widget_set_name(input, "pidgin_conv_entry"); talkatu_input_set_send_binding(TALKATU_INPUT(input), TALKATU_INPUT_SEND_BINDING_RETURN | TALKATU_INPUT_SEND_BINDING_KP_ENTER); g_signal_connect( G_OBJECT(input), "send-message", G_CALLBACK(send_cb), gtkconv ); return vbox; } static PidginConversation * pidgin_conv_find_gtkconv(PurpleConversation * conv) { PurpleBuddy *bud = purple_blist_find_buddy(purple_conversation_get_account(conv), purple_conversation_get_name(conv)); PurpleMetaContact *c; PurpleBlistNode *cn, *bn; if (!bud) return NULL; if (!(c = purple_buddy_get_contact(bud))) return NULL; cn = PURPLE_BLIST_NODE(c); for (bn = purple_blist_node_get_first_child(cn); bn; bn = purple_blist_node_get_sibling_next(bn)) { PurpleBuddy *b = PURPLE_BUDDY(bn); PurpleConversation *im; PurpleConversationManager *manager; manager = purple_conversation_manager_get_default(); im = purple_conversation_manager_find_im(manager, purple_buddy_get_account(b), purple_buddy_get_name(b)); if(PIDGIN_CONVERSATION(im)) { return PIDGIN_CONVERSATION(im); } } return NULL; } static gboolean ignore_middle_click(G_GNUC_UNUSED GtkGestureClick *click, gint n_press, G_GNUC_UNUSED gdouble x, G_GNUC_UNUSED gdouble y, G_GNUC_UNUSED gpointer data) { /* A click on the pane is propagated to the notebook containing the pane. * So if Stu accidentally aims high and middle clicks on the pane-handle, * it causes a conversation tab to close. Let's stop that from happening. */ if (n_press == 1) { return TRUE; } return FALSE; } /************************************************************************** * Conversation UI operations **************************************************************************/ static void private_gtkconv_new(PurpleConversation *conv, G_GNUC_UNUSED gboolean hidden) { PidginConversation *gtkconv; GtkWidget *tab_cont, *pane; GtkGesture *click = NULL; if (PURPLE_IS_IM_CONVERSATION(conv) && (gtkconv = pidgin_conv_find_gtkconv(conv))) { purple_debug_misc("gtkconv", "found existing gtkconv %p", gtkconv); g_object_set_data(G_OBJECT(conv), "pidgin", gtkconv); if (!g_list_find(gtkconv->convs, conv)) gtkconv->convs = g_list_prepend(gtkconv->convs, conv); pidgin_conv_switch_active_conversation(conv); return; } purple_debug_misc("gtkconv", "creating new gtkconv for %p", conv); gtkconv = g_new0(PidginConversation, 1); g_object_set_data(G_OBJECT(conv), "pidgin", gtkconv); gtkconv->active_conv = conv; gtkconv->convs = g_list_prepend(gtkconv->convs, conv); pane = setup_common_pane(gtkconv); if (pane == NULL) { g_free(gtkconv); g_object_set_data(G_OBJECT(conv), "pidgin", NULL); return; } click = gtk_gesture_click_new(); gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(click), GDK_BUTTON_MIDDLE); g_signal_connect(click, "pressed", G_CALLBACK(ignore_middle_click), NULL); gtk_widget_add_controller(pane, GTK_EVENT_CONTROLLER(click)); /* Setup the container for the tab. */ gtkconv->tab_cont = tab_cont = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6); g_object_set_data(G_OBJECT(tab_cont), "PidginConversation", gtkconv); gtk_widget_set_vexpand(pane, TRUE); gtk_box_append(GTK_BOX(tab_cont), pane); talkatu_editor_set_toolbar_visible( TALKATU_EDITOR(gtkconv->editor), purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/show_formatting_toolbar") ); pidgin_conv_placement_place(gtkconv); } static void pidgin_conv_new(PurpleConversation *conv) { private_gtkconv_new(conv, FALSE); if (PIDGIN_IS_PIDGIN_CONVERSATION(conv)) purple_signal_emit(pidgin_conversations_get_handle(), "conversation-displayed", PIDGIN_CONVERSATION(conv)); } void pidgin_conversation_detach(PurpleConversation *conv) { if(PIDGIN_IS_PIDGIN_CONVERSATION(conv)) { PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv); close_conv_cb(NULL, gtkconv); g_free(gtkconv); g_object_set_data(G_OBJECT(conv), "pidgin", NULL); } } static void received_im_msg_cb(G_GNUC_UNUSED PurpleAccount *account, G_GNUC_UNUSED char *sender, G_GNUC_UNUSED char *message, PurpleConversation *conv, G_GNUC_UNUSED PurpleMessageFlags flags) { guint timer; /* Somebody wants to keep this conversation around, so don't time it out */ if (conv) { timer = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(conv), "close-timer")); if (timer) { g_source_remove(timer); g_object_set_data(G_OBJECT(conv), "close-timer", GINT_TO_POINTER(0)); } } } static void pidgin_conv_destroy(PurpleConversation *conv) { PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv); GtkRoot *win = NULL; gtkconv->convs = g_list_remove(gtkconv->convs, conv); /* Don't destroy ourselves until all our convos are gone */ if (gtkconv->convs) { /* Make sure the destroyed conversation is not the active one */ if (gtkconv->active_conv == conv) { gtkconv->active_conv = gtkconv->convs->data; purple_conversation_update(gtkconv->active_conv, PURPLE_CONVERSATION_UPDATE_FEATURES); } return; } win = gtk_widget_get_root(gtkconv->tab_cont); pidgin_display_window_remove(PIDGIN_DISPLAY_WINDOW(win), conv); /* If the "Save Conversation" or "Save Icon" dialogs are open then close them */ purple_request_close_with_handle(gtkconv); purple_notify_close_with_handle(gtkconv); gtk_widget_unparent(gtkconv->tab_cont); purple_signals_disconnect_by_handle(gtkconv); g_clear_object(>kconv->vadjustment); g_free(gtkconv); } static gboolean writing_msg(PurpleConversation *conv, PurpleMessage *msg, G_GNUC_UNUSED gpointer _unused) { PidginConversation *gtkconv; g_return_val_if_fail(msg != NULL, FALSE); if (!(purple_message_get_flags(msg) & PURPLE_MESSAGE_ACTIVE_ONLY)) return FALSE; g_return_val_if_fail(conv != NULL, FALSE); gtkconv = PIDGIN_CONVERSATION(conv); g_return_val_if_fail(gtkconv != NULL, FALSE); if (conv == gtkconv->active_conv) return FALSE; purple_debug_info("gtkconv", "Suppressing message for an inactive conversation"); return TRUE; } static void pidgin_conv_write_conv(PurpleConversation *conv, PurpleMessage *pmsg) { PidginMessage *pidgin_msg = NULL; PurpleMessageFlags flags; PidginConversation *gtkconv; PurpleConnection *gc; PurpleAccount *account; gboolean plugin_return; g_return_if_fail(conv != NULL); gtkconv = PIDGIN_CONVERSATION(conv); g_return_if_fail(gtkconv != NULL); flags = purple_message_get_flags(pmsg); account = purple_conversation_get_account(conv); g_return_if_fail(account != NULL); gc = purple_account_get_connection(account); g_return_if_fail(gc != NULL || !(flags & (PURPLE_MESSAGE_SEND | PURPLE_MESSAGE_RECV))); plugin_return = GPOINTER_TO_INT(purple_signal_emit_return_1( pidgin_conversations_get_handle(), (PURPLE_IS_IM_CONVERSATION(conv) ? "displaying-im-msg" : "displaying-chat-msg"), conv, pmsg)); if (plugin_return) { return; } pidgin_msg = pidgin_message_new(pmsg); talkatu_history_write_message( TALKATU_HISTORY(gtkconv->history), TALKATU_MESSAGE(pidgin_msg) ); purple_signal_emit(pidgin_conversations_get_handle(), (PURPLE_IS_IM_CONVERSATION(conv) ? "displayed-im-msg" : "displayed-chat-msg"), conv, pmsg); } gboolean pidgin_conv_has_focus(PurpleConversation *conv) { PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv); GtkRoot *win; win = gtk_widget_get_root(gtkconv->tab_cont); if(gtk_window_is_active(GTK_WINDOW(win))) { PidginDisplayWindow *displaywin = PIDGIN_DISPLAY_WINDOW(win); if(pidgin_display_window_conversation_is_selected(displaywin, conv)) { return TRUE; } } return FALSE; } static void pidgin_conv_update_fields(PurpleConversation *conv, PidginConvFields fields) { PidginConversation *gtkconv; PidginDisplayWindow *displaywin; GtkRoot *win; gtkconv = PIDGIN_CONVERSATION(conv); if (!gtkconv) return; win = gtk_widget_get_root(gtkconv->tab_cont); displaywin = PIDGIN_DISPLAY_WINDOW(win); if (fields & PIDGIN_CONV_SET_TITLE) { purple_conversation_autoset_title(conv); } if ((fields & PIDGIN_CONV_COLORIZE_TITLE) || (fields & PIDGIN_CONV_SET_TITLE) || (fields & PIDGIN_CONV_TOPIC)) { char *title; PurpleAccount *account = purple_conversation_get_account(conv); if ((account == NULL) || !purple_account_is_connected(account) || (PURPLE_IS_CHAT_CONVERSATION(conv) && purple_chat_conversation_has_left(PURPLE_CHAT_CONVERSATION(conv)))) title = g_strdup_printf("(%s)", purple_conversation_get_title(conv)); else title = g_strdup(purple_conversation_get_title(conv)); if (pidgin_display_window_conversation_is_selected(displaywin, conv)) { const char* current_title = gtk_window_get_title(GTK_WINDOW(win)); if (current_title == NULL || !purple_strequal(current_title, title)) { gtk_window_set_title(GTK_WINDOW(win), title); } } g_free(title); } } static void pidgin_conv_updated(PurpleConversation *conv, PurpleConversationUpdateType type) { PidginConvFields flags = 0; g_return_if_fail(conv != NULL); if (type == PURPLE_CONVERSATION_UPDATE_ACCOUNT) { flags = PIDGIN_CONV_ALL; } else if (type == PURPLE_CONVERSATION_UPDATE_TYPING || type == PURPLE_CONVERSATION_UPDATE_UNSEEN || type == PURPLE_CONVERSATION_UPDATE_TITLE) { flags = PIDGIN_CONV_COLORIZE_TITLE; } else if (type == PURPLE_CONVERSATION_UPDATE_TOPIC) { flags = PIDGIN_CONV_TOPIC; } else if (type == PURPLE_CONVERSATION_ACCOUNT_ONLINE || type == PURPLE_CONVERSATION_ACCOUNT_OFFLINE) { flags = PIDGIN_CONV_MENU | PIDGIN_CONV_TAB_ICON | PIDGIN_CONV_SET_TITLE; } else if (type == PURPLE_CONVERSATION_UPDATE_AWAY) { flags = PIDGIN_CONV_TAB_ICON; } else if (type == PURPLE_CONVERSATION_UPDATE_ADD || type == PURPLE_CONVERSATION_UPDATE_REMOVE || type == PURPLE_CONVERSATION_UPDATE_CHATLEFT) { flags = PIDGIN_CONV_SET_TITLE | PIDGIN_CONV_MENU; } else if (type == PURPLE_CONVERSATION_UPDATE_ICON) { flags = PIDGIN_CONV_BUDDY_ICON; } else if (type == PURPLE_CONVERSATION_UPDATE_FEATURES) { flags = PIDGIN_CONV_MENU; } pidgin_conv_update_fields(conv, flags); } static PurpleConversationUiOps conversation_ui_ops = { .create_conversation = pidgin_conv_new, .destroy_conversation = pidgin_conv_destroy, .write_conv = pidgin_conv_write_conv, .has_focus = pidgin_conv_has_focus, }; PurpleConversationUiOps * pidgin_conversations_get_conv_ui_ops(void) { return &conversation_ui_ops; } /************************************************************************** * Public conversation utility functions **************************************************************************/ static void show_formatting_toolbar_pref_cb(G_GNUC_UNUSED const char *name, G_GNUC_UNUSED PurplePrefType type, gconstpointer value, G_GNUC_UNUSED gpointer data) { GList *list; PurpleConversation *conv; PurpleConversationManager *manager; PidginConversation *gtkconv; gboolean visible = (gboolean)GPOINTER_TO_INT(value); manager = purple_conversation_manager_get_default(); list = purple_conversation_manager_get_all(manager); while(list != NULL) { conv = PURPLE_CONVERSATION(list->data); if (!PIDGIN_IS_PIDGIN_CONVERSATION(conv)) { list = g_list_delete_link(list, list); continue; } gtkconv = PIDGIN_CONVERSATION(conv); talkatu_editor_set_toolbar_visible(TALKATU_EDITOR(gtkconv->editor), visible); list = g_list_delete_link(list, list); } } static PidginConversation * get_gtkconv_with_contact(PurpleMetaContact *contact) { PurpleBlistNode *node; node = ((PurpleBlistNode*)contact)->child; for (; node; node = node->next) { PurpleBuddy *buddy = (PurpleBuddy*)node; PurpleConversation *im; PurpleConversationManager *manager; manager = purple_conversation_manager_get_default(); im = purple_conversation_manager_find_im(manager, purple_buddy_get_account(buddy), purple_buddy_get_name(buddy)); if(PURPLE_IS_IM_CONVERSATION(im)) { return PIDGIN_CONVERSATION(im); } } return NULL; } static void account_signed_off_cb(PurpleConnection *gc, G_GNUC_UNUSED gpointer event) { PurpleConversationManager *manager; GList *list; manager = purple_conversation_manager_get_default(); list = purple_conversation_manager_get_all(manager); while(list != NULL) { PurpleConversation *conv = PURPLE_CONVERSATION(list->data); /* This seems fine in theory, but we also need to cover the * case of this account matching one of the other buddies in * one of the contacts containing the buddy corresponding to * a conversation. It's easier to just update them all. */ /* if (purple_conversation_get_account(conv) == account) */ pidgin_conv_update_fields(conv, PIDGIN_CONV_TAB_ICON | PIDGIN_CONV_MENU | PIDGIN_CONV_COLORIZE_TITLE); if (PURPLE_CONNECTION_IS_CONNECTED(gc) && PURPLE_IS_CHAT_CONVERSATION(conv) && purple_conversation_get_account(conv) == purple_connection_get_account(gc) && g_object_get_data(G_OBJECT(conv), "want-to-rejoin")) { GHashTable *comps = NULL; PurpleChat *chat = purple_blist_find_chat(purple_conversation_get_account(conv), purple_conversation_get_name(conv)); if (chat == NULL) { PurpleProtocol *protocol = purple_connection_get_protocol(gc); comps = purple_protocol_chat_info_defaults(PURPLE_PROTOCOL_CHAT(protocol), gc, purple_conversation_get_name(conv)); } else { comps = purple_chat_get_components(chat); } purple_serv_join_chat(gc, comps); if (chat == NULL && comps != NULL) g_hash_table_destroy(comps); } list = g_list_delete_link(list, list); } } static void account_signing_off(PurpleConnection *gc) { PurpleConversationManager *manager; GList *list; PurpleAccount *account = purple_connection_get_account(gc); manager = purple_conversation_manager_get_default(); list = purple_conversation_manager_get_all(manager); /* We are about to sign off. See which chats we are currently in, and mark * them for rejoin on reconnect. */ while(list != NULL) { if(PURPLE_IS_CHAT_CONVERSATION(list->data)) { PurpleConversation *conv; PurpleChatConversation *chat; gboolean left; conv = PURPLE_CONVERSATION(list->data); chat = PURPLE_CHAT_CONVERSATION(conv); left = purple_chat_conversation_has_left(chat); if(!left && purple_conversation_get_account(conv) == account) { g_object_set_data(G_OBJECT(conv), "want-to-rejoin", GINT_TO_POINTER(TRUE)); purple_conversation_write_system_message( conv, _("The account has disconnected and you are no longer in " "this chat. You will automatically rejoin the chat when " "the account reconnects."), PURPLE_MESSAGE_NO_LOG); } } list = g_list_delete_link(list, list); } } static void update_buddy_status_changed(PurpleBuddy *buddy, PurpleStatus *old, PurpleStatus *newstatus) { PidginConversation *gtkconv; PurpleConversation *conv; gtkconv = get_gtkconv_with_contact(purple_buddy_get_contact(buddy)); if (gtkconv) { conv = gtkconv->active_conv; pidgin_conv_update_fields(conv, PIDGIN_CONV_TAB_ICON | PIDGIN_CONV_COLORIZE_TITLE | PIDGIN_CONV_BUDDY_ICON); if ((purple_status_is_online(old) ^ purple_status_is_online(newstatus)) != 0) pidgin_conv_update_fields(conv, PIDGIN_CONV_MENU); } } static void update_buddy_privacy_changed(PurpleBuddy *buddy) { PidginConversation *gtkconv; PurpleConversation *conv; gtkconv = get_gtkconv_with_contact(purple_buddy_get_contact(buddy)); if (gtkconv) { conv = gtkconv->active_conv; pidgin_conv_update_fields(conv, PIDGIN_CONV_TAB_ICON | PIDGIN_CONV_MENU); } } static void update_buddy_idle_changed(PurpleBuddy *buddy, G_GNUC_UNUSED gboolean old, G_GNUC_UNUSED gboolean newidle) { PurpleConversation *im; PurpleConversationManager *manager; manager = purple_conversation_manager_get_default(); im = purple_conversation_manager_find_im(manager, purple_buddy_get_account(buddy), purple_buddy_get_name(buddy)); if(PURPLE_IS_IM_CONVERSATION(im)) { pidgin_conv_update_fields(im, PIDGIN_CONV_TAB_ICON); } } static void update_buddy_icon(PurpleBuddy *buddy) { PurpleConversation *im; PurpleConversationManager *manager; manager = purple_conversation_manager_get_default(); im = purple_conversation_manager_find_im(manager, purple_buddy_get_account(buddy), purple_buddy_get_name(buddy)); if(PURPLE_IS_IM_CONVERSATION(im)) { pidgin_conv_update_fields(im, PIDGIN_CONV_BUDDY_ICON); } } static void update_buddy_sign(PurpleBuddy *buddy, const char *which) { PurplePresence *presence; PurpleStatus *on, *off; presence = purple_buddy_get_presence(buddy); if (!presence) return; off = purple_presence_get_status(presence, "offline"); on = purple_presence_get_status(presence, "available"); if (*(which+1) == 'f') update_buddy_status_changed(buddy, on, off); else update_buddy_status_changed(buddy, off, on); } static void update_conversation_switched(PurpleConversation *conv) { pidgin_conv_update_fields(conv, PIDGIN_CONV_TAB_ICON | PIDGIN_CONV_SET_TITLE | PIDGIN_CONV_MENU | PIDGIN_CONV_BUDDY_ICON); } static void update_buddy_typing(PurpleAccount *account, const char *who) { PurpleConversation *conv; PurpleConversationManager *manager; PidginConversation *gtkconv; manager = purple_conversation_manager_get_default(); conv = purple_conversation_manager_find_im(manager, account, who); if(!PURPLE_IS_CONVERSATION(conv)) { return; } gtkconv = PIDGIN_CONVERSATION(conv); if(gtkconv && gtkconv->active_conv == conv) { pidgin_conv_update_fields(conv, PIDGIN_CONV_COLORIZE_TITLE); } } static void update_chat(PurpleChatConversation *chat) { pidgin_conv_update_fields(PURPLE_CONVERSATION(chat), PIDGIN_CONV_TOPIC | PIDGIN_CONV_MENU | PIDGIN_CONV_SET_TITLE); } static void update_chat_topic(PurpleChatConversation *chat, G_GNUC_UNUSED const char *old, G_GNUC_UNUSED const char *new) { pidgin_conv_update_fields(PURPLE_CONVERSATION(chat), PIDGIN_CONV_TOPIC); } static void pidgin_conv_attach(PurpleConversation *conv) { int timer; purple_conversation_set_ui_ops(conv, pidgin_conversations_get_conv_ui_ops()); if (!PIDGIN_CONVERSATION(conv)) private_gtkconv_new(conv, FALSE); timer = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(conv), "close-timer")); if (timer) { g_source_remove(timer); g_object_set_data(G_OBJECT(conv), "close-timer", NULL); } } gboolean pidgin_conv_attach_to_conversation(PurpleConversation *conv) { PidginConversation *gtkconv; pidgin_conv_attach(conv); gtkconv = PIDGIN_CONVERSATION(conv); purple_signal_emit(pidgin_conversations_get_handle(), "conversation-displayed", gtkconv); return TRUE; } void * pidgin_conversations_get_handle(void) { static int handle; return &handle; } void pidgin_conversations_init(void) { void *handle = pidgin_conversations_get_handle(); void *blist_handle = purple_blist_get_handle(); /* Conversations */ purple_prefs_add_none(PIDGIN_PREFS_ROOT "/conversations"); purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/show_incoming_formatting", TRUE); purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/minimum_entry_lines", 2); purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/show_formatting_toolbar", TRUE); purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/scrollback_lines", 4000); /* Conversations -> Chat */ purple_prefs_remove(PIDGIN_PREFS_ROOT "/conversations/chat"); /* Conversations -> IM */ purple_prefs_remove(PIDGIN_PREFS_ROOT "/conversations/im"); /* Connect callbacks. */ purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/show_formatting_toolbar", show_formatting_toolbar_pref_cb, NULL); /********************************************************************** * Register signals **********************************************************************/ purple_signal_register(handle, "displaying-im-msg", purple_marshal_BOOLEAN__POINTER_POINTER, G_TYPE_BOOLEAN, 2, PURPLE_TYPE_CONVERSATION, PURPLE_TYPE_MESSAGE); purple_signal_register(handle, "displayed-im-msg", purple_marshal_VOID__POINTER_POINTER, G_TYPE_NONE, 2, PURPLE_TYPE_CONVERSATION, PURPLE_TYPE_MESSAGE); purple_signal_register(handle, "displaying-chat-msg", purple_marshal_BOOLEAN__POINTER_POINTER, G_TYPE_BOOLEAN, 2, PURPLE_TYPE_CONVERSATION, PURPLE_TYPE_MESSAGE); purple_signal_register(handle, "displayed-chat-msg", purple_marshal_VOID__POINTER_POINTER, G_TYPE_NONE, 2, PURPLE_TYPE_CONVERSATION, PURPLE_TYPE_MESSAGE); purple_signal_register(handle, "conversation-switched", purple_marshal_VOID__POINTER, G_TYPE_NONE, 1, PURPLE_TYPE_CONVERSATION); purple_signal_register(handle, "conversation-displayed", purple_marshal_VOID__POINTER, G_TYPE_NONE, 1, G_TYPE_POINTER); /* (PidginConversation *) */ purple_signal_register(handle, "chat-nick-autocomplete", purple_marshal_BOOLEAN__POINTER_BOOLEAN, G_TYPE_BOOLEAN, 1, PURPLE_TYPE_CONVERSATION); purple_signal_register(handle, "chat-nick-clicked", purple_marshal_BOOLEAN__POINTER_POINTER_UINT, G_TYPE_BOOLEAN, 3, PURPLE_TYPE_CONVERSATION, G_TYPE_STRING, G_TYPE_UINT); purple_signal_register(handle, "conversation-window-created", purple_marshal_VOID__POINTER, G_TYPE_NONE, 1, G_TYPE_POINTER); /* (PidginConvWindow *) */ /********************************************************************** * UI operations **********************************************************************/ purple_signal_connect(purple_connections_get_handle(), "signed-on", handle, G_CALLBACK(account_signed_off_cb), GINT_TO_POINTER(PURPLE_CONVERSATION_ACCOUNT_ONLINE)); purple_signal_connect(purple_connections_get_handle(), "signed-off", handle, G_CALLBACK(account_signed_off_cb), GINT_TO_POINTER(PURPLE_CONVERSATION_ACCOUNT_OFFLINE)); purple_signal_connect(purple_connections_get_handle(), "signing-off", handle, G_CALLBACK(account_signing_off), NULL); purple_signal_connect(purple_conversations_get_handle(), "writing-im-msg", handle, G_CALLBACK(writing_msg), NULL); purple_signal_connect(purple_conversations_get_handle(), "writing-chat-msg", handle, G_CALLBACK(writing_msg), NULL); purple_signal_connect(purple_conversations_get_handle(), "received-im-msg", handle, G_CALLBACK(received_im_msg_cb), NULL); purple_conversations_set_ui_ops(&conversation_ui_ops); /* Callbacks to update a conversation */ purple_signal_connect(blist_handle, "buddy-signed-on", handle, G_CALLBACK(update_buddy_sign), "on"); purple_signal_connect(blist_handle, "buddy-signed-off", handle, G_CALLBACK(update_buddy_sign), "off"); purple_signal_connect(blist_handle, "buddy-status-changed", handle, G_CALLBACK(update_buddy_status_changed), NULL); purple_signal_connect(blist_handle, "buddy-privacy-changed", handle, G_CALLBACK(update_buddy_privacy_changed), NULL); purple_signal_connect(blist_handle, "buddy-idle-changed", handle, G_CALLBACK(update_buddy_idle_changed), NULL); purple_signal_connect(blist_handle, "buddy-icon-changed", handle, G_CALLBACK(update_buddy_icon), NULL); purple_signal_connect(purple_conversations_get_handle(), "buddy-typing", handle, G_CALLBACK(update_buddy_typing), NULL); purple_signal_connect(purple_conversations_get_handle(), "buddy-typing-stopped", handle, G_CALLBACK(update_buddy_typing), NULL); purple_signal_connect(pidgin_conversations_get_handle(), "conversation-switched", handle, G_CALLBACK(update_conversation_switched), NULL); purple_signal_connect(purple_conversations_get_handle(), "chat-left", handle, G_CALLBACK(update_chat), NULL); purple_signal_connect(purple_conversations_get_handle(), "chat-joined", handle, G_CALLBACK(update_chat), NULL); purple_signal_connect(purple_conversations_get_handle(), "chat-topic-changed", handle, G_CALLBACK(update_chat_topic), NULL); purple_signal_connect_priority(purple_conversations_get_handle(), "conversation-updated", handle, G_CALLBACK(pidgin_conv_updated), NULL, PURPLE_SIGNAL_PRIORITY_LOWEST); } void pidgin_conversations_uninit(void) { purple_prefs_disconnect_by_handle(pidgin_conversations_get_handle()); purple_signals_disconnect_by_handle(pidgin_conversations_get_handle()); purple_signals_unregister_by_instance(pidgin_conversations_get_handle()); } /************************************************************************** * GTK window ops **************************************************************************/ static void pidgin_conv_placement_place(PidginConversation *conv) { GtkWidget *window = NULL; PidginDisplayWindow *display_window = NULL; window = pidgin_display_window_get_default(); display_window = PIDGIN_DISPLAY_WINDOW(window); pidgin_display_window_add(display_window, conv->active_conv); }