diff --git a/libpurple/purpleprotocolconversation.h b/libpurple/purpleprotocolconversation.h
--- a/libpurple/purpleprotocolconversation.h
+++ b/libpurple/purpleprotocolconversation.h
@@ -35,6 +35,7 @@
 #include "purpleconversation.h"
 #include "purplemessage.h"
 #include "purpleprotocol.h"
+#include "purpletyping.h"
 
 G_BEGIN_DECLS
 
@@ -66,6 +67,8 @@
 	void (*join_channel_async)(PurpleProtocolConversation *protocol, PurpleAccount *account, PurpleChannelJoinDetails *details, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer data);
 	gboolean (*join_channel_finish)(PurpleProtocolConversation *protocol, GAsyncResult *result, GError **error);
 
+	void (*send_typing)(PurpleProtocolConversation *protocol, PurpleConversation *conversation, PurpleTypingState state);
+
 	/*< private >*/
 	gpointer reserved[8];
 };
@@ -199,6 +202,20 @@
  */
 gboolean purple_protocol_conversation_join_channel_finish(PurpleProtocolConversation *protocol, GAsyncResult *result, GError **error);
 
+/**
+ * purple_protocl_conversation_send_typing_state:
+ * @protocol: The instance.
+ * @conversation: The conversation.
+ * @state: The new typing state.
+ *
+ * Sends the libpurple user's typing state for the conversation.
+ *
+ * This primarily be called by the user interface.
+ *
+ * Since: 3.0.0
+ */
+void purple_protocol_conversation_send_typing(PurpleProtocolConversation *protocol, PurpleConversation *conversation, PurpleTypingState state);
+
 G_END_DECLS
 
 #endif /* PURPLE_PROTOCOL_CONVERSATION_H */
# HG changeset patch
# Node ID f770ae994e21
# Parent  cc9e9b7c040a
diff --git a/libpurple/purpleprotocolconversation.c b/libpurple/purpleprotocolconversation.c
--- a/libpurple/purpleprotocolconversation.c
+++ b/libpurple/purpleprotocolconversation.c
@@ -189,3 +189,22 @@
 
 	return FALSE;
 }
+
+void
+purple_protocol_conversation_send_typing(PurpleProtocolConversation *protocol,
+                                         PurpleConversation *conversation,
+                                         PurpleTypingState state)
+{
+	PurpleProtocolConversationInterface *iface = NULL;
+
+	g_return_if_fail(PURPLE_IS_PROTOCOL_CONVERSATION(protocol));
+	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));
+
+	iface = PURPLE_PROTOCOL_CONVERSATION_GET_IFACE(protocol);
+	if(iface != NULL && iface->send_typing != NULL) {
+		iface->send_typing(protocol, conversation, state);
+	} else {
+		g_warning("%s does not implement send_typing",
+		          G_OBJECT_TYPE_NAME(protocol));
+	}
+}
diff --git a/libpurple/tests/test_protocol_conversation.c b/libpurple/tests/test_protocol_conversation.c
--- a/libpurple/tests/test_protocol_conversation.c
+++ b/libpurple/tests/test_protocol_conversation.c
@@ -278,6 +278,35 @@
 	g_test_trap_assert_stderr("*Purple-WARNING*TestPurpleProtocolConversationEmpty*join_channel_finish*");
 }
 
+static void
+test_purple_protocol_conversation_empty_send_typing(void) {
+	if(g_test_subprocess()) {
+		PurpleAccount *account = NULL;
+		PurpleConversation *conversation = NULL;
+		PurpleProtocolConversation *protocol = NULL;
+
+		protocol = g_object_new(test_purple_protocol_conversation_empty_get_type(),
+		                        NULL);
+		account = purple_account_new("test", "test");
+		conversation = g_object_new(
+			PURPLE_TYPE_CONVERSATION,
+			"account", account,
+			"name", "this is required at the moment",
+			"type", PurpleConversationTypeDM,
+			NULL);
+
+		purple_protocol_conversation_send_typing(protocol, conversation,
+		                                         PURPLE_TYPING_STATE_PAUSED);
+
+		g_clear_object(&account);
+		g_clear_object(&conversation);
+		g_clear_object(&protocol);
+	}
+
+	g_test_trap_subprocess(NULL, 0, 0);
+	g_test_trap_assert_stderr("*Purple-WARNING*TestPurpleProtocolConversationEmpty*send_typing*");
+}
+
 /******************************************************************************
  * TestProtocolConversation Implementation
  *****************************************************************************/
