Index: src/bp-activity/bp-activity-classes.php
===================================================================
--- src/bp-activity/bp-activity-classes.php
+++ src/bp-activity/bp-activity-classes.php
@@ -275,6 +275,9 @@
 	 *     @type array $date_query An array of date_query conditions.
 	 *                             See first parameter of WP_Date_Query::__construct()
 	 *                             for description.
+	 *     @type array $filter_query An array of activity query conditions.
+	 *                               See BP_Activity_Query::__construct()
+	 *                               for description.
 	 *     @type array $filter See BP_Activity_Activity::get_filter_sql().
 	 *     @type string $search_terms Limit results by a search term.
 	 *                                Default: false.
@@ -327,7 +330,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
@@ -336,7 +341,6 @@
 			'count_total'       => false,
 		);
 		$r = wp_parse_args( $args, $defaults );
-		extract( $r );
 
 		// Select conditions
 		$select_sql = "SELECT DISTINCT a.id";
@@ -351,44 +355,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 ( $sql = $filter_query->get_sql() ) {
+				$where_conditions['filter_query_sql'] = $sql;
+			}
+		}
+
+		// 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' == $r['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'];
@@ -399,7 +428,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;
@@ -408,13 +437,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';
 		}
 
@@ -468,8 +497,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,
@@ -517,7 +546,6 @@
 			}
 
 		} else {
-
 			// Query first for activity IDs
 			$activity_ids_sql = "{$select_sql} {$from_sql} {$join_sql} {$where_sql} ORDER BY a.date_recorded {$sort}";
 
@@ -559,12 +587,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 );
@@ -589,9 +618,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;
@@ -817,6 +846,165 @@
 	}
 
 	/**
+	 * 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  $r     Current activity arguments. Same as those of BP_Activity_Activity::get(),
+	 *                       but merged with defaults.
+	 * @return array 'sql' WHERE SQL string and 'override' activity args
+	 */
+	public static function get_scope_query_sql( $scope = '', $r = 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( $r['filter']['user_id'] ) ) {
+			$r['user_id'] = $r['filter']['user_id'];
+		}
+		if ( empty( $r['user_id'] ) ) {
+			$r['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 = array(
+						'column' => 'user_id',
+						'value'  => $r['user_id']
+					);
+
+					break;
+
+				case 'just-me' :
+					$scope_args['override']['display_comments'] = 'stream';
+
+					break;
+
+				case 'favorites':
+					$favs = bp_activity_get_user_favorites( $r['user_id'] );
+					if ( empty( $favs ) ) {
+						return $scope_args;
+					}
+
+					$scope_args = array(
+						'column'  => 'id',
+						'compare' => 'IN',
+						'value'   => 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;
+					}
+
+					$scope_args = array(
+						'column'  => 'content',
+						'compare' => 'LIKE',
+
+						// Start search at @ symbol and stop search at closing tag delimiter.
+						'value'   => '@' . bp_activity_get_user_mentionname( $r['user_id'] ) . '<'
+					);
+
+					// wipe out current search terms if any
+					// this is so the 'mentions' scope can be combined with other scopes
+					$scope_args['override']['search_terms'] = false;
+
+					$scope_args['override']['display_comments'] = 'stream';
+					$scope_args['override']['filter']['user_id'] = 0;
+
+					break;
+
+				default :
+					/**
+					 * Plugins can hook here to set their activity arguments for custom scopes.
+					 *
+					 * This is a dynamic filter based on the activity scope. eg:
+					 *   - 'bp_activity_set_groups_scope_args'
+					 *   - 'bp_activity_set_friends_scope_args'
+					 *
+					 * To see how this filter is used, plugin devs should check out:
+					 *   - bp_groups_filter_activity_scope() - used for 'groups' scope
+					 *   - bp_friends_filter_activity_scope() - used for 'friends' scope
+					 *
+					 * @since BuddyPress (2.2.0)
+					 *
+					 *  @param array {
+					 *     Activity query clauses.
+					 *
+					 *     @type array {
+					 *         Activity arguments for your custom scope.
+					 *         See {@link BP_Activity_Query::_construct()} for more details.
+					 *     }
+					 *     @type array $override Optional. Override existing activity arguments passed by $r.
+					 * }
+					 * @param array $r Current activity arguments passed in BP_Activity_Activity::get()
+					 */
+					$scope_args = apply_filters( "bp_activity_set_{$scope}_scope_args", array(), $r );
+					break;
+			}
+
+			if ( ! empty( $scope_args ) ) {
+				// @todo Fix 'hide_sitewide' with multiple scopes... needs more testing
+				if ( 'friends' !== $scope ) {
+					$scope_args['override']['show_hidden'] = ( $r['user_id'] == bp_loggedin_user_id() ) ? true : false;
+				}
+
+				// 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.
@@ -1497,6 +1685,264 @@
 }
 
 /**
+ * Class for generating the WHERE SQL clause for advanced activity fetching.
+ *
+ * This is notably used in {@link BP_Activity_Activity::get()} with the
+ * 'filter_query' parameter.
+ *
+ * @since BuddyPress (2.2.0)
+ */
+class BP_Activity_Query extends BP_Recursive_Query {
+	/**
+	 * Array of activity queries.
+	 *
+	 * See {@see BP_Activity_Query::__construct()} for information on query arguments.
+	 *
+	 * @since BuddyPress (2.2.0)
+	 * @access public
+	 * @var array
+	 */
+	public $queries = array();
+
+	/**
+	 * Table alias.
+	 *
+	 * @since BuddyPress (2.2.0)
+	 * @access public
+	 * @var string
+	 */
+	public $table_alias = '';
+
+	/**
+	 * Supported DB columns.
+	 *
+	 * See the 'wp_bp_activity' DB table schema.
+	 *
+	 * @since BuddyPress (2.2.0)
+	 * @access public
+	 * @var array
+	 */
+	public $db_columns = array(
+		'id', 'user_id', 'component', 'type', 'action', 'content',
+		'item_id', 'secondary_item_id', 'hide_sitewide', 'is_spam',
+	);
+
+	/**
+	 * Constructor.
+	 *
+	 * @since BuddyPress (2.2.0)
+	 *
+	 * @param array $query {
+	 *     Array of query clauses.
+	 *
+	 *     @type array {
+	 *         @type string $column   Required. The column to query against. Basically, any DB column in the main
+	 *                                'wp_bp_activity' table.
+	 *         @type string $value    Required. Value to filter by.
+	 *         @type string $compare  Optional. The comparison operator. Default '='.
+	 *                                Accepts '=', '!=', '>', '>=', '<', '<=', 'IN', 'NOT IN', 'LIKE',
+	 *                                'NOT LIKE', BETWEEN', 'NOT BETWEEN', 'REGEXP', 'NOT REGEXP', 'RLIKE'
+	 *         @type string $relation Optional. The boolean relationship between the activity queries.
+	 *                                Accepts 'OR', 'AND'. Default 'AND'.
+	 *         @type array {
+	 *             Optional. Another fully-formed activity query. See parameters above.
+	 *         }
+	 *     }
+	 * }
+	 */
+	public function __construct( $query = array() ) {
+		if ( ! is_array( $query ) ) {
+			return;
+		}
+
+		$this->queries = $this->sanitize_query( $query );
+	}
+
+	/**
+	 * Generates WHERE SQL clause to be appended to a main query.
+	 *
+	 * @since BuddyPress (2.2.0)
+	 * @access public
+	 *
+	 * @param string $alias An existing table alias that is compatible with the current query clause.
+	 *               Default: 'a'. BP_Activity_Activity::get() uses 'a', so we default to that.
+	 * @return string SQL fragment to append to the main WHERE clause.
+	 * }
+	 */
+	public function get_sql( $alias = 'a' ) {
+		if ( ! empty( $alias ) ) {
+			$this->table_alias = sanitize_title( $alias );
+		}
+
+		$sql = $this->get_sql_clauses();
+
+		// we only need the 'where' clause
+		//
+		// also trim trailing "AND" clause from parent BP_Recursive_Query class
+		// since it's not necessary for our needs
+		return preg_replace( '/^\sAND/', '', $sql['where'] );
+	}
+
+	/**
+	 * Generate WHERE clauses for a first-order clause.
+	 *
+	 * @since BuddyPress (2.2.0)
+	 * @access protected
+	 *
+	 * @param  array $clause       Array of arguments belonging to the clause.
+	 * @param  array $parent_query Parent query to which the clause belongs.
+	 * @return array {
+	 *     @type array $where Array of subclauses for the WHERE statement.
+	 *     @type array $join  Empty array. Not used.
+	 * }
+	 */
+	protected function get_sql_for_clause( $clause, $parent_query ) {
+		global $wpdb;
+
+		$sql_chunks = array(
+			'where' => array(),
+			'join' => array(),
+		);
+
+		$column = isset( $clause['column'] ) ? $this->validate_column( $clause['column'] ) : '';
+		$value  = isset( $clause['value'] )  ? $clause['value'] : '';
+		if ( empty( $column ) || ! isset( $clause['value'] ) ) {
+			return $sql_chunks;
+		}
+
+		if ( isset( $clause['compare'] ) ) {
+			$clause['compare'] = strtoupper( $clause['compare'] );
+		} else {
+			$clause['compare'] = isset( $clause['value'] ) && is_array( $clause['value'] ) ? 'IN' : '=';
+		}
+
+		// default 'compare' to '=' if no valid operator is found
+		if ( ! in_array( $clause['compare'], array(
+			'=', '!=', '>', '>=', '<', '<=',
+			'LIKE', 'NOT LIKE',
+			'IN', 'NOT IN',
+			'BETWEEN', 'NOT BETWEEN',
+			'REGEXP', 'NOT REGEXP', 'RLIKE'
+
+			// not supporting 'EXISTS' for now
+			//'EXISTS', 'NOT EXISTS',
+		) ) ) {
+			$clause['compare'] = '=';
+		}
+
+		$compare = $clause['compare'];
+
+		// we do not support EXISTS / NOT EXISTS (hence no JOINs) so the alias is
+		// simple for now
+		$alias = ! empty( $this->table_alias ) ? "{$this->table_alias}." : '';
+
+		// Next, Build the WHERE clause.
+		$where = '';
+
+		// value.
+		if ( isset( $clause['value'] ) ) {
+			// we don't support 'type' yet (do we even need to? probably not.)
+			//$type = $this->get_cast_for_type( isset( $clause['type'] ) ? $clause['type'] : '' );
+
+			if ( in_array( $compare, array( 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ) ) ) {
+				if ( ! is_array( $value ) ) {
+					$value = preg_split( '/[,\s]+/', $value );
+				}
+			} else {
+				$value = trim( $value );
+			}
+
+			// tinyint
+			if ( ! empty( $column ) && true === in_array( $column, array( 'hide_sitewide', 'is_spam' ) ) ) {
+				$sql_chunks['where'][] = $wpdb->prepare( "{$alias}{$column} = %d", $value );
+
+			} else {
+				switch ( $compare ) {
+					// IN uses different syntax
+					case 'IN' :
+					case 'NOT IN' :
+						$in_sql = BP_Activity_Activity::get_in_operator_sql( "{$alias}{$column}", $value );
+
+						// 'NOT IN' operator is as easy as a string replace!
+						if ( 'NOT IN' === $compare ) {
+							$in_sql = str_replace( 'IN', 'NOT IN', $in_sql );
+						}
+
+						$sql_chunks['where'][] = $in_sql;
+						break;
+
+					case 'BETWEEN' :
+					case 'NOT BETWEEN' :
+						$value = array_slice( $value, 0, 2 );
+						$where = $wpdb->prepare( '%s AND %s', $value );
+						break;
+
+					case 'LIKE' :
+					case 'NOT LIKE' :
+						$value = '%' . bp_esc_like( $value ) . '%';
+						$where = $wpdb->prepare( '%s', $value );
+						break;
+
+					default :
+						$where = $wpdb->prepare( '%s', $value );
+						break;
+
+				}
+			}
+
+			if ( $where ) {
+				// no 'type' yet
+				//$sql_chunks['where'][] = "CAST({$alias}{$column} AS {$type}) {$compare} {$where}";
+
+				$sql_chunks['where'][] = "{$alias}{$column} {$compare} {$where}";
+			}
+		}
+
+		/*
+		 * Multiple WHERE clauses should be joined in parentheses.
+		 */
+		if ( 1 < count( $sql_chunks['where'] ) ) {
+			$sql_chunks['where'] = array( '( ' . implode( ' AND ', $sql_chunks['where'] ) . ' )' );
+		}
+
+		return $sql_chunks;
+	}
+
+	/**
+	 * Determine whether a clause is first-order.
+	 *
+	 * @since BuddyPress (2.2.0)
+	 * @access protected
+	 *
+	 * @param  array $q Clause to check.
+	 * @return bool
+	 */
+        protected function is_first_order_clause( $query ) {
+		return isset( $query['column'] ) || isset( $query['value'] );
+        }
+
+	/**
+	 * Validates a column name parameter.
+	 *
+	 * Column names are checked against a whitelist of known tables.
+	 * See {@link BP_Activity_Query::db_tables}.
+	 *
+	 * @since BuddyPress (2.2.0)
+	 * @access public
+	 *
+	 * @param string $column The user-supplied column name.
+	 * @return string A validated column name value.
+	 */
+	public function validate_column( $column = '' ) {
+		if ( in_array( $column, $this->db_columns ) ) {
+			return $column;
+		} else {
+			return '';
+		}
+	}
+}
+
+/**
  * 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
@@ -1417,12 +1417,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:
@@ -1438,7 +1440,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 ) {
@@ -1451,7 +1453,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'],
@@ -1471,7 +1475,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
@@ -201,9 +201,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',
@@ -249,7 +251,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,
@@ -611,6 +615,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
@@ -635,65 +640,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;
@@ -709,12 +655,13 @@
 	 *
 	 * @param bool $value True if BuddyPress should enable afilter support.
 	 */
