# HG changeset patch
# Node ID 9019918fad24c009334b78b41e9e8d22d00fd259
# Parent  36a64c2e14ce181e57889ffd3c199c6c012ad95c
diff --git a/libpurple/purpleconversationmembers.c b/libpurple/purpleconversationmembers.c
--- a/libpurple/purpleconversationmembers.c
+++ b/libpurple/purpleconversationmembers.c
@@ -26,12 +26,15 @@
 	GObject parent;
 
 	GPtrArray *members;
+
+	GListStore *active_typers;
 };
 
 enum {
 	PROP_0,
 	PROP_ITEM_TYPE,
 	PROP_N_ITEMS,
+	PROP_ACTIVE_TYPERS,
 	N_PROPERTIES,
 };
 static GParamSpec *properties[N_PROPERTIES] = {NULL, };
@@ -82,6 +85,29 @@
 	}
 }
 
+static void
+purple_conversation_members_typing_changed_cb(GObject *self,
+                                              G_GNUC_UNUSED GParamSpec *pspec,
+                                              gpointer data)
+{
+	PurpleConversationMember *member = PURPLE_CONVERSATION_MEMBER(self);
+	PurpleConversationMembers *members = data;
+	PurpleTypingState state = PURPLE_TYPING_STATE_NONE;
+	gboolean found = FALSE;
+	guint position = 0;
+
+	state = purple_conversation_member_get_typing_state(member);
+
+	found = g_list_store_find(members->active_typers, member, &position);
+	if(found && state != PURPLE_TYPING_STATE_TYPING) {
+		g_list_store_remove(members->active_typers, position);
+	}
+
+	if(!found && state == PURPLE_TYPING_STATE_TYPING) {
+		g_list_store_append(members->active_typers, member);
+	}
+}
+
 /******************************************************************************
  * GListModel Implementation
  *****************************************************************************/
@@ -132,6 +158,7 @@
 	PurpleConversationMembers *members = PURPLE_CONVERSATION_MEMBERS(obj);
 
 	g_clear_pointer(&members->members, g_ptr_array_unref);
+	g_clear_object(&members->active_typers);
 
 	G_OBJECT_CLASS(purple_conversation_members_parent_class)->finalize(obj);
 }