@@ -299,6 +328,8 @@
 	guint get_channel_join_details;
 	guint join_channel_async;
 	guint join_channel_finish;
+
+	guint send_typing;
 };
 
 static void
@@ -442,6 +473,19 @@
 	return g_task_propagate_boolean(G_TASK(result), error);
 }
 
+static void
+test_purple_protocol_conversation_send_typing(PurpleProtocolConversation *protocol,
+                                              PurpleConversation *conversation,
+                                              PurpleTypingState state)
+{
+	TestPurpleProtocolConversation *test_protocol = NULL;
+
+	test_protocol = TEST_PURPLE_PROTOCOL_CONVERSATION(protocol);
+	test_protocol->send_typing += 1;
+
+	g_assert_true(PURPLE_IS_CONVERSATION(conversation));
+	g_assert_true(state == PURPLE_TYPING_STATE_TYPING);
+}
 
 static void
 test_purple_protocol_conversation_iface_init(PurpleProtocolConversationInterface *iface) {
@@ -454,6 +498,8 @@
 	iface->get_channel_join_details = test_purple_protocol_conversation_get_channel_join_details;
 	iface->join_channel_async = test_purple_protocol_conversation_join_channel_async;
 	iface->join_channel_finish = test_purple_protocol_conversation_join_channel_finish;
+
+	iface->send_typing = test_purple_protocol_conversation_send_typing;
 }
 
 G_DEFINE_TYPE_WITH_CODE(TestPurpleProtocolConversation, test_purple_protocol_conversation,
@@ -473,6 +519,8 @@
 	protocol->get_channel_join_details = 0;
 	protocol->join_channel_async = 0;
 	protocol->join_channel_finish = 0;
+
+	protocol->send_typing = 0;
 }
 
 static void
@@ -738,6 +786,39 @@
 }
 
 /******************************************************************************
+ * TestProtocolConversation Send Typing Tests
+ ****************************************************************************/
+static void
+test_purple_protocol_conversation_send_typing_normal(void) {
+	TestPurpleProtocolConversation *test_protocol = NULL;
+	PurpleAccount *account = NULL;
+	PurpleConversation *conversation = NULL;
+	PurpleProtocolConversation *protocol = NULL;
+
+	protocol = g_object_new(test_purple_protocol_conversation_get_type(),
+	                        NULL);
+	test_protocol = TEST_PURPLE_PROTOCOL_CONVERSATION(protocol);
+
+	account = purple_account_new("test", "test");
+
+	conversation = g_object_new(
+		PURPLE_TYPE_CONVERSATION,
+		"account", account,
+		"name", "this is required at the moment",
+		"type", PurpleConversationTypeDM,
+		NULL);
+
+	test_purple_protocol_conversation_send_typing(protocol, conversation,
+	                                              PURPLE_TYPING_STATE_TYPING);
+
+	g_assert_cmpuint(test_protocol->send_typing, ==, 1);
+
+	g_clear_object(&conversation);
+	g_clear_object(&account);
+	g_clear_object(&protocol);
+}
+
+/******************************************************************************
  * Main
  *****************************************************************************/
 gint
@@ -768,6 +849,10 @@
 	g_test_add_func("/protocol-conversation/empty/join_channel_finish",
 	                test_purple_protocol_conversation_empty_join_channel_finish);
 
+	/* Empty send typing tests. */
+	g_test_add_func("/protocol-conversation/empty/send-typing",
+	                test_purple_protocol_conversation_empty_send_typing);
+
 	/* Normal send message tests. */
 	g_test_add_data_func("/protocol-conversation/normal/send-message-normal",
 	                     GINT_TO_POINTER(FALSE),
@@ -792,6 +877,10 @@
 	                     GINT_TO_POINTER(TRUE),
 	                     test_purple_protocol_conversation_join_channel_normal);
 
+	/* Normal send typing tests. */
+	g_test_add_func("/protocol-conversation/normal/send-typing",
+	                test_purple_protocol_conversation_send_typing_normal);
+
 	ret = g_test_run();
 
 	g_main_loop_unref(loop);
