# HG changeset patch
# Node ID d22c9693c0de
# Parent  a0ca3c963371
diff --git a/pidgin/meson.build b/pidgin/meson.build
--- a/pidgin/meson.build
+++ b/pidgin/meson.build
@@ -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',
diff --git a/pidgin/pidginapplication.c b/pidgin/pidginapplication.c
--- a/pidgin/pidginapplication.c
+++ b/pidgin/pidginapplication.c
@@ -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,17 @@
 		                                       TRUE);
 	}
 
+	should_enable_channel = PURPLE_PROTOCOL_IMPLEMENTS(protocol, CONVERSATION,
+	                                                   get_channel_join_details);
+	if(should_enable_channel) {
+		g_warning("should enable channel: %d", 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 +1045,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,
diff --git a/pidgin/pidginchanneljoindialog.h b/pidgin/pidginchanneljoindialog.h
new file mode 100644
--- /dev/null
+++ b/pidgin/pidginchanneljoindialog.h
@@ -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 */
diff --git a/pidgin/pidginchanneljoindialog.c b/pidgin/pidginchanneljoindialog.c
new file mode 100644
--- /dev/null
+++ b/pidgin/pidginchanneljoindialog.c
@@ -0,0 +1,372 @@
+/*
+ * 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 *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)
+
+static void
+pidgin_channel_join_dialog_cancelled_cb(GCancellable *cancellable,
+                                        gpointer data);
+
+/******************************************************************************
+ * 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;
+}
+
+static void
+pidgin_channel_join_dialog_reset_cancellable(PidginChannelJoinDialog *dialog) {
+	g_clear_object(&dialog->cancellable);
+
+	dialog->cancellable = g_cancellable_new();
+	g_cancellable_connect(dialog->cancellable,
+	                      G_CALLBACK(pidgin_channel_join_dialog_cancelled_cb),
+	                      g_object_ref(dialog),
+	                      (GDestroyNotify)g_object_unref);
+}
+
+/******************************************************************************
+ * 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->join, TRUE);
+	gtk_widget_set_sensitive(dialog->cancel, TRUE);
+}
+
+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);
+			return;
+		}
+	}
+
+	/* If no join is in progress, we just need to close the window. The
+	 * cancellable has a ref on the dialog, so we remove our ref on the
+	 * cancellable which will break the reference loop.
+	 */
+	g_cancellable_cancel(dialog->cancellable);
+	g_clear_object(&dialog->cancellable);
+	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);
+
+	gtk_widget_set_sensitive(dialog->join, TRUE);
+
+	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);
+	} else {
+		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->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;
+	}
+
+	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->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);
+
+	pidgin_channel_join_dialog_reset_cancellable(dialog);
+
+	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,
+	                                     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);
+}
diff --git a/pidgin/resources/Dialogs/addchat.ui b/pidgin/resources/Dialogs/addchat.ui
--- a/pidgin/resources/Dialogs/addchat.ui
+++ b/pidgin/resources/Dialogs/addchat.ui
@@ -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">
diff --git a/pidgin/resources/channeljoindialog.ui b/pidgin/resources/channeljoindialog.ui
new file mode 100644
--- /dev/null
+++ b/pidgin/resources/channeljoindialog.ui
@@ -0,0 +1,112 @@
+<?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"/>
+  <requires lib="pidgin" version="3.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">
+            <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>
diff --git a/pidgin/resources/gtk/menus.ui b/pidgin/resources/gtk/menus.ui
--- a/pidgin/resources/gtk/menus.ui
+++ b/pidgin/resources/gtk/menus.ui
@@ -34,6 +34,10 @@
           <attribute name="accel">&lt;Primary&gt;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>
diff --git a/pidgin/resources/pidgin.gresource.xml b/pidgin/resources/pidgin.gresource.xml
--- a/pidgin/resources/pidgin.gresource.xml
+++ b/pidgin/resources/pidgin.gresource.xml
@@ -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>