@@ -140,6 +167,8 @@
 purple_conversation_members_get_property(GObject *obj, guint param_id,
                                          GValue *value, GParamSpec *pspec)
 {
+	PurpleConversationMembers *members = PURPLE_CONVERSATION_MEMBERS(obj);
+
 	switch(param_id) {
 	case PROP_ITEM_TYPE:
 		g_value_set_gtype(value,
@@ -148,6 +177,10 @@
 	case PROP_N_ITEMS:
 		g_value_set_uint(value, g_list_model_get_n_items(G_LIST_MODEL(obj)));
 		break;
+	case PROP_ACTIVE_TYPERS:
+		g_value_set_object(value,
+		                   purple_conversation_members_get_active_typers(members));
+		break;
 	default:
 		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
 		break;
@@ -158,6 +191,8 @@
 purple_conversation_members_init(PurpleConversationMembers *members) {
 	/* We allocate 2 as dms have at least 2 members. */
 	members->members = g_ptr_array_new_full(2, g_object_unref);
+
+	members->active_typers = g_list_store_new(PURPLE_TYPE_CONVERSATION_MEMBER);
 }
 
 static void
@@ -191,6 +226,18 @@
 		0, G_MAXUINT, 0,
 		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
 
+	/**
+	 * PurpleConversationMembers:active-typers:
+	 *
+	 * The list of [class@ConversationMember]s that are actively typing.
+	 *
+	 * Since: 3.0
+	 */
+	properties[PROP_ACTIVE_TYPERS] = g_param_spec_object(
+		"active-typers", NULL, NULL,
+		G_TYPE_LIST_MODEL,
+		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
 	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
 
 	/**
@@ -271,7 +318,7 @@
 	                        G_CALLBACK(purple_conversation_members_member_changed_cb),
 	                        members, G_CONNECT_DEFAULT);
 	g_signal_connect_object(member, "notify::typing-state",
-	                        G_CALLBACK(purple_conversation_members_member_changed_cb),
+	                        G_CALLBACK(purple_conversation_members_typing_changed_cb),
 	                        members, G_CONNECT_DEFAULT);
 
 	g_signal_emit(members, signals[SIG_MEMBER_ADDED], 0, member, announce,
@@ -358,3 +405,11 @@
 
 	return TRUE;
 }
+
+GListModel *
+purple_conversation_members_get_active_typers(PurpleConversationMembers *members)
+{
+	g_return_val_if_fail(PURPLE_IS_CONVERSATION_MEMBERS(members), NULL);
+
+	return G_LIST_MODEL(members->active_typers);
+}
diff --git a/libpurple/purpleconversationmembers.h b/libpurple/purpleconversationmembers.h
--- a/libpurple/purpleconversationmembers.h
+++ b/libpurple/purpleconversationmembers.h
@@ -153,6 +153,20 @@
 PURPLE_AVAILABLE_IN_3_0
 gboolean purple_conversation_members_remove_member(PurpleConversationMembers *members, PurpleContactInfo *info, gboolean announce, const char *message);
 
+/**
+ * purple_conversation_members_get_active_typers:
+ * @members: The instances.
+ *
+ * Gets the list of [class@ConversationMember]s that are actively typing.
+ *
+ * Returns: (transfer none): The list of active of members that are actively
+ *          typing.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+GListModel *purple_conversation_members_get_active_typers(PurpleConversationMembers *members);
+
 G_END_DECLS
 
 #endif /* PURPLE_CONVERSATION_MEMBERS_H */
diff --git a/libpurple/tests/test_conversation_members.c b/libpurple/tests/test_conversation_members.c
--- a/libpurple/tests/test_conversation_members.c
+++ b/libpurple/tests/test_conversation_members.c
@@ -169,7 +169,8 @@
 	 * make changes to it, to verify that the items-changed signal gets called.
 	 */
 	info = purple_contact_info_new("second");
-	member = purple_conversation_members_add_member(members, info, FALSE, NULL);
+	member = purple_conversation_members_add_member(members, info, FALSE,
+	                                                NULL);
 	g_assert_true(PURPLE_IS_CONVERSATION_MEMBER(member));
 
 	/* Connect the items-changed signal. We do this after everything is added
@@ -194,11 +195,70 @@
 	purple_conversation_member_set_nickname(member, "nickname");
 	g_assert_cmpuint(counter, ==, 1);
 
-	/* Change the typing-state on the member. */
+	g_assert_finalize_object(members);
+	g_assert_finalize_object(info);
+}
+
+/******************************************************************************
+ * Active Typers Tests
+ *****************************************************************************/
+static void
+test_purple_conversation_members_active_typers_changed_cb(G_GNUC_UNUSED GListModel *model,
+                                                          G_GNUC_UNUSED guint position,
+                                                          G_GNUC_UNUSED guint removed,
+                                                          G_GNUC_UNUSED guint added,
+                                                          gpointer data)
+{
+	guint *counter = data;
+
+	*counter = *counter + 1;
+}
+
+static void
+test_purple_conversation_members_active_typers(void) {
+	PurpleContactInfo *info = NULL;
+	PurpleConversationMember *member = NULL;
+	PurpleConversationMembers *members = NULL;
+	GListModel *active_typers = NULL;
+	guint counter = 0;
+
+	members = purple_conversation_members_new();
+	active_typers = purple_conversation_members_get_active_typers(members);
+	g_signal_connect(active_typers, "items-changed",
+	                 G_CALLBACK(test_purple_conversation_members_active_typers_changed_cb),
+	                 &counter);
+
+	info = purple_contact_info_new(NULL);
+	member = purple_conversation_members_add_member(members, info, FALSE,
+	                                                NULL);
+
+	g_assert_cmpuint(counter, ==, 0);
+
+	purple_conversation_member_set_typing_state(member,
+	                                            PURPLE_TYPING_STATE_TYPING, 0);
+	g_assert_cmpuint(counter, ==, 1);
+	g_assert_cmpuint(g_list_model_get_n_items(active_typers), ==, 1);
+
+	/* If the a user gets a typing refresh nothing should change. */
 	counter = 0;
 	purple_conversation_member_set_typing_state(member,
-	                                            PURPLE_TYPING_STATE_TYPING, 6);
+	                                            PURPLE_TYPING_STATE_TYPING, 0);
+	g_assert_cmpuint(counter, ==, 0);
+	g_assert_cmpuint(g_list_model_get_n_items(active_typers), ==, 1);
+
+	/* Set the user's typing state to none. */
+	counter = 0;
+	purple_conversation_member_set_typing_state(member,
+	                                            PURPLE_TYPING_STATE_NONE, 0);
 	g_assert_cmpuint(counter, ==, 1);
+	g_assert_cmpuint(g_list_model_get_n_items(active_typers), ==, 0);
+
+	/* Set the user's state to none again and verify no changes. */
+	counter = 0;
+	purple_conversation_member_set_typing_state(member,
+	                                            PURPLE_TYPING_STATE_NONE, 0);
+	g_assert_cmpuint(counter, ==, 0);
+	g_assert_cmpuint(g_list_model_get_n_items(active_typers), ==, 0);
 
 	g_assert_finalize_object(members);
 	g_assert_finalize_object(info);
@@ -216,6 +276,8 @@
 	                test_purple_conversation_members_add_remove);
 	g_test_add_func("/conversation-members/items-changed",
 	                test_purple_conversation_members_item_changed);
+	g_test_add_func("/conversation-members/active-typers",
+	                test_purple_conversation_members_active_typers);
 
 	return g_test_run();
 }
