# HG changeset patch
# Node ID f0bacabb4519ba20c80ad06065d4c92cb72f5b0f
# Parent  0253971eeb18dd103477b65bdfe8cbe889b8c40b
diff --git a/pidgin/meson.build b/pidgin/meson.build
--- a/pidgin/meson.build
+++ b/pidgin/meson.build
@@ -39,6 +39,7 @@
 	'pidginpluginsmenu.c',
 	'pidginpresenceicon.c',
 	'pidginprotocolchooser.c',
+	'pidgintypinglabel.c',
 	'pidginui.c',
 	'prefs/pidginprefs.c',
 	'prefs/pidginawayprefs.c',
@@ -86,6 +87,7 @@
 	'pidginpluginsmenu.h',
 	'pidginpresenceicon.h',
 	'pidginprotocolchooser.h',
+	'pidgintypinglabel.h',
 	'pidginui.h',
 	'pidginversion.h',
 ]
diff --git a/pidgin/pidginconversation.h b/pidgin/pidginconversation.h
--- a/pidgin/pidginconversation.h
+++ b/pidgin/pidginconversation.h
@@ -33,6 +33,8 @@
 
 #include <gtk/gtk.h>
 
+#include <purple.h>
+
 #include "pidginversion.h"
 
 G_BEGIN_DECLS
