Index: src/bp-activity/bp-activity-classes.php
===================================================================
--- src/bp-activity/bp-activity-classes.php
+++ src/bp-activity/bp-activity-classes.php
@@ -312,7 +312,9 @@
 			'in'                => false,      // Array of ids to limit query by (IN)
 			'meta_query'        => false,      // Filter by activitymeta
 			'date_query'        => false,      // Filter by date
+			'filter_query'      => false,      // Advanced filtering - see BP_Activity_Query
 			'filter'            => false,      // See self::get_filter_sql()
+			'scope'             => false,      // Preset activity arguments
 			'search_terms'      => false,      // Terms to search by
 			'display_comments'  => false,      // Whether to include activity comments
 			'show_hidden'       => false,      // Show items marked hide_sitewide
@@ -321,7 +323,6 @@
 			'count_total'       => false,
 		);
 		$r = wp_parse_args( $args, $defaults );
-		extract( $r );
 
 		// Select conditions
 		$select_sql = "SELECT DISTINCT a.id";
@@ -336,44 +337,69 @@
 		// Excluded types
 		$excluded_types = array();
 
+		// Scope takes precedence
+		if ( ! empty( $r['scope'] ) ) {
+			$scope_query = self::get_scope_query_sql( $r['scope'], $r );
+
+			if ( ! empty( $scope_query['sql'] ) ) {
+				$where_conditions['scope_query_sql'] = $scope_query['sql'];
+			}
+
+			// override some arguments if needed
+			if ( ! empty( $scope_query['override'] ) ) {
+				$r = array_replace_recursive( $r, $scope_query['override'] );
+			}
+
+		// Advanced filtering
+		} elseif ( ! empty( $r['filter_query'] ) ) {
+			$filter_query = new BP_Activity_Query( $r['filter_query'] );
+			if ( $filter_query = $filter_query->get_sql() ) {
+				$where_conditions['filter_query_sql'] = $filter_query;
+			}
+		}
+
+		// Regular filtering
+		if ( $r['filter'] && $filter_sql = BP_Activity_Activity::get_filter_sql( $r['filter'] ) ) {
+			$where_conditions['filter_sql'] = $filter_sql;
+		}
+
 		// Spam
-		if ( 'ham_only' == $spam )
+		if ( 'ham_only' == $r['spam'] ) {
 			$where_conditions['spam_sql'] = 'a.is_spam = 0';
-		elseif ( 'spam_only' == $spam )
+		} elseif ( 'spam_only' == $spam ) {
 			$where_conditions['spam_sql'] = 'a.is_spam = 1';
+		}
 
 		// Searching