-	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 ) )
@@ -731,9 +678,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-core/bp-core-classes.php
===================================================================
--- src/bp-core/bp-core-classes.php
+++ src/bp-core/bp-core-classes.php
@@ -2711,3 +2711,229 @@
 		return apply_filters( 'bp_members_suggestions_get_suggestions', $results, $this );
 	}
 }
+
+/**
+ * Base class for creating query classes that generate SQL fragments for filtering results based on recursive query params.
+ *
+ * @since BuddyPress (2.2.0)
+ */
+abstract class BP_Recursive_Query {
+
+        /**
+         * Query arguments passed to the constructor.
+         *
+         * @since BuddyPress (2.2.0)
+         * @access public
+         * @var array
+         */
+        public $queries = array();
+
+        /**
+         * Generate SQL clauses to be appended to a main query.
+         *
+         * Extending classes should call this method from within a publicly
+         * accessible get_sql() method, and manipulate the SQL as necessary.
+         * For example, {@link BP_XProfile_Query::get_sql()} is merely a wrapper for
+         * get_sql_clauses(), while {@link BP_Activity_Query::get_sql()} discards
+         * the empty 'join' clauses are discarded, and passes the 'where'
+         * clause through apply_filters().
+         *
+         * @since BuddyPress (2.2.0)
+         * @access protected
+         *
+         * @param  string $primary_table
+         * @param  string $primary_id_column
+         * @return array
+         */
+        protected function get_sql_clauses() {
+                $sql = $this->get_sql_for_query( $this->queries );
+
+                if ( ! empty( $sql['where'] ) ) {
+                        $sql['where'] = ' AND ' . "\n" . $sql['where'] . "\n";
+                }
+
+                return $sql;
+        }
+
+        /**
+         * Generate SQL clauses for a single query array.
+         *
+         * If nested subqueries are found, this method recurses the tree to
+         * produce the properly nested SQL.
+         *
+         * Subclasses generally do not need to call this method. It is invoked
+         * automatically from get_sql_clauses().
+         *
+         * @since BuddyPress (2.2.0)
+         * @access protected
+         *
+         * @param  array $query Query to parse.
+         * @param  int   $depth Optional. Number of tree levels deep we
+         *                      currently are. Used to calculate indentation.
+         * @return array
+         */
+        protected function get_sql_for_query( $query, $depth = 0 ) {
+                $sql_chunks = array(
+                        'join'  => array(),
+                        'where' => array(),
+                );
+
+                $sql = array(
+                        'join'  => '',
+                        'where' => '',
+                );
+
+                $indent = '';
+                for ( $i = 0; $i < $depth; $i++ ) {
+                        $indent .= "\t";
+                }
+
+                foreach ( $query as $key => $clause ) {
+                        if ( 'relation' === $key ) {
+                                $relation = $query['relation'];
+                        } else if ( is_array( $clause ) ) {
+                                // This is a first-order clause
+                                if ( $this->is_first_order_clause( $clause ) ) {
+                                        $clause_sql = $this->get_sql_for_clause( $clause, $query );
+
+                                        $where_count = count( $clause_sql['where'] );
+                                        if ( ! $where_count ) {
+                                                $sql_chunks['where'][] = '';
+                                        } else if ( 1 === $where_count ) {
+                                                $sql_chunks['where'][] = $clause_sql['where'][0];
+                                        } else {
+                                                $sql_chunks['where'][] = '( ' . implode( ' AND ', $clause_sql['where'] ) . ' )';
+                                        }
+
+                                        $sql_chunks['join'] = array_merge( $sql_chunks['join'], $clause_sql['join'] );
+                                // This is a subquery
+                                } else {
+                                        $clause_sql = $this->get_sql_for_query( $clause, $depth + 1 );
+
+                                        $sql_chunks['where'][] = $clause_sql['where'];
+                                        $sql_chunks['join'][]  = $clause_sql['join'];
+
+                                }
+                        }
+                }
+
+                // Filter empties
+                $sql_chunks['join']  = array_filter( $sql_chunks['join'] );
+                $sql_chunks['where'] = array_filter( $sql_chunks['where'] );
+
+                if ( empty( $relation ) ) {
+                        $relation = 'AND';
+                }
+
+                if ( ! empty( $sql_chunks['join'] ) ) {
+                        $sql['join'] = implode( ' ', array_unique( $sql_chunks['join'] ) );
+                }
+
+                if ( ! empty( $sql_chunks['where'] ) ) {
+                        $sql['where'] = '( ' . "\n\t" . $indent . implode( ' ' . "\n\t" . $indent . $relation . ' ' . "\n\t" . $indent, $sql_chunks['where'] ) . "\n" . $indent . ')' . "\n";
+                }
+
+                return $sql;
+        }
+
+	/**
+	 * Recursive-friendly query sanitizer.
+	 *
+	 * Ensures that each query-level clause has a 'relation' key, and that
+	 * each first-order clause contains all the necessary keys from
+	 * $defaults.
+	 *
+	 * Extend this method if your class uses different sanitizing logic.
+	 *
+	 * @since BuddyPress (2.2.0)
+	 * @access public
+	 *
+	 * @param  array $queries Array of query clauses.
+	 * @return array Sanitized array of query clauses.
+	 */
+	protected function sanitize_query( $queries ) {
+		$clean_queries = array();
+
+		if ( ! is_array( $queries ) ) {
+			return $clean_queries;
+		}
+
+		foreach ( $queries as $key => $query ) {
+			if ( 'relation' === $key ) {
+				$relation = $query;
+
+			} else if ( ! is_array( $query ) ) {
+				continue;
+
+			// First-order clause.
+			} else if ( $this->is_first_order_clause( $query ) ) {
+				if ( isset( $query['value'] ) && array() === $query['value'] ) {
+					unset( $query['value'] );
+				}
+
+				$clean_queries[] = $query;
+
+			// Otherwise, it's a nested query, so we recurse.
+			} else {
+				$cleaned_query = $this->sanitize_query( $query );
+
+				if ( ! empty( $cleaned_query ) ) {
+					$clean_queries[] = $cleaned_query;
+				}
+			}
+		}
+
+		if ( empty( $clean_queries ) ) {
+			return $clean_queries;
+		}
+
+		// Sanitize the 'relation' key provided in the query.
+		if ( isset( $relation ) && 'OR' === strtoupper( $relation ) ) {
+			$clean_queries['relation'] = 'OR';
+
+		/*
+		 * If there is only a single clause, call the relation 'OR'.
+		 * This value will not actually be used to join clauses, but it
+		 * simplifies the logic around combining key-only queries.
+		 */
+		} else if ( 1 === count( $clean_queries ) ) {
+			$clean_queries['relation'] = 'OR';
+
+		// Default to AND.
+		} else {
+			$clean_queries['relation'] = 'AND';
+		}
+
+		return $clean_queries;
+	}
+
+        /**
+         * Generate JOIN and WHERE clauses for a first-order clause.
+         *
+         * Must be overridden in a subclass.
+         *
+         * @since BuddyPress (2.2.0)
+         * @access protected
+         *
+         * @param  array $clause       Array of arguments belonging to the clause.
+         * @param  array $parent_query Parent query to which the clause belongs.
+         * @return array {
+         *     @type array $join  Array of subclauses for the JOIN statement.
+         *     @type array $where Array of subclauses for the WHERE statement.
+         * }
+         */
+        abstract protected function get_sql_for_clause( $clause, $parent_query );
+
+        /**
+         * Determine whether a clause is first-order.
+         *
+         * Must be overridden in a subclass.
+         *
+         * @since BuddyPress (2.2.0)
+         * @access protected
+         *
+         * @param  array $q Clause to check.
+         * @return bool
+         */
+        abstract protected function is_first_order_clause( $query );
+}
\ No newline at end of file
Index: src/bp-friends/bp-friends-activity.php
===================================================================
--- src/bp-friends/bp-friends-activity.php
+++ src/bp-friends/bp-friends-activity.php
@@ -228,6 +228,45 @@
 add_filter( 'bp_activity_prefetch_object_data', 'bp_friends_prefetch_activity_object_data' );
 
 /**
+ * Set up activity arguments for use with the 'friends' scope.
+ *
+ * For details on the syntax, see {@link BP_Activity_Query}.
+ *
+ * @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= array(
+		'relation' => 'AND',
+		array(
+			'column'  => 'user_id',
+			'compare' => 'IN',
+			'value'   => implode( ',', (array) $friends )
+		),
+		// we should only be able to view sitewide activity content for friends
+		array(
+			'column' => 'hide_sitewide',
+			'value'  => 0
+		),
+	);
+
+	// 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
@@ -180,6 +180,42 @@
 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= array(
+		'relation' => 'AND',
+		array(
+			'column' => 'component',
+			'value'  => buddypress()->groups->id
+		),
+		array(
+			'column'  => 'item_id',
+			'compare' => 'IN',
+			'value'   => 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
@@ -89,6 +89,7 @@
 	 * limiting query to user favorites
 	 *
 	 * @ticket BP4872
+	 * @group scope
 	 */
 	public function test_bp_has_activities_favorites_action_filter() {
 		$user_id = $this->factory->user->create( array( 'role' => 'subscriber' ) );
@@ -143,6 +144,588 @@
 	}
 
 	/**
+	 * @group scope
+	 * @group filter_query
+	 * @group BP_Activity_Query
+	 */
+	function test_bp_has_activities_mentions_scope() {
+		$u1 = $this->factory->user->create();
+		$u2 = $this->factory->user->create();
+
+		$now = time();
+
+		// mentioned activity item
+		$mention_username = '@' . bp_activity_get_user_mentionname( $u1 );
+		$a1 = $this->factory->activity->create( array(
+			'user_id' => $u2,
+			'type'    => 'activity_update',
+			'content' => "{$mention_username} - You rule, dude!",
+			'recorded_time' => date( 'Y-m-d H:i:s', $now ),
+		) );
+
+		// misc activity items
+		$this->factory->activity->create( array(
+			'user_id'   => $u1,
+			'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'   => $u2,
+			'component' => 'activity',
+			'type'      => 'activity_update',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+		$this->factory->activity->create( array(
+			'user_id'   => $u2,
+			'component' => 'groups',
+			'item_id'   => 324,
+			'type'      => 'activity_update',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+
+		global $activities_template;
+
+		// grab activities from multiple scopes
+		bp_has_activities( array(
+			'user_id' => $u1,
+			'scope' => 'mentions',
+		) );
+
+		// assert!
+		$this->assertEqualSets( array( $a1 ), wp_list_pluck( $activities_template->activities, 'id' ) );
+
+		// clean up!
+		$activities_template = null;
+	}
+
+	/**
+	 * @group scope
+	 * @group filter_query
+	 * @group BP_Activity_Query
+	 */
+	function test_bp_has_activities_friends_and_mentions_scope() {
+		$u1 = $this->factory->user->create();
+		$u2 = $this->factory->user->create();
+		$u3 = $this->factory->user->create();
+
+		// user 1 becomes friends with user 2
+		friends_add_friend( $u1, $u2, true );
+
+		$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 ),
+		) );
+
+		// mentioned item by non-friend
+		$mention_username = '@' . bp_activity_get_user_mentionname( $u1 );
+		$a2 = $this->factory->activity->create( array(
+			'user_id'   => $u3,
+			'component' => 'activity',
+			'type'      => 'activity_update',
+			'content'   => "{$mention_username} - Oy!",
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+
+		// misc activity items
+		$this->factory->activity->create( array(
+			'user_id'   => $u1,
+			'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 ),
+		) );
+
+		global $activities_template;
+
+		// grab activities from multiple scopes
+		bp_has_activities( array(
+			'user_id' => $u1,
+			'scope' => 'mentions,friends',
+		) );
+
+		// assert!
+		$this->assertEqualSets( array( $a1, $a2 ), wp_list_pluck( $activities_template->activities, 'id' ) );
+
+		// clean up!
+		$activities_template = null;
+	}
+
+	/**
+	 * @group scope
+	 * @group filter_query
+	 * @group BP_Activity_Query
+	 */
+	function test_bp_has_activities_groups_and_friends_scope() {
+		$u1 = $this->factory->user->create();
+		$u2 = $this->factory->user->create();
+		$u3 = $this->factory->user->create();
+
+		// 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 ) );
+		$g2 = $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 ),
+		) );
+
+		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;
+	}
+
+	/**
+	 * @group filter_query
+	 * @group BP_Activity_Query
+	 */
+	function test_bp_has_activities_with_filter_query_nested_conditions() {
+		$u1 = $this->factory->user->create();
+		$u2 = $this->factory->user->create();
+		$u3 = $this->factory->user->create();
+
+		$now = time();
+
+		$a1 = $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 ),
+		) );
+		$a2 = $this->factory->activity->create( array(
+			'user_id'   => $u2,
+			'component' => 'activity',
+			'type'      => 'activity_update',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+
+		// misc activity items
+		$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 ),
+		) );
+
+		global $activities_template;
+
+		bp_has_activities( array(
+			'filter_query' => array(
+				'relation' => 'OR',
+				array(
+					'column' => 'component',
+					'value'  => 'blogs',
+				),
+				array(
+					'relation' => 'AND',
+					array(
+						'column' => 'type',
+						'value'  => 'activity_update',
+					),
+					array(
+						'column' => 'user_id',
+						'value'  => $u2,
+					),
+				),
+			)
+		) );
+
+		// assert!
+		$this->assertEqualSets( array( $a1, $a2 ), wp_list_pluck( $activities_template->activities, 'id' ) );
+
+		// clean up!
+		$activities_template = null;
+	}
+
+	/**
+	 * @group filter_query
+	 * @group BP_Activity_Query
+	 */
+	function test_bp_has_activities_with_filter_query_compare_not_in_operator() {
+		$u1 = $this->factory->user->create();
+		$u2 = $this->factory->user->create();
+		$u3 = $this->factory->user->create();
+
+		$now = time();
+
+		// misc activity items
+		$a1 = $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 ),
+		) );
+		$a2 = $this->factory->activity->create( array(
+			'user_id'   => $u2,
+			'component' => 'activity',
+			'type'      => 'activity_update',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+		$a3 = $this->factory->activity->create( array(
+			'user_id'   => $u3,
+			'component' => 'activity',
+			'type'      => 'activity_update',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+		$a4 = $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 ),
+		) );
+
+		global $activities_template;
+
+		bp_has_activities( array(
+			'filter_query' => array(
+				array(
+					'column'  => 'id',
+					'compare' => 'NOT IN',
+					'value'   => array( $a1, $a4 ),
+				),
+			)
+		) );
+
+		// assert!
+		$this->assertEqualSets( array( $a2, $a3 ), wp_list_pluck( $activities_template->activities, 'id' ) );
+
+		// clean up!
+		$activities_template = null;
+	}
+
+	/**
+	 * @group filter_query
+	 * @group BP_Activity_Query
+	 */
+	function test_bp_has_activities_with_filter_query_compare_between_operator() {
+		$u1 = $this->factory->user->create();
+
+		$now = time();
+
+		// misc activity items
+		$a1 = $this->factory->activity->create( array(
+			'user_id'   => $u1,
+			'component' => 'blogs',
+			'item_id'   => 1,
+			'type'      => 'new_blog_post',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+		$a2 = $this->factory->activity->create( array(
+			'user_id'   => $u1,
+			'component' => 'activity',
+			'type'      => 'activity_update',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+		$a3 = $this->factory->activity->create( array(
+			'user_id'   => $u1,
+			'component' => 'activity',
+			'type'      => 'activity_update',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+		$a4 = $this->factory->activity->create( array(
+			'user_id'   => $u1,
+			'component' => 'groups',
+			'item_id'   => 324,
+			'type'      => 'activity_update',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+
+		global $activities_template;
+
+		bp_has_activities( array(
+			'filter_query' => array(
+				array(
+					'column'  => 'id',
+					'compare' => 'BETWEEN',
+					'value'   => array( $a3, $a4 ),
+				),
+			)
+		) );
+
+		// assert!
+		$this->assertEqualSets( array( $a3, $a4 ), wp_list_pluck( $activities_template->activities, 'id' ) );
+
+		// clean up!
+		$activities_template = null;
+	}
+
+	/**
+	 * @group filter_query
+	 * @group BP_Activity_Query
+	 */
+	function test_bp_has_activities_with_filter_query_compare_arithmetic_comparisons() {
+		$u1 = $this->factory->user->create();
+
+		$now = time();
+
+		// misc activity items
+		$a1 = $this->factory->activity->create( array(
+			'user_id'   => $u1,
+			'component' => 'activity',
+			'item_id'   => 1,
+			'type'      => 'activity_update',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+		$a2 = $this->factory->activity->create( array(
+			'user_id'   => $u1,
+			'component' => 'activity',
+			'item_id'   => 10,
+			'type'      => 'activity_update',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+		$a3 = $this->factory->activity->create( array(
+			'user_id'   => $u1,
+			'component' => 'activity',
+			'item_id'   => 25,
+			'type'      => 'activity_update',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+		$a4 = $this->factory->activity->create( array(
+			'user_id'   => $u1,
+			'component' => 'activity',
+			'item_id'   => 100,
+			'type'      => 'activity_update',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+
+		global $activities_template;
+
+		// greater than
+		bp_has_activities( array(
+			'filter_query' => array(
+				array(
+					'column'  => 'item_id',
+					'compare' => '>',
+					'value'   => 10,
+				),
+			)
+		) );
+
+		// assert!
+		$this->assertEqualSets( array( $a3, $a4 ), wp_list_pluck( $activities_template->activities, 'id' ) );
+
+		// greater or equal than
+		bp_has_activities( array(
+			'filter_query' => array(
+				array(
+					'column'  => 'item_id',
+					'compare' => '>=',
+					'value'   => 10,
+				),
+			)
+		) );
+
+		// assert!
+		$this->assertEqualSets( array( $a2, $a3, $a4 ), wp_list_pluck( $activities_template->activities, 'id' ) );
+
+		// less than
+		bp_has_activities( array(
+			'filter_query' => array(
+				array(
+					'column'  => 'item_id',
+					'compare' => '<',
+					'value'   => 10,
+				),
+			)
+		) );
+
+		// assert!
+		$this->assertEqualSets( array( $a1 ), wp_list_pluck( $activities_template->activities, 'id' ) );
+
+		// less or equal than
+		bp_has_activities( array(
+			'filter_query' => array(
+				array(
+					'column'  => 'item_id',
+					'compare' => '<=',
+					'value'   => 10,
+				),
+			)
+		) );
+
+		// assert!
+		$this->assertEqualSets( array( $a1, $a2 ), wp_list_pluck( $activities_template->activities, 'id' ) );
+
+		// not equal to
+		bp_has_activities( array(
+			'filter_query' => array(
+				array(
+					'column'  => 'item_id',
+					'compare' => '!=',
+					'value'   => 10,
+				),
+			)
+		) );
+
+		// assert!
+		$this->assertEqualSets( array( $a1, $a3, $a4 ), wp_list_pluck( $activities_template->activities, 'id' ) );
+
+		// clean up!
+		$activities_template = null;
+	}
+
+	/**
+	 * @group filter_query
+	 * @group BP_Activity_Query
+	 */
+	function test_bp_has_activities_with_filter_query_compare_regex() {
+		$u1 = $this->factory->user->create();
+
+		$now = time();
+
+		// misc activity items
+		$a1 = $this->factory->activity->create( array(
+			'user_id'   => $u1,
+			'component' => 'blogs',
+			'item_id'   => 1,
+			'type'      => 'new_blog_post',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+		$a2 = $this->factory->activity->create( array(
+			'user_id'   => $u1,
+			'component' => 'blogs',
+			'type'      => 'new_blog_comment',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+		$a3 = $this->factory->activity->create( array(
+			'user_id'   => $u1,
+			'component' => 'activity',
+			'type'      => 'activity_update',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+		$a4 = $this->factory->activity->create( array(
+			'user_id'   => $u1,
+			'component' => 'groups',
+			'item_id'   => 324,
+			'type'      => 'activity_update',
+			'recorded_time' => date( 'Y-m-d H:i:s', $now - 100 ),
+		) );
+
+		global $activities_template;
+
+		// REGEXP
+		bp_has_activities( array(
+			'filter_query' => array(
+				array(
+					'column'  => 'type',
+					'compare' => 'REGEXP',
+					'value'   => '^new_blog_',
+				),
+			)
+		) );
+
+		// assert!
+		$this->assertEqualSets( array( $a1, $a2 ), wp_list_pluck( $activities_template->activities, 'id' ) );
+
+		// RLIKE is a synonym for REGEXP
+		bp_has_activities( array(
+			'filter_query' => array(
+				array(
+					'column'  => 'type',
+					'compare' => 'RLIKE',
+					'value'   => '^new_blog_',
+				),
+			)
+		) );
+
+		// assert!
+		$this->assertEqualSets( array( $a1, $a2 ), wp_list_pluck( $activities_template->activities, 'id' ) );
+
+		// NOT REGEXP
+		bp_has_activities( array(
+			'filter_query' => array(
+				array(
+					'column'  => 'type',
+					'compare' => 'NOT REGEXP',
+					'value'   => '^new_blog_',
+				),
+			)
+		) );
+
+		// assert!
+		$this->assertEqualSets( array( $a3, $a4 ), 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() {