diff --git a/pidgin/pidgintypinglabel.c b/pidgin/pidgintypinglabel.c
new file mode 100644
--- /dev/null
+++ b/pidgin/pidgintypinglabel.c
@@ -0,0 +1,290 @@
+/*
+ * 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-lib.h>
+
+#include "pidgintypinglabel.h"
+
+struct _PidginTypingLabel {
+	GtkBox parent;
+
+	PurpleConversation *conversation;
+
+	GtkLabel *label;
+};
+
+enum {
+	PROP_0,
+	PROP_CONVERSATION,
+	N_PROPERTIES,
+};
+static GParamSpec *properties[N_PROPERTIES] = {NULL, };
+
+static void
+pidgin_typing_label_items_changed_cb(GListModel *model, guint position,
+                                     guint removed, guint added,
+                                     gpointer data);
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static gboolean
+pidgin_typing_label_filter_active_typers(GObject *item, gpointer data) {
+	PurpleAccount *account = NULL;
+	PurpleContactInfo *account_info = NULL;
+	PurpleContactInfo *member_info = NULL;
+	PurpleConversation *conversation = data;
+	PurpleConversationMember *member = PURPLE_CONVERSATION_MEMBER(item);
+
+	account = purple_conversation_get_account(conversation);
+	account_info = purple_account_get_contact_info(account);
+
+	member_info = purple_conversation_member_get_contact_info(member);
+
+	/* We don't want to show our own account, so if member_info and
+	 * account_info match, we return FALSE.
+	 */
+	if(member_info == account_info) {
+		return FALSE;
+	}
+
+	return TRUE;
+}
+
+static void
+pidgin_typing_label_set_conversation(PidginTypingLabel *label,
+                                     PurpleConversation *conversation)
+{
+	g_return_if_fail(PIDGIN_IS_TYPING_LABEL(label));
+
+	if(g_set_object(&label->conversation, conversation)) {
+		PurpleConversationMembers *members = NULL;
+
+		if(PURPLE_IS_CONVERSATION(conversation)) {
+			GtkCustomFilter *filter = NULL;
+			GtkFilterListModel *filtered = NULL;
+			GListModel *active_typers = NULL;
+
+			members = purple_conversation_get_members(conversation);
+
+			active_typers = purple_conversation_members_get_active_typers(members);
+
+			filter = gtk_custom_filter_new((GtkCustomFilterFunc)pidgin_typing_label_filter_active_typers,
+			                               conversation, NULL);
+			filtered = gtk_filter_list_model_new(g_object_ref(active_typers),
+			                                     GTK_FILTER(filter));
+
+			g_signal_connect_object(filtered, "items-changed",
+			                        G_CALLBACK(pidgin_typing_label_items_changed_cb),
+			                        label, G_CONNECT_DEFAULT);
+		}
+
+		g_object_notify_by_pspec(G_OBJECT(label),
+		                         properties[PROP_CONVERSATION]);
+	}
+}
+
+/******************************************************************************
+ * Callbacks
+ *****************************************************************************/
+static void
+pidgin_typing_label_items_changed_cb(GListModel *model,
+                                     G_GNUC_UNUSED guint position,
+                                     G_GNUC_UNUSED guint removed,
+                                     G_GNUC_UNUSED guint added,
+                                     gpointer data)
+{
+	PidginTypingLabel *label = data;
+	guint n_items = 0;
+
+	n_items = g_list_model_get_n_items(model);
+	if(n_items == 0) {
+		gtk_label_set_label(label->label, "");
+	} else if(n_items == 1) {
+		PurpleConversationMember *member = NULL;
+		char *text = NULL;
+		const char *name = NULL;
+
+		member = g_list_model_get_item(model, 0);
+		name = purple_conversation_member_get_name_for_display(member);
+		text = g_strdup_printf(_("<b>%s</b> is typing..."), name);
+
+		gtk_label_set_label(label->label, text);
+
+		g_clear_pointer(&text, g_free);
+		g_clear_object(&member);
+	} else if(n_items == 2) {
+		PurpleConversationMember *member1 = NULL;
+		PurpleConversationMember *member2 = NULL;
+		char *text = NULL;
+		const char *name1 = NULL;
+		const char *name2 = NULL;
+
+		member1 = g_list_model_get_item(model, 0);
+		name1 = purple_conversation_member_get_name_for_display(member1);
+
+		member2 = g_list_model_get_item(model, 1);
+		name2 = purple_conversation_member_get_name_for_display(member2);
+
+		text = g_strdup_printf(_("<b>%s</b> and <b>%s</b> are typing..."),
+		                       name1, name2);
+
+		gtk_label_set_label(label->label, text);
+
+		g_clear_pointer(&text, g_free);
+
+		g_clear_object(&member1);
+		g_clear_object(&member2);
+	} else if(n_items == 3) {
+		PurpleConversationMember *member1 = NULL;
+		PurpleConversationMember *member2 = NULL;
+		PurpleConversationMember *member3 = NULL;
+		char *text = NULL;
+		const char *name1 = NULL;
+		const char *name2 = NULL;
+		const char *name3 = NULL;
+
+		member1 = g_list_model_get_item(model, 0);
+		name1 = purple_conversation_member_get_name_for_display(member1);
+
+		member2 = g_list_model_get_item(model, 1);
+		name2 = purple_conversation_member_get_name_for_display(member2);
+
+		member3 = g_list_model_get_item(model, 2);
+		name3 = purple_conversation_member_get_name_for_display(member3);
+
+		text = g_strdup_printf(_("<b>%s</b>, <b>%s</b>, and <b>%s</b> are typing..."),
+		                       name1, name2, name3);
+
+		gtk_label_set_label(label->label, text);
+
+		g_clear_pointer(&text, g_free);
+
+		g_clear_object(&member1);
+		g_clear_object(&member2);
+		g_clear_object(&member3);
+	} else if(n_items > 3) {
+		gtk_label_set_label(label->label, _("Several people are typing..."));
+	}
+}
+
+/******************************************************************************
+ * GObject Implementation
+ *****************************************************************************/
+G_DEFINE_FINAL_TYPE(PidginTypingLabel, pidgin_typing_label, GTK_TYPE_BOX)
+
+static void
+pidgin_typing_label_finalize(GObject *obj) {
+	PidginTypingLabel *label = PIDGIN_TYPING_LABEL(obj);
+
+	g_clear_object(&label->conversation);
+
+	G_OBJECT_CLASS(pidgin_typing_label_parent_class)->finalize(obj);
+}
+
+static void
+pidgin_typing_label_get_property(GObject *obj, guint param_id, GValue *value,
+                                 GParamSpec *pspec)
+{
+	PidginTypingLabel *label = PIDGIN_TYPING_LABEL(obj);
+
+	switch(param_id) {
+	case PROP_CONVERSATION:
+		g_value_set_object(value, pidgin_typing_label_get_conversation(label));
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+		break;
+	}
+}
+
+static void
+pidgin_typing_label_set_property(GObject *obj, guint param_id,
+                                 const GValue *value, GParamSpec *pspec)
+{
+	PidginTypingLabel *label = PIDGIN_TYPING_LABEL(obj);
+
+	switch(param_id) {
+	case PROP_CONVERSATION:
+		pidgin_typing_label_set_conversation(label, g_value_get_object(value));
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+		break;
+	}
+}
+
+static void
+pidgin_typing_label_init(PidginTypingLabel *label) {
+	gtk_widget_init_template(GTK_WIDGET(label));
+}
+
+static void
+pidgin_typing_label_class_init(PidginTypingLabelClass *klass) {
+	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+	GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
+
+	obj_class->finalize = pidgin_typing_label_finalize;
+	obj_class->get_property = pidgin_typing_label_get_property;
+	obj_class->set_property = pidgin_typing_label_set_property;
+
+	/**
+	 * PidginTypingLabel:conversation:
+	 *
+	 * The [class@Purple.Conversation] that this label is for.
+	 *
+	 * Since: 3.0
+	 */
+	properties[PROP_CONVERSATION] = g_param_spec_object(
+		"conversation", NULL, NULL,
+		PURPLE_TYPE_CONVERSATION,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
+
+	/* Template stuff. */
+	gtk_widget_class_set_template_from_resource(widget_class,
+	                                            "/im/pidgin/Pidgin3/typinglabel.ui");
+
+	gtk_widget_class_bind_template_child(widget_class, PidginTypingLabel,
+	                                     label);
+}
+
+/******************************************************************************
+ * Public API
+ *****************************************************************************/
+GtkWidget *
+pidgin_typing_label_new(PurpleConversation *conversation) {
+	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), NULL);
+
+	return g_object_new(
+		PIDGIN_TYPE_TYPING_LABEL,
+		"conversation", conversation,
+		NULL);
+}
+
+PurpleConversation *
+pidgin_typing_label_get_conversation(PidginTypingLabel *label) {
+	g_return_val_if_fail(PIDGIN_IS_TYPING_LABEL(label), NULL);
+
+	return label->conversation;
+}
diff --git a/pidgin/pidgintypinglabel.h b/pidgin/pidgintypinglabel.h
new file mode 100644
--- /dev/null
+++ b/pidgin/pidgintypinglabel.h
@@ -0,0 +1,80 @@
+/*
+ * 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_TYPING_LABEL_H
+#define PIDGIN_TYPING_LABEL_H
+
+#include <glib.h>
+
+#include <gtk/gtk.h>
+
+#include "pidginconversation.h"
+#include "pidginversion.h"
+
+G_BEGIN_DECLS
+
+/**
+ * PidginTypingLabel:
+ *
+ * A [class@Gtk.Widget] for displaying who is typing in a
+ * [class@Purple.Conversation].
+ *
+ * Since: 3.0
+ */
+
+#define PIDGIN_TYPE_TYPING_LABEL (pidgin_typing_label_get_type())
+
+PIDGIN_AVAILABLE_IN_3_0
+G_DECLARE_FINAL_TYPE(PidginTypingLabel, pidgin_typing_label, PIDGIN,
+                     TYPING_LABEL, GtkBox)
+
+/**
+ * pidgin_typing_label_new:
+ * @conversation: A [class@Purple.Conversation] instance.
+ *
+ * Creates the new typing label for @conversation.
+ *
+ * Returns: (transfer full): The new widget.
+ *
+ * Since: 3.0
+ */
+PIDGIN_AVAILABLE_IN_3_0
+GtkWidget *pidgin_typing_label_new(PurpleConversation *conversation);
+
+/**
+ * pidgin_typing_label_get_conversation:
+ * @label: The instance.
+ *
+ * Gets the [class@Purple.Conversation] that @label was created for.
+ *
+ * Since: 3.0
+ */
+PIDGIN_AVAILABLE_IN_3_0
+PurpleConversation *pidgin_typing_label_get_conversation(PidginTypingLabel *label);
+
+G_END_DECLS
+
+#endif /* PIDGIN_TYPING_LABEL_H */
diff --git a/pidgin/resources/Conversations/conversation.ui b/pidgin/resources/Conversations/conversation.ui
--- a/pidgin/resources/Conversations/conversation.ui
+++ b/pidgin/resources/Conversations/conversation.ui
@@ -145,7 +145,11 @@
       </object>
     </child>
     <child>