-		if ( $search_terms ) {
-			$search_terms_like = '%' . bp_esc_like( $search_terms ) . '%';
+		if ( $r['search_terms'] ) {
+			$search_terms_like = '%' . bp_esc_like( $r['search_terms'] ) . '%';
 			$where_conditions['search_sql'] = $wpdb->prepare( 'a.content LIKE %s', $search_terms_like );
 		}
 
-		// Filtering
-		if ( $filter && $filter_sql = BP_Activity_Activity::get_filter_sql( $filter ) )
-			$where_conditions['filter_sql'] = $filter_sql;
-
 		// Sorting
-		if ( $sort != 'ASC' && $sort != 'DESC' )
+		$sort = $r['sort'];
+		if ( $sort != 'ASC' && $sort != 'DESC' ) {
 			$sort = 'DESC';
+		}
 
 		// Hide Hidden Items?
-		if ( !$show_hidden )
+		if ( ! $r['show_hidden'] )
 			$where_conditions['hidden_sql'] = "a.hide_sitewide = 0";
 
 		// Exclude specified items
-		if ( !empty( $exclude ) ) {
-			$exclude = implode( ',', wp_parse_id_list( $exclude ) );
+		if ( ! empty( $r['exclude'] ) ) {
+			$exclude = implode( ',', wp_parse_id_list( $r['exclude'] ) );
 			$where_conditions['exclude'] = "a.id NOT IN ({$exclude})";
 		}
 
 		// The specific ids to which you want to limit the query
-		if ( !empty( $in ) ) {
-			$in = implode( ',', wp_parse_id_list( $in ) );
+		if ( ! empty( $r['in'] ) ) {
+			$in = implode( ',', wp_parse_id_list( $r['in'] ) );
 			$where_conditions['in'] = "a.id IN ({$in})";
 		}
 
 		// Process meta_query into SQL
-		$meta_query_sql = self::get_meta_query_sql( $meta_query );
+		$meta_query_sql = self::get_meta_query_sql( $r['meta_query'] );
 
 		if ( ! empty( $meta_query_sql['join'] ) ) {
 			$join_sql .= $meta_query_sql['join'];
@@ -384,7 +410,7 @@
 		}
 
 		// Process date_query into SQL
-		$date_query_sql = self::get_date_query_sql( $date_query );
+		$date_query_sql = self::get_date_query_sql( $r['date_query'] );
 
 		if ( ! empty( $date_query_sql ) ) {
 			$where_conditions['date'] = $date_query_sql;
@@ -393,13 +419,13 @@
 		// Alter the query based on whether we want to show activity item
 		// comments in the stream like normal comments or threaded below
 		// the activity.
-		if ( false === $display_comments || 'threaded' === $display_comments ) {
+		if ( false === $r['display_comments'] || 'threaded' === $r['display_comments'] ) {
 			$excluded_types[] = 'activity_comment';
 		}
 
 		// Exclude 'last_activity' items unless the 'action' filter has
 		// been explicitly set
-		if ( empty( $filter['object'] ) ) {
+		if ( empty( $r['filter']['object'] ) ) {
 			$excluded_types[] = 'last_activity';
 		}
 
@@ -437,8 +463,8 @@
 		}
 
 		// Sanitize page and per_page parameters
-		$page     = absint( $page     );
-		$per_page = absint( $per_page );
+		$page     = absint( $r['page']     );
+		$per_page = absint( $r['per_page'] );
 
 		$retval = array(
 			'activities'     => null,
@@ -495,12 +521,13 @@
 			$activity_ids[] = $activity->id;
 		}
 
-		if ( ! empty( $activity_ids ) && $update_meta_cache ) {
+		if ( ! empty( $activity_ids ) && $r['update_meta_cache'] ) {
 			bp_activity_update_meta_cache( $activity_ids );
 		}
 
-		if ( $activities && $display_comments )
-			$activities = BP_Activity_Activity::append_comments( $activities, $spam );
+		if ( $activities && $r['display_comments'] ) {
+			$activities = BP_Activity_Activity::append_comments( $activities, $r['spam'] );
+		}
 
 		// Pre-fetch data associated with activity users and other objects
 		BP_Activity_Activity::prefetch_object_data( $activities );
@@ -516,9 +543,9 @@
 			$total_activities_sql = apply_filters( 'bp_activity_total_activities_sql', "SELECT count(DISTINCT a.id) FROM {$bp->activity->table_name} a {$join_sql} {$where_sql}", $where_sql, $sort );
 			$total_activities     = $wpdb->get_var( $total_activities_sql );
 
-			if ( !empty( $max ) ) {
-				if ( (int) $total_activities > (int) $max )
-					$total_activities = $max;
+			if ( !empty( $r['max'] ) ) {
+				if ( (int) $total_activities > (int) $r['max'] )
+					$total_activities = $r['max'];
 			}
 
 			$retval['total'] = $total_activities;
@@ -737,6 +764,129 @@
 	}
 
 	/**
+	 * Get the SQL for the 'scope' param in BP_Activity_Activity::get().
+	 *
+	 * A scope is a predetermined set of activity arguments.  This method is used
+	 * to grab these activity arguments and override any existing args if needed.
+	 *
+	 * Can handle multple scopes.
+	 *
+	 * @since BuddyPress (2.2.0)
+	 *
+	 * @param string $scope The activity scope
+	 * @param array $filter_array Current activity arguments
+	 * @return array 'sql' WHERE SQL string and 'override' activity args
+	 */
+	public static function get_scope_query_sql( $scope = '', $filter_array = array() ) {
+		$query_args = array();
+		$override   = array();
+		$retval     = array();
+
+		$scopes = explode( ',', $scope );
+
+		if ( empty( $scopes ) ) {
+			return $sql;
+		}
+
+		// helper to easily grab the 'user_id'
+		if ( ! empty( $filter_array['filter']['user_id'] ) ) {
+			$filter_array['user_id'] = $filter_array['filter']['user_id'];
+		}
+		if ( empty( $filter_array['user_id'] ) ) {
+			$filter_array['user_id'] = bp_displayed_user_id() ? bp_displayed_user_id() : bp_loggedin_user_id();
+		}
+
+		// parse each scope; yes! we handle multiples!
+		foreach ( $scopes as $scope ) {
+			$scope_args = array();
+
+			switch ( $scope ) {
+				// this is new as of BP 2.2
+				// primarily to be used when combining with other scopes
+				case 'personal' :
+					$scope_args['user_id'] = $filter_array['user_id'];
+
+					break;
+
+				case 'just-me' :
+					$scope_args['override']['display_comments'] = 'stream';
+
+					break;
+
+				case 'favorites':
+					$favs = bp_activity_get_user_favorites( $filter_array['user_id'] );
+					if ( empty( $favs ) ) {
+						return $scope_args;
+					}
+
+					$scope_args['id'] = implode( ',', (array) $favs );
+					$scope_args['override']['display_comments']  = true;
+					$scope_args['override']['filter']['user_id'] = 0;
+
+					break;
+
+				case 'mentions':
+					// Are mentions disabled?
+					if ( ! bp_activity_do_mentions() ) {
+						return $scope_args;
+					}
+
+					// Start search at @ symbol and stop search at closing tag delimiter.
+					$scope_args['override']['search_terms'] = '@' . bp_activity_get_user_mentionname( $filter_array['user_id'] ) . '<';
+					$scope_args['override']['display_comments'] = 'stream';
+					$scope_args['override']['filter']['user_id'] = 0;
+
+					break;
+
+				// plugins can hook here to set their scope args
+				//
+				// - scope args use the activity DB column as the array key
+				// - the 'override' array is to override existing activity args if needed.
+				//   arguments match those passed in BP_Activity_Activity::get()
+				default :
+					$scope_args = apply_filters( "bp_activity_set_{$scope}_scope_args", array(), $filter_array );
+					break;
+			}
+
+			if ( ! empty( $scope_args ) ) {
+				// @todo Fix 'hide_sitewide' with multiple scopes... needs more testing
+				if ( 'friends' !== $scope ) {
+					//$scope_args['hide_sitewide'] = ( $filter_array['user_id'] == bp_loggedin_user_id() ) ? 1 : 0;
+				}
+
+				// merge override properties from other scopes
+				// this might be a problem...
+				if ( ! empty( $scope_args['override'] ) ) {
+					$override = array_merge( $override, $scope_args['override'] );
+					unset( $scope_args['override'] );
+				}
+
+				// save scope args
+				if ( ! empty( $scope_args ) ) {
+					$query_args[] = $scope_args;
+				}
+			}
+		}
+
+		if ( ! empty( $query_args ) ) {
+			// set relation to OR
+			$query_args['relation'] = 'OR';
+
+			$query = new BP_Activity_Query( $query_args );
+
+			if ( $sql = $query->get_sql() ) {
+				$retval['sql'] = $sql;
+			}
+		}
+
+		if ( ! empty( $override ) ) {
+			$retval['override'] = $override;
+		}
+
+		return $retval;
+	}
+
+	/**
 	 * In BuddyPress 1.2.x, this was used to retrieve specific activity stream items (for example, on an activity's permalink page).
 	 *
 	 * As of 1.5.x, use BP_Activity_Activity::get() with an 'in' parameter instead.
@@ -1395,6 +1545,150 @@
 }
 
 /**
+ * Generate a MySQL WHERE clause for the specified activity-based parameters.
+ *
+ * This is for complicated activity queries.
+ *
+ * @since BuddyPress (2.2.0)
+ */
+class BP_Activity_Query {
+	/**
+	 * List of activity queries.
+	 *
+	 * @since BuddyPress (2.2.0)
+	 * @access public
+	 * @var array
+	 */
+	public $queries = array();
+
+	/**
+	 * The relation between the queries. Can be one of 'AND' or 'OR'.
+	 *
+	 * @since BuddyPress (2.2.0)
+	 * @access public
+	 * @var string
+	 */
+	public $relation;
+
+	/**
+	 * Constructor
+	 *
+	 * @param array $query_args
+	 */
+	function __construct( $query_args = false ) {
+		if ( ! $query_args ) {
+			return;
+		}
+
+		if ( isset( $query_args['relation'] ) && strtoupper( $query_args['relation'] ) == 'OR' ) {
+			$this->relation = 'OR';
+		} else {
+			$this->relation = 'AND';
+		}
+
+		$this->queries = array();
+
+		foreach ( $query_args as $key => $query ) {
+			if ( ! is_array( $query ) )
+				continue;
+
+			$this->queries[] = $query;
+		}
+	}
+
+	/**
+	 * Turns an array of activity query parameters into a MySQL string.
+	 *
+	 * @since BuddyPress (2.2.0)
+	 * @access public
+	 *
+	 * @return string MySQL WHERE parameters
+	 */
+	public function get_sql() {
+		$where = array();
+
+		foreach ( $this->queries as $key => $query ) {
+			$where[] = $this->get_sql_for_subquery( $query );
+		}
+
+		$where = array_filter( $where );
+
+		if ( empty( $where ) ) {
+			$where = '';
+		} else {
+			$where = "( " . implode( "\n\t{$this->relation} ", $where ) . "\n)";
+		}
+
+		return apply_filters( 'bp_get_activity_query_sql', $where, $this->queries );
+	}
+
+	/**
+	 * Turns a single activity subquery into pieces for a WHERE clause.
+	 *
+	 * @since BuddyPress (2.2.0)
+	 * return array
+	 */
+	protected function get_sql_for_subquery( $query ) {
+		$count = count( $query );
+
+		// simple query
+		if ( $count < 2 ) {
+			$key = key( $query );
+
+			return "\n\t( " . BP_Activity_Activity::get_in_operator_sql( $key, $query[$key] ) . " )";
+
+		// relational query
+		} else {
+			global $wpdb;
+
+			$sql_array = array();
+
+			// get relation
+			if ( ! empty( $query['relation'] ) ) {
+				$relation = $query['relation'];
+				unset( $query['relation'] );
+
+			// default relation to 'AND' for subqueries if nothing is found
+			} else {
+				$relation = 'AND';
+			}
+
+			// get compare
+			if ( ! empty( $query['compare'] ) ) {
+				$compare = $query['compare'];
+				unset( $query['compare'] );
+
+			// default compare to 'IN' for subqueries if nothing is found
+			} else {
+				$compare = 'IN';
+			}
+
+			foreach ( $query as $key => $value ) {
+				// nested query
+				if ( is_numeric( $key ) ) {
+					$sql_array[] = $this->get_sql_for_subquery( $value );
+
+				// tinyint
+				} elseif ( true === in_array( $key, array( 'hide_sitewide', 'is_spam' ) ) ) {
+					$sql_array[$key] = $wpdb->prepare( "{$key} = %d", $value );
+
+				// anything else uses the IN operator
+				} else {
+					$sql_array[$key] = BP_Activity_Activity::get_in_operator_sql( $key, $value );
+
+					// 'NOT IN' operator is as easy as a string replace!
+					if ( 'NOT IN' === $compare ) {
+						$sql_array[$key] = str_replace( 'IN', 'NOT IN', $sql_array[$key] );
+					}
+				}
+			}
+
+			return "\n\t( " . implode( " {$relation} ", $sql_array ) . " )";
+		}
+	}
+}
+
+/**
  * Create a RSS feed using the activity component.
  *
  * You should only construct a new feed when you've validated that you're on
Index: src/bp-activity/bp-activity-functions.php
===================================================================
--- src/bp-activity/bp-activity-functions.php
+++ src/bp-activity/bp-activity-functions.php
@@ -1020,12 +1020,14 @@
 		'search_terms'      => false,        // Pass search terms as a string
 		'meta_query'        => false,        // Filter by activity meta. See WP_Meta_Query for format
 		'date_query'        => false,        // Filter by date. See first parameter of WP_Date_Query for format
+		'filter_query'      => false,
 		'show_hidden'       => false,        // Show activity items that are hidden site-wide?
 		'exclude'           => false,        // Comma-separated list of activity IDs to exclude
 		'in'                => false,        // Comma-separated list or array of activity IDs to which you want to limit the query
 		'spam'              => 'ham_only',   // 'ham_only' (default), 'spam_only' or 'all'.
 		'update_meta_cache' => true,
 		'count_total'       => false,
+		'scope'             => false,
 
 		/**
 		 * Pass filters as an array -- all filter items can be multiple values comma separated:
@@ -1041,7 +1043,7 @@
 	) );
 
 	// Attempt to return a cached copy of the first page of sitewide activity.
-	if ( ( 1 === (int) $r['page'] ) && empty( $r['max'] ) && empty( $r['search_terms'] ) && empty( $r['meta_query'] ) && empty( $r['date_query'] ) && empty( $r['filter'] ) && empty( $r['exclude'] ) && empty( $r['in'] ) && ( 'DESC' === $r['sort'] ) && empty( $r['exclude'] ) && ( 'ham_only' === $r['spam'] ) ) {
+	if ( ( 1 === (int) $r['page'] ) && empty( $r['max'] ) && empty( $r['search_terms'] ) && empty( $r['meta_query'] ) && empty( $r['date_query'] ) && empty( $r['filter_query'] ) && empty( $r['filter'] ) && empty( $r['scope'] ) && empty( $r['exclude'] ) && empty( $r['in'] ) && ( 'DESC' === $r['sort'] ) && empty( $r['exclude'] ) && ( 'ham_only' === $r['spam'] ) ) {
 
 		$activity = wp_cache_get( 'bp_activity_sitewide_front', 'bp' );
 		if ( false === $activity ) {
@@ -1054,7 +1056,9 @@
 				'search_terms'      => $r['search_terms'],
 				'meta_query'        => $r['meta_query'],
 				'date_query'        => $r['date_query'],
+				'filter_query'      => $r['filter_query'],
 				'filter'            => $r['filter'],
+				'scope'             => $r['scope'],
 				'display_comments'  => $r['display_comments'],
 				'show_hidden'       => $r['show_hidden'],
 				'spam'              => $r['spam'],
@@ -1074,7 +1078,9 @@
 			'search_terms'     => $r['search_terms'],
 			'meta_query'       => $r['meta_query'],
 			'date_query'       => $r['date_query'],
+			'filter_query'     => $r['filter_query'],
 			'filter'           => $r['filter'],
+			'scope'            => $r['scope'],
 			'display_comments' => $r['display_comments'],
 			'show_hidden'      => $r['show_hidden'],
 			'exclude'          => $r['exclude'],
Index: src/bp-activity/bp-activity-template.php
===================================================================
--- src/bp-activity/bp-activity-template.php
+++ src/bp-activity/bp-activity-template.php
@@ -176,9 +176,11 @@
 			'exclude'           => false,
 			'in'                => false,
 			'filter'            => false,
+			'scope'             => false,
 			'search_terms'      => false,
 			'meta_query'        => false,
 			'date_query'        => false,
+			'filter_query'      => false,
 			'display_comments'  => 'threaded',
 			'show_hidden'       => false,
 			'spam'              => 'ham_only',
@@ -222,7 +224,9 @@
 				'search_terms'      => $search_terms,
 				'meta_query'        => $meta_query,
 				'date_query'        => $date_query,
+				'filter_query'      => $filter_query,
 				'filter'            => $filter,
+				'scope'             => $scope,
 				'show_hidden'       => $show_hidden,
 				'exclude'           => $exclude,
 				'in'                => $in,
@@ -570,6 +574,7 @@
 
 		'meta_query'        => false,        // filter on activity meta. See WP_Meta_Query for format
 		'date_query'        => false,        // filter by date. See first parameter of WP_Date_Query for format
+		'filter_query'      => false,        // advanced filtering.  This overrides a lot of stuff.
 
 		// Searching
 		'search_terms'      => false,        // specify terms to search on
@@ -594,65 +599,6 @@
 	if ( empty( $search_terms ) && ! empty( $_REQUEST['s'] ) )
 		$search_terms = $_REQUEST['s'];
 
-	// If you have passed a "scope" then this will override any filters you have passed.
-	if ( 'just-me' == $scope || 'friends' == $scope || 'groups' == $scope || 'favorites' == $scope || 'mentions' == $scope ) {
-		if ( 'just-me' == $scope )
-			$display_comments = 'stream';
-
-		// determine which user_id applies
-		if ( empty( $user_id ) )
-			$user_id = bp_displayed_user_id() ? bp_displayed_user_id() : bp_loggedin_user_id();
-
-		// are we displaying user specific activity?
-		if ( is_numeric( $user_id ) ) {
-			$show_hidden = ( $user_id == bp_loggedin_user_id() && $scope != 'friends' ) ? 1 : 0;
-
-			switch ( $scope ) {
-				case 'friends':
-					if ( bp_is_active( 'friends' ) )
-						$friends = friends_get_friend_user_ids( $user_id );
-						if ( empty( $friends ) )
-							return false;
-
-						$user_id = implode( ',', (array) $friends );
-					break;
-				case 'groups':
-					if ( bp_is_active( 'groups' ) ) {
-						$groups = groups_get_user_groups( $user_id );
-						if ( empty( $groups['groups'] ) )
-							return false;
-
-						$object = $bp->groups->id;
-						$primary_id = implode( ',', (array) $groups['groups'] );
-
-						$user_id = 0;
-					}
-					break;
-				case 'favorites':
-					$favs = bp_activity_get_user_favorites( $user_id );
-					if ( empty( $favs ) )
-						return false;
-
-					$in = implode( ',', (array) $favs );
-					$display_comments = true;
-					$user_id = 0;
-					break;
-				case 'mentions':
-
-					// Are mentions disabled?
-					if ( ! bp_activity_do_mentions() ) {
-						return false;
-					}
-
-					// Start search at @ symbol and stop search at closing tag delimiter.
-					$search_terms     = '@' . bp_activity_get_user_mentionname( $user_id ) . '<';
-					$display_comments = 'stream';
-					$user_id = 0;
-					break;
-			}
-		}
-	}
-
 	// Do not exceed the maximum per page
 	if ( !empty( $max ) && ( (int) $per_page > (int) $max ) )
 		$per_page = $max;
@@ -660,16 +606,18 @@
 	// Support for basic filters in earlier BP versions is disabled by default. To enable, put
 	//   add_filter( 'bp_activity_enable_afilter_support', '__return_true' );
 	// into bp-custom.php or your theme's functions.php
-	if ( isset( $_GET['afilter'] ) && apply_filters( 'bp_activity_enable_afilter_support', false ) )
+	if ( isset( $_GET['afilter'] ) && apply_filters( 'bp_activity_enable_afilter_support', false ) ) {
 		$filter = array( 'object' => $_GET['afilter'] );
-	else if ( ! empty( $user_id ) || ! empty( $object ) || ! empty( $action ) || ! empty( $primary_id ) || ! empty( $secondary_id ) || ! empty( $offset ) || ! empty( $since ) )
+	} else if ( ! empty( $user_id ) || ! empty( $object ) || ! empty( $action ) || ! empty( $primary_id ) || ! empty( $secondary_id ) || ! empty( $offset ) || ! empty( $since ) ) {
 		$filter = array( 'user_id' => $user_id, 'object' => $object, 'action' => $action, 'primary_id' => $primary_id, 'secondary_id' => $secondary_id, 'offset' => $offset, 'since' => $since );
-	else
+	} else {
 		$filter = false;
+	}
 
 	// If specific activity items have been requested, override the $hide_spam argument. This prevents backpat errors with AJAX.
-	if ( !empty( $include ) && ( 'ham_only' == $spam ) )
+	if ( !empty( $include ) && ( 'ham_only' == $spam ) ) {
 		$spam = 'all';
+	}
 
 	$template_args = array(
 		'page'              => $page,
@@ -681,9 +629,11 @@
 		'exclude'           => $exclude,
 		'in'                => $in,
 		'filter'            => $filter,
+		'scope'             => $scope,
 		'search_terms'      => $search_terms,
 		'meta_query'        => $meta_query,
 		'date_query'        => $date_query,
+		'filter_query'      => $filter_query,
 		'display_comments'  => $display_comments,
 		'show_hidden'       => $show_hidden,
 		'spam'              => $spam,
Index: src/bp-friends/bp-friends-activity.php
===================================================================
--- src/bp-friends/bp-friends-activity.php
+++ src/bp-friends/bp-friends-activity.php
@@ -207,6 +207,31 @@
 add_filter( 'bp_activity_prefetch_object_data', 'bp_friends_prefetch_activity_object_data' );
 
 /**
+ * Set up activity arguments for use with the 'friends' scope.
+ *
+ * @since BuddyPress (2.2.0)
+ *
+ * @param array $retval Empty array by default
+ * @param array $filter Current activity arguments
+ * @return array
+ */
+function bp_friends_filter_activity_scope( $retval, $filter ) {
+	$friends = friends_get_friend_user_ids( $filter['user_id'] );
+
+	if ( empty( $friends ) ) {
+		return $retval;
+	}
+
+	$retval['user_id'] = implode( ',', (array) $friends );
+
+	// wipe out the user ID
+	$retval['override']['filter']['user_id'] = 0;
+
+	return $retval;
+}
+add_filter( 'bp_activity_set_friends_scope_args', 'bp_friends_filter_activity_scope', 10, 2 );
+
+/**
  * Add activity stream items when one members accepts another members request
  * for virtual friendship.
  *
Index: src/bp-groups/bp-groups-activity.php
===================================================================
--- src/bp-groups/bp-groups-activity.php
+++ src/bp-groups/bp-groups-activity.php
@@ -178,6 +178,32 @@
 add_filter( 'bp_activity_prefetch_object_data', 'bp_groups_prefetch_activity_object_data' );
 
 /**
+ * Set up activity arguments for use with the 'groups' scope.
+ *
+ * @since BuddyPress (2.2.0)
+ *
+ * @param array $retval Empty array by default
+ * @param array $filter Current activity arguments
+ * @return array
+ */
+function bp_groups_filter_activity_scope( $retval, $filter ) {
+	$groups = groups_get_user_groups( $filter['user_id'] );
+
+	if ( empty( $groups['groups'] ) ) {
+		return $retval;
+	}
+
+	$retval['component'] = buddypress()->groups->id;
+	$retval['item_id']   = implode( ',', (array) $groups['groups'] );
+
+	// wipe out the user ID
+	$retval['override']['filter']['user_id'] = 0;
+
+	return $retval;
+}
+add_filter( 'bp_activity_set_groups_scope_args', 'bp_groups_filter_activity_scope', 10, 2 );
+
+/**
  * Record an activity item related to the Groups component.
  *
  * A wrapper for {@link bp_activity_add()} that provides some Groups-specific
Index: tests/phpunit/testcases/activity/template.php
===================================================================
--- tests/phpunit/testcases/activity/template.php
+++ tests/phpunit/testcases/activity/template.php
@@ -117,6 +117,76 @@
 	}
 
 	/**
+	 * @group scope
+	 */
+	function test_bp_has_activities_multiple_scope() {
+		$u1 = $this->create_user();
+		$u2 = $this->create_user();
+		$u3 = $this->create_user();
+
+		// user 1 becomes friends with user 2
+		friends_add_friend( $u1, $u2, true );
+
+		// user 1 joins a group
+		$g1 = $this->factory->group->create( array( 'creator_id' => $u1 ) );
+
+		$now = time();
+
+		// friend status update
+		$a1 = $this->factory->activity->create( array(
+			'user_id' => $u2,
+			'type' => 'activity_update',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now ),
+		) );
+
+		// group activity
+		$a2 = $this->factory->activity->create( array(
+			'user_id'   => $u3,
+			'component' => 'groups',
+			'item_id'   => $g1,
+			'type'      => 'joined_group',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+
+		// misc activity items
+		$this->factory->activity->create( array(
+			'user_id'   => $u3,
+			'component' => 'blogs',
+			'item_id'   => 1,
+			'type'      => 'new_blog_post',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+		$this->factory->activity->create( array(
+			'user_id'   => $u3,
+			'component' => 'activity',
+			'type'      => 'activity_update',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+		$this->factory->activity->create( array(
+			'user_id'   => $u3,
+			'component' => 'groups',
+			'item_id'   => 324,
+			'type'      => 'activity_update',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+
+		// groan. It sucks that you have to invoke the global
+		global $activities_template;
+
+		// grab activities from multiple scopes
+		bp_has_activities( array(
+			'user_id' => $u1,
+			'scope' => 'groups,friends',
+		) );
+
+		// assert!
+		$this->assertEqualSets( array( $a1, $a2 ), wp_list_pluck( $activities_template->activities, 'id' ) );
+
+		// clean up!
+		$activities_template = null;
+	}
+
+	/**
 	 * Integration test for 'meta_query' param
 	 */
 	function test_bp_has_activities_with_meta_query() {
