diff --git a/libpurple/purplecontactinfo.h b/libpurple/purplecontactinfo.h
--- a/libpurple/purplecontactinfo.h
+++ b/libpurple/purplecontactinfo.h
@@ -52,7 +52,7 @@
  * A representation of whether or not a contact has permission to contact the
  * user.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 PURPLE_AVAILABLE_TYPE_IN_3_0
 typedef enum {
@@ -68,7 +68,7 @@
  *
  * The class struct for [class@Purple.ContactInfo].
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 struct _PurpleContactInfoClass {
 	/*< private >*/
@@ -85,7 +85,7 @@
  * need to refer to a user. Be it a chat, an direct message, a file transfer,
  * etc.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 
 /**
@@ -98,7 +98,7 @@
  *
  * Returns: (transfer full): The new instance.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 PURPLE_AVAILABLE_IN_3_0
 PurpleContactInfo *purple_contact_info_new(const gchar *id);
@@ -114,7 +114,7 @@
  *
  * Returns: The id of the contact.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 PURPLE_AVAILABLE_IN_3_0
 const gchar *purple_contact_info_get_id(PurpleContactInfo *info);
@@ -128,7 +128,7 @@
  * at all. The main intent of this, is for protocols to update the id of an
  * an account when it is connected if the id is missing.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 PURPLE_AVAILABLE_IN_3_0
 void purple_contact_info_set_id(PurpleContactInfo *info, const char *id);
@@ -141,7 +141,7 @@
  *
  * Returns: The username of @info.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 PURPLE_AVAILABLE_IN_3_0
 const gchar *purple_contact_info_get_username(PurpleContactInfo *info);
@@ -156,7 +156,7 @@
  * This is primarily used by protocol plugins like IRC when a user changes
  * their "nick" which is their username.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 PURPLE_AVAILABLE_IN_3_0
 void purple_contact_info_set_username(PurpleContactInfo *info, const gchar *username);
@@ -171,7 +171,7 @@
  * Returns: (nullable): The display name of @info if one is set, otherwise
  *          %NULL will be returned.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 PURPLE_AVAILABLE_IN_3_0
 const gchar *purple_contact_info_get_display_name(PurpleContactInfo *info);
@@ -186,7 +186,7 @@
  * This should primarily only be used by protocol plugins and everyone else
  * should be using [method@Purple.ContactInfo.set_alias].
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 PURPLE_AVAILABLE_IN_3_0
 void purple_contact_info_set_display_name(PurpleContactInfo *info, const gchar *display_name);
@@ -199,7 +199,7 @@
  *
  * Returns: (nullable): The alias of @info if one is set, otherwise %NULL.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 PURPLE_AVAILABLE_IN_3_0
 const gchar *purple_contact_info_get_alias(PurpleContactInfo *info);
@@ -213,7 +213,7 @@
  *
  * Protocol plugins may use this value to synchronize across instances.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 PURPLE_AVAILABLE_IN_3_0
 void purple_contact_info_set_alias(PurpleContactInfo *info, const gchar *alias);
@@ -227,7 +227,7 @@
  *
  * Returns: The RGB hex code.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 PURPLE_AVAILABLE_IN_3_0
 const char *purple_contact_info_get_color(PurpleContactInfo *info);
@@ -243,7 +243,7 @@
  * following it. Different user interfaces may be able to handle additional
  * precision, but using `#RRGGBB` will have the highest compatibility.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 PURPLE_AVAILABLE_IN_3_0
 void purple_contact_info_set_color(PurpleContactInfo *info, const char *color);
@@ -256,7 +256,7 @@
  *
  * Returns: (nullable): The primary email address or %NULL if not set.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 PURPLE_AVAILABLE_IN_3_0
 const char *purple_contact_info_get_email(PurpleContactInfo *info);
@@ -268,7 +268,7 @@
  *
  * Sets the primary email address for @info.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 PURPLE_AVAILABLE_IN_3_0
 void purple_contact_info_set_email(PurpleContactInfo *info, const char *email);
@@ -281,7 +281,7 @@
  *
  * Returns: (nullable): The primary phone number for the contact.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 PURPLE_AVAILABLE_IN_3_0
 const char *purple_contact_info_get_phone_number(PurpleContactInfo *info);
@@ -293,7 +293,7 @@
  *
  * Sets the primary phone number for @info.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 PURPLE_AVAILABLE_IN_3_0
 void purple_contact_info_set_phone_number(PurpleContactInfo *info, const char *phone_number);
@@ -306,7 +306,7 @@
  *
  * Returns: (transfer none) (nullable): The time zone if set, otherwise %NULL.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 PURPLE_AVAILABLE_IN_3_0
 GTimeZone *purple_contact_info_get_time_zone(PurpleContactInfo *info);
@@ -318,7 +318,7 @@
  *
  * Sets the time zone of @info to @time_zone.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 PURPLE_AVAILABLE_IN_3_0
 void purple_contact_info_set_time_zone(PurpleContactInfo *info, GTimeZone *timezone);
@@ -331,7 +331,7 @@
  *
  * Returns: The note.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 PURPLE_AVAILABLE_IN_3_0
 const char *purple_contact_info_get_note(PurpleContactInfo *info);
@@ -343,7 +343,7 @@
  *
  * Sets the note for @info to @note.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 PURPLE_AVAILABLE_IN_3_0
 void purple_contact_info_set_note(PurpleContactInfo *info, const char *note);
@@ -356,7 +356,7 @@
  *
  * Returns: (transfer none): The avatar if set, otherwise %NULL.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 PURPLE_AVAILABLE_IN_3_0
 PurpleAvatar *purple_contact_info_get_avatar(PurpleContactInfo *info);
@@ -371,7 +371,7 @@
  *
  * Typically this should only called by the protocol plugin.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 PURPLE_AVAILABLE_IN_3_0
 void purple_contact_info_set_avatar(PurpleContactInfo *info, PurpleAvatar *avatar);
@@ -385,7 +385,7 @@
  * Returns: (transfer none) (nullable): The presence for @info if one is
  *          set, otherwise %NULL.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 PURPLE_AVAILABLE_IN_3_0
 PurplePresence *purple_contact_info_get_presence(PurpleContactInfo *info);
@@ -398,7 +398,7 @@
  *
  * Returns: (transfer none): The tags for @info.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 PURPLE_AVAILABLE_IN_3_0
 PurpleTags *purple_contact_info_get_tags(PurpleContactInfo *info);
@@ -410,7 +410,7 @@
  *
  * Sets the person that @info belongs to to @person.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 PURPLE_AVAILABLE_IN_3_0
 void purple_contact_info_set_person(PurpleContactInfo *info, PurplePerson *person);
@@ -424,7 +424,7 @@
  * Returns: (transfer none) (nullable): The [class@Purple.Person] that @info
  *          belongs to, or %NULL.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 PURPLE_AVAILABLE_IN_3_0
 PurplePerson *purple_contact_info_get_person(PurpleContactInfo *info);
@@ -437,7 +437,7 @@
  *
  * Returns: The permission for @info.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 PURPLE_AVAILABLE_IN_3_0
 PurpleContactInfoPermission purple_contact_info_get_permission(PurpleContactInfo *info);
@@ -449,7 +449,7 @@
  *
  * Sets the permission of @info to @permission.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 PURPLE_AVAILABLE_IN_3_0
 void purple_contact_info_set_permission(PurpleContactInfo *info, PurpleContactInfoPermission permission);
@@ -462,7 +462,7 @@
  *
  * Returns: (transfer none) (nullable): The sid if set.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 PURPLE_AVAILABLE_IN_3_0
 const char *purple_contact_info_get_sid(PurpleContactInfo *info);
@@ -474,7 +474,7 @@
  *
  * Sets the sid, or secondary id, of @info to @sid.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 PURPLE_AVAILABLE_IN_3_0
 void purple_contact_info_set_sid(PurpleContactInfo *info, const char *sid);
@@ -488,7 +488,7 @@
  *
  * Returns: (transfer none): The name to display for @info.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 PURPLE_AVAILABLE_IN_3_0
 const char *purple_contact_info_get_name_for_display(PurpleContactInfo *info);
@@ -503,7 +503,7 @@
  * Returns: less than 0 if @a should be sorted before @b, 0 if they sorted
  *          equally, and greater than 0 if @a should be sorted after @b.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 PURPLE_AVAILABLE_IN_3_0
 int purple_contact_info_compare(PurpleContactInfo *a, PurpleContactInfo *b);
@@ -522,7 +522,7 @@
  * Returns: %TRUE if @needle matches any of the above properties, otherwise
  *          %FALSE.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 PURPLE_AVAILABLE_IN_3_0
 gboolean purple_contact_info_matches(PurpleContactInfo *info, const char *needle);
@@ -539,7 +539,7 @@
  *
  * Returns: (transfer full) (nullable): The menu for the contact info or %NULL.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 PURPLE_AVAILABLE_IN_3_0
 BirbActionMenu *purple_contact_info_get_menu(PurpleContactInfo *info);
# HG changeset patch
# Node ID 6f1c490e5369
# Parent  73eda937c387
diff --git a/libpurple/purplecontactinfo.c b/libpurple/purplecontactinfo.c
--- a/libpurple/purplecontactinfo.c
+++ b/libpurple/purplecontactinfo.c
@@ -360,7 +360,7 @@
 	 *
 	 * The protocol specific id for the contact.
 	 *
-	 * Since: 3.0.0
+	 * Since: 3.0
 	 */
 	properties[PROP_ID] = g_param_spec_string(
 		"id", "id",
@@ -374,7 +374,7 @@
 	 * The username for this contact. In rare cases this can change, like when
 	 * a user changes their "nick" on IRC which is their user name.
 	 *
-	 * Since: 3.0.0
+	 * Since: 3.0
 	 */
 	properties[PROP_USERNAME] = g_param_spec_string(
 		"username", "username",
@@ -388,7 +388,7 @@
 	 * The display name for this contact. This is generally set by the person
 	 * the contact is representing and controlled via the protocol plugin.
 	 *
-	 * Since: 3.0.0
+	 * Since: 3.0
 	 */
 	properties[PROP_DISPLAY_NAME] = g_param_spec_string(
 		"display-name", "display-name",
@@ -402,7 +402,7 @@
 	 * The alias for this contact. This is controlled by the libpurple user and
 	 * may be used by the protocol if it allows for aliasing.
 	 *
-	 * Since: 3.0.0
+	 * Since: 3.0
 	 */
 	properties[PROP_ALIAS] = g_param_spec_string(
 		"alias", "alias",
@@ -418,7 +418,7 @@
 	 * protocol plugin in the event that the protocol allows people to set a
 	 * highlight/branding color.
 	 *
-	 * Since: 3.0.0
+	 * Since: 3.0
 	 */
 	properties[PROP_COLOR] = g_param_spec_string(
 		"color", "color",
@@ -432,7 +432,7 @@
 	 * The primary email address for the contact. This may also be controlled
 	 * via a protocol plugin in the event that the protocol makes it available.
 	 *
-	 * Since: 3.0.0
+	 * Since: 3.0
 	 */
 	properties[PROP_EMAIL] = g_param_spec_string(
 		"email", "email",
@@ -446,7 +446,7 @@
 	 * The primary phone number for the contact. This may also be set via a
 	 * protocol plugin in the event that the protocol knows it.
 	 *
-	 * Since: 3.0.0
+	 * Since: 3.0
 	 */
 	properties[PROP_PHONE_NUMBER] = g_param_spec_string(
 		"phone-number", "phone-number",
@@ -460,7 +460,7 @@
 	 * The time zone for this contact. This is typically controlled by the
 	 * protocol and should only be read by others.
 	 *
-	 * Since: 3.0.0
+	 * Since: 3.0
 	 */
 	properties[PROP_TIME_ZONE] = g_param_spec_boxed(
 		"time-zone", "time-zone",
@@ -475,7 +475,7 @@
 	 * a contact and store it server side. This property is where that is kept
 	 * track of.
 	 *
-	 * Since: 3.0.0
+	 * Since: 3.0
 	 */
 	properties[PROP_NOTE] = g_param_spec_string(
 		"note", "note",
@@ -489,7 +489,7 @@
 	 * The avatar for this contact. This is typically controlled by the
 	 * protocol and should only be read by others.
 	 *
-	 * Since: 3.0.0
+	 * Since: 3.0
 	 */
 	properties[PROP_AVATAR] = g_param_spec_object(
 		"avatar", "avatar",
@@ -503,7 +503,7 @@
 	 * The [class@Purple.Presence] for this contact. This is typically
 	 * controlled by the protocol and should only be read by others.
 	 *
-	 * Since: 3.0.0
+	 * Since: 3.0
 	 */
 	properties[PROP_PRESENCE] = g_param_spec_object(
 		"presence", "presence",
@@ -516,7 +516,7 @@
 	 *
 	 * The [class@Purple.Tags] for this contact.
 	 *
-	 * Since: 3.0.0
+	 * Since: 3.0
 	 */
 	properties[PROP_TAGS] = g_param_spec_object(
 		"tags", "tags",
@@ -529,7 +529,7 @@
 	 *
 	 * The [class@Purple.Person] that this contact belongs to.
 	 *
-	 * Since: 3.0.0
+	 * Since: 3.0
 	 */
 	properties[PROP_PERSON] = g_param_spec_object(
 		"person", "person",
@@ -542,7 +542,7 @@
 	 *
 	 * The permission level for the contact.
 	 *
-	 * Since: 3.0.0
+	 * Since: 3.0
 	 */
 	properties[PROP_PERMISSION] = g_param_spec_enum(
 		"permission", "permission",
@@ -560,7 +560,7 @@
 	 * This could be used to hold the `ident` for an IRC contact, the
 	 * `resource` for an XMPP contact, or something similar.
 	 *
-	 * Since: 3.0.0
+	 * Since: 3.0
 	 */
 	properties[PROP_SID] = g_param_spec_string(
 		"sid", "sid",
@@ -587,7 +587,7 @@
 	 *  * [property@Purple.ContactInfo:username]
 	 *  * [property@Purple.ContactInfo:id]
 	 *
-	 * Since: 3.0.0
+	 * Since: 3.0
 	 */
 	properties[PROP_NAME_FOR_DISPLAY] = g_param_spec_string(
 		"name-for-display", "name-for-display",
@@ -610,7 +610,7 @@
 	 * something like `presence-changed::message` and your callback will only
 	 * be called when the message property of @presence has been changed.
 	 *
-	 * Since: 3.0.0
+	 * Since: 3.0
 	 */
 	signals[SIG_PRESENCE_CHANGED] = g_signal_new_class_handler(
 		"presence-changed",
@@ -637,7 +637,7 @@
 	 * can add additional items, hide items, and lay them out however it
 	 * pleases.
 	 *
-	 * Since: 3.0.0
+	 * Since: 3.0
 	 */
 	signals[SIG_POPULATE_MENU] = g_signal_new_class_handler(
 		"populate-menu",
diff --git a/libpurple/purplecontactmanager.h b/libpurple/purplecontactmanager.h
--- a/libpurple/purplecontactmanager.h
+++ b/libpurple/purplecontactmanager.h
@@ -43,7 +43,7 @@
  *
  * A manager for [class@Purple.Contact]s.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 
 /**
@@ -56,7 +56,7 @@
  *
  * Returns: (transfer none): The default [class@Purple.ContactManager] instance.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 PURPLE_AVAILABLE_IN_3_0
 PurpleContactManager *purple_contact_manager_get_default(void);
@@ -69,7 +69,7 @@
  * Adds @contact to @manager. If a contact with a matching account and id
  * already exists, no action will be taken.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 PURPLE_AVAILABLE_IN_3_0
 void purple_contact_manager_add(PurpleContactManager *manager, PurpleContact *contact);
@@ -84,7 +84,7 @@
  * Returns: If @contact is found and removed %TRUE will be returned otherwise
  *          %FALSE will be returned.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 PURPLE_AVAILABLE_IN_3_0
 gboolean purple_contact_manager_remove(PurpleContactManager *manager, PurpleContact *contact);
@@ -98,7 +98,7 @@
  *
  * Returns: %TRUE if anything was removed, %FALSE if nothing was removed.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 PURPLE_AVAILABLE_IN_3_0
 gboolean purple_contact_manager_remove_all(PurpleContactManager *manager, PurpleAccount *account);
@@ -113,7 +113,7 @@
  * Returns: (transfer none) (nullable): A [iface@Gio.ListModel] of all the
  *          contacts belonging to @account.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 PURPLE_AVAILABLE_IN_3_0
 GListModel *purple_contact_manager_get_all(PurpleContactManager *manager, PurpleAccount *account);
@@ -130,7 +130,7 @@
  * Returns: (transfer full): The [class@Purple.Contact] if found, otherwise
  *          %NULL.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 PURPLE_AVAILABLE_IN_3_0
 PurpleContact *purple_contact_manager_find_with_username(PurpleContactManager *manager, PurpleAccount *account, const gchar *username);
@@ -146,7 +146,7 @@
  * Returns: (transfer full): The [class@Purple.Contact] if found, otherwise
  *          %NULL.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 PURPLE_AVAILABLE_IN_3_0
 PurpleContact *purple_contact_manager_find_with_id(PurpleContactManager *manager, PurpleAccount *account, const gchar *id);
@@ -163,7 +163,7 @@
  * This method is meant to help us transition to the new API and this method
  * shouldn't be used elsewhere.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 PURPLE_DEPRECATED
 void purple_contact_manager_add_buddy(PurpleContactManager *manager, PurpleBuddy *buddy);
@@ -180,7 +180,7 @@
  * automatically add the [class@Purple.Person] instance when
  * [method@Purple.ContactManager.add] is called.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 PURPLE_AVAILABLE_IN_3_0
 void purple_contact_manager_add_person(PurpleContactManager *manager, PurplePerson *person);
@@ -195,7 +195,7 @@
  * Removes @person from @manager optionally removing all of the contacts
  * contained in @person as well if @remove_contacts is %TRUE.
  *
- * Since: 3.0.0
+ * Since: 3.0
  */
 PURPLE_AVAILABLE_IN_3_0
 void purple_contact_manager_remove_person(PurpleContactManager *manager, PurplePerson *person, gboolean remove_contacts);
diff --git a/libpurple/purplecontactmanager.c b/libpurple/purplecontactmanager.c
--- a/libpurple/purplecontactmanager.c
+++ b/libpurple/purplecontactmanager.c
@@ -18,6 +18,8 @@
 
 #include <glib/gi18n-lib.h>
 
+#include <birb.h>
+
 #include "purplecontactmanager.h"
 
 #include "purplegdkpixbuf.h"
@@ -30,6 +32,7 @@
 	SIG_REMOVED,
 	SIG_PERSON_ADDED,
 	SIG_PERSON_REMOVED,
+	SIG_POPULATE_MENU,
 	N_SIGNALS,
 };
 static guint signals[N_SIGNALS] = {0, };
@@ -191,6 +194,14 @@
 }
 
 static void
+purple_contact_manager_contact_populate_menu_cb(PurpleContactInfo *info,
+                                                GMenu *menu,
+                                                gpointer data)
+{
+	g_signal_emit(data, signals[SIG_POPULATE_MENU], 0, info, menu);
+}
+
+static void
 purple_contact_manager_tags_changed_cb(G_GNUC_UNUSED PurpleTags *tags,
                                        G_GNUC_UNUSED const char *tag,
                                        G_GNUC_UNUSED const char *name,
@@ -295,7 +306,7 @@
 	 *
 	 * Emitted after @contact has been added to @manager.
 	 *
-	 * Since: 3.0.0
+	 * Since: 3.0
 	 */
 	signals[SIG_ADDED] = g_signal_new_class_handler(
 		"added",
@@ -316,7 +327,7 @@
 	 *
 	 * Emitted after @contact has been removed from @manager.
 	 *
-	 * Since: 3.0.0
+	 * Since: 3.0
 	 */
 	signals[SIG_REMOVED] = g_signal_new_class_handler(
 		"removed",
@@ -339,7 +350,7 @@
 	 * when a contact is added via [method@Purple.ContactManager.add] but can
 	 * also happen if [method@Purple.ContactManager.add_person] is called.
 	 *
-	 * Since: 3.0.0
+	 * Since: 3.0
 	 */
 	signals[SIG_PERSON_ADDED] = g_signal_new_class_handler(
 		"person-added",
@@ -361,7 +372,7 @@
 	 * Emitted after @person has been removed from @manager. This typically
 	 * happens when [method@Purple.ContactManager.remove_person] is called.
 	 *
-	 * Since: 3.0.0
+	 * Since: 3.0
 	 */
 	signals[SIG_PERSON_REMOVED] = g_signal_new_class_handler(
 		"person-removed",
@@ -374,6 +385,31 @@
 		G_TYPE_NONE,
 		1,
 		PURPLE_TYPE_PERSON);
+
+	/**
+	 * PurpleContactManager::populate-menu:
+	 * @manager: The instance.
+	 * @info: The [class@ContactInfo] instance.
+	 * @menu: The [class@Gio.Menu] to populate.
+	 *
+	 * This is a propagation of [signal@ContactInfo::populate-menu]. This means
+	 * that your callback will be called for any contact info to populate its
+	 * context menu.
+	 *
+	 * Since: 3.0
+	 */
+	signals[SIG_POPULATE_MENU] = g_signal_new_class_handler(
+		"populate-menu",
+		G_OBJECT_CLASS_TYPE(klass),
+		G_SIGNAL_RUN_LAST,
+		NULL,
+		NULL,
+		NULL,
+		NULL,
+		G_TYPE_NONE,
+		2,
+		PURPLE_TYPE_CONTACT_INFO,
+		BIRB_TYPE_ACTION_MENU);
 }
 
 /******************************************************************************
@@ -455,15 +491,19 @@
 		tags = purple_contact_info_get_tags(info);
 
 		/* Add some notify signals to track changes. */
-		g_signal_connect_object(contact, "notify::alias",
+		g_signal_connect_object(info, "notify::alias",
+		                        G_CALLBACK(purple_contact_manager_contact_update_cb),
+		                        manager, 0);
+		g_signal_connect_object(info, "notify::permission",
 		                        G_CALLBACK(purple_contact_manager_contact_update_cb),
 		                        manager, 0);
-		g_signal_connect_object(contact, "notify::permission",
-		                        G_CALLBACK(purple_contact_manager_contact_update_cb),
-		                        manager, 0);
-		g_signal_connect_object(contact, "notify::person",
+		g_signal_connect_object(info, "notify::person",
 		                        G_CALLBACK(purple_contact_manager_contact_person_changed_cb),
 		                        manager, 0);
+		g_signal_connect_object(info, "populate-menu",
+		                        G_CALLBACK(purple_contact_manager_contact_populate_menu_cb),
+		                        manager, 0);
+
 		g_signal_connect_object(tags, "added",
 		                        G_CALLBACK(purple_contact_manager_tags_changed_cb),
 		                        contact, 0);
diff --git a/libpurple/tests/test_contact_manager.c b/libpurple/tests/test_contact_manager.c
--- a/libpurple/tests/test_contact_manager.c
+++ b/libpurple/tests/test_contact_manager.c
@@ -20,6 +20,8 @@
 
 #include <purple.h>
 
+#include <birb.h>
+
 #include "test_ui.h"
 
 /******************************************************************************
@@ -466,6 +468,64 @@
 }
 
 /******************************************************************************
+ * propagate_populate_menu tests
+ *****************************************************************************/
+static void
+test_purple_contact_manager_propagate_populate_menu_cb(PurpleContactManager *manager,
+                                                       PurpleContactInfo *info,
+                                                       BirbActionMenu *action_menu,
+                                                       gpointer data)
+{
+	GMenu *menu = NULL;
+	guint *counter = data;
+
+	g_assert_true(PURPLE_IS_CONTACT_MANAGER(manager));
+	g_assert_true(PURPLE_IS_CONTACT_INFO(info));
+	g_assert_true(BIRB_IS_ACTION_MENU(action_menu));
+
+	menu = birb_action_menu_get_menu(action_menu);
+	g_menu_append(menu, "test", "test.action");
+
+	/* Increment our counter. */
+	*counter = *counter + 1;
+}
+
+static void
+test_purple_contact_manager_propagate_populate_menu(void) {
+	PurpleAccount *account = NULL;
+	PurpleContactManager *manager = NULL;
+	PurpleContact *contact = NULL;
+	BirbActionMenu *action_menu = NULL;
+	GMenu *menu = NULL;
+	guint counter = 0;
+
+	account = purple_account_new("test", "test");
+
+	manager = g_object_new(PURPLE_TYPE_CONTACT_MANAGER, NULL);
+	g_signal_connect(manager, "populate-menu",
+	                 G_CALLBACK(test_purple_contact_manager_propagate_populate_menu_cb),
+	                 &counter);
+
+	contact = purple_contact_new(account, NULL);
+	purple_contact_manager_add(manager, contact);
+
+	action_menu = purple_contact_info_get_menu(PURPLE_CONTACT_INFO(contact));
+	g_assert_true(BIRB_IS_ACTION_MENU(action_menu));
+
+	menu = birb_action_menu_get_menu(action_menu);
+	g_assert_true(G_IS_MENU(menu));
+
+	g_assert_cmpuint(counter, ==, 1);
+	g_assert_cmpuint(g_menu_model_get_n_items(G_MENU_MODEL(menu)), ==, 1);
+
+	g_assert_finalize_object(action_menu);
+
+	g_clear_object(&account);
+	g_clear_object(&manager);
+	g_clear_object(&contact);
+}
+
+/******************************************************************************
  * Main
  *****************************************************************************/
 gint
@@ -501,6 +561,9 @@
 	g_test_add_func("/contact-manager/person/add-via-contact-remove-person-with-contacts",
 	                test_purple_contact_manager_person_add_via_contact_remove_person_with_contacts);
 
+	g_test_add_func("/contact-manager/propagate-populate-menu",
+	                test_purple_contact_manager_propagate_populate_menu);
+
 	ret = g_test_run();
 
 	test_ui_purple_uninit();
