diff --git a/libpurple/purpleconversationmember.h b/libpurple/purpleconversationmember.h
--- a/libpurple/purpleconversationmember.h
+++ b/libpurple/purpleconversationmember.h
@@ -129,6 +129,50 @@
 PURPLE_AVAILABLE_IN_3_0
 void purple_conversation_member_set_typing_state(PurpleConversationMember *member, PurpleTypingState state, guint seconds);
 
+/**
+ * purple_conversation_member_get_nickname:
+ * @member: The instance.
+ *
+ * Sets the custom nickname for the remote user.
+ *
+ * Returns: (nullable): The custom nickname if set, otherwise %NULL.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+const char *purple_conversation_member_get_nickname(PurpleConversationMember *member);
+
+/**
+ * purple_conversation_member_set_nickname:
+ * @member: The instance.
+ * @nickname: (nullable): The new nickname.
+ *
+ * Sets the custom nickname for @member.
+ *
+ * This can be unset by passing in %NULL for @nickname.
+ *
+ * > Note: This should typically only be called by protocol plugins.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+void purple_conversation_member_set_nickname(PurpleConversationMember *member, const char *nickname);
+
+/**
+ * purple_conversation_member_get_name_for_display:
+ * @member: The instance.
+ *
+ * Gets the name that should be displayed for @member.
+ *
+ * See [property@ConversationMember:name-for-display] for more information.
+ *
+ * Returns: (transfer none): The name that should be displayed for @member.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+const char *purple_conversation_member_get_name_for_display(PurpleConversationMember *member);
+
 G_END_DECLS
 
 #endif /* PURPLE_CONVERSATION_MEMBER_H */
# HG changeset patch
# Node ID 8bacd505b2944e768b9c0a3e90353dc54dd70f01
# Parent  4aa4ab7b24a7e3080aabc545b7b0fc80b4bbbf9d
diff --git a/libpurple/purpleconversationmember.c b/libpurple/purpleconversationmember.c
--- a/libpurple/purpleconversationmember.c
+++ b/libpurple/purpleconversationmember.c
@@ -23,6 +23,7 @@
 #include "purpleconversationmember.h"
 
 #include "purpleenums.h"
+#include "util.h"
 
 struct _PurpleConversationMember {
 	GObject parent;
@@ -32,6 +33,7 @@
 
 	guint typing_timeout;
 	PurpleTypingState typing_state;
+	char *nickname;
 };
 
 enum {
@@ -39,6 +41,8 @@
 	PROP_CONTACT_INFO,
 	PROP_TAGS,
 	PROP_TYPING_STATE,
+	PROP_NICKNAME,
+	PROP_NAME_FOR_DISPLAY,
 	N_PROPERTIES,
 };
 static GParamSpec *properties[N_PROPERTIES] = {NULL, };
@@ -46,6 +50,10 @@
 G_DEFINE_FINAL_TYPE(PurpleConversationMember, purple_conversation_member,
                     G_TYPE_OBJECT)
 
+static void
+purple_conversation_member_info_changed_cb(GObject *self, GParamSpec *pspec,
+                                           gpointer data);
+
 /******************************************************************************
  * Helpers
  *****************************************************************************/
@@ -56,9 +64,26 @@
 	g_return_if_fail(PURPLE_IS_CONVERSATION_MEMBER(member));
 	g_return_if_fail(PURPLE_IS_CONTACT_INFO(contact_info));
 
+	if(PURPLE_IS_CONTACT_INFO(member->contact_info)) {
+		g_signal_handlers_disconnect_by_func(member->contact_info,
+		                                     purple_conversation_member_info_changed_cb,
+		                                     member);
+	}
+
 	if(g_set_object(&member->contact_info, contact_info)) {
-		g_object_notify_by_pspec(G_OBJECT(member),
-		                         properties[PROP_CONTACT_INFO]);
+		GObject *obj = G_OBJECT(member);
+
+		if(PURPLE_IS_CONTACT_INFO(member->contact_info)) {
+			g_signal_connect_object(member->contact_info,
+			                        "notify::name-for-display",
+			                        G_CALLBACK(purple_conversation_member_info_changed_cb),
+			                        member, G_CONNECT_DEFAULT);
+		}
+
+		g_object_freeze_notify(obj);
+		g_object_notify_by_pspec(obj, properties[PROP_CONTACT_INFO]);
+		g_object_notify_by_pspec(obj, properties[PROP_NAME_FOR_DISPLAY]);
+		g_object_thaw_notify(obj);
 	}
 }
 
@@ -76,6 +101,15 @@
 	return G_SOURCE_REMOVE;
 }
 
+static void
+purple_conversation_member_info_changed_cb(G_GNUC_UNUSED GObject *self,
+                                           G_GNUC_UNUSED GParamSpec *pspec,
+                                           gpointer data)
+{
+	g_object_notify_by_pspec(G_OBJECT(data),
+	                         properties[PROP_NAME_FOR_DISPLAY]);
+}
+
 /******************************************************************************
  * GObject Implementation
  *****************************************************************************/
