diff --git a/libpurple/purpleconversation.h b/libpurple/purpleconversation.h
--- a/libpurple/purpleconversation.h
+++ b/libpurple/purpleconversation.h
@@ -912,6 +912,47 @@
 PURPLE_AVAILABLE_IN_3_0
 void purple_conversation_set_avatar(PurpleConversation *conversation, PurpleAvatar *avatar);
 
+/**
+ * purple_conversation_get_title_for_display:
+ * @conversation: The instance.
+ *
+ * Gets the title that should be displayed for @conversation.
+ *
+ * A full breakdown for how this works can be found in the documentation for
+ * [property@Conversation:title-for-display].
+ *
+ * Returns: The title to display.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+const char *purple_conversation_get_title_for_display(PurpleConversation *conversation);
+
+/**
+ * purple_conversation_get_alias:
+ * @conversation: The instance.
+ *
+ * Gets the libpurple user defined alias for @conversation.
+ *
+ * Returns: (nullable): The alias or %NULL.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+const char *purple_conversation_get_alias(PurpleConversation *conversation);
+
+/**
+ * purple_conversation_set_alias:
+ * @conversation: The instance.
+ * @alias: (nullable): The new alias.
+ *
+ * Sets the libpurple user defined alias of @conversation to @alias.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+void purple_conversation_set_alias(PurpleConversation *conversation, const char *alias);
+
 G_END_DECLS
 
 #endif /* PURPLE_CONVERSATION_H */
# HG changeset patch
# Node ID a166ad358177
# Parent  5ebb4beb29b7
diff --git a/libpurple/purpleconversation.c b/libpurple/purpleconversation.c
--- a/libpurple/purpleconversation.c
+++ b/libpurple/purpleconversation.c
@@ -46,6 +46,7 @@
 
 	PurpleAvatar *avatar;
 	char *name;
+	char *alias;
 	char *title;
 	gboolean title_generated;
 
@@ -75,7 +76,9 @@
 	PROP_ACCOUNT,
 	PROP_AVATAR,
 	PROP_NAME,
+	PROP_ALIAS,
 	PROP_TITLE,
+	PROP_TITLE_FOR_DISPLAY,
 	PROP_TITLE_GENERATED,
 	PROP_FEATURES,
 	PROP_AGE_RESTRICTED,
@@ -461,6 +464,9 @@
 	case PROP_NAME:
 		purple_conversation_set_name(conversation, g_value_get_string(value));
 		break;
+	case PROP_ALIAS:
+		purple_conversation_set_alias(conversation, g_value_get_string(value));
+		break;
 	case PROP_TITLE:
 		purple_conversation_set_title(conversation, g_value_get_string(value));
 		break;
@@ -542,9 +548,16 @@
 	case PROP_NAME:
 		g_value_set_string(value, purple_conversation_get_name(conversation));
 		break;
+	case PROP_ALIAS:
+		g_value_set_string(value, purple_conversation_get_alias(conversation));
+		break;
 	case PROP_TITLE:
 		g_value_set_string(value, purple_conversation_get_title(conversation));
 		break;
+	case PROP_TITLE_FOR_DISPLAY:
+		g_value_set_string(value,
+		                   purple_conversation_get_title_for_display(conversation));
+		break;
 	case PROP_TITLE_GENERATED:
 		g_value_set_boolean(value,
 		                    purple_conversation_get_title_generated(conversation));
@@ -667,6 +680,7 @@
 	g_clear_pointer(&conversation->id, g_free);
 	g_clear_object(&conversation->avatar);
 	g_clear_pointer(&conversation->name, g_free);
+	g_clear_pointer(&conversation->alias, g_free);
 	g_clear_pointer(&conversation->title, g_free);
 
 	g_clear_pointer(&conversation->description, g_free);