-      <object class="GtkSeparator"/>
+      <object class="PidginTypingLabel">
+        <binding name="conversation">
+          <lookup name="conversation">PidginConversation</lookup>
+        </binding>
+      </object>
     </child>
     <child>
       <object class="GtkScrolledWindow">
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
@@ -40,6 +40,7 @@
     <file compressed="true" preprocess="xml-stripblanks">infopane.ui</file>
     <file compressed="true" preprocess="xml-stripblanks">notificationlist.ui</file>
     <file compressed="true" preprocess="xml-stripblanks">presenceicon.ui</file>
+    <file compressed="true" preprocess="xml-stripblanks">typinglabel.ui</file>
     <file compressed="true">style.css</file>
     <file>icons/16x16/status/pidgin-user-available.png</file>
     <file>icons/16x16/status/pidgin-user-away.png</file>
diff --git a/pidgin/resources/typinglabel.ui b/pidgin/resources/typinglabel.ui
new file mode 100644
--- /dev/null
+++ b/pidgin/resources/typinglabel.ui
@@ -0,0 +1,40 @@
+<?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 library; if not, see <https://www.gnu.org/licenses/>.
+-->
+<interface>
+  <requires lib="gtk" version="4.0"/>
+  <!-- interface-license-type gplv2 -->
+  <!-- interface-name Pidgin -->
+  <!-- interface-description Internet Messenger -->
+  <!-- interface-copyright Pidgin Developers <devel@pidgin.im> -->
+  <template class="PidginTypingLabel" parent="GtkBox">
+    <property name="orientation">horizontal</property>
+    <property name="halign">fill</property>
+    <property name="hexpand">true</property>
+    <child>
+      <object class="GtkImage" id="icon"/>
+    </child>
+    <child>
+      <object class="GtkLabel" id="label">
+        <property name="use-markup">true</property>
+        <property name="justify">left</property>
+        <property name="single-line-mode">true</property>
+      </object>
+    </child>
+  </template>
+</interface>