@@ -86,21 +120,29 @@
 	PurpleConversationMember *member = PURPLE_CONVERSATION_MEMBER(obj);
 
 	switch(param_id) {
-		case PROP_CONTACT_INFO:
-			g_value_set_object(value,
-			                   purple_conversation_member_get_contact_info(member));
-			break;
-		case PROP_TAGS:
-			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;
+	case PROP_CONTACT_INFO:
+		g_value_set_object(value,
+		                   purple_conversation_member_get_contact_info(member));
+		break;
+	case PROP_TAGS:
+		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;
+	case PROP_NICKNAME:
+		g_value_set_string(value,
+		                   purple_conversation_member_get_nickname(member));
+		break;
+	case PROP_NAME_FOR_DISPLAY:
+		g_value_set_string(value,
+		                   purple_conversation_member_get_name_for_display(member));
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+		break;
 	}
 }
 
@@ -111,18 +153,22 @@
 	PurpleConversationMember *member = PURPLE_CONVERSATION_MEMBER(obj);
 
 	switch(param_id) {
-		case PROP_CONTACT_INFO:
-			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;
+	case PROP_CONTACT_INFO:
+		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;
+	case PROP_NICKNAME:
+		purple_conversation_member_set_nickname(member,
+		                                        g_value_get_string(value));
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+		break;
 	}
 }
 
@@ -142,6 +188,7 @@
 	PurpleConversationMember *member = PURPLE_CONVERSATION_MEMBER(obj);
 
 	g_clear_object(&member->tags);
+	g_clear_pointer(&member->nickname, g_free);
 
 	G_OBJECT_CLASS(purple_conversation_member_parent_class)->finalize(obj);
 }