@@ -765,6 +779,18 @@
 		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
 
 	/**
+	 * PurpleConversation:alias:
+	 *
+	 * An alias for the conversation that is local to the libpurple user.
+	 *
+	 * Since: 3.0
+	 */
+	properties[PROP_ALIAS] = g_param_spec_string(
+		"alias", NULL, NULL,
+		NULL,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+	/**
 	 * PurpleConversation:title:
 	 *
 	 * The title of the conversation.
@@ -778,6 +804,27 @@
 		G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
 
 	/**
+	 * PurpleConversation:title-for-display:
+	 *
+	 * The title that should be displayed for the conversation based on which
+	 * properties are set.
+	 *
+	 * If [property@Conversation:alias] is set, that will be returned.
+	 *
+	 * If alias is not set but [property@Conversation:title] is set, then value
+	 * of title will be returned.
+	 *
+	 * As a fallback, [property@Conversation:id] will be returned if nothing
+	 * else is set.
+	 *
+	 * Since: 3.0
+	 */
+	properties[PROP_TITLE_FOR_DISPLAY] = g_param_spec_string(
+		"title-for-display", NULL, NULL,
+		NULL,
+		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+	/**
 	 * PurpleConversation:title-generated:
 	 *
 	 * Whether or not the title of the conversation was generated by
@@ -1220,6 +1267,7 @@
 		 */
 		g_object_freeze_notify(obj);
 		g_object_notify_by_pspec(obj, properties[PROP_TITLE]);
+		g_object_notify_by_pspec(obj, properties[PROP_TITLE_FOR_DISPLAY]);
 		purple_conversation_set_title_generated(conversation, FALSE);
 		g_object_thaw_notify(obj);
 	}
@@ -1863,3 +1911,41 @@
 		                         properties[PROP_AVATAR]);
 	}
 }
+
+const char *
+purple_conversation_get_title_for_display(PurpleConversation *conversation) {
+	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), NULL);
+
+	if(!purple_strempty(conversation->alias)) {
+		return conversation->alias;
+	}
+
+	if(!purple_strempty(conversation->title)) {
+		return conversation->title;
+	}
+
+	return conversation->id;
+}
+
+const char *
+purple_conversation_get_alias(PurpleConversation *conversation) {
+	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), NULL);
+
+	return conversation->alias;
+}
+
+void
+purple_conversation_set_alias(PurpleConversation *conversation,
+                              const char *alias)
+{
+	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));
+
+	if(g_set_str(&conversation->alias, alias)) {
+		GObject *obj = G_OBJECT(conversation);
+
+		g_object_freeze_notify(obj);
+		g_object_notify_by_pspec(obj, properties[PROP_ALIAS]);
+		g_object_notify_by_pspec(obj, properties[PROP_TITLE_FOR_DISPLAY]);
+		g_object_thaw_notify(obj);
+	}
+}
diff --git a/libpurple/tests/test_conversation.c b/libpurple/tests/test_conversation.c
--- a/libpurple/tests/test_conversation.c
+++ b/libpurple/tests/test_conversation.c
@@ -23,6 +23,21 @@
 #include "test_ui.h"
 
 /******************************************************************************
+ * Callbacks
+ *****************************************************************************/
+static void
+test_purple_conversation_notify_counter_cb(GObject *obj,
+                                           G_GNUC_UNUSED GParamSpec *pspec,
+                                           gpointer data)
+{
+	guint *counter = data;
+
+	g_assert_true(PURPLE_IS_CONVERSATION(obj));
+
+	*counter = *counter + 1;
+}
+
+/******************************************************************************
  * Tests
  *****************************************************************************/
 static void
@@ -44,10 +59,12 @@
 	GDateTime *topic_updated = NULL;
 	GDateTime *topic_updated1 = NULL;
 	GListModel *members = NULL;
+	char *alias = NULL;
 	char *description = NULL;
 	char *id = NULL;
 	char *name = NULL;
 	char *title = NULL;
+	char *title_for_display = NULL;
 	char *topic = NULL;
 	char *user_nickname = NULL;
 	gboolean age_restricted = FALSE;
@@ -68,6 +85,7 @@
 		PURPLE_TYPE_CONVERSATION,
 		"account", account,
 		"age-restricted", TRUE,
+		"alias", "talky talk",
 		"avatar", avatar,
 		"created-on", created_on,
 		"creator", creator,
@@ -88,6 +106,7 @@
 	g_object_get(conversation,
 		"account", &account1,
 		"age-restricted", &age_restricted,
+		"alias", &alias,
 		"avatar", &avatar1,
 		"created-on", &created_on1,
 		"creator", &creator1,
@@ -99,6 +118,7 @@
 		"name", &name,
 		"tags", &tags,
 		"title", &title,
+		"title-for-display", &title_for_display,
 		"topic", &topic,
 		"topic-author", &topic_author1,
 		"topic-updated", &topic_updated1,
@@ -112,6 +132,9 @@
 
 	g_assert_true(age_restricted);
 
+	g_assert_cmpstr(alias, ==, "talky talk");
+	g_clear_pointer(&alias, g_free);
+
 	g_assert_true(avatar1 == avatar);
 	g_clear_object(&avatar1);
 
@@ -144,6 +167,9 @@
 	g_assert_cmpstr(title, ==, "test conversation");
 	g_clear_pointer(&title, g_free);
 
+	g_assert_cmpstr(title_for_display, ==, "talky talk");
+	g_clear_pointer(&title_for_display, g_free);
+
 	g_assert_cmpstr(topic, ==, "the topic...");
 	g_clear_pointer(&topic, g_free);
 
@@ -204,6 +230,70 @@
 	g_clear_object(&conversation);
 }
 
