--- a/ChangeLog.API Tue May 07 23:06:04 2024 -0500
+++ b/ChangeLog.API Wed May 08 03:37:46 2024 -0500
@@ -503,6 +503,7 @@
* purple_conversation::conversation-switched signal
* purple_conversation_add_smiley
+ * purple_conversation_autoset_title * purple_conversation_clear_message_history
* purple_conversation_close_logs
* purple_conversation_do_command
--- a/libpurple/purpleconversation.c Tue May 07 23:06:04 2024 -0500
+++ b/libpurple/purpleconversation.c Wed May 08 03:37:46 2024 -0500
@@ -47,6 +47,7 @@
+ gboolean title_generated; PurpleConnectionFlags features;
@@ -75,6 +76,7 @@
@@ -112,6 +114,41 @@
**************************************************************************/
+purple_conversation_set_title_generated(PurpleConversation *conversation, + gboolean title_generated) + g_return_if_fail(PURPLE_IS_CONVERSATION(conversation)); + /* If conversation isn't a dm or group dm, and title_generated is being set + * to %TRUE exit immediately because generating the title is only allowed + if(conversation->type != PURPLE_CONVERSATION_TYPE_DM && + conversation->type != PURPLE_CONVERSATION_TYPE_GROUP_DM && + if(conversation->title_generated != title_generated) { + GObject *obj = G_OBJECT(conversation); + conversation->title_generated = title_generated; + g_object_freeze_notify(obj); + if(conversation->title_generated) { + purple_conversation_generate_title(conversation); + g_object_notify_by_pspec(G_OBJECT(conversation), + properties[PROP_TITLE_GENERATED]); + g_object_thaw_notify(obj); purple_conversation_set_id(PurpleConversation *conversation, const char *id) {
g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));
@@ -157,6 +194,20 @@
+purple_conversation_set_conversation_type(PurpleConversation *conversation, + PurpleConversationType type) + g_return_if_fail(PURPLE_IS_CONVERSATION(conversation)); + if(type != conversation->type) { + conversation->type = type; + g_object_notify_by_pspec(G_OBJECT(conversation), + properties[PROP_TYPE]); purple_conversation_set_federated(PurpleConversation *conversation,
@@ -353,6 +404,18 @@
**************************************************************************/
+purple_conversation_member_name_changed_cb(G_GNUC_UNUSED GObject *source, + G_GNUC_UNUSED GParamSpec *pspec, + PurpleConversation *conversation = data; + if(purple_conversation_get_title_generated(conversation)) { + purple_conversation_generate_title(conversation); purple_conversation_account_connected_cb(GObject *obj,
G_GNUC_UNUSED GParamSpec *pspec,
@@ -485,6 +548,10 @@
g_value_set_string(value, purple_conversation_get_title(conversation));
+ case PROP_TITLE_GENERATED: + g_value_set_boolean(value, + purple_conversation_get_title_generated(conversation)); purple_conversation_get_features(conversation));
@@ -564,6 +631,17 @@
G_OBJECT_CLASS(purple_conversation_parent_class)->constructed(object);
+ if(purple_strempty(conversation->title)) { + if(conversation->type == PURPLE_CONVERSATION_TYPE_DM || + conversation->type == PURPLE_CONVERSATION_TYPE_GROUP_DM) + /* There's no way to add members during construction, so just call + purple_conversation_set_title_generated(conversation, TRUE); g_object_get(object, "account", &account, NULL);
gc = purple_account_get_connection(account);
@@ -575,9 +653,6 @@
purple_connection_get_flags(gc));
- /* Auto-set the title. */
- purple_conversation_autoset_title(conversation);
@@ -648,7 +723,7 @@
"The type of the conversation.",
PURPLE_TYPE_CONVERSATION_TYPE,
PURPLE_CONVERSATION_TYPE_UNSET,
- G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); * PurpleConversation:account:
@@ -690,7 +765,7 @@
"The name of the conversation.",
- G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); * PurpleConversation:title:
@@ -703,7 +778,27 @@
"The title of the conversation.",
- G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS); + * PurpleConversation:title-generated: + * Whether or not the title of the conversation was generated by + * [method@Conversation.generate_title]. + * Note: This only works on DMs and GroupDMs. + * If this is %TRUE, [method@Conversation.generate_title] will + * automatically be called whenever a member is added or removed, or when + * their display name changes. + properties[PROP_TITLE_GENERATED] = g_param_spec_boolean( + "title-generated", "title-generated", + "Whether or not the current title was generated.", + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); * PurpleConversation:features:
@@ -1088,20 +1183,6 @@
return conversation->type;
-purple_conversation_set_conversation_type(PurpleConversation *conversation,
- PurpleConversationType type)
- g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));
- if(type != conversation->type) {
- conversation->type = type;
- g_object_notify_by_pspec(G_OBJECT(conversation),
- properties[PROP_TYPE]);
purple_conversation_get_account(PurpleConversation *conversation) {
g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), NULL);
@@ -1129,14 +1210,24 @@
g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));
- g_return_if_fail(title != NULL);
if(!purple_strequal(conversation->title, title)) {
+ GObject *obj = G_OBJECT(conversation); g_free(conversation->title);
conversation->title = g_strdup(title);
- g_object_notify_by_pspec(G_OBJECT(conversation),
- properties[PROP_TITLE]);
+ /* We have to g_object_freeze_notify here because we're modifying more + * than one property. However, purple_conversation_generate_title will + * also have called g_object_freeze_notify before calling us because it + * needs to set the title-generated property to TRUE even though we set + * it to FALSE here. We do this, because we didn't want to write + * additional API that skips that part. + g_object_freeze_notify(obj); + g_object_notify_by_pspec(obj, properties[PROP_TITLE]); + purple_conversation_set_title_generated(conversation, FALSE); + g_object_thaw_notify(obj); @@ -1148,14 +1239,80 @@
-purple_conversation_autoset_title(PurpleConversation *conversation) {
- const char *name = NULL;
+purple_conversation_generate_title(PurpleConversation *conversation) { + PurpleAccount *account = NULL; + PurpleContactInfo *account_info = NULL; g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));
- name = purple_conversation_get_name(conversation);
+ if(conversation->type != PURPLE_CONVERSATION_TYPE_DM && + conversation->type != PURPLE_CONVERSATION_TYPE_GROUP_DM) + g_warning("purple_conversation_generate_title called for non DM/Group " + account = purple_conversation_get_account(conversation); + account_info = PURPLE_CONTACT_INFO(account); + str = g_string_new(""); + n_members = g_list_model_get_n_items(G_LIST_MODEL(conversation->members)); + for(guint i = 0; i < n_members; i++) { + PurpleContactInfo *info = NULL; + PurpleConversationMember *member = NULL; + const char *name = NULL; + member = g_list_model_get_item(G_LIST_MODEL(conversation->members), i); + info = purple_conversation_member_get_contact_info(member); + if(purple_contact_info_compare(info, account_info) == 0) { + g_clear_object(&member); + name = purple_contact_info_get_name_for_display(info); + if(purple_strempty(name)) { + g_warning("contact %p has no displayable name", info); - purple_conversation_set_title(conversation, name);
+ g_clear_object(&member); + g_string_append_printf(str, ", %s", name); + g_string_append(str, name); + g_clear_object(&member); + /* If we found at least 1 user to add, then we set the title. */ + GObject *obj = G_OBJECT(conversation); + g_object_freeze_notify(obj); + purple_conversation_set_title(conversation, str->str); + purple_conversation_set_title_generated(conversation, TRUE); + g_object_thaw_notify(obj); + g_string_free(str, TRUE); +purple_conversation_get_title_generated(PurpleConversation *conversation) { + g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), FALSE); + return conversation->title_generated; @@ -1639,6 +1796,16 @@
member = purple_conversation_member_new(info);
g_list_store_append(conversation->members, member);
+ /* Add a callback for notify::name-for-display on info. */ + g_signal_connect_object(info, "notify::name-for-display", + G_CALLBACK(purple_conversation_member_name_changed_cb), + conversation, G_CONNECT_DEFAULT); + /* Update the title if necessary. */ + if(purple_conversation_get_title_generated(conversation)) { + purple_conversation_generate_title(conversation); g_signal_emit(conversation, signals[SIG_MEMBER_ADDED], 0, member, announce,
@@ -1667,6 +1834,16 @@
g_list_store_remove(conversation->members, position);
+ /* Remove our signal handlers for the member. */ + g_signal_handlers_disconnect_by_func(info, + purple_conversation_member_name_changed_cb, + /* Update our title if necessary. */ + if(purple_conversation_get_title_generated(conversation)) { + purple_conversation_generate_title(conversation); g_signal_emit(conversation, signals[SIG_MEMBER_REMOVED], 0, member,
--- a/libpurple/purpleconversation.h Tue May 07 23:06:04 2024 -0500
+++ b/libpurple/purpleconversation.h Wed May 08 03:37:46 2024 -0500
@@ -203,21 +203,6 @@
PurpleConversationType purple_conversation_get_conversation_type(PurpleConversation *conversation);
- * purple_conversation_set_conversation_type:
- * @conversation: The instance.
- * Sets the type of @conversation to @type.
- * > Note this only for the internal representation in libpurple and the
- * protocol will not be told to change the type.
-void purple_conversation_set_conversation_type(PurpleConversation *conversation, PurpleConversationType type);
* purple_conversation_get_account:
* @conversation: The conversation.
@@ -272,18 +257,37 @@
const char *purple_conversation_get_title(PurpleConversation *conversation);
- * purple_conversation_autoset_title:
- * @conversation: The conversation.
+ * purple_conversation_generate_title: + * @conversation: The instance. + * Sets the title for @conversation, which must be a DM or Group DM, to a comma + * separated string of the display names for each [class@ConversationMember]. - * Automatically sets the specified conversation's title.
+ * If @conversation is not a DM or Group DM, no changes will be made. + * If no members are found, [property@Conversation:title] will not be changed. + * If the title is updated, [property@Conversation:title-generated] will be - * This function takes OPT_IM_ALIAS_TAB into account, as well as the
+void purple_conversation_generate_title(PurpleConversation *conversation); + * purple_conversation_get_title_generated: + * @conversation: The instance.
+ * Gets whether or not the current title was automatically generated via + * [method@Conversation.generate_title]. + * Returns: %TRUE if the title was automatically generated.
-void purple_conversation_autoset_title(PurpleConversation *conversation);
+gboolean purple_conversation_get_title_generated(PurpleConversation *conversation); * purple_conversation_set_name:
--- a/libpurple/tests/test_conversation.c Tue May 07 23:06:04 2024 -0500
+++ b/libpurple/tests/test_conversation.c Wed May 08 03:37:46 2024 -0500
@@ -47,6 +47,7 @@
char *description = NULL;
char *user_nickname = NULL;
gboolean age_restricted = FALSE;
@@ -62,9 +63,6 @@
/* Use g_object_new so we can test setting properties by name. All of them
* call the setter methods, so by doing it this way we exercise more of the
- * We don't currently test title because purple_conversation_autoset_title
- * makes it something we don't expect it to be.
conversation = g_object_new(
PURPLE_TYPE_CONVERSATION,
@@ -78,6 +76,7 @@
"features", PURPLE_CONNECTION_FLAG_HTML,
+ "title", "test conversation", "topic-author", topic_author,
"topic-updated", topic_updated,
@@ -99,6 +98,7 @@
"topic-author", &topic_author1,
"topic-updated", &topic_updated1,
@@ -141,6 +141,9 @@
g_assert_true(PURPLE_IS_TAGS(tags));
+ g_assert_cmpstr(title, ==, "test conversation"); + g_clear_pointer(&title, g_free); g_assert_cmpstr(topic, ==, "the topic...");
g_clear_pointer(&topic, g_free);
@@ -178,7 +181,6 @@
conversation = g_object_new(
PURPLE_TYPE_CONVERSATION,
- "name", "this is required for some reason",
g_assert_true(PURPLE_IS_CONVERSATION(conversation));
@@ -215,7 +217,6 @@
PURPLE_TYPE_CONVERSATION,
"type", PURPLE_CONVERSATION_TYPE_DM,
- "name", "this is required for some reason",
g_assert_true(PURPLE_IS_CONVERSATION(conversation));
@@ -239,7 +240,6 @@
PURPLE_TYPE_CONVERSATION,
"type", PURPLE_CONVERSATION_TYPE_GROUP_DM,
- "name", "this is required for some reason",
g_assert_true(PURPLE_IS_CONVERSATION(conversation));
@@ -263,7 +263,6 @@
PURPLE_TYPE_CONVERSATION,
"type", PURPLE_CONVERSATION_TYPE_CHANNEL,
- "name", "this is required for some reason",
g_assert_true(PURPLE_IS_CONVERSATION(conversation));
@@ -287,7 +286,6 @@
PURPLE_TYPE_CONVERSATION,
"type", PURPLE_CONVERSATION_TYPE_THREAD,
- "name", "this is required for some reason",
g_assert_true(PURPLE_IS_CONVERSATION(conversation));
@@ -336,8 +334,10 @@
- /* Create our instances. */
- info = purple_contact_info_new(NULL);
+ /* Create our instances. The id is just a uuid 4 to help us avoid a + info = purple_contact_info_new("745c50ba-1189-48d9-827c-051783026c96"); account = purple_account_new("test", "test");
conversation = g_object_new(
PURPLE_TYPE_CONVERSATION,
@@ -427,7 +427,6 @@
conversation = g_object_new(
PURPLE_TYPE_CONVERSATION,
- "name", "this is required",
messages = purple_conversation_get_messages(conversation);
@@ -488,6 +487,125 @@
/******************************************************************************
+ *****************************************************************************/ +test_purple_conversation_generate_title_empty(void) { + PurpleAccount *account = NULL; + PurpleConversation *conversation = NULL; + const char *title = NULL; + account = purple_account_new("test", "test"); + conversation = g_object_new( + PURPLE_TYPE_CONVERSATION, + "type", PURPLE_CONVERSATION_TYPE_DM, + title = purple_conversation_get_title(conversation); + purple_conversation_set_title(conversation, "test"); + title = purple_conversation_get_title(conversation); + /* There are no members in this conversation, so calling generate_title + * doesn't change the title. + purple_conversation_generate_title(conversation); + title = purple_conversation_get_title(conversation); + g_assert_cmpstr(title, ==, "test"); + g_assert_finalize_object(conversation); + g_clear_object(&account); +test_purple_conversation_generate_title_dm(void) { + PurpleAccount *account = NULL; + PurpleContact *contact = NULL; + PurpleConversation *conversation = NULL; + const char *title = NULL; + account = purple_account_new("test", "test"); + conversation = g_object_new( + PURPLE_TYPE_CONVERSATION, + "type", PURPLE_CONVERSATION_TYPE_DM, + title = purple_conversation_get_title(conversation); + contact = purple_contact_new(account, NULL); + purple_contact_info_set_username(PURPLE_CONTACT_INFO(contact), "Alice"); + purple_conversation_add_member(conversation, PURPLE_CONTACT_INFO(contact), + title = purple_conversation_get_title(conversation); + g_assert_cmpstr(title, ==, "Alice"); + /* Make sure the title updates when the display name changes. */ + purple_contact_info_set_username(PURPLE_CONTACT_INFO(contact), "alice!"); + title = purple_conversation_get_title(conversation); + g_assert_cmpstr(title, ==, "alice!"); + g_assert_finalize_object(conversation); + g_assert_finalize_object(contact); + g_clear_object(&account); +test_purple_conversation_generate_title_group_dm(void) { + PurpleAccount *account = NULL; + PurpleContact *contact1 = NULL; + PurpleContact *contact2 = NULL; + PurpleContact *contact3 = NULL; + PurpleConversation *conversation = NULL; + const char *title = NULL; + account = purple_account_new("test", "test"); + conversation = g_object_new( + PURPLE_TYPE_CONVERSATION, + "type", PURPLE_CONVERSATION_TYPE_GROUP_DM, + title = purple_conversation_get_title(conversation); + contact1 = purple_contact_new(account, NULL); + purple_contact_info_set_username(PURPLE_CONTACT_INFO(contact1), "Alice"); + purple_conversation_add_member(conversation, PURPLE_CONTACT_INFO(contact1), + contact2 = purple_contact_new(account, NULL); + purple_contact_info_set_username(PURPLE_CONTACT_INFO(contact2), "Bob"); + purple_conversation_add_member(conversation, PURPLE_CONTACT_INFO(contact2), + contact3 = purple_contact_new(account, NULL); + purple_contact_info_set_username(PURPLE_CONTACT_INFO(contact3), "Eve"); + purple_conversation_add_member(conversation, PURPLE_CONTACT_INFO(contact3), + title = purple_conversation_get_title(conversation); + g_assert_cmpstr(title, ==, "Alice, Bob, Eve"); + /* Change some names around and verify the title was generated properly. */ + purple_contact_info_set_username(PURPLE_CONTACT_INFO(contact2), "Robert"); + purple_contact_info_set_username(PURPLE_CONTACT_INFO(contact3), "Evelyn"); + title = purple_conversation_get_title(conversation); + g_assert_cmpstr(title, ==, "Alice, Robert, Evelyn"); + g_assert_finalize_object(conversation); + g_assert_finalize_object(contact1); + g_assert_finalize_object(contact2); + g_assert_finalize_object(contact3); + g_clear_object(&account); +/****************************************************************************** *****************************************************************************/
@@ -520,6 +638,13 @@
g_test_add_func("/conversation/signals/present",
test_purple_conversation_signals_present);
+ g_test_add_func("/conversation/generate-title/empty", + test_purple_conversation_generate_title_empty); + g_test_add_func("/conversation/generate-title/dm", + test_purple_conversation_generate_title_dm); + g_test_add_func("/conversation/generate-title/group-dm", + test_purple_conversation_generate_title_group_dm); --- a/libpurple/tests/test_conversation_manager.c Tue May 07 23:06:04 2024 -0500
+++ b/libpurple/tests/test_conversation_manager.c Wed May 08 03:37:46 2024 -0500
@@ -69,7 +69,6 @@
conversation = g_object_new(
PURPLE_TYPE_CONVERSATION,
- "name", "purple_conversation_autoset_title sucks",
/* Add the conversation to the manager. */
@@ -136,7 +135,6 @@
conversation = g_object_new(
PURPLE_TYPE_CONVERSATION,
purple_conversation_manager_register(manager, conversation);
@@ -185,7 +183,6 @@
PURPLE_TYPE_CONVERSATION,
"type", PURPLE_CONVERSATION_TYPE_DM,
ret = purple_conversation_manager_register(manager, conversation);
@@ -238,13 +235,13 @@
manager = g_object_new(PURPLE_TYPE_CONVERSATION_MANAGER, NULL);
account = purple_account_new("test", "test");
- contact = purple_contact_new(account, NULL);
+ contact = purple_contact_new(account, + "a9780f2a-eeb5-4d6b-89cb-52e5dad3973f"); conversation1 = g_object_new(
PURPLE_TYPE_CONVERSATION,
"type", PURPLE_CONVERSATION_TYPE_DM,
- "name", "this is required for some reason",
purple_conversation_manager_register(manager, conversation1);
purple_conversation_add_member(conversation1, PURPLE_CONTACT_INFO(contact),
@@ -287,7 +284,6 @@
PURPLE_TYPE_CONVERSATION,
"type", PURPLE_CONVERSATION_TYPE_CHANNEL,
- "name", "this is required for some reason",
purple_conversation_manager_register(manager, conversation1);
@@ -298,7 +294,6 @@
PURPLE_TYPE_CONVERSATION,
"type", PURPLE_CONVERSATION_TYPE_DM,
- "name", "this is required for some reason",
purple_conversation_manager_register(manager, conversation2);
@@ -307,7 +302,6 @@
PURPLE_TYPE_CONVERSATION,
"type", PURPLE_CONVERSATION_TYPE_CHANNEL,
- "name", "this is required for some reason",
purple_conversation_manager_register(manager, conversation2);
--- a/libpurple/tests/test_protocol_conversation.c Tue May 07 23:06:04 2024 -0500
+++ b/libpurple/tests/test_protocol_conversation.c Wed May 08 03:37:46 2024 -0500
@@ -203,7 +203,6 @@
conversation = g_object_new(
PURPLE_TYPE_CONVERSATION,
- "name", "this is required at the moment",
"type", PURPLE_CONVERSATION_TYPE_DM,
message = g_object_new(PURPLE_TYPE_MESSAGE, NULL);
@@ -262,7 +261,6 @@
conversation = g_object_new(
PURPLE_TYPE_CONVERSATION,
- "name", "this is required at the moment",
"type", PURPLE_CONVERSATION_TYPE_DM,
@@ -395,7 +393,6 @@
conversation = g_object_new(
PURPLE_TYPE_CONVERSATION,
- "name", "this is required at the moment",
"type", PURPLE_CONVERSATION_TYPE_DM,
@@ -451,7 +448,6 @@
conversation = g_object_new(
PURPLE_TYPE_CONVERSATION,
- "name", "this is required at the moment",
"type", PURPLE_CONVERSATION_TYPE_DM,
@@ -545,7 +541,6 @@
conversation = g_object_new(
PURPLE_TYPE_CONVERSATION,
g_task_return_pointer(task, conversation, g_object_unref);