# HG changeset patch
# Node ID 62a2e3616404
# Parent  bccfce865431
diff --git a/libpurple/meson.build b/libpurple/meson.build
--- a/libpurple/meson.build
+++ b/libpurple/meson.build
@@ -194,6 +194,7 @@
 	'purplesqlite3.h',
 	'purplesqlitehistoryadapter.h',
 	'purpletags.h',
+	'purpletyping.h',
 	'purpleui.h',
 	'purplewhiteboard.h',
 	'purplewhiteboardmanager.h',
@@ -271,6 +272,7 @@
 	'purplepresence.h',
 	'purpleprotocol.h',
 	'purpleproxyinfo.h',
+	'purpletyping.h',
 	'roomlist.h',
 	'status.h',
 	'xfer.h',
diff --git a/libpurple/purpleconversationmember.h b/libpurple/purpleconversationmember.h
--- a/libpurple/purpleconversationmember.h
+++ b/libpurple/purpleconversationmember.h
@@ -28,6 +28,7 @@
 
 #include <libpurple/purplecontactinfo.h>
 #include <libpurple/purpletags.h>
+#include <libpurple/purpletyping.h>
 
 G_BEGIN_DECLS
 
@@ -89,6 +90,33 @@
  */
 PurpleTags *purple_conversation_member_get_tags(PurpleConversationMember *conversation_member);
 
+/**
+ * purple_conversation_member_get_typing_state:
+ * @member: The instance.
+ *
+ * Gets the current [enum@Purple.TypingState] for @conversation_member.
+ *
+ * Returns: The current typing state for @conversation_member.
+ *
+ * Since: 3.0.0
+ */
+PurpleTypingState purple_conversation_member_get_typing_state(PurpleConversationMember *member);
+
+/**
+ * purple_conversation_member_set_typing_state:
+ * @member: The instance.
+ * @state: The new typing state.
+ * @seconds: The number of seconds before resetting the state.
+ *
+ * Sets the typing state of @conversation_member to @state.
+ *
+ * If @seconds is greater than %0, a timeout will be added for @seconds to
+ * reset the state to none.
+ *
+ * Since: 3.0.0
+ */
+void purple_conversation_member_set_typing_state(PurpleConversationMember *member, PurpleTypingState state, guint seconds);
+
 G_END_DECLS
 
 #endif /* PURPLE_CONVERSATION_MEMBER_H */
diff --git a/libpurple/purpleconversationmember.c b/libpurple/purpleconversationmember.c
--- a/libpurple/purpleconversationmember.c
+++ b/libpurple/purpleconversationmember.c
@@ -18,17 +18,23 @@
 
 #include "purpleconversationmember.h"
 
+#include "purpleenums.h"
+
 struct _PurpleConversationMember {
 	GObject parent;
 
 	PurpleContactInfo *contact_info;
 	PurpleTags *tags;
+
+	guint typing_timeout;
+	PurpleTypingState typing_state;
 };
 
 enum {
 	PROP_0,
 	PROP_CONTACT_INFO,
 	PROP_TAGS,
+	PROP_TYPING_STATE,
 	N_PROPERTIES
 };
 static GParamSpec *properties[N_PROPERTIES] = {NULL, };
@@ -53,6 +59,20 @@
 }
 
 /******************************************************************************
+ * Callbacks
+ *****************************************************************************/
+static gboolean
+purple_conversation_member_reset_typing_state(gpointer data) {
+	PurpleConversationMember *member = data;
+
+	purple_conversation_member_set_typing_state(member,
+	                                            PURPLE_TYPING_STATE_NONE,
+	                                            0);
+
+	return G_SOURCE_REMOVE;
+}
+
+/******************************************************************************
  * GObject Implementation
  *****************************************************************************/
 static void
@@ -70,6 +90,10 @@
 			g_value_set_object(value,
 			                   purple_conversation_member_get_tags(member));
 			break;
+		case PROP_TYPING_STATE:
+			g_value_set_enum(value,
+			                 purple_conversation_member_get_typing_state(member));
+			break;
 		default:
 			G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
 			break;
@@ -87,6 +111,11 @@
 			purple_conversation_member_set_contact_info(member,
 			                                            g_value_get_object(value));
 			break;