@@ -168,8 +215,7 @@
 	 * Since: 3.0
 	 */
 	properties[PROP_CONTACT_INFO] = g_param_spec_object(
-		"contact-info", "contact-info",
-		"The contact-info this member is for",
+		"contact-info", NULL, NULL,
 		PURPLE_TYPE_CONTACT_INFO,
 		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
 
@@ -181,8 +227,7 @@
 	 * Since: 3.0
 	 */
 	properties[PROP_TAGS] = g_param_spec_object(
-		"tags", "tags",
-		"The tags for this member",
+		"tags", NULL, NULL,
 		PURPLE_TYPE_TAGS,
 		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
 
@@ -194,12 +239,40 @@
 	 * Since: 3.0
 	 */
 	properties[PROP_TYPING_STATE] = g_param_spec_enum(
-		"typing-state", "typing-state",
-		"The typing state for this member",
+		"typing-state", NULL, NULL,
 		PURPLE_TYPE_TYPING_STATE,
 		PURPLE_TYPING_STATE_NONE,
 		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
 
+	/**
+	 * PurpleConversationMember:nickname:
+	 *
+	 * A custom nick name that the remote user has chosen for themselves in the
+	 * chat.
+	 *
+	 * Since: 3.0
+	 */
+	properties[PROP_NICKNAME] = g_param_spec_string(
+		"nickname", NULL, NULL,
+		NULL,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * PurpleConversationMember:name-for-display:
+	 *
+	 * The nick name that should be display for the conversation member.
+	 *
+	 * If [property@ConversationMember:nickname] is set, that will be returned.
+	 *
+	 * Otherwise [property@ContactInfo:name-for-display] will be returned.
+	 *
+	 * Since: 3.0
+	 */
+	properties[PROP_NAME_FOR_DISPLAY] = g_param_spec_string(
+		"name-for-display", NULL, NULL,
+		NULL,
+		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
 	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
 }
 
@@ -268,3 +341,38 @@
 		member->typing_timeout = source;
 	}
 }
+
+const char *
+purple_conversation_member_get_nickname(PurpleConversationMember *member) {
+	g_return_val_if_fail(PURPLE_IS_CONVERSATION_MEMBER(member), NULL);
+
+	return member->nickname;
+}
+
+void
+purple_conversation_member_set_nickname(PurpleConversationMember *member,
+                                        const char *nickname)
+{
+	g_return_if_fail(PURPLE_IS_CONVERSATION_MEMBER(member));
+
+	if(g_set_str(&member->nickname, nickname)) {
+		GObject *obj = G_OBJECT(member);
+
+		g_object_freeze_notify(obj);
+		g_object_notify_by_pspec(obj, properties[PROP_NICKNAME]);
+		g_object_notify_by_pspec(obj, properties[PROP_NAME_FOR_DISPLAY]);
+		g_object_thaw_notify(obj);
+	}
+}
+
+const char *
+purple_conversation_member_get_name_for_display(PurpleConversationMember *member)
+{
+	g_return_val_if_fail(PURPLE_IS_CONVERSATION_MEMBER(member), NULL);
+
+	if(!purple_strempty(member->nickname)) {
+		return member->nickname;
+	}
+
+	return purple_contact_info_get_name_for_display(member->contact_info);
+}
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
@@ -21,6 +21,21 @@
 #include <purple.h>
 
 /******************************************************************************
+ * Callbacks
+ *****************************************************************************/
+static void
+test_purple_conversation_member_notify_counter(GObject *self,
+                                               G_GNUC_UNUSED GParamSpec *pspec,
+                                               gpointer data)
+{
+	guint *counter = data;
+
+	g_assert_true(PURPLE_IS_CONVERSATION_MEMBER(self));
+
+	*counter = *counter + 1;
+}
+
+/******************************************************************************
  * Tests
  *****************************************************************************/
 static void
@@ -45,6 +60,8 @@
 	PurpleConversationMember *member = NULL;
 	PurpleTags *tags = NULL;
 	PurpleTypingState typing_state = PURPLE_TYPING_STATE_NONE;
+	char *name_for_display = NULL;
+	char *nickname = NULL;
 
 	info = purple_contact_info_new("abc123");
 
@@ -55,27 +72,81 @@
 	member = g_object_new(
 		PURPLE_TYPE_CONVERSATION_MEMBER,
 		"contact-info", info,
+		"nickname", "pidgy",
 		"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,
+		"name-for-display", &name_for_display,
+		"nickname", &nickname,
 		"tags", &tags,
 		"typing-state", &typing_state,
 		NULL);
 
-	/* Compare all the things. */
 	g_assert_true(info1 == info);
+	g_clear_object(&info1);
+
+	g_assert_cmpstr(name_for_display, ==, "pidgy");
+	g_clear_pointer(&name_for_display, g_free);
+
+	g_assert_cmpstr(nickname, ==, "pidgy");
+	g_clear_pointer(&nickname, g_free);
+
 	g_assert_true(PURPLE_IS_TAGS(tags));
+	g_clear_object(&tags);
+
 	g_assert_cmpint(typing_state, ==, PURPLE_TYPING_STATE_TYPING);
 
-	/* Free/unref all the things. */
-	g_clear_object(&info1);
-	g_clear_object(&tags);
+	g_assert_finalize_object(member);
+	g_assert_finalize_object(info);
+}
+
+static void
+test_purple_conversation_member_name_for_display(void) {
+	PurpleContactInfo *info = NULL;
+	PurpleConversationMember *member = NULL;
+	const char *name_for_display = NULL;
+	guint counter = 0;
+
+	info = purple_contact_info_new("abc123");
+	member = purple_conversation_member_new(info);
+	g_signal_connect(member, "notify::name-for-display",
+	                 G_CALLBACK(test_purple_conversation_member_notify_counter),
+	                 &counter);
+
+	/* Make sure our counter is still 0. */
+	g_assert_cmpuint(counter, ==, 0);
+
+	/* Make sure the default falls back to the contact info's id. */
+	name_for_display = purple_conversation_member_get_name_for_display(member);
+	g_assert_cmpstr(name_for_display, ==, "abc123");
 
-	g_clear_object(&info);
-	g_clear_object(&member);
+	/* Set the username on the contact info and make sure it propagates up. */
+	purple_contact_info_set_username(info, "tron");
+	name_for_display = purple_conversation_member_get_name_for_display(member);
+	g_assert_cmpstr(name_for_display, ==, "tron");
+	g_assert_cmpuint(counter, ==, 1);
+
+	/* Set the nickname on the conversation member and make sure that takes
+	 * precedence.
+	 */
+	purple_conversation_member_set_nickname(member, "rinzler");
+	name_for_display = purple_conversation_member_get_name_for_display(member);
+	g_assert_cmpstr(name_for_display, ==, "rinzler");
+	g_assert_cmpuint(counter, ==, 2);
+
+	/* Remove the nickname and verify it falls back to the value from the
+	 * contact info.
+	 */
+	purple_conversation_member_set_nickname(member, NULL);
+	name_for_display = purple_conversation_member_get_name_for_display(member);
+	g_assert_cmpstr(name_for_display, ==, "tron");
+	g_assert_cmpuint(counter, ==, 3);
+
+	g_assert_finalize_object(member);
+	g_assert_finalize_object(info);
 }
 
 /******************************************************************************
@@ -162,6 +233,8 @@
 	                test_purple_conversation_member_new);
 	g_test_add_func("/conversation-member/properties",
 	                test_purple_conversation_member_properties);
+	g_test_add_func("/conversation-member/name-for-display",
+	                test_purple_conversation_member_name_for_display);
 
 	g_test_add_func("/conversation-member/typing-state/timeout",
 	                test_purple_conversation_member_typing_state_timeout);
