Fri, 20 Oct 2023 02:13:51 -0500
Create a new PidginChannelJoinDialog widget
This works with PurpleProtocolConversation using get_channel_join_details,
join_channel_async, and join_channel_finish.
Testing Done:
Used a half finished implementation in IRCv3 of the join channel functions in `PurplProtocolConversation` for testing.
Reviewed at https://reviews.imfreedom.org/r/2672/
--- a/pidgin/meson.build Fri Oct 20 02:10:54 2023 -0500 +++ b/pidgin/meson.build Fri Oct 20 02:13:51 2023 -0500 @@ -32,6 +32,7 @@ 'pidginaddchatdialog.c', 'pidginapplication.c', 'pidginavatar.c', + 'pidginchanneljoindialog.c', 'pidgincolor.c', 'pidgincommands.c', 'pidgincontactinfomenu.c', @@ -100,6 +101,7 @@ 'pidginaddchatdialog.h', 'pidginapplication.h', 'pidginavatar.h', + 'pidginchanneljoindialog.h', 'pidgincolor.h', 'pidgincontactinfomenu.h', 'pidgincontactlist.h',
--- a/pidgin/pidginapplication.c Fri Oct 20 02:10:54 2023 -0500 +++ b/pidgin/pidginapplication.c Fri Oct 20 02:13:51 2023 -0500 @@ -45,6 +45,7 @@ #include "pidginaccountmanager.h" #include "pidginaccountsdisabledmenu.h" #include "pidginaccountsenabledmenu.h" +#include "pidginchanneljoindialog.h" #include "pidgincore.h" #include "pidgindebug.h" #include "pidgindisplaywindow.h" @@ -219,6 +220,16 @@ "new-message", }; +/** + * pidgin_application_channel_actions: (skip) + * + * This list keeps track of which actions should only be enabled if a protocol + * supporting channels is connected. + */ +static const gchar *pidgin_application_channel_actions[] = { + "join-channel", +}; + /*< private > * pidgin_application_chat_actions: * @@ -434,6 +445,23 @@ } static void +pidgin_application_join_channel(G_GNUC_UNUSED GSimpleAction *simple, + G_GNUC_UNUSED GVariant *parameter, + gpointer data) +{ + PidginApplication *application = data; + static GtkWidget *dialog = NULL; + + if(!GTK_IS_WIDGET(dialog)) { + dialog = pidgin_channel_join_dialog_new(); + g_object_add_weak_pointer(G_OBJECT(dialog), (gpointer)&dialog); + } + + pidgin_application_present_transient_window(application, + GTK_WINDOW(dialog)); +} + +static void pidgin_application_join_chat(G_GNUC_UNUSED GSimpleAction *simple, G_GNUC_UNUSED GVariant *parameter, G_GNUC_UNUSED gpointer data) @@ -576,6 +604,9 @@ .name = "get-user-info", .activate = pidgin_application_get_user_info, }, { + .name = "join-channel", + .activate = pidgin_application_join_channel, + }, { .name = "join-chat", .activate = pidgin_application_join_chat, }, { @@ -635,7 +666,9 @@ pidgin_application_signed_on_cb(PurpleAccount *account, gpointer data) { PidginApplication *application = PIDGIN_APPLICATION(data); PurpleProtocol *protocol = NULL; - gboolean should_enable_chat = FALSE, should_enable_room_list = FALSE; + gboolean should_enable_channel = FALSE; + gboolean should_enable_chat = FALSE; + gboolean should_enable_room_list = FALSE; gint n_actions = 0; protocol = purple_account_get_protocol(account); @@ -653,6 +686,16 @@ TRUE); } + should_enable_channel = PURPLE_PROTOCOL_IMPLEMENTS(protocol, CONVERSATION, + get_channel_join_details); + if(should_enable_channel) { + n_actions = G_N_ELEMENTS(pidgin_application_channel_actions); + pidgin_application_actions_set_enabled(application, + pidgin_application_channel_actions, + n_actions, + TRUE); + } + /* likewise, for the room list, we only care about enabling in this * handler. */ @@ -1001,6 +1044,12 @@ n_actions, online); + n_actions = G_N_ELEMENTS(pidgin_application_channel_actions); + pidgin_application_actions_set_enabled(application, + pidgin_application_channel_actions, + n_actions, + online); + n_actions = G_N_ELEMENTS(pidgin_application_room_list_actions); pidgin_application_actions_set_enabled(application, pidgin_application_room_list_actions,
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pidgin/pidginchanneljoindialog.c Fri Oct 20 02:13:51 2023 -0500 @@ -0,0 +1,371 @@ +/* + * Pidgin - Internet Messenger + * Copyright (C) Pidgin Developers <devel@pidgin.im> + * + * 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, see <https://www.gnu.org/licenses/>. + */ + +#include <glib/gi18n.h> + +#include "pidginchanneljoindialog.h" + +#include "pidginaccountrow.h" +#include "pidgincore.h" + +struct _PidginChannelJoinDialog { + GtkWindow parent; + + GtkWidget *account; + GtkCustomFilter *filter; + GtkWidget *entries; + GtkWidget *name; + GtkWidget *nickname; + GtkWidget *password; + GtkWidget *error; + GtkWidget *join; + GtkWidget *cancel; + + GCancellable *cancellable; + + PurpleChannelJoinDetails *details; +}; + +G_DEFINE_TYPE(PidginChannelJoinDialog, pidgin_channel_join_dialog, + GTK_TYPE_WINDOW) + +/****************************************************************************** + * Helpers + *****************************************************************************/ +static gboolean +pidgin_channel_join_dialog_filter_accounts(gpointer item, + G_GNUC_UNUSED gpointer data) +{ + if(PURPLE_IS_ACCOUNT(item)) { + PurpleAccount *account = PURPLE_ACCOUNT(item); + PurpleProtocol *protocol = purple_account_get_protocol(account); + + if(PURPLE_IS_PROTOCOL(protocol)) { + return PURPLE_PROTOCOL_IMPLEMENTS(protocol, CONVERSATION, + get_channel_join_details); + } + } + + return FALSE; +} + +static void +pidgin_channel_join_dialog_update(PidginChannelJoinDialog *dialog) { + PurpleAccount *account = NULL; + PurpleChannelJoinDetails *details = NULL; + PurpleProtocol *protocol = NULL; + PurpleProtocolConversation *protocol_conversation = NULL; + + account = pidgin_account_row_get_account(PIDGIN_ACCOUNT_ROW(dialog->account)); + if(!PURPLE_IS_ACCOUNT(account)) { + return; + } + + protocol = purple_account_get_protocol(account); + if(!PURPLE_IS_PROTOCOL_CONVERSATION(protocol)) { + return; + } + + protocol_conversation = PURPLE_PROTOCOL_CONVERSATION(protocol); + + details = purple_protocol_conversation_get_channel_join_details(protocol_conversation, + account); + if(PURPLE_IS_CHANNEL_JOIN_DETAILS(details)) { + purple_channel_join_details_merge(details, dialog->details); + } +} + +static gboolean +pidgin_channel_join_dialog_details_to_row(G_GNUC_UNUSED GBinding *binding, + const GValue *from_value, + GValue *to_value, + G_GNUC_UNUSED gpointer data) +{ + const char *str = NULL; + + str = g_value_get_string(from_value); + if(purple_strempty(str)) { + g_value_set_string(to_value, ""); + } else { + g_value_set_string(to_value, str); + } + + return TRUE; +} + +static gboolean +pidgin_channel_join_dialog_row_to_details(G_GNUC_UNUSED GBinding *binding, + const GValue *from_value, + GValue *to_value, + G_GNUC_UNUSED gpointer data) +{ + const char *str = NULL; + + str = g_value_get_string(from_value); + if(purple_strempty(str)) { + g_value_set_string(to_value, NULL); + } else { + g_value_set_string(to_value, str); + } + + return TRUE; +} + +/****************************************************************************** + * Callbacks + *****************************************************************************/ +static void +pidgin_channel_join_dialog_account_changed_cb(G_GNUC_UNUSED GObject *obj, + G_GNUC_UNUSED GParamSpec *pspec, + gpointer data) +{ + pidgin_channel_join_dialog_update(data); +} + +static void +pidgin_channel_join_dialog_join_named_changed_cb(GtkEditable *self, + gpointer data) +{ + PidginChannelJoinDialog *dialog = data; + const char *text = NULL; + gboolean sensitive = FALSE; + + text = gtk_editable_get_text(self); + sensitive = !purple_strempty(text); + + gtk_widget_set_sensitive(dialog->join, sensitive); +} + +static void +pidgin_channel_join_dialog_cancelled_cb(G_GNUC_UNUSED GCancellable *cancellable, + gpointer data) +{ + PidginChannelJoinDialog *dialog = data; + + gtk_widget_set_sensitive(dialog->entries, TRUE); + gtk_widget_set_sensitive(dialog->join, TRUE); + gtk_widget_set_sensitive(dialog->cancel, TRUE); + + g_clear_object(&dialog->cancellable); +} + +static void +pidgin_channel_join_dialog_cancel_clicked(G_GNUC_UNUSED GtkButton *self, + gpointer data) +{ + PidginChannelJoinDialog *dialog = data; + + gtk_widget_set_sensitive(dialog->cancel, FALSE); + + /* Check if the user is cancelling a join that was started but hadn't + * completed yet. + */ + if(G_IS_CANCELLABLE(dialog->cancellable)) { + if(!g_cancellable_is_cancelled(dialog->cancellable)) { + g_cancellable_cancel(dialog->cancellable); + } + + g_clear_object(&dialog->cancellable); + + return; + } + + gtk_window_destroy(GTK_WINDOW(dialog)); +} + +static void +pidgin_channel_join_dialog_join_cb(GObject *source, GAsyncResult *result, + gpointer data) +{ + PidginChannelJoinDialog *dialog = data; + PurpleProtocolConversation *protocol_conversation = NULL; + GError *error = NULL; + gboolean joined = FALSE; + + protocol_conversation = PURPLE_PROTOCOL_CONVERSATION(source); + + joined = purple_protocol_conversation_join_channel_finish(protocol_conversation, + result, &error); + + if(!joined) { + const char *error_message = _("Unknown error"); + + if(error != NULL) { + error_message = error->message; + } + + gtk_label_set_text(GTK_LABEL(dialog->error), error_message); + + gtk_widget_set_sensitive(dialog->entries, TRUE); + gtk_widget_set_sensitive(dialog->join, TRUE); + } else { + g_clear_object(&dialog->cancellable); + gtk_window_destroy(GTK_WINDOW(dialog)); + } + + g_clear_error(&error); +} + +static void +pidgin_channel_join_dialog_join_clicked(G_GNUC_UNUSED GtkButton *self, + gpointer data) +{ + PidginChannelJoinDialog *dialog = data; + PurpleAccount *account = NULL; + PurpleProtocol *protocol = NULL; + PurpleProtocolConversation *protocol_conversation = NULL; + + gtk_label_set_text(GTK_LABEL(dialog->error), ""); + + gtk_widget_set_sensitive(dialog->entries, FALSE); + gtk_widget_set_sensitive(dialog->join, FALSE); + + account = pidgin_account_row_get_account(PIDGIN_ACCOUNT_ROW(dialog->account)); + if(!PURPLE_IS_ACCOUNT(account)) { + return; + } + + protocol = purple_account_get_protocol(account); + if(!PURPLE_IS_PROTOCOL_CONVERSATION(protocol)) { + return; + } + + g_return_if_fail(!G_IS_CANCELLABLE(dialog->cancellable)); + + dialog->cancellable = g_cancellable_new(); + g_cancellable_connect(dialog->cancellable, + G_CALLBACK(pidgin_channel_join_dialog_cancelled_cb), + dialog, NULL); + + protocol_conversation = PURPLE_PROTOCOL_CONVERSATION(protocol); + + purple_protocol_conversation_join_channel_async(protocol_conversation, + account, + dialog->details, + dialog->cancellable, + pidgin_channel_join_dialog_join_cb, + dialog); +} + +/****************************************************************************** + * GObject Implementation + *****************************************************************************/ +static void +pidgin_channel_join_dialog_finalize(GObject *obj) { + PidginChannelJoinDialog *dialog = PIDGIN_CHANNEL_JOIN_DIALOG(obj); + + g_clear_object(&dialog->cancellable); + g_clear_object(&dialog->details); + + G_OBJECT_CLASS(pidgin_channel_join_dialog_parent_class)->finalize(obj); +} + +static void +pidgin_channel_join_dialog_init(PidginChannelJoinDialog *dialog) { + gtk_widget_init_template(GTK_WIDGET(dialog)); + + gtk_custom_filter_set_filter_func(dialog->filter, + pidgin_channel_join_dialog_filter_accounts, + NULL, NULL); + + dialog->details = purple_channel_join_details_new(FALSE, FALSE); + + /* Bind the visible properties. */ + g_object_bind_property(dialog->details, "nickname-supported", + dialog->nickname, "visible", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); + g_object_bind_property(dialog->details, "password-supported", + dialog->password, "visible", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); + + /* Bind the data properties. */ + g_object_bind_property_full(dialog->details, "name", + dialog->name, "text", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL, + pidgin_channel_join_dialog_details_to_row, + pidgin_channel_join_dialog_row_to_details, + NULL, NULL); + g_object_bind_property_full(dialog->details, "nickname", + dialog->nickname, "text", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL, + pidgin_channel_join_dialog_details_to_row, + pidgin_channel_join_dialog_row_to_details, + NULL, NULL); + g_object_bind_property_full(dialog->details, "password", + dialog->password, "text", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL, + pidgin_channel_join_dialog_details_to_row, + pidgin_channel_join_dialog_row_to_details, + NULL, NULL); + + /* Make sure we are synchronized with the account that is selected. */ + pidgin_channel_join_dialog_update(dialog); +} + +static void +pidgin_channel_join_dialog_class_init(PidginChannelJoinDialogClass *klass) { + GObjectClass *obj_class = G_OBJECT_CLASS(klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass); + + obj_class->finalize = pidgin_channel_join_dialog_finalize; + + gtk_widget_class_set_template_from_resource( + widget_class, + "/im/pidgin/Pidgin3/channeljoindialog.ui" + ); + + gtk_widget_class_bind_template_child(widget_class, PidginChannelJoinDialog, + account); + gtk_widget_class_bind_template_child(widget_class, PidginChannelJoinDialog, + filter); + gtk_widget_class_bind_template_child(widget_class, PidginChannelJoinDialog, + entries); + gtk_widget_class_bind_template_child(widget_class, PidginChannelJoinDialog, + name); + gtk_widget_class_bind_template_child(widget_class, PidginChannelJoinDialog, + nickname); + gtk_widget_class_bind_template_child(widget_class, PidginChannelJoinDialog, + password); + gtk_widget_class_bind_template_child(widget_class, PidginChannelJoinDialog, + error); + gtk_widget_class_bind_template_child(widget_class, PidginChannelJoinDialog, + join); + gtk_widget_class_bind_template_child(widget_class, PidginChannelJoinDialog, + cancel); + + gtk_widget_class_bind_template_callback(widget_class, + pidgin_channel_join_dialog_account_changed_cb); + gtk_widget_class_bind_template_callback(widget_class, + pidgin_channel_join_dialog_join_named_changed_cb); + gtk_widget_class_bind_template_callback(widget_class, + pidgin_channel_join_dialog_cancel_clicked); + gtk_widget_class_bind_template_callback(widget_class, + pidgin_channel_join_dialog_join_clicked); +} + +/****************************************************************************** + * Public API + *****************************************************************************/ +GtkWidget * +pidgin_channel_join_dialog_new(void) { + return g_object_new(PIDGIN_TYPE_CHANNEL_JOIN_DIALOG, NULL); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pidgin/pidginchanneljoindialog.h Fri Oct 20 02:13:51 2023 -0500 @@ -0,0 +1,59 @@ +/* + * Pidgin - Internet Messenger + * Copyright (C) Pidgin Developers <devel@pidgin.im> + * + * 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, see <https://www.gnu.org/licenses/>. + */ + +#if !defined(PIDGIN_GLOBAL_HEADER_INSIDE) && !defined(PIDGIN_COMPILATION) +# error "only <pidgin.h> may be included directly" +#endif + +#ifndef PIDGIN_CHANNEL_JOIN_DIALOG_H +#define PIDGIN_CHANNEL_JOIN_DIALOG_H + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +/** + * PidginChannelJoinDialog: + * + * A dialog for joining channels. + * + * Since: 3.0.0 + */ + +#define PIDGIN_TYPE_CHANNEL_JOIN_DIALOG (pidgin_channel_join_dialog_get_type()) +G_DECLARE_FINAL_TYPE(PidginChannelJoinDialog, pidgin_channel_join_dialog, PIDGIN, + CHANNEL_JOIN_DIALOG, GtkWindow) + +/** + * pidgin_channel_join_dialog_new: + * + * Creates a new channel join dialog. + * + * Returns: (transfer full): The widget. + * + * Since: 3.0.0 + */ +GtkWidget *pidgin_channel_join_dialog_new(void); + +G_END_DECLS + +#endif /* PIDGIN_CHANNEL_JOIN_DIALOG_H */
--- a/pidgin/resources/Dialogs/addchat.ui Fri Oct 20 02:10:54 2023 -0500 +++ b/pidgin/resources/Dialogs/addchat.ui Fri Oct 20 02:13:51 2023 -0500 @@ -27,7 +27,6 @@ <!-- interface-copyright Pidgin Developers <devel@pidgin.im> --> <template class="PidginAddChatDialog" parent="GtkDialog"> <property name="title" translatable="1">Add Chat</property> - <property name="resizable">0</property> <signal name="response" handler="pidgin_add_chat_dialog_response_cb" swapped="no"/> <child internal-child="content_area"> <object class="GtkBox">
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pidgin/resources/channeljoindialog.ui Fri Oct 20 02:13:51 2023 -0500 @@ -0,0 +1,111 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +Pidgin - Internet Messenger +Copyright (C) Pidgin Developers <devel@pidgin.im> + +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, see <https://www.gnu.org/licenses/>. +--> +<interface> + <requires lib="gtk" version="4.0"/> + <template class="PidginChannelJoinDialog" parent="GtkWindow"> + <property name="resizable">1</property> + <property name="title" translatable="1">Join Channel</property> + <child> + <object class="GtkBox"> + <property name="margin-bottom">24</property> + <property name="margin-end">24</property> + <property name="margin-start">24</property> + <property name="margin-top">24</property> + <property name="orientation">vertical</property> + <property name="spacing">24</property> + <child> + <object class="GtkLabel"> + <property name="css-classes">title-1</property> + <property name="label" translatable="1">Join a channel</property> + </object> + </child> + <child> + <object class="AdwPreferencesGroup" id="entries"> + <child> + <object class="PidginAccountRow" id="account"> + <property name="filter"> + <object class="GtkEveryFilter"> + <child> + <object class="GtkCustomFilter" id="filter"/> + </child> + <child> + <object class="PidginAccountFilterConnected"/> + </child> + </object> + </property> + <signal name="notify::account" handler="pidgin_channel_join_dialog_account_changed_cb"/> + </object> + </child> + <child> + <object class="AdwEntryRow" id="name"> + <property name="title" translatable="1">Name</property> + <signal name="changed" handler="pidgin_channel_join_dialog_join_named_changed_cb"/> + </object> + </child> + <child> + <object class="AdwEntryRow" id="nickname"> + <property name="title" translatable="1">Custom nickname (optional)</property> + </object> + </child> + <child> + <object class="AdwEntryRow" id="password"> + <property name="title" translatable="1">Password (optional)</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkLabel" id="error"> + <property name="wrap-mode">word-char</property> + <style> + <class name="error"/> + </style> + </object> + </child> + <child> + <object class="GtkBox"> + <property name="orientation">horizontal</property> + <property name="spacing">6</property> + <!-- To get the behavior of pack_end we need to set halign to end + and then add the children in the reverse order. --> + <property name="halign">end</property> + <child> + <object class="GtkButton" id="cancel"> + <property name="label" translatable="1">_Cancel</property> + <property name="use-underline">1</property> + <signal name="clicked" handler="pidgin_channel_join_dialog_cancel_clicked"/> + </object> + </child> + <child> + <object class="GtkButton" id="join"> + <property name="label" translatable="1">_Join</property> + <property name="sensitive">0</property> + <property name="use-underline">1</property> + <signal name="clicked" handler="pidgin_channel_join_dialog_join_clicked"/> + <style> + <class name="suggested-action"/> + </style> + </object> + </child> + </object> + </child> + </object> + </child> + </template> +</interface>
--- a/pidgin/resources/gtk/menus.ui Fri Oct 20 02:10:54 2023 -0500 +++ b/pidgin/resources/gtk/menus.ui Fri Oct 20 02:13:51 2023 -0500 @@ -34,6 +34,10 @@ <attribute name="accel"><Primary>M</attribute> </item> <item> + <attribute name="label" translatable="yes">Join Channel</attribute> + <attribute name="action">app.join-channel</attribute> + </item> + <item> <attribute name="label" translatable="yes">Join a _Chat...</attribute> <attribute name="action">app.join-chat</attribute> </item>
--- a/pidgin/resources/pidgin.gresource.xml Fri Oct 20 02:10:54 2023 -0500 +++ b/pidgin/resources/pidgin.gresource.xml Fri Oct 20 02:13:51 2023 -0500 @@ -46,6 +46,7 @@ <file compressed="true" preprocess="xml-stripblanks">Xfer/xfer.ui</file> <file compressed="true" preprocess="xml-stripblanks">gtk/menus.ui</file> <file compressed="true" preprocess="xml-stripblanks">account-row.ui</file> + <file compressed="true" preprocess="xml-stripblanks">channeljoindialog.ui</file> <file compressed="true" preprocess="xml-stripblanks">presenceicon.ui</file> <file compressed="true" preprocess="xml-stripblanks">statusprimitivechooser.ui</file> <file>icons/16x16/status/pidgin-user-available.png</file>