+		case PROP_TYPING_STATE:
+			purple_conversation_member_set_typing_state(member,
+			                                            g_value_get_enum(value),
+			                                            0);
+			break;
 		default:
 			G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
 			break;
@@ -99,6 +128,11 @@
 
 	g_clear_object(&member->contact_info);
 
+	if(member->typing_timeout != 0) {
+		g_source_remove(member->typing_timeout);
+		member->typing_timeout = 0;
+	}
+
 	G_OBJECT_CLASS(purple_conversation_member_parent_class)->dispose(obj);
 }
 
@@ -151,6 +185,20 @@
 		PURPLE_TYPE_TAGS,
 		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
 
+	/**
+	 * PurpleConversationMember:typing-state:
+	 *
+	 * The [enum@Purple.TypingState] for this member.
+	 *
+	 * Since: 3.0.0
+	 */
+	properties[PROP_TYPING_STATE] = g_param_spec_enum(
+		"typing-state", "typing-state",
+		"The typing state for this member",
+		PURPLE_TYPE_TYPING_STATE,
+		PURPLE_TYPING_STATE_NONE,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
 	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
 }
 
@@ -180,3 +228,45 @@
 
 	return member->tags;
 }
+
+PurpleTypingState
+purple_conversation_member_get_typing_state(PurpleConversationMember *member)
+{
+	g_return_val_if_fail(PURPLE_IS_CONVERSATION_MEMBER(member),
+	                     PURPLE_TYPING_STATE_NONE);
+
+	return member->typing_state;
+}
+
+void
+purple_conversation_member_set_typing_state(PurpleConversationMember *member,
+                                            PurpleTypingState state,
+                                            guint seconds)
+{
+	g_return_if_fail(PURPLE_IS_CONVERSATION_MEMBER(member));
+
+	/* Remove an existing timeout if necessary. */
+	if(member->typing_timeout != 0) {
+		g_source_remove(member->typing_timeout);
+		member->typing_timeout = 0;
+	}
+
+	/* If the state has changed, notify. */
+	if(state != member->typing_state) {
+		member->typing_state = state;
+
+		g_object_notify_by_pspec(G_OBJECT(member),
+		                         properties[PROP_TYPING_STATE]);
+	}
+
+	/* If we got a timeout, add it. */
+	if(seconds > 0) {
+		guint source = 0;
+
+		source = g_timeout_add_seconds(seconds,
+		                               purple_conversation_member_reset_typing_state,
+		                               member);
+
+		member->typing_timeout = source;
+	}
+}
diff --git a/libpurple/purpletyping.h b/libpurple/purpletyping.h
new file mode 100644
--- /dev/null
+++ b/libpurple/purpletyping.h
@@ -0,0 +1,44 @@
+/*
+ * Purple - Internet Messaging Library
+ * 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/>.
+ */
+
+#if !defined(PURPLE_GLOBAL_HEADER_INSIDE) && !defined(PURPLE_COMPILATION)
+# error "only <pidgin.h> may be included directly"
+#endif
+
+#ifndef PURPLE_TYPING_H
+#define PURPLE_TYPING_H
+
+/**
+ * PurpleTypingState:
+ * @PURPLE_TYPING_STATE_NONE: The user is not currently typing and has nothing
+ *                            in their buffer.
+ * @PURPLE_TYPING_STATE_TYPING: The user is currently typing.
+ * @PURPLE_TYPING_STATE_PAUSED: The user has typed some text, but stopped
+ *                              without deleting it.
+ *
+ * Defines the state of a user composing a message.
+ *
+ * Since: 3.0.0
+ */
+typedef enum {
+    PURPLE_TYPING_STATE_NONE,
+    PURPLE_TYPING_STATE_TYPING,
+    PURPLE_TYPING_STATE_PAUSED,
+} PurpleTypingState;
+
+#endif /* PURPLE_TYPING_H */
diff --git a/libpurple/tests/test_conversation_member.c b/libpurple/tests/test_conversation_member.c
--- a/libpurple/tests/test_conversation_member.c
+++ b/libpurple/tests/test_conversation_member.c
@@ -44,6 +44,7 @@
 	PurpleContactInfo *info1 = NULL;
 	PurpleConversationMember *member = NULL;
 	PurpleTags *tags = NULL;