+static void
+test_purple_conversation_title_for_display(void) {
+	PurpleAccount *account = NULL;
+	PurpleConversation *conversation = NULL;
+	const char *title_for_display = NULL;
+	guint counter = 0;
+
+	account = purple_account_new("test", "test");
+	conversation = g_object_new(
+		PURPLE_TYPE_CONVERSATION,
+		"account", account,
+		"id", "id1",
+		NULL);
+	g_signal_connect(conversation, "notify::title-for-display",
+	                 G_CALLBACK(test_purple_conversation_notify_counter_cb),
+	                 &counter);
+
+	/* Make sure the defaults are all as expected. */
+	g_assert_cmpuint(counter, ==, 0);
+	title_for_display = purple_conversation_get_title_for_display(conversation);
+	g_assert_cmpstr(title_for_display, ==, "id1");
+
+	/* Set the alias, verify that the notify was sent for name-for-display and
+	 * that it returns the right value.
+	 */
+	purple_conversation_set_alias(conversation, "alias1");
+	g_assert_cmpuint(counter, ==, 1);
+	title_for_display = purple_conversation_get_title_for_display(conversation);
+	g_assert_cmpstr(title_for_display, ==, "alias1");
+
+	/* Set the title, which won't change the name for display because alias is
+	 * set, but it should still emit the signal because we don't track the
+	 * name-for-display in every setter.
+	 */
+	purple_conversation_set_title(conversation, "title1");
+	g_assert_cmpuint(counter, ==, 2);
+	title_for_display = purple_conversation_get_title_for_display(conversation);
+	g_assert_cmpstr(title_for_display, ==, "alias1");
+
+	/* Now set the alias to NULL which should emit the signal and
+	 * title_for_display should now return title1.
+	 */
+	purple_conversation_set_alias(conversation, NULL);
+	g_assert_cmpuint(counter, ==, 3);
+	title_for_display = purple_conversation_get_title_for_display(conversation);
+	g_assert_cmpstr(title_for_display, ==, "title1");
+
+	/* Finally remove the title as well, which should emit the signal and
+	 * get_title_for_display should return the id.
+	 */
+	purple_conversation_set_title(conversation, NULL);
+	g_assert_cmpuint(counter, ==, 4);
+	title_for_display = purple_conversation_get_title_for_display(conversation);
+	g_assert_cmpstr(title_for_display, ==, "id1");
+
+	/* We're done!! */
+	g_assert_finalize_object(conversation);
+
+	/* We can't use g_assert_finalize_object because accounts automatically
+	 * get added to the account manager.
+	 */
+	g_clear_object(&account);
+}
+
 /******************************************************************************
  * "is" tests
  *****************************************************************************/
@@ -620,6 +710,8 @@
 	                test_purple_conversation_properties);
 	g_test_add_func("/conversation/set-topic-full",
 	                test_purple_conversation_set_topic_full);
+	g_test_add_func("/conversation/title-for-display",
+	                test_purple_conversation_title_for_display);
 
 	g_test_add_func("/conversation/is-dm", test_purple_conversation_is_dm);
 	g_test_add_func("/conversation/is-group-dm",
diff --git a/libpurple/tests/test_conversation_manager.c b/libpurple/tests/test_conversation_manager.c
--- a/libpurple/tests/test_conversation_manager.c
+++ b/libpurple/tests/test_conversation_manager.c
@@ -142,7 +142,10 @@
 	purple_conversation_set_features(conversation, PURPLE_CONNECTION_FLAG_NO_IMAGES);
 	purple_conversation_set_title(conversation, "A place for kool kats");
 
-	g_assert_cmpuint(counter, ==, 2);
+	/* The title also notifies for the title-for-display property which is why
+	 * we get 3 callbacks called instead of just 2.
+	 */
+	g_assert_cmpuint(counter, ==, 3);
 	g_assert_cmpuint(detailed_counter, ==, 1);
 
 	/* Unregister the conversation, to make sure signals are no longer
