Index: src/bp-activity/bp-activity-actions.php
===================================================================
--- src/bp-activity/bp-activity-actions.php
+++ src/bp-activity/bp-activity-actions.php
@@ -19,7 +19,6 @@
  *
  */
 function bp_register_activity_actions() {
-
 	/**
 	 * Fires on bp_init to allow core components and dependent plugins to register activity actions.
 	 *
@@ -33,79 +32,25 @@
  * Catch and route requests for single activity item permalinks.
  *
  * @since 1.2.0
- *
- * @return bool False on failure.
  */
 function bp_activity_action_permalink_router() {
-
 	// Not viewing activity.
-	if ( ! bp_is_activity_component() || ! bp_is_current_action( 'p' ) )
-		return false;
-
-	// No activity to display.
-	if ( ! bp_action_variable( 0 ) || ! is_numeric( bp_action_variable( 0 ) ) )
-		return false;
-
-	// Get the activity details.
-	$activity = bp_activity_get_specific( array( 'activity_ids' => bp_action_variable( 0 ), 'show_hidden' => true ) );
-
-	// 404 if activity does not exist
-	if ( empty( $activity['activities'][0] ) ) {
-		bp_do_404();
+	if ( ! bp_is_activity_component() || ! bp_is_current_action( 'p' ) ) {
 		return;
-	} else {
-		$activity = $activity['activities'][0];
-	}
-
-	// Do not redirect at default.
-	$redirect = false;
-
-	// Redirect based on the type of activity.
-	if ( bp_is_active( 'groups' ) && $activity->component == buddypress()->groups->id ) {
-
-		// Activity is a user update.
-		if ( ! empty( $activity->user_id ) ) {
-			$redirect = bp_core_get_user_domain( $activity->user_id, $activity->user_nicename, $activity->user_login ) . bp_get_activity_slug() . '/' . $activity->id . '/';
-
-		// Activity is something else.
-		} else {
-
-			// Set redirect to group activity stream.
-			if ( $group = groups_get_group( $activity->item_id ) ) {
-				$redirect = bp_get_group_permalink( $group ) . bp_get_activity_slug() . '/' . $activity->id . '/';
-			}
-		}
-
-	// Set redirect to users' activity stream.
-	} elseif ( ! empty( $activity->user_id ) ) {
-		$redirect = bp_core_get_user_domain( $activity->user_id, $activity->user_nicename, $activity->user_login ) . bp_get_activity_slug() . '/' . $activity->id . '/';
 	}
 
-	// If set, add the original query string back onto the redirect URL.
-	if ( ! empty( $_SERVER['QUERY_STRING'] ) ) {
-		$query_frags = array();
-		wp_parse_str( $_SERVER['QUERY_STRING'], $query_frags );
-		$redirect = add_query_arg( urlencode_deep( $query_frags ), $redirect );
-	}
-
-	/**
-	 * Filter the intended redirect url before the redirect occurs for the single activity item.
-	 *
-	 * @since 1.2.2
-	 *
-	 * @param array $value Array with url to redirect to and activity related to the redirect.
-	 */
-	if ( ! $redirect = apply_filters_ref_array( 'bp_activity_permalink_redirect_url', array( $redirect, &$activity ) ) ) {
-		bp_core_redirect( bp_get_root_domain() );
+	// No activity to display.
+	if ( ! bp_action_variable( 0 ) || ! is_numeric( bp_action_variable( 0 ) ) ) {
+		return;
 	}
 
-	// Redirect to the actual activity permalink page.
-	bp_core_redirect( $redirect );
+	$action = new BP_Activity_Action_Permalink_Router();
+	$action->init();
 }
 add_action( 'bp_actions', 'bp_activity_action_permalink_router' );
 
 /**
- * Delete specific activity item and redirect to previous page.
+ * Delete activity item handler when JS is disabled.
  *
  * @since 1.1.0
  *
@@ -113,271 +58,85 @@
  * @return bool False on failure. 
  */
 function bp_activity_action_delete_activity( $activity_id = 0 ) {
-
 	// Not viewing activity or action is not delete.
-	if ( !bp_is_activity_component() || !bp_is_current_action( 'delete' ) )
+	if ( ! bp_is_activity_component() || ! bp_is_current_action( 'delete' ) ) {
 		return false;
+	}
 
-	if ( empty( $activity_id ) && bp_action_variable( 0 ) )
+	if ( empty( $activity_id ) && bp_action_variable( 0 ) ) {
 		$activity_id = (int) bp_action_variable( 0 );
+	}
 
 	// Not viewing a specific activity item.
-	if ( empty( $activity_id ) )
-		return false;
-
-	// Check the nonce.
-	check_admin_referer( 'bp_activity_delete_link' );
-
-	// Load up the activity item.
-	$activity = new BP_Activity_Activity( $activity_id );
-
-	// Check access.
-	if ( ! bp_activity_user_can_delete( $activity ) )
+	if ( empty( $activity_id ) ) {
 		return false;
+	}
 
-	/**
-	 * Fires before the deletion so plugins can still fetch information about it.
-	 *
-	 * @since 1.5.0
-	 *
-	 * @param int $activity_id The activity ID.
-	 * @param int $user_id     The user associated with the activity.
-	 */
-	do_action( 'bp_activity_before_action_delete_activity', $activity_id, $activity->user_id );
-
-	// Delete the activity item and provide user feedback.
-	if ( bp_activity_delete( array( 'id' => $activity_id, 'user_id' => $activity->user_id ) ) )
-		bp_core_add_message( __( 'Activity deleted successfully', 'buddypress' ) );
-	else
-		bp_core_add_message( __( 'There was an error when deleting that activity', 'buddypress' ), 'error' );
-
-	/**
-	 * Fires after the deletion so plugins can act afterwards based on the activity.
-	 *
-	 * @since 1.1.0
-	 *
-	 * @param int $activity_id The activity ID.
-	 * @param int $user_id     The user associated with the activity.
-	 */
-	do_action( 'bp_activity_action_delete_activity', $activity_id, $activity->user_id );
-
-	// Check for the redirect query arg, otherwise let WP handle things.
-	if ( !empty( $_GET['redirect_to'] ) )
-		bp_core_redirect( esc_url( $_GET['redirect_to'] ) );
-	else
-		bp_core_redirect( wp_get_referer() );
+	$action = new BP_Activity_Action_Delete();
+	return $action->init( $activity_id );
 }
 add_action( 'bp_actions', 'bp_activity_action_delete_activity' );
 
 /**
- * Mark specific activity item as spam and redirect to previous page.
+ * Mark activity item as spam handler when JS is disabled.
  *
  * @since 1.6.0
  *
  * @param int $activity_id Activity id to be deleted. Defaults to 0.
- * @return bool False on failure.
+ * @return bool|void False on failure.
  */
 function bp_activity_action_spam_activity( $activity_id = 0 ) {
-	$bp = buddypress();
-
 	// Not viewing activity, or action is not spam, or Akismet isn't present.
-	if ( !bp_is_activity_component() || !bp_is_current_action( 'spam' ) || empty( $bp->activity->akismet ) )
+	if ( ! bp_is_activity_component() || ! bp_is_current_action( 'spam' ) || empty( buddypress()->activity->akismet ) ) {
 		return false;
+	}
 
-	if ( empty( $activity_id ) && bp_action_variable( 0 ) )
+	if ( empty( $activity_id ) && bp_action_variable( 0 ) ) {
 		$activity_id = (int) bp_action_variable( 0 );
+	}
 
 	// Not viewing a specific activity item.
-	if ( empty( $activity_id ) )
-		return false;
-
-	// Is the current user allowed to spam items?
-	if ( !bp_activity_user_can_mark_spam() )
-		return false;
-
-	// Load up the activity item.
-	$activity = new BP_Activity_Activity( $activity_id );
-	if ( empty( $activity->id ) )
+	if ( empty( $activity_id ) ) {
 		return false;
+	}
 
-	// Check nonce.
-	check_admin_referer( 'bp_activity_akismet_spam_' . $activity->id );
-
-	/**
-	 * Fires before the marking activity as spam so plugins can modify things if they want to.
-	 *
-	 * @since 1.6.0
-	 *
-	 * @param int    $activity_id Activity ID to be marked as spam.
-	 * @param object $activity    Activity object for the ID to be marked as spam.
-	 */
-	do_action( 'bp_activity_before_action_spam_activity', $activity->id, $activity );
-
-	// Mark as spam.
-	bp_activity_mark_as_spam( $activity );
-	$activity->save();
-
-	// Tell the user the spamming has been successful.
-	bp_core_add_message( __( 'The activity item has been marked as spam and is no longer visible.', 'buddypress' ) );
-
-	/**
-	 * Fires after the marking activity as spam so plugins can act afterwards based on the activity.
-	 *
-	 * @since 1.6.0
-	 *
-	 * @param int $activity_id Activity ID that was marked as spam.
-	 * @param int $user_id     User ID associated with activity.
-	 */
-	do_action( 'bp_activity_action_spam_activity', $activity_id, $activity->user_id );
-
-	// Check for the redirect query arg, otherwise let WP handle things.
-	if ( !empty( $_GET['redirect_to'] ) )
-		bp_core_redirect( esc_url( $_GET['redirect_to'] ) );
-	else
-		bp_core_redirect( wp_get_referer() );
+	$action = new BP_Activity_Action_Spam();
+	return $action->init( $activity_id );
 }
 add_action( 'bp_actions', 'bp_activity_action_spam_activity' );
 
 /**
- * Post user/group activity update.
+ * Post user/group activity update handler when JS is disabled.
  *
  * @since 1.2.0
  *
  * @return bool False on failure.
  */
 function bp_activity_action_post_update() {
-
 	// Do not proceed if user is not logged in, not viewing activity, or not posting.
-	if ( !is_user_logged_in() || !bp_is_activity_component() || !bp_is_current_action( 'post' ) )
-		return false;
-
-	// Check the nonce.
-	check_admin_referer( 'post_update', '_wpnonce_post_update' );
-
-	/**
-	 * Filters the content provided in the activity input field.
-	 *
-	 * @since 1.2.0
-	 *
-	 * @param string $value Activity message being posted.
-	 */
-	$content = apply_filters( 'bp_activity_post_update_content', $_POST['whats-new'] );
-
-	if ( ! empty( $_POST['whats-new-post-object'] ) ) {
-
-		/**
-		 * Filters the item type that the activity update should be associated with.
-		 *
-		 * @since 1.2.0
-		 *
-		 * @param string $value Item type to associate with.
-		 */
-		$object = apply_filters( 'bp_activity_post_update_object', $_POST['whats-new-post-object'] );
-	}
-
-	if ( ! empty( $_POST['whats-new-post-in'] ) ) {
-
-		/**
-		 * Filters what component the activity is being to.
-		 *
-		 * @since 1.2.0
-		 *
-		 * @param string $value Chosen component to post activity to.
-		 */
-		$item_id = apply_filters( 'bp_activity_post_update_item_id', $_POST['whats-new-post-in'] );
-	}
-
-	// No activity content so provide feedback and redirect.
-	if ( empty( $content ) ) {
-		bp_core_add_message( __( 'Please enter some content to post.', 'buddypress' ), 'error' );
-		bp_core_redirect( wp_get_referer() );
-	}
-
-	// No existing item_id.
-	if ( empty( $item_id ) ) {
-		$activity_id = bp_activity_post_update( array( 'content' => $content ) );
-
-	// Post to groups object.
-	} elseif ( 'groups' == $object && bp_is_active( 'groups' ) ) {
-		if ( (int) $item_id ) {
-			$activity_id = groups_post_update( array( 'content' => $content, 'group_id' => $item_id ) );
-		}
-
-	} else {
-
-		/**
-		 * Filters activity object for BuddyPress core and plugin authors before posting activity update.
-		 *
-		 * @since 1.2.0
-		 *
-		 * @param string $object  Activity item being associated to.
-		 * @param string $item_id Component ID being posted to.
-		 * @param string $content Activity content being posted.
-		 */
-		$activity_id = apply_filters( 'bp_activity_custom_update', $object, $item_id, $content );
+	if ( ! is_user_logged_in() || ! bp_is_activity_component() || ! bp_is_current_action( 'post' ) ) {
+		return;
 	}
 
-	// Provide user feedback.
-	if ( !empty( $activity_id ) )
-		bp_core_add_message( __( 'Update Posted!', 'buddypress' ) );
-	else
-		bp_core_add_message( __( 'There was an error when posting your update. Please try again.', 'buddypress' ), 'error' );
-
-	// Redirect.
-	bp_core_redirect( wp_get_referer() );
+	$action = new BP_Activity_Action_Post_Update();
+	$action->init();
 }
 add_action( 'bp_actions', 'bp_activity_action_post_update' );
 
 /**
- * Post new activity comment.
+ * Post new activity comment handler when JS is disabled.
  *
  * @since 1.2.0
  *
  * @return bool False on failure.
  */
 function bp_activity_action_post_comment() {
-
-	if ( !is_user_logged_in() || !bp_is_activity_component() || !bp_is_current_action( 'reply' ) )
-		return false;
-
-	// Check the nonce.
-	check_admin_referer( 'new_activity_comment', '_wpnonce_new_activity_comment' );
-
-	/**
-	 * Filters the activity ID a comment will be in reply to.
-	 *
-	 * @since 1.2.0
-	 *
-	 * @param string $value ID of the activity being replied to.
-	 */
-	$activity_id = apply_filters( 'bp_activity_post_comment_activity_id', $_POST['comment_form_id'] );
-
-	/**
-	 * Filters the comment content for a comment reply.
-	 *
-	 * @since 1.2.0
-	 *
-	 * @param string $value Comment content being posted.
-	 */
-	$content = apply_filters( 'bp_activity_post_comment_content', $_POST['ac_input_' . $activity_id] );
-
-	if ( empty( $content ) ) {
-		bp_core_add_message( __( 'Please do not leave the comment area blank.', 'buddypress' ), 'error' );
-		bp_core_redirect( wp_get_referer() . '#ac-form-' . $activity_id );
+	if ( ! is_user_logged_in() || ! bp_is_activity_component() || ! bp_is_current_action( 'reply' ) ) {
+		return;
 	}
 
-	$comment_id = bp_activity_new_comment( array(
-		'content'     => $content,
-		'activity_id' => $activity_id,
-		'parent_id'   => false
-	));
-
-	if ( !empty( $comment_id ) )
-		bp_core_add_message( __( 'Reply Posted!', 'buddypress' ) );
-	else
-		bp_core_add_message( __( 'There was an error posting that reply. Please try again.', 'buddypress' ), 'error' );
-
-	bp_core_redirect( wp_get_referer() . '#ac-form-' . $activity_id );
+	$action = new BP_Activity_Action_Post_Comment();
+	$action->init();
 }
 add_action( 'bp_actions', 'bp_activity_action_post_comment' );
 
@@ -385,23 +144,14 @@
  * Mark activity as favorite.
  *
  * @since 1.2.0
- *
- * @return bool False on failure.
  */
 function bp_activity_action_mark_favorite() {
+	if ( ! is_user_logged_in() || ! bp_is_activity_component() || ! bp_is_current_action( 'favorite' ) ) {
+		return;
+	}
 
-	if ( !is_user_logged_in() || !bp_is_activity_component() || !bp_is_current_action( 'favorite' ) )
-		return false;
-
-	// Check the nonce.
-	check_admin_referer( 'mark_favorite' );
-
-	if ( bp_activity_add_user_favorite( bp_action_variable( 0 ) ) )
-		bp_core_add_message( __( 'Activity marked as favorite.', 'buddypress' ) );
-	else
-		bp_core_add_message( __( 'There was an error marking that activity as a favorite. Please try again.', 'buddypress' ), 'error' );
-
-	bp_core_redirect( wp_get_referer() . '#activity-' . bp_action_variable( 0 ) );
+	$action = new BP_Activity_Action_Favorite();
+	$action->init();
 }
 add_action( 'bp_actions', 'bp_activity_action_mark_favorite' );
 
@@ -413,226 +163,25 @@
  * @return bool False on failure.
  */
 function bp_activity_action_remove_favorite() {
-
-	if ( ! is_user_logged_in() || ! bp_is_activity_component() || ! bp_is_current_action( 'unfavorite' ) )
-		return false;
-
-	// Check the nonce.
-	check_admin_referer( 'unmark_favorite' );
-
-	if ( bp_activity_remove_user_favorite( bp_action_variable( 0 ) ) )
-		bp_core_add_message( __( 'Activity removed as favorite.', 'buddypress' ) );
-	else
-		bp_core_add_message( __( 'There was an error removing that activity as a favorite. Please try again.', 'buddypress' ), 'error' );
-
-	bp_core_redirect( wp_get_referer() . '#activity-' . bp_action_variable( 0 ) );
-}
-add_action( 'bp_actions', 'bp_activity_action_remove_favorite' );
-
-/**
- * Load the sitewide activity feed.
- *
- * @since 1.0.0
- *
- * @return bool False on failure.
- */
-function bp_activity_action_sitewide_feed() {
-	$bp = buddypress();
-
-	if ( ! bp_is_activity_component() || ! bp_is_current_action( 'feed' ) || bp_is_user() || ! empty( $bp->groups->current_group ) )
-		return false;
-
-	// Setup the feed.
-	buddypress()->activity->feed = new BP_Activity_Feed( array(
-		'id'            => 'sitewide',
-
-		/* translators: Sitewide activity RSS title - "[Site Name] | Site Wide Activity" */
-		'title'         => sprintf( __( '%s | Site-Wide Activity', 'buddypress' ), bp_get_site_name() ),
-
-		'link'          => bp_get_activity_directory_permalink(),
-		'description'   => __( 'Activity feed for the entire site.', 'buddypress' ),
-		'activity_args' => 'display_comments=threaded'
-	) );
-}
-add_action( 'bp_actions', 'bp_activity_action_sitewide_feed' );
-
-/**
- * Load a user's personal activity feed.
- *
- * @since 1.0.0
- *
- * @return bool False on failure.
- */
-function bp_activity_action_personal_feed() {
-	if ( ! bp_is_user_activity() || ! bp_is_current_action( 'feed' ) ) {
-		return false;
-	}
-
-	// Setup the feed.
-	buddypress()->activity->feed = new BP_Activity_Feed( array(
-		'id'            => 'personal',
-
-		/* translators: Personal activity RSS title - "[Site Name] | [User Display Name] | Activity" */
-		'title'         => sprintf( __( '%1$s | %2$s | Activity', 'buddypress' ), bp_get_site_name(), bp_get_displayed_user_fullname() ),
-
-		'link'          => trailingslashit( bp_displayed_user_domain() . bp_get_activity_slug() ),
-		'description'   => sprintf( __( 'Activity feed for %s.', 'buddypress' ), bp_get_displayed_user_fullname() ),
-		'activity_args' => 'user_id=' . bp_displayed_user_id()
-	) );
-}
-add_action( 'bp_actions', 'bp_activity_action_personal_feed' );
-
-/**
- * Load a user's friends' activity feed.
- *
- * @since 1.0.0
- *
- * @return bool False on failure.
- */
-function bp_activity_action_friends_feed() {
-	if ( ! bp_is_active( 'friends' ) || ! bp_is_user_activity() || ! bp_is_current_action( bp_get_friends_slug() ) || ! bp_is_action_variable( 'feed', 0 ) ) {
-		return false;
-	}
-
-	// Setup the feed.
-	buddypress()->activity->feed = new BP_Activity_Feed( array(
-		'id'            => 'friends',
-
-		/* translators: Friends activity RSS title - "[Site Name] | [User Display Name] | Friends Activity" */
-		'title'         => sprintf( __( '%1$s | %2$s | Friends Activity', 'buddypress' ), bp_get_site_name(), bp_get_displayed_user_fullname() ),
-
-		'link'          => trailingslashit( bp_displayed_user_domain() . bp_get_activity_slug() . '/' . bp_get_friends_slug() ),
-		'description'   => sprintf( __( "Activity feed for %s's friends.", 'buddypress' ), bp_get_displayed_user_fullname() ),
-		'activity_args' => 'scope=friends'
-	) );
-}
-add_action( 'bp_actions', 'bp_activity_action_friends_feed' );
-
-/**
- * Load the activity feed for a user's groups.
- *
- * @since 1.2.0
- *
- * @return bool False on failure.
- */
-function bp_activity_action_my_groups_feed() {
-	if ( ! bp_is_active( 'groups' ) || ! bp_is_user_activity() || ! bp_is_current_action( bp_get_groups_slug() ) || ! bp_is_action_variable( 'feed', 0 ) ) {
-		return false;
-	}
-
-	// Get displayed user's group IDs.
-	$groups    = groups_get_user_groups();
-	$group_ids = implode( ',', $groups['groups'] );
-
-	// Setup the feed.
-	buddypress()->activity->feed = new BP_Activity_Feed( array(
-		'id'            => 'mygroups',
-
-		/* translators: Member groups activity RSS title - "[Site Name] | [User Display Name] | Groups Activity" */
-		'title'         => sprintf( __( '%1$s | %2$s | Group Activity', 'buddypress' ), bp_get_site_name(), bp_get_displayed_user_fullname() ),
-
-		'link'          => trailingslashit( bp_displayed_user_domain() . bp_get_activity_slug() . '/' . bp_get_groups_slug() ),
-		'description'   => sprintf( __( "Public group activity feed of which %s is a member.", 'buddypress' ), bp_get_displayed_user_fullname() ),
-		'activity_args' => array(
-			'object'           => buddypress()->groups->id,
-			'primary_id'       => $group_ids,
-			'display_comments' => 'threaded'
-		)
-	) );
-}
-add_action( 'bp_actions', 'bp_activity_action_my_groups_feed' );
-
-/**
- * Load a user's @mentions feed.
- *
- * @since 1.2.0
- *
- * @return bool False on failure.
- */
-function bp_activity_action_mentions_feed() {
-	if ( ! bp_activity_do_mentions() ) {
-		return false;
-	}
-
-	if ( !bp_is_user_activity() || ! bp_is_current_action( 'mentions' ) || ! bp_is_action_variable( 'feed', 0 ) ) {
-		return false;
+	if ( ! is_user_logged_in() || ! bp_is_activity_component() || ! bp_is_current_action( 'unfavorite' ) ) {
+		return;
 	}
 
-	// Setup the feed.
-	buddypress()->activity->feed = new BP_Activity_Feed( array(
-		'id'            => 'mentions',
-
-		/* translators: User mentions activity RSS title - "[Site Name] | [User Display Name] | Mentions" */
-		'title'         => sprintf( __( '%1$s | %2$s | Mentions', 'buddypress' ), bp_get_site_name(), bp_get_displayed_user_fullname() ),
-
-		'link'          => bp_displayed_user_domain() . bp_get_activity_slug() . '/mentions/',
-		'description'   => sprintf( __( "Activity feed mentioning %s.", 'buddypress' ), bp_get_displayed_user_fullname() ),
-		'activity_args' => array(
-			'search_terms' => '@' . bp_core_get_username( bp_displayed_user_id() )
-		)
-	) );
+	$action = BP_Activity_Action_Unfavorite();
+	$action->init();
 }
-add_action( 'bp_actions', 'bp_activity_action_mentions_feed' );
-
-/**
- * Load a user's favorites feed.
- *
- * @since 1.2.0
- *
- * @return bool False on failure.
- */
-function bp_activity_action_favorites_feed() {
-	if ( ! bp_is_user_activity() || ! bp_is_current_action( 'favorites' ) || ! bp_is_action_variable( 'feed', 0 ) ) {
-		return false;
-	}
-
-	// Get displayed user's favorite activity IDs.
-	$favs = bp_activity_get_user_favorites( bp_displayed_user_id() );
-	$fav_ids = implode( ',', (array) $favs );
-
-	// Setup the feed.
-	buddypress()->activity->feed = new BP_Activity_Feed( array(
-		'id'            => 'favorites',
-
-		/* translators: User activity favorites RSS title - "[Site Name] | [User Display Name] | Favorites" */
-		'title'         => sprintf( __( '%1$s | %2$s | Favorites', 'buddypress' ), bp_get_site_name(), bp_get_displayed_user_fullname() ),
-
-		'link'          => bp_displayed_user_domain() . bp_get_activity_slug() . '/favorites/',
-		'description'   => sprintf( __( "Activity feed of %s's favorites.", 'buddypress' ), bp_get_displayed_user_fullname() ),
-		'activity_args' => 'include=' . $fav_ids
-	) );
-}
-add_action( 'bp_actions', 'bp_activity_action_favorites_feed' );
+add_action( 'bp_actions', 'bp_activity_action_remove_favorite' );
 
 /**
  * AJAX endpoint for Suggestions API lookups.
  *
  * @since 2.1.0
+ *
+ * @todo This should be moved out of Activity and into Core or Members.
  */
 function bp_ajax_get_suggestions() {
-	if ( ! bp_is_user_active() || empty( $_GET['term'] ) || empty( $_GET['type'] ) ) {
-		wp_send_json_error( 'missing_parameter' );
-		exit;
-	}
-
-	$args = array(
-		'term' => sanitize_text_field( $_GET['term'] ),
-		'type' => sanitize_text_field( $_GET['type'] ),
-	);
-
-	// Support per-Group suggestions.
-	if ( ! empty( $_GET['group-id'] ) ) {
-		$args['group_id'] = absint( $_GET['group-id'] );
-	}
-
-	$results = bp_core_get_suggestions( $args );
-
-	if ( is_wp_error( $results ) ) {
-		wp_send_json_error( $results->get_error_message() );
-		exit;
-	}
-
-	wp_send_json_success( $results );
+	$action = new BP_Core_Ajax_Get_Suggestions();
+	$action->init();
 }
 add_action( 'wp_ajax_bp_get_suggestions', 'bp_ajax_get_suggestions' );
 
@@ -648,83 +197,8 @@
  * @param object $post       Post data.
  */
 function bp_activity_catch_transition_post_type_status( $new_status, $old_status, $post ) {
-	if ( ! post_type_supports( $post->post_type, 'buddypress-activity' ) ) {
-		return;
-	}
-
-	// This is an edit.
-	if ( $new_status === $old_status ) {
-		// An edit of an existing post should update the existing activity item.
-		if ( $new_status == 'publish' ) {
-			$edit = bp_activity_post_type_update( $post );
-
-			// Post was never recorded into activity stream, so record it now!
-			if ( null === $edit ) {
-				bp_activity_post_type_publish( $post->ID, $post );
-			}
-
-		// Allow plugins to eventually deal with other post statuses.
-		} else {
-			/**
-			 * Fires when editing the post and the new status is not 'publish'.
-			 *
-			 * This is a variable filter that is dependent on the post type
-			 * being untrashed.
-			 *
-			 * @since 2.5.0
-			 *
-			 * @param WP_Post $post Post data.
-			 * @param string $new_status New status for the post.
-			 * @param string $old_status Old status for the post.
-			 */
-			do_action( 'bp_activity_post_type_edit_' . $post->post_type, $post, $new_status, $old_status );
-		}
-
-		return;
-	}
-
-	// Publishing a previously unpublished post.
-	if ( 'publish' === $new_status ) {
-		// Untrashing the post type - nothing here yet.
-		if ( 'trash' == $old_status ) {
-
-			/**
-			 * Fires if untrashing post in a post type.
-			 *
-			 * This is a variable filter that is dependent on the post type
-			 * being untrashed.
-			 *
-			 * @since 2.2.0
-			 *
-			 * @param WP_Post $post Post data.
-			 */
-			do_action( 'bp_activity_post_type_untrash_' . $post->post_type, $post );
-		} else {
-			// Record the post.
-			bp_activity_post_type_publish( $post->ID, $post );
-		}
-
-	// Unpublishing a previously published post.
-	} elseif ( 'publish' === $old_status ) {
-		// Some form of pending status - only remove the activity entry.
-		bp_activity_post_type_unpublish( $post->ID, $post );
-
-	// For any other cases, allow plugins to eventually deal with it.
-	} else {
-		/**
-		 * Fires when the old and the new post status are not 'publish'.
-		 *
-		 * This is a variable filter that is dependent on the post type
-		 * being untrashed.
-		 *
-		 * @since 2.5.0
-		 *
-		 * @param WP_Post $post Post data.
-		 * @param string $new_status New status for the post.
-		 * @param string $old_status Old status for the post.
-		 */
-		do_action( 'bp_activity_post_type_transition_status_' . $post->post_type, $post, $new_status, $old_status );
-	}
+	$action = new BP_Activity_Transition_Post_Status();
+	$action->init( $post, $new_status, $old_status );
 }
 add_action( 'transition_post_status', 'bp_activity_catch_transition_post_type_status', 10, 3 );
 
@@ -738,117 +212,7 @@
  * @param WP_Comment $comment Comment data.
  */
 function bp_activity_transition_post_type_comment_status( $new_status, $old_status, $comment ) {
-	$post_type = get_post_type( $comment->comment_post_ID );
-	if ( ! $post_type ) {
-		return;
-	}
-
-	// Get the post type tracking args.
-	$activity_post_object = bp_activity_get_post_type_tracking_args( $post_type );
-
-	// Bail if the activity type does not exist
-	if ( empty( $activity_post_object->comments_tracking->action_id ) ) {
-		return false;
-
-	// Set the $activity_comment_object
-	} else {
-		$activity_comment_object = $activity_post_object->comments_tracking;
-	}
-
-	// Init an empty activity ID
-	$activity_id = 0;
-
-	/**
-	 * Activity currently doesn't have any concept of a trash, or an unapproved/approved state.
-	 *
-	 * If a blog comment transitions to a "delete" or "hold" status, delete the activity item.
-	 * If a blog comment transitions to trashed, or spammed, mark the activity as spam.
-	 * If a blog comment transitions to approved (and the activity exists), mark the activity as ham.
-	 * If a blog comment transitions to unapproved (and the activity exists), mark the activity as spam.
-	 * Otherwise, record the comment into the activity stream.
-	 */
-
-	// This clause handles delete/hold.
-	if ( in_array( $new_status, array( 'delete', 'hold' ) ) ) {
-		return bp_activity_post_type_remove_comment( $comment->comment_ID, $activity_post_object );
-
-	// These clauses handle trash, spam, and un-spams.
-	} elseif ( in_array( $new_status, array( 'trash', 'spam', 'unapproved' ) ) ) {
-		$action = 'spam_activity';
-	} elseif ( 'approved' == $new_status ) {
-		$action = 'ham_activity';
-	}
-
-	// Get the activity
-	if ( bp_disable_blogforum_comments() ) {
-		$activity_id = bp_activity_get_activity_id( array(
-			'component'         => $activity_comment_object->component_id,
-			'item_id'           => get_current_blog_id(),
-			'secondary_item_id' => $comment->comment_ID,
-			'type'              => $activity_comment_object->action_id,
-		) );
-	} else {
-		$activity_id = get_comment_meta( $comment->comment_ID, 'bp_activity_comment_id', true );
-	}
-
-	/**
-	 * Leave a chance to plugins to manage activity comments differently.
-	 *
-	 * @since  2.5.0
-	 *
-	 * @param bool        $value       True to override BuddyPress management.
-	 * @param string      $post_type   The post type name.
-	 * @param int         $activity_id The post type activity (0 if not found).
-	 * @param string      $new_status  The new status of the post type comment.
-	 * @param string      $old_status  The old status of the post type comment.
-	 * @param WP_Comment  $comment Comment data.
-	 */
-	if ( true === apply_filters( 'bp_activity_pre_transition_post_type_comment_status', false, $post_type, $activity_id, $new_status, $old_status, $comment ) ) {
-		return false;
-	}
-
-	// Check activity item exists
-	if ( empty( $activity_id ) ) {
-		// If no activity exists, but the comment has been approved, record it into the activity table.
-		if ( 'approved' == $new_status ) {
-			return bp_activity_post_type_comment( $comment->comment_ID, true, $activity_post_object );
-		}
-
-		return;
-	}
-
-	// Create an activity object
-	$activity = new BP_Activity_Activity( $activity_id );
-	if ( empty( $activity->component ) ) {
-		return;
-	}
-
-	// Spam/ham the activity if it's not already in that state
-	if ( 'spam_activity' === $action && ! $activity->is_spam ) {
-		bp_activity_mark_as_spam( $activity );
-	} elseif ( 'ham_activity' == $action) {
-		bp_activity_mark_as_ham( $activity );
-	}
-
-	// Add "new_post_type_comment" to the whitelisted activity types, so that the activity's Akismet history is generated
-	$post_type_comment_action = $activity_comment_object->action_id;
-	$comment_akismet_history = function ( $activity_types ) use ( $post_type_comment_action ) {
-		$activity_types[] = $post_type_comment_action;
-
-		return $activity_types;
-	};
-	add_filter( 'bp_akismet_get_activity_types', $comment_akismet_history );
-
-	// Make sure the activity change won't edit the comment if sync is on
-	remove_action( 'bp_activity_before_save', 'bp_blogs_sync_activity_edit_to_post_comment', 20 );
-
-	// Save the updated activity
-	$activity->save();
-
-	// Restore the action
-	add_action( 'bp_activity_before_save', 'bp_blogs_sync_activity_edit_to_post_comment', 20 );
-
-	// Remove the "new_blog_comment" activity type whitelist so we don't break anything
-	remove_filter( 'bp_akismet_get_activity_types', $comment_akismet_history );
+	$action = new BP_Activity_Transition_Comment_status();
+	$action->init( $comment, $new_status, $old_status );
 }
 add_action( 'transition_comment_status', 'bp_activity_transition_post_type_comment_status', 10, 3 );
Index: src/bp-activity/bp-activity-feeds.php
new file mode 100644
===================================================================
--- /dev/null
+++ src/bp-activity/bp-activity-feeds.php
@@ -0,0 +1,99 @@
+<?php
+/**
+ * BuddyPress Activity Feeds.
+ *
+ * @package BuddyPress
+ * @since 3.0.0
+ */
+
+/**
+ * Load the sitewide activity feed.
+ *
+ * @since 1.0.0
+ */
+function bp_activity_action_sitewide_feed() {
+	if ( ! bp_is_activity_component() || ! bp_is_current_action( 'feed' ) || bp_is_user() || ! empty( buddypress()->groups->current_group ) ) {
+		return;
+	}
+
+	$action = new BP_Activity_Action_Feed_Sitewide();
+	$action->init();
+}
+add_action( 'bp_actions', 'bp_activity_action_sitewide_feed' );
+
+/**
+ * Load a user's personal activity feed.
+ *
+ * @since 1.0.0
+ */
+function bp_activity_action_personal_feed() {
+	if ( ! bp_is_user_activity() || ! bp_is_current_action( 'feed' ) ) {
+		return;
+	}
+
+	$action = new BP_Activity_Action_Feed_Personal();
+	$action->init();
+}
+add_action( 'bp_actions', 'bp_activity_action_personal_feed' );
+
+/**
+ * Load a user's friends' activity feed.
+ *
+ * @since 1.0.0
+ *
+ * @return bool False on failure.
+ */
+function bp_activity_action_friends_feed() {
+	if ( ! bp_is_active( 'friends' ) || ! bp_is_user_activity() || ! bp_is_current_action( bp_get_friends_slug() ) || ! bp_is_action_variable( 'feed', 0 ) ) {
+		return;
+	}
+
+	$action = new BP_Activity_Action_Feed_Friends();
+	$action->init();
+}
+add_action( 'bp_actions', 'bp_activity_action_friends_feed' );
+
+/**
+ * Load the activity feed for a user's groups.
+ *
+ * @since 1.2.0
+ */
+function bp_activity_action_my_groups_feed() {
+	if ( ! bp_is_active( 'groups' ) || ! bp_is_user_activity() || ! bp_is_current_action( bp_get_groups_slug() ) || ! bp_is_action_variable( 'feed', 0 ) ) {
+		return;
+	}
+
+	$action = new BP_Activity_Action_Feed_Groups();
+	$action->init();
+}
+add_action( 'bp_actions', 'bp_activity_action_my_groups_feed' );
+
+/**
+ * Load a user's @mentions feed.
+ *
+ * @since 1.2.0
+ */
+function bp_activity_action_mentions_feed() {
+	if ( ! bp_activity_do_mentions() || ! bp_is_user_activity() || ! bp_is_current_action( 'mentions' ) || ! bp_is_action_variable( 'feed', 0 ) ) {
+		return;
+	}
+
+	$action = new BP_Activity_Action_Feed_Mentions();
+	$action->init();
+}
+add_action( 'bp_actions', 'bp_activity_action_mentions_feed' );
+
+/**
+ * Load a user's favorites feed.
+ *
+ * @since 1.2.0
+ */
+function bp_activity_action_favorites_feed() {
+	if ( ! bp_is_user_activity() || ! bp_is_current_action( 'favorites' ) || ! bp_is_action_variable( 'feed', 0 ) ) {
+		return;
+	}
+
+	$action = new BP_Activity_Action_Feed_Favorites();
+	$action->init();
+}
+add_action( 'bp_actions', 'bp_activity_action_favorites_feed' );
\ No newline at end of file
Index: src/bp-activity/bp-activity-screens.php
===================================================================
--- src/bp-activity/bp-activity-screens.php
+++ src/bp-activity/bp-activity-screens.php
@@ -21,25 +21,27 @@
  *
  */
 function bp_activity_screen_index() {
-	if ( bp_is_activity_directory() ) {
-		bp_update_is_directory( true, 'activity' );
+	if ( ! bp_is_activity_directory() ) {
+		return;
+	}
 
-		/**
-		 * Fires right before the loading of the Activity directory screen template file.
-		 *
-		 * @since 1.5.0
-		 */
-		do_action( 'bp_activity_screen_index' );
+	bp_update_is_directory( true, 'activity' );
 
-		/**
-		 * Filters the template to load for the Activity directory screen.
-		 *
-		 * @since 1.5.0
-		 *
-		 * @param string $template Path to the activity template to load.
-		 */
-		bp_core_load_template( apply_filters( 'bp_activity_screen_index', 'activity/index' ) );
-	}
+	/**
+	 * Fires right before the loading of the Activity directory screen template file.
+	 *
+	 * @since 1.5.0
+	 */
+	do_action( 'bp_activity_screen_index' );
+
+	/**
+	 * Filters the template to load for the Activity directory screen.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param string $template Path to the activity template to load.
+	 */
+	bp_core_load_template( apply_filters( 'bp_activity_screen_index', 'activity/index' ) );
 }
 add_action( 'bp_screens', 'bp_activity_screen_index' );
 
@@ -50,7 +52,6 @@
  *
  */
 function bp_activity_screen_my_activity() {
-
 	/**
 	 * Fires right before the loading of the "My Activity" screen template file.
 	 *
@@ -75,8 +76,9 @@
  *
  */
 function bp_activity_screen_friends() {
-	if ( !bp_is_active( 'friends' ) )
-		return false;
+	if ( ! bp_is_active( 'friends' ) ) {
+		return;
+	}
 
 	bp_update_is_item_admin( bp_current_user_can( 'bp_moderate' ), 'activity' );
 
@@ -104,8 +106,9 @@
  *
  */
 function bp_activity_screen_groups() {
-	if ( !bp_is_active( 'groups' ) )
-		return false;
+	if ( ! bp_is_active( 'groups' ) ) {
+		return;
+	}
 
 	bp_update_is_item_admin( bp_current_user_can( 'bp_moderate' ), 'activity' );
 
@@ -185,8 +188,9 @@
  *
  */
 function bp_activity_reset_my_new_mentions() {
-	if ( bp_is_my_profile() )
+	if ( bp_is_my_profile() ) {
 		bp_activity_clear_new_mentions( bp_loggedin_user_id() );
+	}
 }
 add_action( 'bp_activity_screen_mentions', 'bp_activity_reset_my_new_mentions' );
 
@@ -198,88 +202,13 @@
  * @return bool|string Boolean on false or the template for a single activity item on success.
  */
 function bp_activity_screen_single_activity_permalink() {
-	// No displayed user or not viewing activity component.
-	if ( ! bp_is_activity_component() ) {
-		return false;
-	}
-
-	$action = bp_current_action();
-	if ( ! $action || ! is_numeric( $action ) ) {
-		return false;
-	}
-
-	// Get the activity details.
-	$activity = bp_activity_get_specific( array(
-		'activity_ids' => $action,
-		'show_hidden'  => true,
-		'spam'         => 'ham_only',
-	) );
-
-	// 404 if activity does not exist
-	if ( empty( $activity['activities'][0] ) || bp_action_variables() ) {
-		bp_do_404();
+	// Not on an activity permalink page? Bail.
+	if ( ! bp_is_activity_component() || ! bp_current_action() || ! is_numeric( bp_current_action() ) ) {
 		return;
-
-	} else {
-		$activity = $activity['activities'][0];
 	}
 
-	$user_id = bp_displayed_user_id();
-
-	/**
-	 * Check user access to the activity item.
-	 *
-	 * @since 3.0.0
-	 */
-	$has_access = bp_activity_user_can_read( $activity, $user_id );
-
-	// If activity author does not match displayed user, block access.
-	// More info:https://buddypress.trac.wordpress.org/ticket/7048#comment:28
-	if ( true === $has_access && $user_id !== $activity->user_id ) {
-		$has_access = false;
-	}
-
-	/**
-	 * Fires before the loading of a single activity template file.
-	 *
-	 * @since 1.2.0
-	 *
-	 * @param BP_Activity_Activity $activity   Object representing the current activity item being displayed.
-	 * @param bool                 $has_access Whether or not the current user has access to view activity.
-	 */
-	do_action( 'bp_activity_screen_single_activity_permalink', $activity, $has_access );
-
-	// Access is specifically disallowed.
-	if ( false === $has_access ) {
-
-		// User feedback.
-		bp_core_add_message( __( 'You do not have access to this activity.', 'buddypress' ), 'error' );
-
-		// Redirect based on logged in status.
-		if ( is_user_logged_in() ) {
-			$url = bp_loggedin_user_domain();
-
-		} else {
-			$url = sprintf(
-				wp_login_url( 'wp-login.php?redirect_to=%s' ),
-				esc_url_raw( bp_activity_get_permalink( $action ) )
-			);
-		}
-
-		bp_core_redirect( $url );
-	}
-
-	/**
-	 * Filters the template to load for a single activity screen.
-	 *
-	 * @since 1.0.0
-	 *
-	 * @param string $template Path to the activity template to load.
-	 */
-	$template = apply_filters( 'bp_activity_template_profile_activity_permalink', 'members/single/activity/permalink' );
-
-	// Load the template.
-	bp_core_load_template( $template );
+	$action = new BP_Activity_Screen_Permalink();
+	$action->init();
 }
 add_action( 'bp_screens', 'bp_activity_screen_single_activity_permalink' );
 
@@ -287,73 +216,10 @@
  * Add activity notifications settings to the notifications settings page.
  *
  * @since 1.2.0
- *
  */
 function bp_activity_screen_notification_settings() {
-
-	if ( bp_activity_do_mentions() ) {
-		if ( ! $mention = bp_get_user_meta( bp_displayed_user_id(), 'notification_activity_new_mention', true ) ) {
-			$mention = 'yes';
-		}
-	}
-
-	if ( ! $reply = bp_get_user_meta( bp_displayed_user_id(), 'notification_activity_new_reply', true ) ) {
-		$reply = 'yes';
-	}
-
-	?>
-
-	<table class="notification-settings" id="activity-notification-settings">
-		<thead>
-			<tr>
-				<th class="icon">&nbsp;</th>
-				<th class="title"><?php _e( 'Activity', 'buddypress' ) ?></th>
-				<th class="yes"><?php _e( 'Yes', 'buddypress' ) ?></th>
-				<th class="no"><?php _e( 'No', 'buddypress' )?></th>
-			</tr>
-		</thead>
-
-		<tbody>
-			<?php if ( bp_activity_do_mentions() ) : ?>
-				<tr id="activity-notification-settings-mentions">
-					<td>&nbsp;</td>
-					<td><?php printf( __( 'A member mentions you in an update using "@%s"', 'buddypress' ), bp_core_get_username( bp_displayed_user_id() ) ) ?></td>
-					<td class="yes"><input type="radio" name="notifications[notification_activity_new_mention]" id="notification-activity-new-mention-yes" value="yes" <?php checked( $mention, 'yes', true ) ?>/><label for="notification-activity-new-mention-yes" class="bp-screen-reader-text"><?php
-						/* translators: accessibility text */
-						_e( 'Yes, send email', 'buddypress' );
-					?></label></td>
-					<td class="no"><input type="radio" name="notifications[notification_activity_new_mention]" id="notification-activity-new-mention-no" value="no" <?php checked( $mention, 'no', true ) ?>/><label for="notification-activity-new-mention-no" class="bp-screen-reader-text"><?php
-						/* translators: accessibility text */
-						_e( 'No, do not send email', 'buddypress' );
-					?></label></td>
-				</tr>
-			<?php endif; ?>
-
-			<tr id="activity-notification-settings-replies">
-				<td>&nbsp;</td>
-				<td><?php _e( "A member replies to an update or comment you've posted", 'buddypress' ) ?></td>
-				<td class="yes"><input type="radio" name="notifications[notification_activity_new_reply]" id="notification-activity-new-reply-yes" value="yes" <?php checked( $reply, 'yes', true ) ?>/><label for="notification-activity-new-reply-yes" class="bp-screen-reader-text"><?php
-					/* translators: accessibility text */
-					_e( 'Yes, send email', 'buddypress' );
-				?></label></td>
-				<td class="no"><input type="radio" name="notifications[notification_activity_new_reply]" id="notification-activity-new-reply-no" value="no" <?php checked( $reply, 'no', true ) ?>/><label for="notification-activity-new-reply-no" class="bp-screen-reader-text"><?php
-					/* translators: accessibility text */
-					_e( 'No, do not send email', 'buddypress' );
-				?></label></td>
-			</tr>
-
-			<?php
-
-			/**
-			 * Fires inside the closing </tbody> tag for activity screen notification settings.
-			 *
-			 * @since 1.2.0
-			 */
-			do_action( 'bp_activity_screen_notification_settings' ) ?>
-		</tbody>
-	</table>
-
-<?php
+	$action = new BP_Activity_Screen_User_Settings_Notifications();
+	$action->init();
 }
 add_action( 'bp_notification_settings', 'bp_activity_screen_notification_settings', 1 );
 
Index: src/bp-activity/classes/class-bp-activity-action-delete.php
new file mode 100644
===================================================================
--- /dev/null
+++ src/bp-activity/classes/class-bp-activity-action-delete.php
@@ -0,0 +1,74 @@
+<?php
+/**
+ * BuddyPress Activity Classes
+ *
+ * @package BuddyPress
+ * @since 3.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Delete specific activity item and redirect to previous page.
+ *
+ * @since 3.0.0
+ *
+ * @see bp_activity_action_delete_activity()
+ */
+class BP_Activity_Action_Delete {
+	/**
+	 * Init method.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param  int $activity_id The activity ID.
+	 * @return bool|void False on failure, nothing on success.
+	 */
+	public function init( $activity_id = 0 ) {
+		// Check the nonce.
+		check_admin_referer( 'bp_activity_delete_link' );
+	
+		// Load up the activity item.
+		$activity = new BP_Activity_Activity( $activity_id );
+	
+		// Check access.
+		if ( ! bp_activity_user_can_delete( $activity ) ) {
+			return false;
+		}
+	
+		/**
+		 * Fires before the deletion so plugins can still fetch information about it.
+		 *
+		 * @since 1.5.0
+		 *
+		 * @param int $activity_id The activity ID.
+		 * @param int $user_id     The user associated with the activity.
+		 */
+		do_action( 'bp_activity_before_action_delete_activity', $activity_id, $activity->user_id );
+	
+		// Delete the activity item and provide user feedback.
+		if ( bp_activity_delete( array( 'id' => $activity_id, 'user_id' => $activity->user_id ) ) ) {
+			bp_core_add_message( __( 'Activity deleted successfully', 'buddypress' ) );
+		} else {
+			bp_core_add_message( __( 'There was an error when deleting that activity', 'buddypress' ), 'error' );
+		}
+	
+		/**
+		 * Fires after the deletion so plugins can act afterwards based on the activity.
+		 *
+		 * @since 1.1.0
+		 *
+		 * @param int $activity_id The activity ID.
+		 * @param int $user_id     The user associated with the activity.
+		 */
+		do_action( 'bp_activity_action_delete_activity', $activity_id, $activity->user_id );
+	
+		// Check for the redirect query arg, otherwise let WP handle things.
+		if ( ! empty( $_GET['redirect_to'] ) ) {
+			bp_core_redirect( esc_url( $_GET['redirect_to'] ) );
+		} else {
+			bp_core_redirect( wp_get_referer() );
+		}
+	}
+}
\ No newline at end of file
Index: src/bp-activity/classes/class-bp-activity-action-favorite.php
new file mode 100644
===================================================================
--- /dev/null
+++ src/bp-activity/classes/class-bp-activity-action-favorite.php
@@ -0,0 +1,37 @@
+<?php
+/**
+ * BuddyPress Activity Classes
+ *
+ * @package BuddyPress
+ * @since 3.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Mark activity as favorite.
+ *
+ * @since 3.0.0
+ *
+ * @see bp_activity_action_mark_favorite()
+ */
+class BP_Activity_Action_Favorite {
+	/**
+	 * Init method.
+	 *
+	 * @since 3.0.0
+	 */
+	public function init() {
+		// Check the nonce.
+		check_admin_referer( 'mark_favorite' );
+
+		if ( bp_activity_add_user_favorite( bp_action_variable( 0 ) ) ) {
+			bp_core_add_message( __( 'Activity marked as favorite.', 'buddypress' ) );
+		} else {
+			bp_core_add_message( __( 'There was an error marking that activity as a favorite. Please try again.', 'buddypress' ), 'error' );
+		}
+
+		bp_core_redirect( wp_get_referer() . '#activity-' . bp_action_variable( 0 ) );
+	}
+}
\ No newline at end of file
Index: src/bp-activity/classes/class-bp-activity-action-feed-favorites.php
new file mode 100644
===================================================================
--- /dev/null
+++ src/bp-activity/classes/class-bp-activity-action-feed-favorites.php
@@ -0,0 +1,42 @@
+<?php
+/**
+ * BuddyPress Activity Classes
+ *
+ * @package BuddyPress
+ * @since 3.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * User's favorites RSS feed loader.
+ *
+ * @since 3.0.0
+ *
+ * @see bp_activity_action_favorites_feed()
+ */
+class BP_Activity_Action_Feed_Favorites {
+	/**
+	 * Init method.
+	 *
+	 * @since 3.0.0
+	 */
+	public function init() {
+		// Get displayed user's favorite activity IDs.
+		$favs = bp_activity_get_user_favorites( bp_displayed_user_id() );
+		$fav_ids = implode( ',', (array) $favs );
+
+		// Setup the feed.
+		buddypress()->activity->feed = new BP_Activity_Feed( array(
+			'id'            => 'favorites',
+
+			/* translators: User activity favorites RSS title - "[Site Name] | [User Display Name] | Favorites" */
+			'title'         => sprintf( __( '%1$s | %2$s | Favorites', 'buddypress' ), bp_get_site_name(), bp_get_displayed_user_fullname() ),
+
+			'link'          => bp_displayed_user_domain() . bp_get_activity_slug() . '/favorites/',
+			'description'   => sprintf( __( "Activity feed of %s's favorites.", 'buddypress' ), bp_get_displayed_user_fullname() ),
+			'activity_args' => 'include=' . $fav_ids
+		) );
+	}
+}
\ No newline at end of file
Index: src/bp-activity/classes/class-bp-activity-action-feed-friends.php
new file mode 100644
===================================================================
--- /dev/null
+++ src/bp-activity/classes/class-bp-activity-action-feed-friends.php
@@ -0,0 +1,38 @@
+<?php
+/**
+ * BuddyPress Activity Classes
+ *
+ * @package BuddyPress
+ * @since 3.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * User's friend RSS feed loader.
+ *
+ * @since 3.0.0
+ *
+ * @see bp_activity_action_friends_feed()
+ */
+class BP_Activity_Action_Feed_Friends {
+	/**
+	 * Init method.
+	 *
+	 * @since 3.0.0
+	 */
+	public function init() {
+		// Setup the feed.
+		buddypress()->activity->feed = new BP_Activity_Feed( array(
+			'id'            => 'friends',
+
+			/* translators: Friends activity RSS title - "[Site Name] | [User Display Name] | Friends Activity" */
+			'title'         => sprintf( __( '%1$s | %2$s | Friends Activity', 'buddypress' ), bp_get_site_name(), bp_get_displayed_user_fullname() ),
+
+			'link'          => trailingslashit( bp_displayed_user_domain() . bp_get_activity_slug() . '/' . bp_get_friends_slug() ),
+			'description'   => sprintf( __( "Activity feed for %s's friends.", 'buddypress' ), bp_get_displayed_user_fullname() ),
+			'activity_args' => 'scope=friends'
+		) );
+	}
+}
\ No newline at end of file
Index: src/bp-activity/classes/class-bp-activity-action-feed-groups.php
new file mode 100644
===================================================================
--- /dev/null
+++ src/bp-activity/classes/class-bp-activity-action-feed-groups.php
@@ -0,0 +1,46 @@
+<?php
+/**
+ * BuddyPress Activity Classes
+ *
+ * @package BuddyPress
+ * @since 3.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * User's public group RSS feed loader.
+ *
+ * @since 3.0.0
+ *
+ * @see bp_activity_action_groups_feed()
+ */
+class BP_Activity_Action_Feed_Groups {
+	/**
+	 * Init method.
+	 *
+	 * @since 3.0.0
+	 */
+	public function init() {
+		// Get displayed user's group IDs.
+		$groups    = groups_get_user_groups();
+		$group_ids = implode( ',', $groups['groups'] );
+
+		// Setup the feed.
+		buddypress()->activity->feed = new BP_Activity_Feed( array(
+			'id'            => 'mygroups',
+
+			/* translators: Member groups activity RSS title - "[Site Name] | [User Display Name] | Groups Activity" */
+			'title'         => sprintf( __( '%1$s | %2$s | Group Activity', 'buddypress' ), bp_get_site_name(), bp_get_displayed_user_fullname() ),
+
+			'link'          => trailingslashit( bp_displayed_user_domain() . bp_get_activity_slug() . '/' . bp_get_groups_slug() ),
+			'description'   => sprintf( __( "Public group activity feed of which %s is a member.", 'buddypress' ), bp_get_displayed_user_fullname() ),
+			'activity_args' => array(
+				'object'           => buddypress()->groups->id,
+				'primary_id'       => $group_ids,
+				'display_comments' => 'threaded'
+			)
+		) );
+	}
+}
\ No newline at end of file
Index: src/bp-activity/classes/class-bp-activity-action-feed-mentions.php
new file mode 100644
===================================================================
--- /dev/null
+++ src/bp-activity/classes/class-bp-activity-action-feed-mentions.php
@@ -0,0 +1,40 @@
+<?php
+/**
+ * BuddyPress Activity Classes
+ *
+ * @package BuddyPress
+ * @since 3.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * User's mentions RSS feed loader.
+ *
+ * @since 3.0.0
+ *
+ * @see bp_activity_action_mentions_feed()
+ */
+class BP_Activity_Action_Feed_Mentions {
+	/**
+	 * Init method.
+	 *
+	 * @since 3.0.0
+	 */
+	public function init() {
+		// Setup the feed.
+		buddypress()->activity->feed = new BP_Activity_Feed( array(
+			'id'            => 'mentions',
+
+			/* translators: User mentions activity RSS title - "[Site Name] | [User Display Name] | Mentions" */
+			'title'         => sprintf( __( '%1$s | %2$s | Mentions', 'buddypress' ), bp_get_site_name(), bp_get_displayed_user_fullname() ),
+
+			'link'          => bp_displayed_user_domain() . bp_get_activity_slug() . '/mentions/',
+			'description'   => sprintf( __( "Activity feed mentioning %s.", 'buddypress' ), bp_get_displayed_user_fullname() ),
+			'activity_args' => array(
+				'search_terms' => '@' . bp_core_get_username( bp_displayed_user_id() )
+			)
+		) );
+	}
+}
\ No newline at end of file
Index: src/bp-activity/classes/class-bp-activity-action-feed-personal.php
new file mode 100644
===================================================================
--- /dev/null
+++ src/bp-activity/classes/class-bp-activity-action-feed-personal.php
@@ -0,0 +1,38 @@
+<?php
+/**
+ * BuddyPress Activity Classes
+ *
+ * @package BuddyPress
+ * @since 3.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * User's personal RSS feed loader.
+ *
+ * @since 3.0.0
+ *
+ * @see bp_activity_action_personal_feed()
+ */
+class BP_Activity_Action_Feed_Personal {
+	/**
+	 * Init method.
+	 *
+	 * @since 3.0.0
+	 */
+	public function init() {
+		// Setup the feed.
+		buddypress()->activity->feed = new BP_Activity_Feed( array(
+			'id'            => 'personal',
+
+			/* translators: Personal activity RSS title - "[Site Name] | [User Display Name] | Activity" */
+			'title'         => sprintf( __( '%1$s | %2$s | Activity', 'buddypress' ), bp_get_site_name(), bp_get_displayed_user_fullname() ),
+
+			'link'          => trailingslashit( bp_displayed_user_domain() . bp_get_activity_slug() ),
+			'description'   => sprintf( __( 'Activity feed for %s.', 'buddypress' ), bp_get_displayed_user_fullname() ),
+			'activity_args' => 'user_id=' . bp_displayed_user_id()
+		) );
+	}
+}
\ No newline at end of file
Index: src/bp-activity/classes/class-bp-activity-action-feed-sitewide.php
new file mode 100644
===================================================================
--- /dev/null
+++ src/bp-activity/classes/class-bp-activity-action-feed-sitewide.php
@@ -0,0 +1,38 @@
+<?php
+/**
+ * BuddyPress Activity Classes
+ *
+ * @package BuddyPress
+ * @since 3.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Sitewide RSS feed loader.
+ *
+ * @since 3.0.0
+ *
+ * @see bp_activity_action_sitewide_feed()
+ */
+class BP_Activity_Action_Feed_Sitewide {
+	/**
+	 * Init method.
+	 *
+	 * @since 3.0.0
+	 */
+	public function init() {
+		// Setup the feed.
+		buddypress()->activity->feed = new BP_Activity_Feed( array(
+			'id'            => 'sitewide',
+
+			/* translators: Sitewide activity RSS title - "[Site Name] | Site Wide Activity" */
+			'title'         => sprintf( __( '%s | Site-Wide Activity', 'buddypress' ), bp_get_site_name() ),
+
+			'link'          => bp_get_activity_directory_permalink(),
+			'description'   => __( 'Activity feed for the entire site.', 'buddypress' ),
+			'activity_args' => 'display_comments=threaded'
+		) );
+	}
+}
\ No newline at end of file
Index: src/bp-activity/classes/class-bp-activity-action-permalink-router.php
new file mode 100644
===================================================================
--- /dev/null
+++ src/bp-activity/classes/class-bp-activity-action-permalink-router.php
@@ -0,0 +1,81 @@
+<?php
+/**
+ * BuddyPress Activity Classes
+ *
+ * @package BuddyPress
+ * @since 3.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Catch and route requests for single activity item permalinks.
+ *
+ * @since 3.0.0
+ *
+ * @see bp_activity_action_permalink_router()
+ */
+class BP_Activity_Action_Permalink_Router {
+	/**
+	 * Init method.
+	 *
+	 * @since 3.0.0
+	 */
+	public function init() {
+		// Get the activity details.
+		$activity = bp_activity_get_specific( array( 'activity_ids' => bp_action_variable( 0 ), 'show_hidden' => true ) );
+
+		// 404 if activity does not exist
+		if ( empty( $activity['activities'][0] ) ) {
+			bp_do_404();
+			return;
+		} else {
+			$activity = $activity['activities'][0];
+		}
+
+		// Do not redirect at default.
+		$redirect = false;
+
+		// Redirect based on the type of activity.
+		if ( bp_is_active( 'groups' ) && $activity->component == buddypress()->groups->id ) {
+
+			// Activity is a user update.
+			if ( ! empty( $activity->user_id ) ) {
+				$redirect = bp_core_get_user_domain( $activity->user_id, $activity->user_nicename, $activity->user_login ) . bp_get_activity_slug() . '/' . $activity->id . '/';
+
+			// Activity is something else.
+			} else {
+				// Set redirect to group activity stream.
+				if ( $group = groups_get_group( $activity->item_id ) ) {
+					$redirect = bp_get_group_permalink( $group ) . bp_get_activity_slug() . '/' . $activity->id . '/';
+				}
+			}
+
+		// Set redirect to users' activity stream.
+		} elseif ( ! empty( $activity->user_id ) ) {
+			$redirect = bp_core_get_user_domain( $activity->user_id, $activity->user_nicename, $activity->user_login ) . bp_get_activity_slug() . '/' . $activity->id . '/';
+		}
+
+		// If set, add the original query string back onto the redirect URL.
+		if ( ! empty( $_SERVER['QUERY_STRING'] ) ) {
+			$query_frags = array();
+			wp_parse_str( $_SERVER['QUERY_STRING'], $query_frags );
+			$redirect = add_query_arg( urlencode_deep( $query_frags ), $redirect );
+		}
+
+		/**
+		 * Filter the intended redirect url before the redirect occurs for the single activity item.
+		 *
+		 * @since 1.2.2
+		 *
+		 * @param array $value Array with url to redirect to and activity related to the redirect.
+		 */
+		if ( ! $redirect = apply_filters_ref_array( 'bp_activity_permalink_redirect_url', array( $redirect, &$activity ) ) ) {
+			bp_core_redirect( bp_get_root_domain() );
+		}
+
+		// Redirect to the actual activity permalink page.
+		bp_core_redirect( $redirect );
+	}
+}
\ No newline at end of file
Index: src/bp-activity/classes/class-bp-activity-action-post-comment.php
new file mode 100644
===================================================================
--- /dev/null
+++ src/bp-activity/classes/class-bp-activity-action-post-comment.php
@@ -0,0 +1,66 @@
+<?php
+/**
+ * BuddyPress Activity Classes
+ *
+ * @package BuddyPress
+ * @since 3.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Post user/group activity update.
+ *
+ * @since 3.0.0
+ *
+ * @see bp_activity_action_post_comment()
+ */
+class BP_Activity_Action_Post_Comment {
+	/**
+	 * Init method.
+	 *
+	 * @since 3.0.0
+	 */
+	public function init() {
+		// Check the nonce.
+		check_admin_referer( 'new_activity_comment', '_wpnonce_new_activity_comment' );
+
+		/**
+		 * Filters the activity ID a comment will be in reply to.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param string $value ID of the activity being replied to.
+		 */
+		$activity_id = apply_filters( 'bp_activity_post_comment_activity_id', $_POST['comment_form_id'] );
+
+		/**
+		 * Filters the comment content for a comment reply.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param string $value Comment content being posted.
+		 */
+		$content = apply_filters( 'bp_activity_post_comment_content', $_POST['ac_input_' . $activity_id] );
+
+		if ( empty( $content ) ) {
+			bp_core_add_message( __( 'Please do not leave the comment area blank.', 'buddypress' ), 'error' );
+			bp_core_redirect( wp_get_referer() . '#ac-form-' . $activity_id );
+		}
+
+		$comment_id = bp_activity_new_comment( array(
+			'content'     => $content,
+			'activity_id' => $activity_id,
+			'parent_id'   => false
+		) );
+
+		if ( ! empty( $comment_id ) ) {
+			bp_core_add_message( __( 'Reply Posted!', 'buddypress' ) );
+		} else {
+			bp_core_add_message( __( 'There was an error posting that reply. Please try again.', 'buddypress' ), 'error' );
+		}
+
+		bp_core_redirect( wp_get_referer() . '#ac-form-' . $activity_id );
+	}
+}
\ No newline at end of file
Index: src/bp-activity/classes/class-bp-activity-action-post-update.php
new file mode 100644
===================================================================
--- /dev/null
+++ src/bp-activity/classes/class-bp-activity-action-post-update.php
@@ -0,0 +1,102 @@
+<?php
+/**
+ * BuddyPress Activity Classes
+ *
+ * @package BuddyPress
+ * @since 3.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Post user/group activity update.
+ *
+ * @since 3.0.0
+ *
+ * @see bp_activity_action_post_update()
+ */
+class BP_Activity_Action_Post_Update {
+	/**
+	 * Init method.
+	 *
+	 * @since 3.0.0
+	 */
+	public function init() {
+		// Check the nonce.
+		check_admin_referer( 'post_update', '_wpnonce_post_update' );
+
+		/**
+		 * Filters the content provided in the activity input field.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param string $value Activity message being posted.
+		 */
+		$content = apply_filters( 'bp_activity_post_update_content', $_POST['whats-new'] );
+
+		if ( ! empty( $_POST['whats-new-post-object'] ) ) {
+
+			/**
+			 * Filters the item type that the activity update should be associated with.
+			 *
+			 * @since 1.2.0
+			 *
+			 * @param string $value Item type to associate with.
+			 */
+			$object = apply_filters( 'bp_activity_post_update_object', $_POST['whats-new-post-object'] );
+		}
+
+		if ( ! empty( $_POST['whats-new-post-in'] ) ) {
+
+			/**
+			 * Filters what component the activity is being to.
+			 *
+			 * @since 1.2.0
+			 *
+			 * @param string $value Chosen component to post activity to.
+			 */
+			$item_id = apply_filters( 'bp_activity_post_update_item_id', $_POST['whats-new-post-in'] );
+		}
+
+		// No activity content so provide feedback and redirect.
+		if ( empty( $content ) ) {
+			bp_core_add_message( __( 'Please enter some content to post.', 'buddypress' ), 'error' );
+			bp_core_redirect( wp_get_referer() );
+		}
+
+		// No existing item_id.
+		if ( empty( $item_id ) ) {
+			$activity_id = bp_activity_post_update( array( 'content' => $content ) );
+
+		// Post to groups object.
+		} elseif ( 'groups' == $object && bp_is_active( 'groups' ) ) {
+			if ( (int) $item_id ) {
+				$activity_id = groups_post_update( array( 'content' => $content, 'group_id' => $item_id ) );
+			}
+
+		} else {
+
+			/**
+			 * Filters activity object for BuddyPress core and plugin authors before posting activity update.
+			 *
+			 * @since 1.2.0
+			 *
+			 * @param string $object  Activity item being associated to.
+			 * @param string $item_id Component ID being posted to.
+			 * @param string $content Activity content being posted.
+			 */
+			$activity_id = apply_filters( 'bp_activity_custom_update', $object, $item_id, $content );
+		}
+
+		// Provide user feedback.
+		if ( ! empty( $activity_id ) ) {
+			bp_core_add_message( __( 'Update Posted!', 'buddypress' ) );
+		} else {
+			bp_core_add_message( __( 'There was an error when posting your update. Please try again.', 'buddypress' ), 'error' );
+		}
+
+		// Redirect.
+		bp_core_redirect( wp_get_referer() );
+	}
+}
\ No newline at end of file
Index: src/bp-activity/classes/class-bp-activity-action-spam.php
new file mode 100644
===================================================================
--- /dev/null
+++ src/bp-activity/classes/class-bp-activity-action-spam.php
@@ -0,0 +1,76 @@
+<?php
+/**
+ * BuddyPress Activity Classes
+ *
+ * @package BuddyPress
+ * @since 3.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Mark specific activity item as spam and redirect to previous page.
+ *
+ * @since 3.0.0
+ *
+ * @see bp_activity_action_spam_activity()
+ */
+class BP_Activity_Action_Spam {
+	/**
+	 * Init method.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param  int $activity_id The activity ID.
+	 * @return bool|void False on failure, void on success.
+	 */
+	public function init( $activity_id = 0 ) {
+		// Is the current user allowed to spam items?
+		if ( ! bp_activity_user_can_mark_spam() ) {
+			return false;
+		}
+	
+		// Load up the activity item.
+		$activity = new BP_Activity_Activity( $activity_id );
+		if ( empty( $activity->id ) ) {
+			return false;
+		}
+	
+		// Check nonce.
+		check_admin_referer( 'bp_activity_akismet_spam_' . $activity->id );
+	
+		/**
+		 * Fires before the marking activity as spam so plugins can modify things if they want to.
+		 *
+		 * @since 1.6.0
+		 *
+		 * @param int    $activity_id Activity ID to be marked as spam.
+		 * @param object $activity    Activity object for the ID to be marked as spam.
+		 */
+		do_action( 'bp_activity_before_action_spam_activity', $activity->id, $activity );
+	
+		// Mark as spam.
+		bp_activity_mark_as_spam( $activity );
+		$activity->save();
+	
+		// Tell the user the spamming has been successful.
+		bp_core_add_message( __( 'The activity item has been marked as spam and is no longer visible.', 'buddypress' ) );
+	
+		/**
+		 * Fires after the marking activity as spam so plugins can act afterwards based on the activity.
+		 *
+		 * @since 1.6.0
+		 *
+		 * @param int $activity_id Activity ID that was marked as spam.
+		 * @param int $user_id     User ID associated with activity.
+		 */
+		do_action( 'bp_activity_action_spam_activity', $activity_id, $activity->user_id );
+	
+		// Check for the redirect query arg, otherwise let WP handle things.
+		if ( ! empty( $_GET['redirect_to'] ) ) {
+			bp_core_redirect( esc_url( $_GET['redirect_to'] ) );
+		} else {
+			bp_core_redirect( wp_get_referer() );
+		}
+}
\ No newline at end of file
Index: src/bp-activity/classes/class-bp-activity-action-unfavorite.php
new file mode 100644
===================================================================
--- /dev/null
+++ src/bp-activity/classes/class-bp-activity-action-unfavorite.php
@@ -0,0 +1,37 @@
+<?php
+/**
+ * BuddyPress Activity Classes
+ *
+ * @package BuddyPress
+ * @since 3.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Unfavorite activity items.
+ *
+ * @since 3.0.0
+ *
+ * @see bp_activity_action_remove_favorite()
+ */
+class BP_Activity_Action_Unfavorite {
+	/**
+	 * Init method.
+	 *
+	 * @since 3.0.0
+	 */
+	public function init() {
+		// Check the nonce.
+		check_admin_referer( 'unmark_favorite' );
+
+		if ( bp_activity_remove_user_favorite( bp_action_variable( 0 ) ) ) {
+			bp_core_add_message( __( 'Activity removed as favorite.', 'buddypress' ) );
+		} else {
+			bp_core_add_message( __( 'There was an error removing that activity as a favorite. Please try again.', 'buddypress' ), 'error' );
+		}
+
+		bp_core_redirect( wp_get_referer() . '#activity-' . bp_action_variable( 0 ) );
+	}
+}
\ No newline at end of file
Index: src/bp-activity/classes/class-bp-activity-component.php
===================================================================
--- src/bp-activity/classes/class-bp-activity-component.php
+++ src/bp-activity/classes/class-bp-activity-component.php
@@ -85,6 +85,18 @@
 		parent::includes( $includes );
 	}
 
+	/**
+	 * Late includes.
+	 *
+	 * @since 3.0.0
+	 */
+	public function late_includes() {
+		// RSS feeds.
+		if ( bp_is_current_action( 'feed' ) || bp_is_action_variable( 'feed', 0 ) ) {
+			require $this->path . 'bp-activity/bp-activity-feeds.php';
+		}
+	}
+
 	/**
 	 * Set up component global variables.
 	 *
Index: src/bp-activity/classes/class-bp-activity-screen-permalink.php
new file mode 100644
===================================================================
--- /dev/null
+++ src/bp-activity/classes/class-bp-activity-screen-permalink.php
@@ -0,0 +1,98 @@
+<?php
+/**
+ * BuddyPress Activity Classes
+ *
+ * @package BuddyPress
+ * @since 3.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Screen handler for activity permalinks.
+ *
+ * @since 3.0.0
+ *
+ * @see bp_activity_screen_single_activity_permalink()
+ */
+class BP_Activity_Screen_Permalink {
+	/**
+	 * Init method.
+	 *
+	 * @since 3.0.0
+	 */
+	public function init() {
+		// Get the activity details.
+		$activity = bp_activity_get_specific( array(
+			'activity_ids' => bp_current_action(),
+			'show_hidden'  => true,
+			'spam'         => 'ham_only',
+		) );
+
+		// 404 if activity does not exist
+		if ( empty( $activity['activities'][0] ) || bp_action_variables() ) {
+			bp_do_404();
+			return;
+
+		} else {
+			$activity = $activity['activities'][0];
+		}
+
+		$user_id = bp_displayed_user_id();
+
+		/**
+		 * Check user access to the activity item.
+		 *
+		 * @since 3.0.0
+		 */
+		$has_access = bp_activity_user_can_read( $activity, $user_id );
+
+		// If activity author does not match displayed user, block access.
+		// More info:https://buddypress.trac.wordpress.org/ticket/7048#comment:28
+		if ( true === $has_access && $user_id !== $activity->user_id ) {
+			$has_access = false;
+		}
+
+		/**
+		 * Fires before the loading of a single activity template file.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param BP_Activity_Activity $activity   Object representing the current activity item being displayed.
+		 * @param bool                 $has_access Whether or not the current user has access to view activity.
+		 */
+		do_action( 'bp_activity_screen_single_activity_permalink', $activity, $has_access );
+
+		// Access is specifically disallowed.
+		if ( false === $has_access ) {
+			// User feedback.
+			bp_core_add_message( __( 'You do not have access to this activity.', 'buddypress' ), 'error' );
+
+			// Redirect based on logged in status.
+			if ( is_user_logged_in() ) {
+				$url = bp_loggedin_user_domain();
+
+			} else {
+				$url = sprintf(
+					wp_login_url( 'wp-login.php?redirect_to=%s' ),
+					esc_url_raw( bp_activity_get_permalink( $action ) )
+				);
+			}
+
+			bp_core_redirect( $url );
+		}
+
+		/**
+		 * Filters the template to load for a single activity screen.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param string $template Path to the activity template to load.
+		 */
+		$template = apply_filters( 'bp_activity_template_profile_activity_permalink', 'members/single/activity/permalink' );
+
+		// Load the template.
+		bp_core_load_template( $template );
+	}
+}
\ No newline at end of file
Index: src/bp-activity/classes/class-bp-activity-screen-user-settings-notifications.php
new file mode 100644
===================================================================
--- /dev/null
+++ src/bp-activity/classes/class-bp-activity-screen-user-settings-notifications.php
@@ -0,0 +1,89 @@
+<?php
+/**
+ * BuddyPress Activity Classes
+ *
+ * @package BuddyPress
+ * @since 3.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Render the Activity settings fields on the "Settings > Notifications" page.
+ *
+ * @since 3.0.0
+ *
+ * @see bp_activity_screen_notification_settings()
+ */
+class BP_Activity_Screen_User_Settings_Notifications {
+	/**
+	 * Init method.
+	 *
+	 * @since 3.0.0
+	 */
+	public function init() {
+		if ( bp_activity_do_mentions() ) {
+			if ( ! $mention = bp_get_user_meta( bp_displayed_user_id(), 'notification_activity_new_mention', true ) ) {
+				$mention = 'yes';
+			}
+		}
+
+		if ( ! $reply = bp_get_user_meta( bp_displayed_user_id(), 'notification_activity_new_reply', true ) ) {
+			$reply = 'yes';
+		}
+		?>
+
+		<table class="notification-settings" id="activity-notification-settings">
+			<thead>
+				<tr>
+					<th class="icon">&nbsp;</th>
+					<th class="title"><?php _e( 'Activity', 'buddypress' ) ?></th>
+					<th class="yes"><?php _e( 'Yes', 'buddypress' ) ?></th>
+					<th class="no"><?php _e( 'No', 'buddypress' )?></th>
+				</tr>
+			</thead>
+
+			<tbody>
+				<?php if ( bp_activity_do_mentions() ) : ?>
+					<tr id="activity-notification-settings-mentions">
+						<td>&nbsp;</td>
+						<td><?php printf( __( 'A member mentions you in an update using "@%s"', 'buddypress' ), bp_core_get_username( bp_displayed_user_id() ) ) ?></td>
+						<td class="yes"><input type="radio" name="notifications[notification_activity_new_mention]" id="notification-activity-new-mention-yes" value="yes" <?php checked( $mention, 'yes', true ) ?>/><label for="notification-activity-new-mention-yes" class="bp-screen-reader-text"><?php
+							/* translators: accessibility text */
+							_e( 'Yes, send email', 'buddypress' );
+						?></label></td>
+						<td class="no"><input type="radio" name="notifications[notification_activity_new_mention]" id="notification-activity-new-mention-no" value="no" <?php checked( $mention, 'no', true ) ?>/><label for="notification-activity-new-mention-no" class="bp-screen-reader-text"><?php
+							/* translators: accessibility text */
+							_e( 'No, do not send email', 'buddypress' );
+						?></label></td>
+					</tr>
+				<?php endif; ?>
+
+				<tr id="activity-notification-settings-replies">
+					<td>&nbsp;</td>
+					<td><?php _e( "A member replies to an update or comment you've posted", 'buddypress' ) ?></td>
+					<td class="yes"><input type="radio" name="notifications[notification_activity_new_reply]" id="notification-activity-new-reply-yes" value="yes" <?php checked( $reply, 'yes', true ) ?>/><label for="notification-activity-new-reply-yes" class="bp-screen-reader-text"><?php
+						/* translators: accessibility text */
+						_e( 'Yes, send email', 'buddypress' );
+					?></label></td>
+					<td class="no"><input type="radio" name="notifications[notification_activity_new_reply]" id="notification-activity-new-reply-no" value="no" <?php checked( $reply, 'no', true ) ?>/><label for="notification-activity-new-reply-no" class="bp-screen-reader-text"><?php
+						/* translators: accessibility text */
+						_e( 'No, do not send email', 'buddypress' );
+					?></label></td>
+				</tr>
+
+				<?php
+
+				/**
+				 * Fires inside the closing </tbody> tag for activity screen notification settings.
+				 *
+				 * @since 1.2.0
+				 */
+				do_action( 'bp_activity_screen_notification_settings' ) ?>
+			</tbody>
+		</table>
+
+	<?php
+	}
+}
\ No newline at end of file
Index: src/bp-activity/classes/class-bp-activity-transition-comment-status.php
new file mode 100644
===================================================================
--- /dev/null
+++ src/bp-activity/classes/class-bp-activity-transition-comment-status.php
@@ -0,0 +1,143 @@
+<?php
+/**
+ * BuddyPress Activity Classes
+ *
+ * @package BuddyPress
+ * @since 3.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Comment status handler for BuddyPress activity items.
+ *
+ * @since 3.0.0
+ *
+ * @see bp_activity_transition_post_type_comment_status()
+ */
+class BP_Activity_Transition_Comment_Status {
+	/**
+	 * Init method.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param WP_Comment $comment    Comment data.
+	 * @param string     $new_status New comment status.
+	 * @param string     $old_status Previous comment status.
+	 */
+	public function init( $comment, $new_status, $old_status ) {
+		$post_type = get_post_type( $comment->comment_post_ID );
+		if ( ! $post_type ) {
+			return;
+		}
+
+		// Get the post type tracking args.
+		$activity_post_object = bp_activity_get_post_type_tracking_args( $post_type );
+
+		// Bail if the activity type does not exist
+		if ( empty( $activity_post_object->comments_tracking->action_id ) ) {
+			return false;
+
+		// Set the $activity_comment_object
+		} else {
+			$activity_comment_object = $activity_post_object->comments_tracking;
+		}
+
+		// Init an empty activity ID
+		$activity_id = 0;
+
+		/**
+		 * Activity currently doesn't have any concept of a trash, or an unapproved/approved state.
+		 *
+		 * If a blog comment transitions to a "delete" or "hold" status, delete the activity item.
+		 * If a blog comment transitions to trashed, or spammed, mark the activity as spam.
+		 * If a blog comment transitions to approved (and the activity exists), mark the activity as ham.
+		 * If a blog comment transitions to unapproved (and the activity exists), mark the activity as spam.
+		 * Otherwise, record the comment into the activity stream.
+		 */
+
+		// This clause handles delete/hold.
+		if ( in_array( $new_status, array( 'delete', 'hold' ) ) ) {
+			return bp_activity_post_type_remove_comment( $comment->comment_ID, $activity_post_object );
+
+		// These clauses handle trash, spam, and un-spams.
+		} elseif ( in_array( $new_status, array( 'trash', 'spam', 'unapproved' ) ) ) {
+			$action = 'spam_activity';
+		} elseif ( 'approved' == $new_status ) {
+			$action = 'ham_activity';
+		}
+
+		// Get the activity
+		if ( bp_disable_blogforum_comments() ) {
+			$activity_id = bp_activity_get_activity_id( array(
+				'component'         => $activity_comment_object->component_id,
+				'item_id'           => get_current_blog_id(),
+				'secondary_item_id' => $comment->comment_ID,
+				'type'              => $activity_comment_object->action_id,
+			) );
+		} else {
+			$activity_id = get_comment_meta( $comment->comment_ID, 'bp_activity_comment_id', true );
+		}
+
+		/**
+		 * Leave a chance to plugins to manage activity comments differently.
+		 *
+		 * @since  2.5.0
+		 *
+		 * @param bool        $value       True to override BuddyPress management.
+		 * @param string      $post_type   The post type name.
+		 * @param int         $activity_id The post type activity (0 if not found).
+		 * @param string      $new_status  The new status of the post type comment.
+		 * @param string      $old_status  The old status of the post type comment.
+		 * @param WP_Comment  $comment Comment data.
+		 */
+		if ( true === apply_filters( 'bp_activity_pre_transition_post_type_comment_status', false, $post_type, $activity_id, $new_status, $old_status, $comment ) ) {
+			return false;
+		}
+
+		// Check activity item exists
+		if ( empty( $activity_id ) ) {
+			// If no activity exists, but the comment has been approved, record it into the activity table.
+			if ( 'approved' == $new_status ) {
+				return bp_activity_post_type_comment( $comment->comment_ID, true, $activity_post_object );
+			}
+
+			return;
+		}
+
+		// Create an activity object
+		$activity = new BP_Activity_Activity( $activity_id );
+		if ( empty( $activity->component ) ) {
+			return;
+		}
+
+		// Spam/ham the activity if it's not already in that state
+		if ( 'spam_activity' === $action && ! $activity->is_spam ) {
+			bp_activity_mark_as_spam( $activity );
+		} elseif ( 'ham_activity' == $action) {
+			bp_activity_mark_as_ham( $activity );
+		}
+
+		// Add "new_post_type_comment" to the whitelisted activity types, so that the activity's Akismet history is generated
+		$post_type_comment_action = $activity_comment_object->action_id;
+		$comment_akismet_history = function ( $activity_types ) use ( $post_type_comment_action ) {
+			$activity_types[] = $post_type_comment_action;
+
+			return $activity_types;
+		};
+		add_filter( 'bp_akismet_get_activity_types', $comment_akismet_history );
+
+		// Make sure the activity change won't edit the comment if sync is on
+		remove_action( 'bp_activity_before_save', 'bp_blogs_sync_activity_edit_to_post_comment', 20 );
+
+		// Save the updated activity
+		$activity->save();
+
+		// Restore the action
+		add_action( 'bp_activity_before_save', 'bp_blogs_sync_activity_edit_to_post_comment', 20 );
+
+		// Remove the "new_blog_comment" activity type whitelist so we don't break anything
+		remove_filter( 'bp_akismet_get_activity_types', $comment_akismet_history );
+	}
+}
\ No newline at end of file
Index: src/bp-activity/classes/class-bp-activity-transition-post-status.php
new file mode 100644
===================================================================
--- /dev/null
+++ src/bp-activity/classes/class-bp-activity-transition-post-status.php
@@ -0,0 +1,108 @@
+<?php
+/**
+ * BuddyPress Activity Classes
+ *
+ * @package BuddyPress
+ * @since 3.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Post type status handler for BuddyPress activity items.
+ *
+ * @since 3.0.0
+ *
+ * @see bp_activity_catch_transition_post_type_status()
+ */
+class BP_Activity_Transition_Post_Status {
+	/**
+	 * Init method.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param object $post       Post data.
+	 * @param string $new_status New status for the post.
+	 * @param string $old_status Old status for the post.
+	 */
+	public function init( $post, $new_status, $old_status ) {
+		if ( ! post_type_supports( $post->post_type, 'buddypress-activity' ) ) {
+			return;
+		}
+
+		// This is an edit.
+		if ( $new_status === $old_status ) {
+			// An edit of an existing post should update the existing activity item.
+			if ( $new_status == 'publish' ) {
+				$edit = bp_activity_post_type_update( $post );
+
+				// Post was never recorded into activity stream, so record it now!
+				if ( null === $edit ) {
+					bp_activity_post_type_publish( $post->ID, $post );
+				}
+
+			// Allow plugins to eventually deal with other post statuses.
+			} else {
+				/**
+				 * Fires when editing the post and the new status is not 'publish'.
+				 *
+				 * This is a variable filter that is dependent on the post type
+				 * being untrashed.
+				 *
+				 * @since 2.5.0
+				 *
+				 * @param WP_Post $post Post data.
+				 * @param string $new_status New status for the post.
+				 * @param string $old_status Old status for the post.
+				 */
+				do_action( 'bp_activity_post_type_edit_' . $post->post_type, $post, $new_status, $old_status );
+			}
+
+			return;
+		}
+
+		// Publishing a previously unpublished post.
+		if ( 'publish' === $new_status ) {
+			// Untrashing the post type - nothing here yet.
+			if ( 'trash' == $old_status ) {
+
+				/**
+				 * Fires if untrashing post in a post type.
+				 *
+				 * This is a variable filter that is dependent on the post type
+				 * being untrashed.
+				 *
+				 * @since 2.2.0
+				 *
+				 * @param WP_Post $post Post data.
+				 */
+				do_action( 'bp_activity_post_type_untrash_' . $post->post_type, $post );
+			} else {
+				// Record the post.
+				bp_activity_post_type_publish( $post->ID, $post );
+			}
+
+		// Unpublishing a previously published post.
+		} elseif ( 'publish' === $old_status ) {
+			// Some form of pending status - only remove the activity entry.
+			bp_activity_post_type_unpublish( $post->ID, $post );
+
+		// For any other cases, allow plugins to eventually deal with it.
+		} else {
+			/**
+			 * Fires when the old and the new post status are not 'publish'.
+			 *
+			 * This is a variable filter that is dependent on the post type
+			 * being untrashed.
+			 *
+			 * @since 2.5.0
+			 *
+			 * @param WP_Post $post Post data.
+			 * @param string $new_status New status for the post.
+			 * @param string $old_status Old status for the post.
+			 */
+			do_action( 'bp_activity_post_type_transition_status_' . $post->post_type, $post, $new_status, $old_status );
+		}
+	}
+}
\ No newline at end of file
Index: src/bp-core/classes/class-bp-component.php
===================================================================
--- src/bp-core/classes/class-bp-component.php
+++ src/bp-core/classes/class-bp-component.php
@@ -393,6 +393,17 @@
 		do_action( 'bp_' . $this->id . '_includes' );
 	}
 
+	/**
+	 * Late includes method.
+	 *
+	 * Components should include files here only on specific pages using
+	 * conditionals such as {@link bp_is_current_component()}. Intentionally left
+	 * empty.
+	 *
+	 * @since 2.9.0
+	 */
+	public function late_includes() {}
+
 	/**
 	 * Set up the actions.
 	 *
@@ -414,6 +425,9 @@
 		// extending this base class.
 		add_action( 'bp_include',                array( $this, 'includes'               ), 8 );
 
+		// Late includes.
+		add_action( 'bp_setup_canonical_stack',  array( $this, 'late_includes'          ), 20 );
+
 		// Setup navigation.
 		add_action( 'bp_setup_nav',              array( $this, 'setup_nav'              ), 10 );
 
Index: src/bp-core/classes/class-bp-core-ajax-get-suggestions.php
new file mode 100644
===================================================================
--- /dev/null
+++ src/bp-core/classes/class-bp-core-ajax-get-suggestions.php
@@ -0,0 +1,51 @@
+<?php
+/**
+ * BuddyPress Activity Classes
+ *
+ * @package BuddyPress
+ * @since 3.0.0
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * AJAX handler for Suggestions API lookups.
+ *
+ * @since 3.0.0
+ *
+ * @see  bp_ajax_get_suggestions()
+ * @todo Add nonce check.
+ */
+class BP_Core_Ajax_Get_Suggestions {
+	/**
+	 * Init method.
+	 *
+	 * @since 3.0.0
+	 */
+	public function init() {
+		if ( ! bp_is_user_active() || empty( $_GET['term'] ) || empty( $_GET['type'] ) ) {
+			wp_send_json_error( 'missing_parameter' );
+			exit;
+		}
+	
+		$args = array(
+			'term' => sanitize_text_field( $_GET['term'] ),
+			'type' => sanitize_text_field( $_GET['type'] ),
+		);
+	
+		// Support per-Group suggestions.
+		if ( ! empty( $_GET['group-id'] ) ) {
+			$args['group_id'] = absint( $_GET['group-id'] );
+		}
+	
+		$results = bp_core_get_suggestions( $args );
+	
+		if ( is_wp_error( $results ) ) {
+			wp_send_json_error( $results->get_error_message() );
+			exit;
+		}
+	
+		wp_send_json_success( $results );
+	}
+}
\ No newline at end of file