+	PurpleTypingState typing_state = PURPLE_TYPING_STATE_NONE;
 
 	info = purple_contact_info_new("abc123");
 
@@ -54,17 +55,20 @@
 	member = g_object_new(
 		PURPLE_TYPE_CONVERSATION_MEMBER,
 		"contact-info", info,
+		"typing-state", PURPLE_TYPING_STATE_TYPING,
 		NULL);
 
 	/* Now use g_object_get to read all of the properties. */
 	g_object_get(member,
 		"contact-info", &info1,
 		"tags", &tags,
+		"typing-state", &typing_state,
 		NULL);
 
 	/* Compare all the things. */
 	g_assert_true(info1 == info);
 	g_assert_true(PURPLE_IS_TAGS(tags));
+	g_assert_cmpint(typing_state, ==, PURPLE_TYPING_STATE_TYPING);
 
 	/* Free/unref all the things. */
 	g_clear_object(&info1);
@@ -75,6 +79,79 @@
 }
 
 /******************************************************************************
+ * Typing State Timeout
+ *****************************************************************************/
+static void
+test_purple_conversation_manager_timeout_notify(G_GNUC_UNUSED GObject *obj,
+                                                G_GNUC_UNUSED GParamSpec *pspec,
+                                                gpointer data)
+{
+	GMainLoop *loop = data;
+	static guint count = 0;
+
+	/* Increment count each time we're called. We're expecting to be called
+	 * twice, so after that quit the main loop.
+	 */
+	count++;
+	if(count >= 2) {
+		g_main_loop_quit(loop);
+	}
+}
+
+static gboolean
+test_purple_conversation_manager_timeout_fail_safe(gpointer data) {
+	GMainLoop *loop = data;
+
+	g_warning("fail safe triggered");
+
+	g_main_loop_quit(loop);
+
+	return G_SOURCE_REMOVE;
+}
+
+static void
+test_purple_conversation_member_typing_state_timeout(void) {
+	PurpleContactInfo *info = NULL;
+	PurpleConversationMember *member = NULL;
+	PurpleTypingState typing_state = PURPLE_TYPING_STATE_TYPING;
+	GMainLoop *loop = NULL;
+
+	/* Create the main loop as we'll need it to let the timeout fire. */
+	loop = g_main_loop_new(NULL, FALSE);
+
+	/* Create the member and add a notify callback on the typing-state property
+	 * so we can check it and exit the main loop.
+	 */
+	info = purple_contact_info_new(NULL);
+	member = purple_conversation_member_new(info);
+	g_signal_connect(member, "notify::typing-state",
+	                 G_CALLBACK(test_purple_conversation_manager_timeout_notify),
+	                 loop);
+
+	/* Set the state to typing with a timeout of 1 second. */
+	purple_conversation_member_set_typing_state(member,
+	                                            PURPLE_TYPING_STATE_TYPING, 1);
+
+	/* Add a fail safe timeout at 2 seconds to make sure the test won't get
+	 * stuck waiting forever.
+	 */
+	g_timeout_add_seconds(2,
+	                      test_purple_conversation_manager_timeout_fail_safe,
+	                      loop);
+
+	/* Run the main loop and let the timeouts fire. */
+	g_main_loop_run(loop);
+
+	/* Verify that our state got reset back to PURPLE_TYPING_STATE_NONE. */
+	typing_state = purple_conversation_member_get_typing_state(member);
+	g_assert_cmpint(typing_state, ==, PURPLE_TYPING_STATE_NONE);
+
+	/* Clean everything up. */
+	g_clear_object(&info);
+	g_clear_object(&member);
+}
+
+/******************************************************************************
  * Main
  *****************************************************************************/
 gint
@@ -86,5 +163,8 @@
 	g_test_add_func("/conversation-member/properties",
 	                test_purple_conversation_member_properties);
 
+	g_test_add_func("/conversation-member/typing-state/timeout",
+	                test_purple_conversation_member_typing_state_timeout);
+
 	return g_test_run();
 }
